From d074bba3a96f18548d4d331270f532867c2e0d24 Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Thu, 3 Mar 2016 17:32:45 +0000 Subject: [PATCH 1/4] Adding tests for sendWebPush --- gulp-tasks/test.js | 26 ++++++++++++++++++++ package.json | 1 + src/push.js | 11 +++++++-- test/index.js | 59 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 gulp-tasks/test.js diff --git a/gulp-tasks/test.js b/gulp-tasks/test.js new file mode 100644 index 0000000..711cef7 --- /dev/null +++ b/gulp-tasks/test.js @@ -0,0 +1,26 @@ +/* + Copyright 2014 Google Inc. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +/* eslint-env node */ + +'use strict'; + +const gulp = require('gulp'); +const mocha = require('gulp-mocha'); + +gulp.task('test:manual', function() { + return gulp.src('./test/*.js', {read: false}) + .pipe(mocha()); +}); diff --git a/package.json b/package.json index 3ce28ca..747fc72 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "eslint-config-google": "^0.3.0", "gulp": "^3.9.0", "gulp-eslint": "^1.1.1", + "gulp-mocha": "^2.2.0", "proxyquire": "^1.7.4", "require-dir": "^0.3.0", "sinon": "^1.17.3" diff --git a/src/push.js b/src/push.js index 333d056..9041f72 100644 --- a/src/push.js +++ b/src/push.js @@ -72,13 +72,20 @@ function addAuthToken(pattern, token) { /** * Sends a message using the Web Push protocol - * @param {String} message The message to send * @param {Object} subscription The subscription details for the client we * are sending to + * @param {String} message The message to send * @return {Promise} A promise that resolves if the push was sent successfully * with status and body. */ -function sendWebPush(message, subscription) { +function sendWebPush(subscription, message) { + if ( + !subscription || !subscription.endpoint || + !message || typeof message !== 'string') { + throw new Error('sendWebPush() expects a subscription endpoint with ' + + 'an endpoint parameter and a string send with the push message.'); + } + let endpoint = subscription.endpoint; const authToken = getAuthToken(endpoint); diff --git a/test/index.js b/test/index.js index 72a8863..37b5900 100644 --- a/test/index.js +++ b/test/index.js @@ -204,4 +204,63 @@ describe('Test the Libraries Top Level API', function() { ).to.throw('Payload is too large. The max number of bytes is 4080, input is 5000 bytes.'); }); }); + + describe('Test sendWebPush() method', function() { + it('should throw an error when no input provided', function() { + const library = require('../src/index.js'); + + expect( + () => library.sendWebPush() + ).to.throw('sendWebPush() expects a subscription endpoint with ' + + 'an endpoint parameter and a string send with the push message.'); + }); + + it('should throw an error when the subscription object has no endpoint', function() { + const library = require('../src/index.js'); + + expect( + () => library.sendWebPush({}) + ).to.throw('sendWebPush() expects a subscription endpoint with ' + + 'an endpoint parameter and a string send with the push message.'); + }); + + it('should throw an error when a subscription is passed in with no payload data', function() { + const library = require('../src/index.js'); + + expect( + () => library.sendWebPush({ + endpoint: 'http://fakendpoint' + }) + ).to.throw('sendWebPush() expects a subscription endpoint with ' + + 'an endpoint parameter and a string send with the push message.'); + }); + + it('should throw an error when a subscription is passed in with array as payload data', function() { + const library = require('../src/index.js'); + + expect( + () => library.sendWebPush({ + endpoint: 'http://fakendpoint' + }, [ + { + hello: 'world' + }, + 'This is a test', + Promise.resolve('Promise Resolve'), + Promise.reject('Promise Reject') + ]) + ).to.throw('sendWebPush() expects a subscription endpoint with ' + + 'an endpoint parameter and a string send with the push message.'); + }); + + it('should throw an error when a subscription with no encryption details is passed in with string as payload data', function() { + const library = require('../src/index.js'); + + expect( + () => library.sendWebPush({ + endpoint: 'http://fakendpoint' + }, 'Hello, World!') + ).to.throw('Subscription has no encryption details.'); + }); + }); }); From 0460026527a7e56d79ca3b5a80eb67262df4bd6b Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Thu, 3 Mar 2016 17:33:33 +0000 Subject: [PATCH 2/4] Gulp + npm script for tests --- .travis.yml | 1 - package.json | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 002e29d..3c1c9ad 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,5 +5,4 @@ node_js: - 'stable' install: - - npm install -g mocha - npm install diff --git a/package.json b/package.json index 747fc72..5ef5940 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "sinon": "^1.17.3" }, "scripts": { - "test": "gulp lint && mocha" + "test": "gulp lint && gulp test:manual" }, "keywords": [ "web", From 47cf29d15295693aa4b58e9bfc79ec093e4eeb03 Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Mon, 7 Mar 2016 11:12:19 +0000 Subject: [PATCH 3/4] Adding tests for send web push. Some touches to error cases --- src/push.js | 16 ++++--- test/index.js | 121 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 7 deletions(-) diff --git a/src/push.js b/src/push.js index 9041f72..ce4c6ef 100644 --- a/src/push.js +++ b/src/push.js @@ -89,11 +89,6 @@ function sendWebPush(subscription, message) { let endpoint = subscription.endpoint; const authToken = getAuthToken(endpoint); - // If the endpoint is GCM then we temporarily need to rewrite it, as not all - // GCM servers support the Web Push protocol. This should go away in the - // future. - endpoint = endpoint.replace(GCM_URL, TEMP_GCM_URL); - const payload = encrypt(message, subscription); const headers = { 'Encryption': createHeaderField('salt', payload.salt), @@ -102,8 +97,16 @@ function sendWebPush(subscription, message) { if (authToken) { headers.Authorization = 'key=' + authToken; + } else if (endpoint.indexOf(GCM_URL) !== -1) { + throw new Error('GCM requires an Auth Token. Please add one using the' + + 'addAuthToken() method.'); } + // If the endpoint is GCM then we temporarily need to rewrite it, as not all + // GCM servers support the Web Push protocol. This should go away in the + // future. + endpoint = endpoint.replace(GCM_URL, TEMP_GCM_URL); + return new Promise(function(resolve, reject) { request.post(endpoint, { body: payload.ciphertext, @@ -113,7 +116,8 @@ function sendWebPush(subscription, message) { reject(error); } else { resolve({ - status: `${response.statusCode} ${response.statusMessage}`, + statusCode: response.statusCode, + statusMessage: response.statusMessage, body: body }); } diff --git a/test/index.js b/test/index.js index 37b5900..8573b70 100644 --- a/test/index.js +++ b/test/index.js @@ -25,7 +25,6 @@ const EXAMPLE_SERVER_KEYS = { public: 'BOg5KfYiBdDDRF12Ri17y3v+POPr8X0nVP2jDjowPVI/DMKU1aQ3OLdPH1iaakvR9/PHq6tNCzJH35v/JUz2crY=', private: 'uDNsfsz91y2ywQeOHljVoiUg3j5RGrDVAswRqjP3v90=' }; - const EXAMPLE_SALT = 'AAAAAAAAAAAAAAAAAAAAAA=='; const EXAMPLE_INPUT = 'Hello, World.'; @@ -59,6 +58,11 @@ const SUBSCRIPTION_NO_KEYS = { endpoint: 'https://example-endpoint.com/example/1234' }; +const GCM_SUBSCRIPTION_EXAMPLE = { + original: 'https://android.googleapis.com/gcm/send/AAAAAAAAAAA:AAA91AAAA2_A7AAAAAAAAAAAAAAAAAAAAAAAAAAA9AAAAA9AAA_AAAA8AAAAAA5-AAAAAA2AAAA_AAAAA4A51A_A3AAA1AAAAAAAAAAAAAAA3AAAAAAAAA6AA2AAAAAAAA80AAAAAA', + webpush: 'https://gcm-http.googleapis.com/gcm/AAAAAAAAAAA:AAA91AAAA2_A7AAAAAAAAAAAAAAAAAAAAAAAAAAA9AAAAA9AAA_AAAA8AAAAAA5-AAAAAA2AAAA_AAAAA4A51A_A3AAA1AAAAAAAAAAAAAAA3AAAAAAAAA6AA2AAAAAAAA80AAAAAA' +}; + const SALT_LENGTH = 16; const SERVER_PUBLIC_KEY_LENGTH = 65; @@ -262,5 +266,120 @@ describe('Test the Libraries Top Level API', function() { }, 'Hello, World!') ).to.throw('Subscription has no encryption details.'); }); + + it('should attempt a web push protocol request', function() { + const requestReplacement = { + post: (endpoint, data, cb) => { + endpoint.should.equal(VALID_SUBSCRIPTION.endpoint); + + Buffer.isBuffer(data.body).should.equal(true); + data.headers.Encryption.should.have.length(27); + data.headers['Crypto-Key'].should.have.length(90); + + cb( + null, + { + statusCode: 200, + statusMessage: 'Status message' + }, + 'Response body' + ); + } + }; + const pushProxy = proxyquire('../src/push.js', { + 'request': requestReplacement + }); + const library = proxyquire('../src/index.js', { + './push': pushProxy + }); + return library.sendWebPush(VALID_SUBSCRIPTION, 'Hello, World!') + .then(response => { + response.statusCode.should.equal(200); + response.statusMessage.should.equal('Status message'); + response.body.should.equal('Response body'); + }); + }); + + it('should attempt a web push protocol request for GCM', function() { + const API_KEY = 'AAAA'; + const gcmSubscription = { + endpoint: GCM_SUBSCRIPTION_EXAMPLE.original, + keys: VALID_SUBSCRIPTION.keys + }; + const requestReplacement = { + post: (endpoint, data, cb) => { + endpoint.should.equal(GCM_SUBSCRIPTION_EXAMPLE.webpush); + + Buffer.isBuffer(data.body).should.equal(true); + data.headers.Encryption.should.have.length(27); + data.headers['Crypto-Key'].should.have.length(90); + data.headers.Authorization.should.equal('key=' + API_KEY); + + cb( + null, + { + statusCode: 200, + statusMessage: 'Status message' + }, + 'Response body' + ); + } + }; + + const pushProxy = proxyquire('../src/push.js', { + 'request': requestReplacement + }); + const library = proxyquire('../src/index.js', { + './push': pushProxy + }); + library.addAuthToken('https://android.googleapis.com/gcm/send', API_KEY); + return library.sendWebPush(gcmSubscription, 'Hello, World!') + .then(response => { + response.statusCode.should.equal(200); + response.statusMessage.should.equal('Status message'); + response.body.should.equal('Response body'); + }); + }); + + it('should throw an error when not providing an AuthToken for a GCM subscription', function() { + const gcmSubscription = { + endpoint: GCM_SUBSCRIPTION_EXAMPLE.original, + keys: VALID_SUBSCRIPTION.keys + }; + + const library = require('../src/index.js'); + expect( + () => library.sendWebPush(gcmSubscription, 'Hello, World!') + ).to.throw('GCM requires an Auth Token. Please add one using theaddAuthToken() method.'); + }); + + it('should handle errors from request', function() { + const EXAMPLE_ERROR = 'Example Error'; + + const requestReplacement = { + post: (endpoint, data, cb) => { + endpoint.should.equal(VALID_SUBSCRIPTION.endpoint); + + Buffer.isBuffer(data.body).should.equal(true); + data.headers.Encryption.should.have.length(27); + data.headers['Crypto-Key'].should.have.length(90); + + cb(EXAMPLE_ERROR); + } + }; + const pushProxy = proxyquire('../src/push.js', { + 'request': requestReplacement + }); + const library = proxyquire('../src/index.js', { + './push': pushProxy + }); + return library.sendWebPush(VALID_SUBSCRIPTION, 'Hello, World!') + .then(() => { + throw new Error('The promise was expected to reject.'); + }) + .catch(err => { + err.should.equal(EXAMPLE_ERROR); + }); + }); }); }); From 2b119845917d9481d9d032d2e19104e167407f56 Mon Sep 17 00:00:00 2001 From: Matt Gaunt Date: Tue, 8 Mar 2016 10:49:25 +0000 Subject: [PATCH 4/4] Return error status code if mocha tests fail --- gulp-tasks/test.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/gulp-tasks/test.js b/gulp-tasks/test.js index 711cef7..848ffef 100644 --- a/gulp-tasks/test.js +++ b/gulp-tasks/test.js @@ -1,5 +1,5 @@ /* - Copyright 2014 Google Inc. All Rights Reserved. + Copyright 2016 Google Inc. All Rights Reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -22,5 +22,8 @@ const mocha = require('gulp-mocha'); gulp.task('test:manual', function() { return gulp.src('./test/*.js', {read: false}) - .pipe(mocha()); + .pipe(mocha()) + .on('error', () => { + process.exit(1); + }); });