diff --git a/.gitignore b/.gitignore index aedd71b..350ba7d 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ pids logs results +test.js + node_modules npm-debug.log .idea +.vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index c4171bd..2f5b926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,15 @@ Changelog ========= +**1.0.0-alpha.1** + * Removed the Message abstraction, now expect plain objects. + * Smaller lodash dependency (only depending on the part that is used). + * Simplified the recipient argument so it is now closer to the actual API interface. + * Removed `sendNoRetry` method on sender --- use `send` with the option `retries: 0` instead. + +**1.0.0-alpha.0** + * Removed deprecated things: constants, Result, MulticastResult, Message#addDataWith... + **0.14.2** * Updated README, added note on v1 development diff --git a/README.md b/README.md index 0a2be20..d92761c 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,10 @@ If you are developing an open-source project with a broader scope (like a full F See the [official FCM documentation](https://firebase.google.com/docs/cloud-messaging/) for more information. -We are currently working on version 1.0.0 of the project, and it is available in an early alpha version. +This is the README for the `v1` branch, and it is currently work in progress. +Version 1.0.0 is only available in an alpha version. Follow [PR #238](https://github.com/ToothlessGear/node-gcm/pull/238) to see current status. +We currently recommend you use the mainline version of node-gcm (found on the master branch) for production. ## Installation @@ -31,17 +33,14 @@ If you are new to GCM you should probably look into the [documentation](https:// According to below **Usage** reference, we could create such application: ```js -var gcm = require('node-gcm'); +var gcm = require('node-gcm')('YOUR_API_KEY_HERE'); -var message = new gcm.Message({ +var message = { data: { key1: 'msg1' } -}); - -// Set up the sender with you API key, prepare your recipients' registration tokens. -var sender = new gcm.Sender('YOUR_API_KEY_HERE'); -var regTokens = ['YOUR_REG_TOKEN_HERE']; +}; +var recipient = 'YOUR_REG_TOKEN_HERE'; -sender.send(message, { registrationTokens: regTokens }, function (err, response) { +gcm.send(message, recipient, function (err, response) { if(err) console.error(err); else console.log(response); }); @@ -50,21 +49,17 @@ sender.send(message, { registrationTokens: regTokens }, function (err, response) ## Usage ```js -var gcm = require('node-gcm'); +var gcm = require('node-gcm')('insert Google Server API Key here'); -// Create a message -// ... with default values -var message = new gcm.Message(); - -// ... or some given values -var message = new gcm.Message({ - collapseKey: 'demo', +// Create a message (all possible values shown) +var message = { + collapse_key: 'demo', priority: 'high', - contentAvailable: true, - delayWhileIdle: true, - timeToLive: 3, - restrictedPackageName: "somePackageName", - dryRun: true, + content_available: true, + delay_while_idle: true, + time_to_live: 3, + restricted_package_name: "somePackageName", + dry_run: true, data: { key1: 'message1', key2: 'message2' @@ -74,21 +69,7 @@ var message = new gcm.Message({ icon: "ic_launcher", body: "This is a notification that will be displayed ASAP." } -}); - -// Change the message data -// ... as key-value -message.addData('key1','message1'); -message.addData('key2','message2'); - -// ... or as a data object (overwrites previous data object) -message.addData({ - key1: 'message1', - key2: 'message2' -}); - -// Set up the sender with you API key -var sender = new gcm.Sender('insert Google Server API Key here'); +}; // Add the registration tokens of the devices you want to send to var registrationTokens = []; @@ -97,37 +78,28 @@ registrationTokens.push('regToken2'); // Send the message // ... trying only once -sender.sendNoRetry(message, { registrationTokens: registrationTokens }, function(err, response) { +gcm.send(message, registrationTokens, { retries: 0 }, function(err, response) { if(err) console.error(err); else console.log(response); }); // ... or retrying -sender.send(message, { registrationTokens: registrationTokens }, function (err, response) { +gcm.send(message, registrationTokens, function (err, response) { if(err) console.error(err); else console.log(response); }); // ... or retrying a specific number of times (10) -sender.send(message, { registrationTokens: registrationTokens }, 10, function (err, response) { +gcm.send(message, registrationTokens, 10, function (err, response) { if(err) console.error(err); else console.log(response); }); ``` -## Recipients - -You can send push notifications to various recipient types by providing one of the following recipient keys: - -|Key|Type|Description| -|---|---|---| -|to|String|A single [registration token](https://developers.google.com/cloud-messaging/android/client#sample-register), [notification key](https://developers.google.com/cloud-messaging/notifications), or [topic](https://developers.google.com/cloud-messaging/topic-messaging). -|topic|String|A single publish/subscribe topic. -|notificationKey|String|Deprecated. A key that groups multiple registration tokens linked to the same user. -|registrationIds|String[]|Deprecated. Use registrationTokens instead.| -|registrationTokens|String[]|A list of registration tokens. Must contain at least 1 and at most 1000 registration tokens.| +## Recipients -If you provide an incorrect recipient key or object type, an `Error` object will be returned to your callback. +You can send a push notification to various recipient or topic, by providing a notification key, registration token or topic as a string. +Alternatively, you can send it to several recipients at once, by providing an array of registration tokens. Notice that [you can *at most* send notifications to 1000 registration tokens at a time](https://github.com/ToothlessGear/node-gcm/issues/42). This is due to [a restriction](http://developer.android.com/training/cloudsync/gcm.html) on the side of the GCM API. @@ -135,21 +107,13 @@ This is due to [a restriction](http://developer.android.com/training/cloudsync/g ## Notification usage ```js - -var message = new gcm.Message(); - -// Add notification payload as key value -message.addNotification('title', 'Alert!!!'); -message.addNotification('body', 'Abnormal data access'); -message.addNotification('icon', 'ic_launcher'); - -// as object -message.addNotification({ - title: 'Alert!!!', - body: 'Abnormal data access', - icon: 'ic_launcher' -}); - +var message = { + notification: { + title: 'Alert!!!', + body: 'Abnormal data access', + icon: 'ic_launcher' + } +}; ``` ### Notification payload option table @@ -182,15 +146,15 @@ var requestOptions = { timeout: 5000 }; -// Set up the sender with your API key and request options -var sender = new gcm.Sender('YOUR_API_KEY_HERE', requestOptions); +// Set up gcm with your API key and request options +var gcm = require("node-gcm")('YOUR_API_KEY_HERE', requestOptions); // Prepare a GCM message... // Send it to GCM endpoint with modified request options -sender.send(message, { registrationTokens: regTokens }, function (err, response) { +gcm.send(message, regTokens, function (err, response) { if(err) console.error(err); - else console.log(response); + else console.log(response); }); ``` diff --git a/examples/notification.js b/examples/notification.js index 11ec000..19cb348 100644 --- a/examples/notification.js +++ b/examples/notification.js @@ -1,19 +1,21 @@ -var gcm = require('../lib/node-gcm'); - -var message = new gcm.Message(); - -message.addData('hello', 'world'); -message.addNotification('title', 'Hello'); -message.addNotification('icon', 'ic_launcher'); -message.addNotification('body', 'World'); +//Replace your developer API key with GCM enabled here +var gcm = require('../index')('AIza*******************5O6FM'); +var message = { + data: { + hello: 'world' + }, + notification: { + title: 'Hello', + icon: 'ic_launcher', + body: 'World' + } +}; //Add your mobile device registration tokens here var regTokens = ['ecG3ps_bNBk:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxXl7TDJkW']; -//Replace your developer API key with GCM enabled here -var sender = new gcm.Sender('AIza*******************5O6FM'); -sender.send(message, regTokens, function (err, response) { +gcm.send(message, regTokens, function (err, response) { if(err) { console.error(err); } else { diff --git a/lib/constants.js b/lib/constants.js index 7761955..429646c 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -8,21 +8,6 @@ var Constants = { /** DEPRECATED **/ - 'TOKEN_MESSAGE_ID' : 'id', - 'TOKEN_CANONICAL_REG_ID' : 'registration_id', - 'TOKEN_ERROR' : 'Error', - 'JSON_REGISTRATION_IDS' : 'registration_ids', - 'JSON_PAYLOAD' : 'data', - 'JSON_NOTIFICATION' : 'notification', - 'JSON_SUCCESS' : 'success', - 'JSON_FAILURE' : 'failure', - 'JSON_CANONICAL_IDS' : 'canonical_ids', - 'JSON_MULTICAST_ID' : 'multicast_id', - 'JSON_RESULTS' : 'results', - 'JSON_ERROR' : 'error', - 'JSON_MESSAGE_ID' : 'message_id', - 'UTF8' : 'UTF-8', - //These errors could probably be structured more nicely, and could be used in the code. // -- maybe just as an Error abstraction? 'ERROR_QUOTA_EXCEEDED' : 'QuotaExceeded', diff --git a/lib/message-options.js b/lib/message-options.js index 1fee354..c82feee 100644 --- a/lib/message-options.js +++ b/lib/message-options.js @@ -1,43 +1,31 @@ /** * This module defines all the arguments that may be passed to a message. - * - * Each argument may contain a field `__argName`, if the name of the field - * should be different when sent to the server. - * - * The argument may also contain a field `__argType`, if the given + * + * Each argument may contain a field `__argType`, in which case the given * argument must be of that type. The types are the strings resulting from * calling `typeof ` where `` is the argument. - * - * Other than that, the arguments are expected to follow the indicated - * structure. */ module.exports = { - collapseKey: { - __argName: "collapse_key", + collapse_key: { __argType: "string" }, priority: { __argType: "string" }, - contentAvailable: { - __argName: "content_available", + content_available: { __argType: "boolean" }, - delayWhileIdle: { - __argName: "delay_while_idle", + delay_while_idle: { __argType: "boolean" }, - timeToLive: { - __argName: "time_to_live", + time_to_live: { __argType: "number" }, - restrictedPackageName: { - __argName: "restricted_package_name", + restricted_package_name: { __argType: "string" }, - dryRun: { - __argName: "dry_run", + dry_run: { __argType: "boolean" }, data: { diff --git a/lib/message.js b/lib/message.js deleted file mode 100644 index 2834f0c..0000000 --- a/lib/message.js +++ /dev/null @@ -1,80 +0,0 @@ -var messageOptions = require("./message-options"); - -function Message(raw) { - if (!(this instanceof Message)) { - return new Message(raw); - } - this.params = cleanParams(raw || {}); -} - -function cleanParams(raw) { - var params = {}; - Object.keys(raw).forEach(function(param) { - if(messageOptions[param]) { - params[param] = raw[param]; - } - }); - return params; -} - -Message.prototype.addNotification = function() { - return handleParamSet.call(this, arguments, "notification"); -}; - -function handleParamSet(args, paramType) { - if(args.length == 1) { - return setParam.call(this, paramType, args[0]); - } - if(args.length == 2) { - return addParam.call(this, paramType, args[0], args[1]); - } - throw new Error("Invalid number of arguments given to for setting " + paramType + " (" + args.length + ")"); -} - -function setParam(paramType, obj) { - if (typeof obj === 'object' && Object.keys(obj).length > 0) { - this.params[paramType] = obj; - } -} - -function addParam(paramType, key, value) { - if(!this.params[paramType]) { - this.params[paramType] = {}; - } - return this.params[paramType][key] = value; -} - -Message.prototype.addData = function() { - return handleParamSet.call(this, arguments, "data"); -}; - -Message.prototype.toJson = function() { - var json = {}; - - Object.keys(this.params).forEach(function(param) { - var optionDescription = messageOptions[param]; - if(!optionDescription) { - return; - } - var key = optionDescription.__argName || param; - json[key] = this.params[param]; - }.bind(this)); - - return json; -}; - -/** DEPRECATED */ - -Message.prototype.addDataWithKeyValue = function (key, value) { - console.warn("Message#addDataWithKeyValue has been deprecated. Please use Message#addData instead."); - this.addData(key, value); -}; - -Message.prototype.addDataWithObject = function (obj) { - console.warn("Message#addDataWithObject has been deprecated. Please use Message#addData instead."); - this.addData(obj); -}; - -/** END DEPRECATED */ - -module.exports = Message; diff --git a/lib/multicastresult.js b/lib/multicastresult.js deleted file mode 100644 index 8390b1c..0000000 --- a/lib/multicastresult.js +++ /dev/null @@ -1,28 +0,0 @@ -/** DEPRECATED **/ - -function MulitcastResult() { - if (!(this instanceof MulitcastResult)) { - return new MulitcastResult(); - } - - console.warn("You are using node-gcm MulticastResult, which is deprecated."); - - this.success = undefined; - this.failure = undefined; - this.canonicalIds = undefined; - this.multicastId = undefined; - this.results = []; - this.retryMulticastIds = []; -} - -MulitcastResult.prototype.addResult = function (result) { - this.results.push(result); -}; - -MulitcastResult.prototype.getTotal = function () { - return this.success + this.failure; -}; - -module.exports = MulitcastResult; - -/** END DEPRECATED **/ diff --git a/lib/node-gcm.js b/lib/node-gcm.js index 107f64e..0c9d93c 100644 --- a/lib/node-gcm.js +++ b/lib/node-gcm.js @@ -1,11 +1 @@ -/*! - * node-gcm - * Copyright(c) 2013 Marcus Farkas - * MIT Licensed - */ - -exports.Constants = require('./constants'); -exports.Message = require('./message'); -exports.Result = require('./result'); -exports.MulitcastResult = require('./multicastresult'); -exports.Sender = require('./sender'); +module.exports = require("./sender"); diff --git a/lib/result.js b/lib/result.js deleted file mode 100644 index e76fedf..0000000 --- a/lib/result.js +++ /dev/null @@ -1,13 +0,0 @@ -/** DEPRECATED **/ - -function Result() { - this.messageId = undefined; - this.canonicalRegistrationId = undefined; - this.errorCode = undefined; - - console.warn("You are using node-gcm Result which is deprecated"); -} - -module.exports = Result; - -/** END DEPRECATED **/ diff --git a/lib/sender.js b/lib/sender.js index ef0af71..e90d015 100644 --- a/lib/sender.js +++ b/lib/sender.js @@ -1,63 +1,53 @@ var Constants = require('./constants'); -var _ = require('lodash'); +var defaultsDeep = require('lodash.defaultsdeep'); var request = require('request'); var debug = require('debug')('node-gcm'); +var messageOptions = require("./message-options"); function Sender(key, options) { if (!(this instanceof Sender)) { return new Sender(key, options); } - this.key = key; - this.options = options || {}; + this.requestOptions = defaultsDeep({ + method: 'POST', + headers: { + 'Authorization': 'key=' + key + }, + uri: Constants.GCM_SEND_URI + }, options, { + timeout: Constants.SOCKET_TIMEOUT + }); } Sender.prototype.send = function(message, recipient, options, callback) { + var rVal; if(typeof options == "function") { callback = options; options = null; } else if(!callback) { - callback = function() {}; + rVal = new Promise(function(resolve, reject) { + callback = function(err, response) { + if (err) { + return reject(err); + } + return resolve(response); + } + }); } options = cleanOptions(options); - if(options.retries == 0) { - return this.sendNoRetry(message, recipient, callback); - } - - var self = this; - - this.sendNoRetry(message, recipient, function(err, response, attemptedRegTokens) { - if (err) { - if (typeof err === 'number' && err > 399 && err < 500) { - debug("Error 4xx -- no use retrying. Something is wrong with the request (probably authentication?)"); - return callback(err); - } - return retry(self, message, recipient, options, callback); + getRequestBody(message, recipient, function(err, body) { + if(err) { + return callback(err); } - if(!response.results) { - return callback(null, response); + if(options.retries == 0) { + return sendMessage(this.requestOptions, body, callback); } - checkForBadTokens(response.results, attemptedRegTokens, function(err, unsentRegTokens, regTokenPositionMap) { - if(err) { - return callback(err); - } - if (unsentRegTokens.length == 0) { - return callback(null, response); - } - - debug("Retrying " + unsentRegTokens.length + " unsent registration tokens"); - - retry(self, message, unsentRegTokens, options, function(err, retriedResponse) { - if(err) { - return callback(null, response); - } - response = updateResponse(response, retriedResponse, regTokenPositionMap, unsentRegTokens); - callback(null, response); - }); - }); - }); + sendMessageWithRetries(this.requestOptions, body, options, callback); + }.bind(this)); + return rVal; }; function cleanOptions(options) { @@ -85,13 +75,86 @@ function cleanOptions(options) { return options; } -function retry(self, message, recipient, options, callback) { - return setTimeout(function() { - self.send(message, recipient, { - retries: options.retries - 1, - backoff: options.backoff * 2 +function getRequestBody(message, recipient, callback) { + var body = cleanParams(message); + + if(typeof recipient == "string") { + body.to = recipient; + return nextTick(callback, null, body); + } + if(Array.isArray(recipient)) { + if(recipient.length < 1) { + return nextTick(callback, new Error('Empty recipient array passed!')); + } + body.registration_ids = recipient; + return nextTick(callback, null, body); + } + return nextTick(callback, new Error('Invalid recipient (' + recipient + ', type ' + typeof recipient + ') provided (must be array or string)!')); +} + +function cleanParams(raw) { + var params = {}; + Object.keys(raw).forEach(function(param) { + var paramOptions = messageOptions[param]; + if(!paramOptions) { + return console.warn("node-gcm ignored unknown message parameter " + param); + } + if(paramOptions.__argType != typeof raw[param]) { + return console.warn("node-gcm ignored wrongly typed message parameter " + param + " (was " + typeof raw[param] + ", expected " + paramOptions.__argType + ")"); + } + params[param] = raw[param]; + }); + return params; +} + +function nextTick(func) { + var args = Array.prototype.slice.call(arguments, 1); + process.nextTick(function() { + func.apply(this, args); + }.bind(this)); +} + +function sendMessageWithRetries(requestOptions, body, messageOptions, callback) { + sendMessage(requestOptions, body, function(err, response, attemptedRegTokens) { + if (err) { + if (typeof err === 'number' && err > 399 && err < 500) { + debug("Error 4xx -- no use retrying. Something is wrong with the request (probably authentication?)"); + return callback(err); + } + return retry(requestOptions, body, messageOptions, callback); + } + checkForBadTokens(response.results, attemptedRegTokens, function(err, unsentRegTokens, regTokenPositionMap) { + if(err) { + return callback(err); + } + if (unsentRegTokens.length == 0) { + return callback(null, response); + } + + debug("Retrying " + unsentRegTokens.length + " unsent registration tokens"); + + body.registration_ids = unsentRegTokens; + retry(requestOptions, body, messageOptions, function(err, retriedResponse) { + if(err) { + return callback(null, response); + } + response = updateResponse(response, retriedResponse, regTokenPositionMap, unsentRegTokens); + callback(null, response); + }); + }); + }); +} + +function retry(requestOptions, body, messageOptions, callback) { + setTimeout(function() { + if(messageOptions.retries <= 1) { + return sendMessage(requestOptions, body, callback); + } + sendMessageWithRetries(requestOptions, body, { + retries: messageOptions.retries - 1, + backoff: messageOptions.backoff * 2 }, callback); - }, options.backoff); + }, messageOptions.backoff); } function checkForBadTokens(results, originalRecipients, callback) { @@ -124,129 +187,26 @@ function updateResponseMetaData(response, retriedResponse, unsentRegTokens) { response.failure -= unsentRegTokens.length - retriedResponse.failure; } -Sender.prototype.sendNoRetry = function(message, recipient, callback) { - if(!callback) { - callback = function() {}; - } - - getRequestBody(message, recipient, function(err, body) { - if(err) { +function sendMessage(requestOptions, body, callback) { + requestOptions.json = body; + request(requestOptions, function (err, res, resBodyJSON) { + if (err) { return callback(err); } - - //Build request options, allowing some to be overridden - var request_options = _.defaultsDeep({ - method: 'POST', - headers: { - 'Authorization': 'key=' + this.key - }, - uri: Constants.GCM_SEND_URI, - json: body - }, this.options, { - timeout: Constants.SOCKET_TIMEOUT - }); - - request(request_options, function (err, res, resBodyJSON) { - if (err) { - return callback(err); - } - if (res.statusCode >= 500) { - debug('GCM service is unavailable (500)'); - return callback(res.statusCode); - } - if (res.statusCode === 401) { - debug('Unauthorized (401). Check that your API token is correct.'); - return callback(res.statusCode); - } - if (res.statusCode !== 200) { - debug('Invalid request (' + res.statusCode + '): ' + resBodyJSON); - return callback(res.statusCode); - } - callback(null, resBodyJSON, body.registration_ids || [ body.to ]); - }); - }.bind(this)); -}; - -function getRequestBody(message, recipient, callback) { - var body = message.toJson(); - - if(typeof recipient == "string") { - body.to = recipient; - return nextTick(callback, null, body); - } - if(Array.isArray(recipient)) { - if(!recipient.length) { - return nextTick(callback, 'No recipient provided!'); + if (res.statusCode >= 500) { + debug('GCM service is unavailable (500)'); + return callback(res.statusCode); } - else if(recipient.length == 1) { - body.to = recipient[0]; - return nextTick(callback, null, body); + if (res.statusCode === 401) { + debug('Unauthorized (401). Check that your API token is correct.'); + return callback(res.statusCode); } - body.registration_ids = recipient; - return nextTick(callback, null, body); - } - if (typeof recipient == "object") { - return extractRecipient(recipient, function(err, recipient) { - if(err) { - return callback(err); - } - if (Array.isArray(recipient)) { - body.registration_ids = recipient; - return callback(null, body); - } - body.to = recipient; - return callback(null, body); - }); - } - return nextTick(callback, 'Invalid recipient (' + recipient + ', type ' + typeof recipient + ') provided!'); -} - -function nextTick(func) { - var args = Array.prototype.slice.call(arguments, 1); - process.nextTick(function() { - func.apply(this, args); - }.bind(this)); -} - -function extractRecipient(recipient, callback) { - var recipientKeys = Object.keys(recipient); - - if(recipientKeys.length !== 1) { - return nextTick(callback, new Error("Please specify exactly one recipient key (you specified [" + recipientKeys + "])")); - } - - var key = recipientKeys[0]; - var value = recipient[key]; - - if(!value) { - return nextTick(callback, new Error("Falsy value for recipient key '" + key + "'.")); - } - - var keyValidators = { - to: isString, - topic: isString, - notificationKey: isString, - registrationIds: isArray, - registrationTokens: isArray - }; - - var validator = keyValidators[key]; - if(!validator) { - return nextTick(callback, new Error("Key '" + key + "' is not a valid recipient key.")); - } - if(!validator(value)) { - return nextTick(callback, new Error("Recipient key '" + key + "' was provided as an incorrect type.")); - } - - return nextTick(callback, null, value); -} - -function isString(x) { - return typeof x == "string"; -} - -function isArray(x) { - return Array.isArray(x); + if (res.statusCode !== 200) { + debug('Invalid request (' + res.statusCode + '): ' + resBodyJSON); + return callback(res.statusCode); + } + callback(null, resBodyJSON, body.registration_ids || [ body.to ]); + }); } module.exports = Sender; diff --git a/package.json b/package.json index c4b0ad4..921a02d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "node-gcm", "description": "Easy interface for Google's Cloud Messaging service (now Firebase Cloud Messaging)", - "version": "0.14.2", + "version": "1.0.0-alpha.1", "author": "Marcus Farkas ", "contributors": [ "Marcus Farkas ", @@ -36,7 +36,9 @@ "Elad Nava (https://github.com/eladnava)", "Marc Obrador (https://github.com/marcobrador)", "Chirag Choudhary (https://github.com/chirag200666)", - "Alexander Amin (https://github.com/AlexAmin)" + "Alexander Amin (https://github.com/AlexAmin)", + "Ratson (https://github.com/ratson)", + "Dan Perron (https://github.com/dan-perron)" ], "repository": { "type": "git", @@ -66,8 +68,8 @@ "test": "mocha test/**/*Spec.js" }, "dependencies": { - "debug": "^0.8.1", - "lodash": "^3.10.1", + "debug": "^2.2.0", + "lodash.defaultsdeep": "^4.4.0", "request": "^2.27.0" }, "devDependencies": { diff --git a/test/unit/messageSpec.js b/test/unit/messageSpec.js deleted file mode 100644 index b898fc3..0000000 --- a/test/unit/messageSpec.js +++ /dev/null @@ -1,314 +0,0 @@ -"use strict"; - -var Message = require('../../lib/message'), - chai = require('chai'), - expect = chai.expect; - -describe('UNIT Message', function () { - describe('constructor', function () { - it('can be instantiated with no state', function () { - var mess = new Message(); - var json = mess.toJson(); - expect(json).to.deep.equal({}); - }); - - it('should call new on constructor if user does not', function () { - var mess = Message(); - expect(mess).to.not.be.an("undefined"); - expect(mess).to.be.instanceOf(Message); - }); - - it('should create an message with properties passed in', function () { - var obj = { - collapseKey: 'Message', - delayWhileIdle: true, - timeToLive: 100, - dryRun: true, - priority: 'high', - contentAvailable: false, - restrictedPackageName: "com.example.App", - data: { - score: 98 - }, - notification: {} - }; - var mess = new Message(obj); - - var json = mess.toJson(); - - expect(json).to.deep.equal({ - collapse_key: "Message", - delay_while_idle: true, - time_to_live: 100, - dry_run: true, - priority: 'high', - content_available: false, - restricted_package_name: "com.example.App", - data: { - score: 98 - }, - notification: {} - }); - }); - - it('should only set properties passed into constructor', function () { - var obj = { - collapseKey: 'Message', - delayWhileIdle: true, - data: { - score: 98 - }, - notification: {} - }; - var mess = new Message(obj); - - var json = mess.toJson(); - - expect(json).to.deep.equal({ - collapse_key: "Message", - delay_while_idle: true, - data: { - score: 98 - }, - notification: {} - }); - expect(json.time_to_live).to.be.an("undefined"); - expect(json.dry_run).to.be.an("undefined"); - expect(json.priority).to.be.an("undefined"); - expect(json.content_available).to.be.an("undefined"); - expect(json.restricted_package_name).to.be.an("undefined"); - }); - }); - - describe('addData()', function () { - it('should add properties to the message data object given a key and value', function () { - var mess = new Message(); - mess.addData('myKey', 'Message'); - - var json = mess.toJson(); - - expect(json.data.myKey).to.equal('Message'); - }); - - it('should only set values on data object, not top level message', function () { - var mess = new Message(); - mess.addData('collapseKey', 'Message'); - - var json = mess.toJson(); - - expect(json.collapse_key).to.not.equal('Message'); - expect(json.data.collapseKey).to.equal('Message'); - }); - - it('should set the data property to the object passed in', function () { - var mess = new Message(); - var obj = { - message: 'hello', - key: 'value' - }; - mess.addData(obj); - - var json = mess.toJson(); - - expect(json.data).to.deep.equal(obj); - }); - - it('should overwrite data object when an object is passed in', function () { - var data = { - message: 'hello', - key: 'value' - }; - var mess = new Message({ data: { message: 'bye', prop: 'none' } }); - mess.addData(data); - - var json = mess.toJson(); - - expect(json.data).to.deep.equal(data); - }); - - it('should not overwrite data if not passed an object', function () { - var data = { - message: 'hello', - key: 'value' - }; - var mess = new Message({ data: data }); - mess.addData('adding'); - - var json = mess.toJson(); - - expect(json.data).to.deep.equal(data); - }); - - it('should not overwrite data if passed an empty object', function () { - var data = { - message: 'hello', - key: 'value' - }; - var mess = new Message({ data: data }); - mess.addData({}); - - var json = mess.toJson(); - - expect(json.data).to.deep.equal(data); - }); - - it.skip('should do something if not called properly'); - }); - - describe('addDataWithKeyValue()', function () { - it('should add properties to the message data object given a key and value', function () { - var mess = new Message(); - mess.addDataWithKeyValue('myKey', 'Message'); - - var json = mess.toJson(); - - expect(json.data.myKey).to.equal('Message'); - }); - - it('should only set values on data object, not top level message', function () { - var mess = new Message(); - mess.addDataWithKeyValue('collapseKey', 'Message'); - - var json = mess.toJson(); - - expect(json.collapse_key).to.not.equal('Message'); - expect(json.data.collapseKey).to.equal('Message'); - }); - - it.skip('should do something if not called properly'); - }); - - describe('addDataWithObject()', function () { - it('should set the data property to the object passed in', function () { - var mess = new Message(); - var obj = { - message: 'hello', - key: 'value' - }; - mess.addDataWithObject(obj); - - var json = mess.toJson(); - - expect(json.data).to.deep.equal(obj); - }); - - it('should overwrite data object when an object is passed in', function () { - var data = { - message: 'hello', - key: 'value' - }; - var mess = new Message({ data: { message: 'bye', prop: 'none' } }); - mess.addDataWithObject(data); - - var json = mess.toJson(); - - expect(json.data).to.deep.equal(data); - }); - - it('should not overwrite data if not passed an object', function () { - var data = { - message: 'hello', - key: 'value' - }; - var mess = new Message({ data: data }); - mess.addDataWithObject('adding'); - - var json = mess.toJson(); - - expect(json.data).to.deep.equal(data); - }); - - it('should not overwrite data if passed an empty object', function () { - var data = { - message: 'hello', - key: 'value' - }; - var mess = new Message({ data: data }); - mess.addDataWithObject({}); - - var json = mess.toJson(); - - expect(json.data).to.deep.equal(data); - }); - }); - - describe('addNotification()', function () { - it('should add attribute on notification object if pass key and value', function () { - var mess = new Message(); - mess.addNotification('title', 'hello'); - mess.addNotification('icon', 'ic_launcher'); - mess.addNotification('body', 'world'); - - var json = mess.toJson(); - - expect(json.notification.title).to.equal('hello'); - expect(json.notification.icon).to.equal('ic_launcher'); - expect(json.notification.body).to.equal('world'); - }); - - it('should set the notification property to the object passed in', function () { - var mess = new Message(); - var obj = { - title: 'hello', - icon: 'ic_launcher', - body: 'world' - }; - mess.addNotification(obj); - - var json = mess.toJson(); - - expect(json.notification).to.deep.equal(obj); - }); - }); - - describe('toJson()', function() { - it('should return well-formed data for GCM if it is valid', function() { - var m = new Message({ - delayWhileIdle: true, - dryRun: true, - data: { - hello: "world" - } - }); - - var json = m.toJson(); - - expect(json.delay_while_idle).to.equal(true); - expect(json.dry_run).to.equal(true); - expect(json.data.hello).to.equal("world"); - expect(json.delayWhileIdle).to.be.an("undefined"); - expect(json.dryRun).to.be.an("undefined"); - }); - - it('should return well-formed data for GCM if it describes a notification', function() { - var notificationData = { - title: "Hello, World", - icon: 'ic_launcher', - body: "This is a quick notification." - }; - - var m = new Message({ delayWhileIdle: true }); - m.addNotification(notificationData); - - var json = m.toJson(); - - expect(json.delay_while_idle).to.equal(true); - expect(json.notification).not.to.be.an("undefined"); - expect(json.notification).to.deep.equal(notificationData); - }); - - it('should ignore non-standard fields when serializing', function() { - var m = new Message({ - timeToLive: 60 * 60 * 24, - wrongField: "should be excluded", - alsoThisFieldIsWrong: "and should not show up" - }); - - var json = m.toJson(); - - expect(json.time_to_live).to.equal(60 * 60 * 24); - expect(Object.keys(json).length).to.equal(1); - }); - }); - -}); diff --git a/test/unit/senderSpec.js b/test/unit/senderSpec.js index e736848..39aed07 100644 --- a/test/unit/senderSpec.js +++ b/test/unit/senderSpec.js @@ -4,57 +4,46 @@ var chai = require('chai'), expect = chai.expect, sinon = require('sinon'), proxyquire = require('proxyquire'), - senderPath = '../../lib/sender', - Constants = require('../../lib/constants'), - Message = require('../../lib/message'); + senderPath = '../../lib/sender'; describe('UNIT Sender', function () { // Use object to set arguments passed into callback var args = {}; - var requestStub = function (options, callback) { + var requestStub = sinon.spy(function (options, callback) { args.options = options; - return callback( args.err, args.res, args.resBody ); - }; + var resBody = args.resBody; + if(Array.isArray(args.resBody)) { + resBody = args.resBody[0]; + args.resBody = args.resBody.slice(1); + } + return callback( args.err, args.res, resBody ); + }); var Sender = proxyquire(senderPath, { 'request': requestStub }); describe('constructor', function () { - var Sender = require(senderPath); + var gcm = require(senderPath); it('should call new on constructor if user does not', function () { - var sender = Sender(); + var sender = gcm(); expect(sender).to.not.be.undefined; - expect(sender).to.be.instanceOf(Sender); + expect(sender).to.be.instanceOf(gcm); }); - - it('should create a Sender with key and options passed in', function () { - var options = { - proxy: 'http://myproxy.com', - maxSockets: 100, - timeout: 100 - }; - var key = 'myAPIKey', - sender = new Sender(key, options); - expect(sender).to.be.instanceOf(Sender); - expect(sender.key).to.equal(key); - expect(sender.options).to.deep.equal(options); - }); - - it.skip('should do something if not passed a valid key'); }); - describe('sendNoRetry()', function () { + describe('send() without retries', function () { function setArgs(err, res, resBody) { args = { err: err, res: res, resBody: resBody }; - }; - before(function() { + } + beforeEach(function() { + requestStub.reset(); setArgs(null, { statusCode: 200 }, {}); }); - it('should set proxy, maxSockets, timeout and/or strictSSL of req object if passed into constructor', function (done) { + it('should set key, proxy, maxSockets, timeout and/or strictSSL of req object if passed into constructor', function (done) { var options = { proxy: 'http://myproxy.com', maxSockets: 100, @@ -62,9 +51,10 @@ describe('UNIT Sender', function () { strictSSL: false }; var sender = new Sender('mykey', options); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', function () {}); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, function () {}); setTimeout(function() { + expect(args.options.headers["Authorization"]).to.equal("key=mykey"); expect(args.options.proxy).to.equal(options.proxy); expect(args.options.maxSockets).to.equal(options.maxSockets); expect(args.options.timeout).to.equal(options.timeout); @@ -83,8 +73,8 @@ describe('UNIT Sender', function () { json: { test: true } }; var sender = new Sender('mykey', options); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', function () {}); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, function () {}); setTimeout(function() { expect(args.options.method).to.not.equal(options.method); expect(args.options.headers).to.not.deep.equal(options.headers); @@ -101,8 +91,8 @@ describe('UNIT Sender', function () { } }; var sender = new Sender('mykey', options); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', function () {}); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, function () {}); setTimeout(function() { expect(args.options.headers.Authorization).to.not.equal(options.headers.Auhtorization); done(); @@ -116,8 +106,8 @@ describe('UNIT Sender', function () { } }; var sender = new Sender('mykey', options); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', function () {}); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, function () {}); setTimeout(function() { expect(args.options.headers.Custom).to.deep.equal(options.headers.Custom); done(); @@ -131,8 +121,8 @@ describe('UNIT Sender', function () { timeout: 1000 }; var sender = new Sender('mykey', options); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', function () {}); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, function () {}); setTimeout(function() { expect(args.options.strictSSL).to.be.an('undefined'); done(); @@ -141,8 +131,8 @@ describe('UNIT Sender', function () { it('should set the API key of req object if passed in API key', function (done) { var sender = new Sender('myKey'); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', function () {}); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, function () {}); setTimeout(function() { expect(args.options.headers.Authorization).to.equal('key=myKey'); done(); @@ -151,8 +141,8 @@ describe('UNIT Sender', function () { it('should send a JSON object as the body of the request', function (done) { var sender = new Sender('mykey'); - var m = new Message({ collapseKey: 'Message', data: {} }); - sender.sendNoRetry(m, '', function () {}); + var m = { collapseKey: 'Message', data: {} }; + sender.send(m, '', { retries: 0 }, function () {}); setTimeout(function() { expect(args.options.json).to.be.a('object'); done(); @@ -160,67 +150,68 @@ describe('UNIT Sender', function () { }); it('should set properties of body with message properties', function (done) { - var mess = new Message({ - delayWhileIdle: true, - collapseKey: 'Message', - timeToLive: 100, - dryRun: true, + var mess = { + delay_while_idle: true, + collapse_key: 'Message', + time_to_live: 100, + dry_run: true, data: { name: 'Matt' } - }); + }; var sender = new Sender('mykey'); - sender.sendNoRetry(mess, '', function () {}); - setTimeout(function() { - var body = args.options.json; - expect(body[Constants.PARAM_DELAY_WHILE_IDLE]).to.equal(mess.delayWhileIdle); - expect(body[Constants.PARAM_COLLAPSE_KEY]).to.equal(mess.collapseKey); - expect(body[Constants.PARAM_TIME_TO_LIVE]).to.equal(mess.timeToLive); - expect(body[Constants.PARAM_DRY_RUN]).to.equal(mess.dryRun); - expect(body[Constants.PARAM_PAYLOAD_KEY]).to.deep.equal(mess.data); - done(); - }, 10); - }); - - it('should set the registration_ids to reg tokens implicitly passed in', function (done) { - var sender = new Sender('myKey'); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, ["registration token 1", "registration token 2"], function () {}); + sender.send(mess, '', { retries: 0 }, function () {}); setTimeout(function() { var body = args.options.json; - expect(body.registration_ids).to.deep.equal(["registration token 1", "registration token 2"]); + expect(body.delay_while_idle).to.equal(mess.delay_while_idle); + expect(body.collapse_key).to.equal(mess.collapse_key); + expect(body.time_to_live).to.equal(mess.time_to_live); + expect(body.dry_run).to.equal(mess.dry_run); + expect(body.data).to.deep.equal(mess.data); done(); }, 10); }); - it('should set the registration_ids to reg tokens explicitly passed in', function (done) { - var sender = new Sender('myKey'); - var m = new Message({ data: {} }); - var regTokens = ["registration token 1", "registration token 2"]; - sender.sendNoRetry(m, { registrationIds: regTokens }, function () {}); + it('should ignore properties of body that are unknown or invalid types', function(done) { + var mess = { + delay_while_idle: "a string", + collapse_key: true, + time_to_live: 100, + dry_run: true, + data: { + name: 'Matt' + }, + unknown_property: "hello" + }; + var sender = new Sender('mykey'); + sender.send(mess, '', { retries: 0 }, function () {}); setTimeout(function() { var body = args.options.json; - expect(body.registration_ids).to.deep.equal(regTokens); + expect(body.delay_while_idle).to.equal(undefined); + expect(body.collapse_key).to.equal(undefined); + expect(body.time_to_live).to.equal(mess.time_to_live); + expect(body.dry_run).to.equal(mess.dry_run); + expect(body.data).to.deep.equal(mess.data); + expect(body.unknown_property).to.equal(undefined); done(); }, 10); }); - it('should set the registration_ids to reg tokens explicitly passed in', function (done) { + it('should set the registration_ids to reg tokens implicitly passed in', function (done) { var sender = new Sender('myKey'); - var m = new Message({ data: {} }); - var regTokens = ["registration token 1", "registration token 2"]; - sender.sendNoRetry(m, { registrationTokens: regTokens }, function () {}); + var m = { data: {} }; + sender.send(m, ["registration token 1", "registration token 2"], { retries: 0 }, function () {}); setTimeout(function() { var body = args.options.json; - expect(body.registration_ids).to.deep.equal(regTokens); + expect(body.registration_ids).to.deep.equal(["registration token 1", "registration token 2"]); done(); }, 10); }); it('should set the to field if a single reg (or other) token is passed in', function(done) { var sender = new Sender('myKey'); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, "registration token 1", function () {}); + var m = { data: {} }; + sender.send(m, "registration token 1", { retries: 0 }, function () {}); setTimeout(function() { var body = args.options.json; expect(body.to).to.deep.equal("registration token 1"); @@ -231,9 +222,9 @@ describe('UNIT Sender', function () { it('should set the to field if a single reg token is passed in as a string', function(done) { var sender = new Sender('myKey'); - var m = new Message({ data: {} }); + var m = { data: {} }; var token = "registration token 1"; - sender.sendNoRetry(m, token, function () {}); + sender.send(m, token, { retries: 0 }, function () {}); setTimeout(function() { var body = args.options.json; expect(body.to).to.deep.equal(token); @@ -242,67 +233,15 @@ describe('UNIT Sender', function () { }, 10); }) - it('should set the to field if a single reg token is passed inside the recipient array', function(done) { + it('should set the registration_id field if a single reg token is passed inside the recipient array', function(done) { var sender = new Sender('myKey'); - var m = new Message({ data: {} }); + var m = { data: {} }; var token = "registration token 1"; - sender.sendNoRetry(m, [ token ], function () {}); + sender.send(m, [ token ], { retries: 0 }, function () {}); setTimeout(function() { var body = args.options.json; - expect(body.to).to.deep.equal(token); - expect(body.registration_ids).to.be.an("undefined"); - done(); - }, 10); - }) - - it('should set the to field if a single reg token is passed inside the registrationTokens array', function(done) { - var sender = new Sender('myKey'); - var m = new Message({ data: {} }); - var token = "registration token 1"; - sender.sendNoRetry(m, { registrationTokens: token }, function () {}); - setTimeout(function() { - var body = args.options.json; - expect(body.to).to.deep.equal(token); - expect(body.registration_ids).to.be.an("undefined"); - done(); - }, 10); - }) - - it('should set the to field if a single reg token is passed inside the registrationIDs array', function(done) { - var sender = new Sender('myKey'); - var m = new Message({ data: {} }); - var token = "registration token 1"; - sender.sendNoRetry(m, { registrationIDs: token }, function () {}); - setTimeout(function() { - var body = args.options.json; - expect(body.to).to.deep.equal(token); - expect(body.registration_ids).to.be.an("undefined"); - done(); - }, 10); - }) - - it('should set the to field if a topic is passed in', function(done) { - var sender = new Sender('myKey'); - var m = new Message({ data: {} }); - var topic = '/topics/tests'; - sender.sendNoRetry(m, { topic: topic }, function () {}); - setTimeout(function() { - var body = args.options.json; - expect(body.to).to.deep.equal(topic); - expect(body.registration_ids).to.be.an("undefined"); - done(); - }, 10); - }) - - it('should set the to field if a to recipient is passed in', function(done) { - var sender = new Sender('myKey'); - var m = new Message({ data: {} }); - var token = "registration token 1"; - sender.sendNoRetry(m, { to: token }, function () {}); - setTimeout(function() { - var body = args.options.json; - expect(body.to).to.deep.equal(token); - expect(body.registration_ids).to.be.an("undefined"); + expect(body.registration_ids).to.deep.equal([ token ]); + expect(body.to).to.be.an("undefined"); done(); }, 10); }) @@ -310,117 +249,7 @@ describe('UNIT Sender', function () { it('should pass an error into callback if recipient is an empty object', function (done) { var callback = sinon.spy(); var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {}, callback); - setTimeout(function() { - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.be.a('object'); - done(); - }, 10); - }); - - it('should pass an error into callback if recipient keys are invalid', function (done) { - var callback = sinon.spy(); - var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {invalid: true}, callback); - setTimeout(function() { - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.be.a('object'); - done(); - }, 10); - }); - - it('should pass an error into callback if provided more than one recipient key', function (done) { - var callback = sinon.spy(); - var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {registrationIds: ['string'], topic: 'string'}, callback); - setTimeout(function() { - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.be.a('object'); - done(); - }, 10); - }); - - it('should pass an error into callback if registrationIds is not an array', function (done) { - var callback = sinon.spy(); - var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {registrationIds: 'string'}, callback); - setTimeout(function() { - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.be.a('object'); - done(); - }, 10); - }); - - it('should pass an error into callback if registrationTokens is not an array', function (done) { - var callback = sinon.spy(); - var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {registrationTokens: 'string'}, callback); - setTimeout(function() { - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.be.a('object'); - done(); - }, 10); - }); - - it('should pass an error into callback if to is not a string', function (done) { - var callback = sinon.spy(); - var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {to: ['array']}, callback); - setTimeout(function() { - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.be.a('object'); - done(); - }, 10); - }); - - it('should pass an error into callback if topic is not a string', function (done) { - var callback = sinon.spy(); - var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {topic: ['array']}, callback); - setTimeout(function() { - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.be.a('object'); - done(); - }, 10); - }); - - it('should pass an error into callback if notificationKey is not a string', function (done) { - var callback = sinon.spy(); - var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {notificationKey: ['array']}, callback); - setTimeout(function() { - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.be.a('object'); - done(); - }, 10); - }); - - it('should pass an error into callback if to is empty', function (done) { - var callback = sinon.spy(); - var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {to: ''}, callback); - setTimeout(function() { - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.be.a('object'); - done(); - }, 10); - }); - - it('should pass an error into callback if topic is empty', function (done) { - var callback = sinon.spy(); - var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {topic: ''}, callback); - setTimeout(function() { - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.be.a('object'); - done(); - }, 10); - }); - - it('should pass an error into callback if notificationKey is empty', function (done) { - var callback = sinon.spy(); - var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {notificationKey: ''}, callback); + sender.send({}, {}, { retries: 0 }, callback); setTimeout(function() { expect(callback.calledOnce).to.be.ok; expect(callback.args[0][0]).to.be.a('object'); @@ -431,7 +260,7 @@ describe('UNIT Sender', function () { it('should pass an error into callback if no recipient provided', function (done) { var callback = sinon.spy(); var sender = new Sender('myKey'); - sender.sendNoRetry(new Message(), {}, callback); + sender.send({}, [], { retries: 0 }, callback); setTimeout(function() { expect(callback.calledOnce).to.be.ok; expect(callback.args[0][0]).to.be.a('object'); @@ -443,8 +272,8 @@ describe('UNIT Sender', function () { var callback = sinon.spy(), sender = new Sender('myKey'); setArgs('an error', {}, {}); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', callback); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, callback); setTimeout(function() { expect(callback.calledOnce).to.be.ok; expect(callback.calledWith('an error')).to.be.ok; @@ -456,8 +285,8 @@ describe('UNIT Sender', function () { var callback = sinon.spy(), sender = new Sender('myKey'); setArgs(null, { statusCode: 500 }, {}); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', callback); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, callback); setTimeout(function() { expect(callback.calledOnce).to.be.ok; expect(callback.args[0][0]).to.equal(500); @@ -469,8 +298,8 @@ describe('UNIT Sender', function () { var callback = sinon.spy(), sender = new Sender('myKey'); setArgs(null, { statusCode: 401 }, {}); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', callback); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, callback); setTimeout(function() { expect(callback.calledOnce).to.be.ok; expect(callback.args[0][0]).to.equal(401); @@ -482,8 +311,8 @@ describe('UNIT Sender', function () { var callback = sinon.spy(), sender = new Sender('myKey'); setArgs(null, { statusCode: 400 }, {}); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', callback); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, callback); setTimeout(function() { expect(callback.calledOnce).to.be.ok; expect(callback.args[0][0]).to.equal(400); @@ -496,8 +325,8 @@ describe('UNIT Sender', function () { sender = new Sender('myKey'), parseError = {error: 'Failed to parse JSON'}; setArgs(parseError, null, null); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', callback); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, callback); setTimeout(function() { expect(callback.calledOnce).to.be.ok; expect(callback.args[0][0]).to.deep.equal(parseError); @@ -513,8 +342,8 @@ describe('UNIT Sender', function () { }; var sender = new Sender('myKey'); setArgs(null, { statusCode: 200 }, resBody); - var m = new Message({ data: {} }); - sender.sendNoRetry(m, '', callback); + var m = { data: {} }; + sender.send(m, '', { retries: 0 }, callback); setTimeout(function() { expect(callback.calledOnce).to.be.ok; expect(callback.args[0][1]).to.deep.equal(resBody); @@ -524,165 +353,114 @@ describe('UNIT Sender', function () { }); describe('send()', function () { - var restore = {}, - backoff = Constants.BACKOFF_INITIAL_DELAY; - // Set args passed into sendNoRetry - function setArgs(err, response) { + function setArgs(err, res, resBody) { args = { err: err, - response: response, - tries: 0 - }; - }; - - before( function () { - restore.sendNoRetry = Sender.prototype.sendNoRetry; - Sender.prototype.sendNoRetry = function (message, reg_tokens, callback) { - console.log('Firing send'); - args.message = message; - args.reg_tokens = reg_tokens; - args.tries++; - var nextResponse; - if(!args.response) { - nextResponse = args.response; - } - else if(args.response.length > 1) { - nextResponse = args.response.slice(0,1)[0]; - args.response = args.response.slice(1,args.response.length); - } - else if(args.response.length == 1) { - args.response = args.response[0]; - nextResponse = args.response; - } - else { - nextResponse = args.response; - } - callback( args.err, nextResponse, args.reg_tokens ); - }; - }); - - after( function () { - Sender.prototype.sendNoRetry = restore.sendNoRetry; - }); - - it('should pass reg tokens to sendNoRetry, even if it is an empty array', function (done) { - var emptyRegTokenArray = []; - var callback = function(error) { - expect(args.reg_tokens).to.equal(emptyRegTokenArray); - done(); - }; - var sender = new Sender('myKey'); - sender.send({}, emptyRegTokenArray, 0, callback); - }); - - it('should pass reg tokens to sendNoRetry, even if it is an empty object', function (done) { - var emptyRegTokenObject = {}; - var callback = function(error) { - expect(args.reg_tokens).to.equal(emptyRegTokenObject); - done(); - }; - var sender = new Sender('myKey'); - sender.send({}, emptyRegTokenObject, 0, callback); - }); - - it('should pass reg tokens to sendNoRetry, even if some keys are invalid', function (done) { - var invalidRegTokenObject = { invalid: ['regToken'] }; - var callback = function(error) { - expect(args.reg_tokens).to.equal(invalidRegTokenObject); - done(); + res: res, + resBody: resBody }; - var sender = new Sender('myKey'); - sender.send({}, invalidRegTokenObject, 0, callback); - }); - - it('should pass the message and the regToken to sendNoRetry on call', function () { - var sender = new Sender('myKey'), - message = { data: {} }, - regToken = [24]; - setArgs(null, {}); - sender.send(message, regToken, 0, function () {}); - expect(args.message).to.equal(message); - expect(args.reg_tokens).to.equal(regToken); - expect(args.tries).to.equal(1); - }); + } - it('should pass the message and the regTokens to sendNoRetry on call', function () { - var sender = new Sender('myKey'), - message = { data: {} }, - regTokens = [24, 34, 44]; - setArgs(null, {}); - sender.send(message, regTokens, 0, function () {}); - expect(args.message).to.equal(message); - expect(args.reg_tokens).to.equal(regTokens); - expect(args.tries).to.equal(1); + beforeEach(function() { + requestStub.reset(); + setArgs(null, { statusCode: 200 }, {}); }); - it('should pass the response into callback if successful for token', function () { + it('should pass the response into callback if successful for token', function (done) { var callback = sinon.spy(), - response = { success: true }, + response = { + success: 1, + failure: 0, + results: [ + { message_id: "something" } + ] + }, sender = new Sender('myKey'); - setArgs(null, response); - sender.send({}, [1], 0, callback); - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][1]).to.equal(response); - expect(args.tries).to.equal(1); + setArgs(null, { statusCode: 200 }, response); + sender.send({}, [1], callback); + setTimeout(function() { + expect(callback.calledOnce).to.be.ok; + expect(callback.args[0][1]).to.equal(response); + expect(requestStub.args.length).to.equal(1); + done(); + }, 10); }); - it('should pass the response into callback if successful for tokens', function () { + it('should pass the response into callback if successful for tokens', function (done) { var callback = sinon.spy(), - response = { success: true }, + response = { + success: 1, + failure: 0, + results: [ + { message_id: "something" } + ] + }, sender = new Sender('myKey'); - setArgs(null, response); - sender.send({}, [1, 2, 3], 0, callback); - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][1]).to.equal(response); - expect(args.tries).to.equal(1); + setArgs(null, { statusCode: 200 }, response); + sender.send({}, [1, 2, 3], callback); + setTimeout(function() { + expect(callback.calledOnce).to.be.ok; + expect(callback.args[0][1]).to.equal(response); + expect(requestStub.args.length).to.equal(1); + done(); + }, 10); }); - it('should pass the error into callback if failure and no retry for token', function () { + it('should pass the error into callback if failure for token', function (done) { var callback = sinon.spy(), error = 'my error', sender = new Sender('myKey'); setArgs(error); sender.send({}, [1], 0, callback); - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.equal(error); - expect(args.tries).to.equal(1); + setTimeout(function() { + expect(callback.calledOnce).to.be.ok; + expect(callback.args[0][0]).to.equal(error); + expect(requestStub.args.length).to.equal(1); + done(); + }, 10); }); - it('should pass the error into callback if failure and no retry for tokens', function () { + it('should pass the error into callback if failure for tokens', function (done) { var callback = sinon.spy(), error = 'my error', sender = new Sender('myKey'); setArgs(error); sender.send({}, [1, 2, 3], 0, callback); - expect(callback.calledOnce).to.be.ok; - expect(callback.args[0][0]).to.equal(error); - expect(args.tries).to.equal(1); + setTimeout(function() { + expect(callback.calledOnce).to.be.ok; + expect(callback.args[0][0]).to.equal(error); + expect(requestStub.args.length).to.equal(1); + done(); + }, 10); }); it('should retry number of times passed into call and do exponential backoff', function (done) { var start = new Date(); var callback = function () { - expect(args.tries).to.equal(2); - expect(new Date() - start).to.be.gte(Math.pow(2, 0) * backoff); + expect(requestStub.args.length).to.equal(2); + expect(new Date() - start).to.be.gte(200); done(); }; var sender = new Sender('myKey'); - setArgs('my error'); - sender.send({ data: {}}, [1], 1, callback); + setArgs(null, { statusCode: 500 }); + sender.send({ data: {}}, [1], { + retries: 1, + backoff: 200 + }, callback); }); it('should retry if not all regTokens were successfully sent', function (done) { var callback = function () { - expect(args.tries).to.equal(3); - // Last call of sendNoRetry should be for only failed regTokens - expect(args.reg_tokens.length).to.equal(1); - expect(args.reg_tokens[0]).to.equal(3); + // These expect calls don't get reported to the test runner, they just result + // in a timeout. + expect(requestStub.args.length).to.equal(3); + var requestOptions = requestStub.args[2][0]; + expect(requestOptions.json.registration_ids.length).to.equal(1); + expect(requestOptions.json.registration_ids[0]).to.equal(3); done(); }; var sender = new Sender('myKey'); - setArgs(null, [{ results: [{}, { error: 'Unavailable' }, { error: 'Unavailable' }]}, { results: [ {}, { error: 'Unavailable' } ] }, { results: [ {} ] } ]); + setArgs(null, { statusCode: 200}, [{ results: [{}, { error: 'Unavailable' }, { error: 'Unavailable' }]}, { results: [ {}, { error: 'Unavailable' } ] }, { results: [ {} ] } ]); sender.send({ data: {}}, [1,2,3], { retries: 5, backoff: 100 @@ -692,8 +470,9 @@ describe('UNIT Sender', function () { it('should retry all regTokens in event of an error', function (done) { var start = new Date(); var callback = function () { - expect(args.tries).to.equal(2); - expect(args.reg_tokens.length).to.equal(3); + expect(requestStub.args.length).to.equal(2); + var requestOptions = requestStub.args[1][0]; + expect(requestOptions.json.registration_ids.length).to.equal(3); done(); }; var sender = new Sender('myKey'); @@ -710,7 +489,7 @@ describe('UNIT Sender', function () { done(); }; var sender = new Sender('myKey'); - setArgs(null, [ + setArgs(null, { statusCode: 200 }, [ { success: 1, failure: 2, canonical_ids: 0, results: [ {}, { error: 'Unavailable' }, { error: 'Unavailable' } ] }, { success: 1, canonical_ids: 1, failure: 0, results: [ {}, {} ] } ]); @@ -726,15 +505,158 @@ describe('UNIT Sender', function () { done(); }; var sender = new Sender('myKey'); - setArgs(null, [ + setArgs(null, { statusCode: 200 }, [ { success: 0, failure: 3, canonical_ids: 0, results: [ { error: 'Unavailable' }, { error: 'Unavailable' }, { error: 'Unavailable' } ] }, { success: 1, canonical_ids: 0, failure: 2, results: [ { error: 'Unavailable' }, { error: 'Unavailable' }, {} ] }, { success: 0, canonical_ids: 0, failure: 2, results: [ { error: 'Unavailable' }, { error: 'Unavailable' } ] } ]); sender.send({ data: {}}, [1,2,3], { - retries: 3, + retries: 2, backoff: 100 }, callback); }); }); + + describe('send() with promise', function () { + function setArgs(err, res, resBody) { + args = { + err: err, + res: res, + resBody: resBody + }; + } + + beforeEach(function() { + requestStub.reset(); + setArgs(null, { statusCode: 200 }, {}); + }); + + it('should return the response in the promise if successful for token', function () { + var response = { + success: 1, + failure: 0, + results: [ + { message_id: "something" } + ] + }, + sender = new Sender('myKey'); + setArgs(null, { statusCode: 200 }, response); + return sender.send({}, [1]).then(function(output) { + expect(output).to.equal(response); + expect(requestStub.args.length).to.equal(1); + }); + }); + + it('should return the response in the promise if successful for tokens', function () { + var response = { + success: 1, + failure: 0, + results: [ + { message_id: "something" } + ] + }, + sender = new Sender('myKey'); + setArgs(null, { statusCode: 200 }, response); + return sender.send({}, [1, 2, 3]).then(function(output) { + expect(output).to.equal(response); + expect(requestStub.args.length).to.equal(1); + }); + }); + + it('should reject the promise with the appropriate error if failure for token', function () { + var error = 'my error', + sender = new Sender('myKey'); + setArgs(error); + sender.send({}, [1], 0).then(function() { + chai.assert.fail(); + }).catch(function(err) { + expect(err).to.equal(error); + expect(requestStub.args.length).to.equal(1); + }); + }); + + it('should reject the promise with the appropriate error if failure for tokens', function () { + var error = 'my error', + sender = new Sender('myKey'); + setArgs(error); + return sender.send({}, [1, 2, 3], 0).then(function() { + chai.assert.fail(); + }).catch(function(err) { + expect(err).to.equal(error); + expect(requestStub.args.length).to.equal(1); + }); + }); + + it('should retry number of times passed into call and do exponential backoff', function () { + var start = new Date(); + var sender = new Sender('myKey'); + setArgs(null, { statusCode: 500 }); + return sender.send({ data: {}}, [1], { + retries: 1, + backoff: 200 + }).catch(function() {}).then(function() { + expect(requestStub.args.length).to.equal(2); + expect(new Date() - start).to.be.gte(200); + }); + }); + + it('should retry if not all regTokens were successfully sent', function () { + var sender = new Sender('myKey'); + setArgs( + null, + { statusCode: 200}, + [ + { results: [{}, { error: 'Unavailable' }, + { error: 'Unavailable' }]}, + { results: [ {}, { error: 'Unavailable' } ] }, + { results: [ {} ] } + ]); + return sender.send({data: {}}, [1,2,3], { + retries: 5, + backoff: 100 + }).catch(function() {}).then(function() { + expect(requestStub.args.length).to.equal(3); + var requestOptions = requestStub.args[2][0]; + expect(requestOptions.json.registration_ids.length).to.equal(1); + expect(requestOptions.json.registration_ids[0]).to.equal(3); + }); + }); + + it('should retry all regTokens in event of an error', function () { + var sender = new Sender('myKey'); + setArgs('my error'); + return sender.send({ data: {}}, [1,2,3], 1).catch(function() {}).then(function() { + expect(requestStub.args.length).to.equal(2); + var requestOptions = requestStub.args[1][0]; + expect(requestOptions.json.registration_ids.length).to.equal(3); + }); + }); + + it('should update the failures and successes correctly when retrying', function () { + var sender = new Sender('myKey'); + setArgs(null, { statusCode: 200 }, [ + { success: 1, failure: 2, canonical_ids: 0, results: [ {}, { error: 'Unavailable' }, { error: 'Unavailable' } ] }, + { success: 1, canonical_ids: 1, failure: 0, results: [ {}, {} ] } + ]); + return sender.send({ data: {}}, [1,2,3], 3).then(function(response) { + expect(response.canonical_ids).to.equal(1); + expect(response.success).to.equal(2); + expect(response.failure).to.equal(0); + }); + }); + + it('should update the failures and successes correctly when retrying and failing some', function () { + var sender = new Sender('myKey'); + setArgs(null, { statusCode: 200 }, [ + { success: 0, failure: 3, canonical_ids: 0, results: [ { error: 'Unavailable' }, { error: 'Unavailable' }, { error: 'Unavailable' } ] }, + { success: 1, canonical_ids: 0, failure: 2, results: [ { error: 'Unavailable' }, { error: 'Unavailable' }, {} ] }, + { success: 0, canonical_ids: 0, failure: 2, results: [ { error: 'Unavailable' }, { error: 'Unavailable' } ] } + ]); + return sender.send({ data: {}}, [1,2,3], {retries: 2, backoff: 100}).then(function(response) { + expect(response.canonical_ids).to.equal(0); + expect(response.success).to.equal(1); + expect(response.failure).to.equal(2); + }); + }); + }); });