From 78bc2f280488edde9a9b9fd818173128c9abf58c Mon Sep 17 00:00:00 2001 From: Schwobaland Date: Thu, 2 Feb 2017 15:26:46 +0100 Subject: [PATCH 1/7] update to apn version 2 --- package.json | 4 +- spec/APNS.spec.js | 372 +++++++++-------------------- spec/ParsePushAdapter.spec.js | 30 +-- src/APNS.js | 436 ++++++++++++++++------------------ 4 files changed, 342 insertions(+), 500 deletions(-) diff --git a/package.json b/package.json index 11f17375..3e85e5c4 100644 --- a/package.json +++ b/package.json @@ -33,10 +33,10 @@ "nyc": "^10.1.2" }, "dependencies": { - "apn": "^1.7.8", + "apn": "^2.1.2", "node-gcm": "^0.14.0", "npmlog": "^2.0.3", - "parse": "^1.8.1" + "parse": "^1.9.2" }, "engines": { "node": ">= 4.6.0" diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index c2da41a8..6520a410 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -1,23 +1,23 @@ -var APNS = require('../src/APNS').default; -var Parse = require('parse/node'); +const Parse = require('parse/node'); +const APNS = require('../src/APNS').default; describe('APNS', () => { - it('can initialize with single cert', (done) => { - var args = { - cert: 'prodCert.pem', - key: 'prodKey.pem', + it('can initialize with cert', (done) => { + let args = { + cert: new Buffer('testCert'), + key: new Buffer('testKey'), production: true, - bundleId: 'bundleId' + topic: 'topic' } - var apns = new APNS(args); + let apns = new APNS(args); - expect(apns.conns.length).toBe(1); - var apnsConnection = apns.conns[0]; - expect(apnsConnection.index).toBe(0); - expect(apnsConnection.bundleId).toBe(args.bundleId); + expect(apns.providers.length).toBe(1); + let apnsProvider = apns.providers[0]; + expect(apnsProvider.index).toBe(0); + expect(apnsProvider.topic).toBe(args.topic); // TODO: Remove this checking onec we inject APNS - var prodApnsOptions = apnsConnection.options; + let prodApnsOptions = apnsProvider.client.config; expect(prodApnsOptions.cert).toBe(args.cert); expect(prodApnsOptions.key).toBe(args.key); expect(prodApnsOptions.production).toBe(args.production); @@ -46,7 +46,7 @@ describe('APNS', () => { fail('should not be reached'); }); - it('fails to initialize without a bundleID', (done) => { + xit('fails to initialize without a bundleID', (done) => { const apnsArgs = { production: true, bundle: 'hello' @@ -101,7 +101,7 @@ describe('APNS', () => { it('can generate APNS notification', (done) => { //Mock request data - var data = { + let data = { 'alert': 'alert', 'badge': 100, 'sound': 'test', @@ -111,335 +111,191 @@ describe('APNS', () => { 'key': 'value', 'keyAgain': 'valueAgain' }; - var expirationTime = 1454571491354 + let expirationTime = 1454571491354 - var notification = APNS.generateNotification(data, expirationTime); + let notification = APNS.prototype._generateNotification(data, expirationTime); - expect(notification.alert).toEqual(data.alert); - expect(notification.badge).toEqual(data.badge); - expect(notification.sound).toEqual(data.sound); - expect(notification.contentAvailable).toEqual(1); - expect(notification.mutableContent).toEqual(1); - expect(notification.category).toEqual(data.category); + expect(notification.aps.alert).toEqual(data.alert); + expect(notification.aps.badge).toEqual(data.badge); + expect(notification.aps.sound).toEqual(data.sound); + expect(notification.aps['content-available']).toEqual(1); + expect(notification.aps['mutable-content']).toEqual(1); + expect(notification.aps.category).toEqual(data.category); expect(notification.payload).toEqual({ 'key': 'value', 'keyAgain': 'valueAgain' }); - expect(notification.expiry).toEqual(expirationTime/1000); - done(); - }); - - it('can choose conns for device without appIdentifier', (done) => { - // Mock conns - var conns = [ - { - bundleId: 'bundleId' - }, - { - bundleId: 'bundleIdAgain' - } - ]; - // Mock device - var device = {}; - - var qualifiedConns = APNS.chooseConns(conns, device); - expect(qualifiedConns).toEqual([0, 1]); - done(); - }); - - it('can choose conns for device with valid appIdentifier', (done) => { - // Mock conns - var conns = [ - { - bundleId: 'bundleId' - }, - { - bundleId: 'bundleIdAgain' - } - ]; - // Mock device - var device = { - appIdentifier: 'bundleId' - }; - - var qualifiedConns = APNS.chooseConns(conns, device); - expect(qualifiedConns).toEqual([0]); + expect(notification.expiry).toEqual(expirationTime / 1000); done(); }); - it('can choose conns for device with invalid appIdentifier', (done) => { - // Mock conns - var conns = [ + it('can choose providers for device with valid appIdentifier', (done) => { + let appIdentifier = 'topic'; + // Mock providers + let providers = [ { - bundleId: 'bundleId' + topic: appIdentifier }, { - bundleId: 'bundleIdAgain' + topic: 'topicAgain' } ]; - // Mock device - var device = { - appIdentifier: 'invalid' - }; - - var qualifiedConns = APNS.chooseConns(conns, device); - expect(qualifiedConns).toEqual([]); - done(); - }); - - it('can handle transmission error when notification is not in cache or device is missing', (done) => { - // Mock conns - var conns = []; - var errorCode = 1; - var notification = undefined; - var device = {}; - - APNS.handleTransmissionError(conns, errorCode, notification, device); - - var notification = {}; - var device = undefined; - - APNS.handleTransmissionError(conns, errorCode, notification, device); - done(); - }); - - it('can handle transmission error when there are other qualified conns', (done) => { - // Mock conns - var conns = [ - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId1' - }, - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId1' - }, - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId2' - }, - ]; - var errorCode = 1; - var notification = {}; - var apnDevice = { - connIndex: 0, - appIdentifier: 'bundleId1' - }; - - APNS.handleTransmissionError(conns, errorCode, notification, apnDevice); - expect(conns[0].pushNotification).not.toHaveBeenCalled(); - expect(conns[1].pushNotification).toHaveBeenCalled(); - expect(conns[2].pushNotification).not.toHaveBeenCalled(); + let qualifiedProviders = APNS.prototype._chooseProviders.call({ providers: providers }, appIdentifier); + expect(qualifiedProviders).toEqual([{ + topic: 'topic' + }]); done(); }); - it('can handle transmission error when there is no other qualified conns', (done) => { - // Mock conns - var conns = [ - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId1' - }, - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId1' - }, + it('can choose providers for device with invalid appIdentifier', (done) => { + let appIdentifier = 'invalid'; + // Mock providers + let providers = [ { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId1' + topic: 'bundleId' }, { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId2' - }, - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId1' + topic: 'bundleIdAgain' } ]; - var errorCode = 1; - var notification = {}; - var apnDevice = { - connIndex: 2, - appIdentifier: 'bundleId1' - }; - - APNS.handleTransmissionError(conns, errorCode, notification, apnDevice); - - expect(conns[0].pushNotification).not.toHaveBeenCalled(); - expect(conns[1].pushNotification).not.toHaveBeenCalled(); - expect(conns[2].pushNotification).not.toHaveBeenCalled(); - expect(conns[3].pushNotification).not.toHaveBeenCalled(); - expect(conns[4].pushNotification).toHaveBeenCalled(); - done(); - }); - - it('can handle transmission error when device has no appIdentifier', (done) => { - // Mock conns - var conns = [ - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId1' - }, - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId2' - }, - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId3' - }, - ]; - var errorCode = 1; - var notification = {}; - var apnDevice = { - connIndex: 1, - }; - - APNS.handleTransmissionError(conns, errorCode, notification, apnDevice); - expect(conns[0].pushNotification).not.toHaveBeenCalled(); - expect(conns[1].pushNotification).not.toHaveBeenCalled(); - expect(conns[2].pushNotification).toHaveBeenCalled(); + let qualifiedProviders = APNS.prototype._chooseProviders.call({ providers: providers }, appIdentifier); + expect(qualifiedProviders).toEqual([]); done(); }); it('can send APNS notification', (done) => { - var args = { - cert: 'prodCert.pem', - key: 'prodKey.pem', + let args = { + cert: new Buffer('testCert'), + key: new Buffer('testKey'), production: true, - bundleId: 'bundleId' + topic: 'topic' } - var apns = new APNS(args); - var conn = { - pushNotification: jasmine.createSpy('send'), - bundleId: 'bundleId' - }; - apns.conns = [ conn ]; + let apns = new APNS(args); + let provider = apns.providers[0]; + spyOn(provider, 'send').and.callFake((notification, devices) => { + return Promise.resolve({ + sent: devices, + failed: [] + }) + }); // Mock data - var expirationTime = 1454571491354 - var data = { + let expirationTime = 1454571491354 + let data = { 'expiration_time': expirationTime, 'data': { 'alert': 'alert' } } // Mock devices - var devices = [ + let mockedDevices = [ { deviceToken: '112233', - appIdentifier: 'bundleId' + appIdentifier: 'topic' }, { deviceToken: '112234', - appIdentifier: 'bundleId' + appIdentifier: 'topic' }, { deviceToken: '112235', - appIdentifier: 'bundleId' + appIdentifier: 'topic' }, { deviceToken: '112236', - appIdentifier: 'bundleId' + appIdentifier: 'topic' } ]; - - var promise = apns.send(data, devices); - expect(conn.pushNotification).toHaveBeenCalled(); - var args = conn.pushNotification.calls.first().args; - var notification = args[0]; - expect(notification.alert).toEqual(data.data.alert); - expect(notification.expiry).toEqual(data['expiration_time']/1000); - var apnDevices = args[1]; - apnDevices.forEach((apnDevice) => { - expect(apnDevice.connIndex).toEqual(0); - expect(apnDevice.appIdentifier).toEqual('bundleId'); - }) + let promise = apns.send(data, mockedDevices); + expect(provider.send).toHaveBeenCalled(); + let calledArgs = provider.send.calls.first().args; + let notification = calledArgs[0]; + expect(notification.aps.alert).toEqual(data.data.alert); + expect(notification.expiry).toEqual(data['expiration_time'] / 1000); + let apnDevices = calledArgs[1]; + expect(apnDevices.length).toEqual(4); done(); }); it('can send APNS notification to multiple bundles', (done) => { - var args = [{ - cert: 'prodCert.pem', - key: 'prodKey.pem', + let args = [{ + cert: new Buffer('testCert'), + key: new Buffer('testKey'), production: true, - bundleId: 'bundleId' - },{ - cert: 'devCert.pem', - key: 'devKey.pem', + topic: 'topic' + }, { + cert: new Buffer('testCert'), + key: new Buffer('testKey'), production: false, - bundleId: 'bundleId.dev' + topic: 'topic.dev' }]; - var apns = new APNS(args); - var conn = { - pushNotification: jasmine.createSpy('send'), - bundleId: 'bundleId' - }; - var conndev = { - pushNotification: jasmine.createSpy('send'), - bundleId: 'bundleId.dev' - }; - apns.conns = [ conn, conndev ]; + let apns = new APNS(args); + let provider = apns.providers[0]; + spyOn(provider, 'send').and.callFake((notification, devices) => { + return Promise.resolve({ + sent: devices, + failed: [] + }) + }); + let providerDev = apns.providers[1]; + spyOn(providerDev, 'send').and.callFake((notification, devices) => { + return Promise.resolve({ + sent: devices, + failed: [] + }) + }); + apns.providers = [provider, providerDev]; // Mock data - var expirationTime = 1454571491354 - var data = { + let expirationTime = 1454571491354 + let data = { 'expiration_time': expirationTime, 'data': { 'alert': 'alert' } } // Mock devices - var devices = [ + let mockedDevices = [ { deviceToken: '112233', - appIdentifier: 'bundleId' + appIdentifier: 'topic' }, { deviceToken: '112234', - appIdentifier: 'bundleId' + appIdentifier: 'topic' }, { deviceToken: '112235', - appIdentifier: 'bundleId' + appIdentifier: 'topic' }, { deviceToken: '112235', - appIdentifier: 'bundleId.dev' + appIdentifier: 'topic.dev' }, { deviceToken: '112236', - appIdentifier: 'bundleId.dev' + appIdentifier: 'topic.dev' } ]; - var promise = apns.send(data, devices); + let promise = apns.send(data, mockedDevices); - expect(conn.pushNotification).toHaveBeenCalled(); - var args = conn.pushNotification.calls.first().args; - var notification = args[0]; - expect(notification.alert).toEqual(data.data.alert); - expect(notification.expiry).toEqual(data['expiration_time']/1000); - var apnDevices = args[1]; + expect(provider.send).toHaveBeenCalled(); + let calledArgs = provider.send.calls.first().args; + let notification = calledArgs[0]; + expect(notification.aps.alert).toEqual(data.data.alert); + expect(notification.expiry).toEqual(data['expiration_time'] / 1000); + let apnDevices = calledArgs[1]; expect(apnDevices.length).toBe(3); - apnDevices.forEach((apnDevice) => { - expect(apnDevice.connIndex).toEqual(0); - expect(apnDevice.appIdentifier).toEqual('bundleId'); - }) - expect(conndev.pushNotification).toHaveBeenCalled(); - args = conndev.pushNotification.calls.first().args; - notification = args[0]; - expect(notification.alert).toEqual(data.data.alert); - expect(notification.expiry).toEqual(data['expiration_time']/1000); - apnDevices = args[1]; + expect(providerDev.send).toHaveBeenCalled(); + calledArgs = providerDev.send.calls.first().args; + notification = calledArgs[0]; + expect(notification.aps.alert).toEqual(data.data.alert); + expect(notification.expiry).toEqual(data['expiration_time'] / 1000); + apnDevices = calledArgs[1]; expect(apnDevices.length).toBe(2); - apnDevices.forEach((apnDevice) => { - expect(apnDevice.connIndex).toEqual(1); - expect(apnDevice.appIdentifier).toEqual('bundleId.dev'); - }); done(); }); diff --git a/spec/ParsePushAdapter.spec.js b/spec/ParsePushAdapter.spec.js index 5b8e0fbb..cb62bf9c 100644 --- a/spec/ParsePushAdapter.spec.js +++ b/spec/ParsePushAdapter.spec.js @@ -32,16 +32,16 @@ describe('ParsePushAdapter', () => { }, ios: [ { - cert: 'prodCert.pem', - key: 'prodKey.pem', + cert: new Buffer('testCert'), + key: new Buffer('testKey'), production: true, - bundleId: 'bundleId' + topic: 'topic' }, { - cert: 'devCert.pem', - key: 'devKey.pem', + cert: new Buffer('testCert'), + key: new Buffer('testKey'), production: false, - bundleId: 'bundleIdAgain' + topic: 'topicAgain' } ] }; @@ -65,7 +65,7 @@ describe('ParsePushAdapter', () => { } }; - expect(function() { + expect(function () { new ParsePushAdapter(pushConfig); }).toThrow(); done(); @@ -267,7 +267,7 @@ describe('ParsePushAdapter', () => { done(); }); - it('reports properly results', (done) => { + it('reports properly results', (done) => { var pushConfig = { android: { senderId: 'senderId', @@ -275,10 +275,11 @@ describe('ParsePushAdapter', () => { }, ios: [ { - cert: 'cert.cer', - key: 'key.pem', + cert: new Buffer('testCert'), + key: new Buffer('testKey'), production: false, - bundleId: 'iosbundleId' + bundleId: 'iosbundleId', + topic: 'topic' } ], osx: [ @@ -286,7 +287,8 @@ describe('ParsePushAdapter', () => { cert: 'cert.cer', key: 'key.pem', production: false, - bundleId: 'osxbundleId' + bundleId: 'osxbundleId', + topic: 'topic2' } ] }; @@ -326,7 +328,7 @@ describe('ParsePushAdapter', () => { ]; var parsePushAdapter = new ParsePushAdapter(pushConfig); - parsePushAdapter.send({data: {alert: 'some'}}, installations).then((results) => { + parsePushAdapter.send({ data: { alert: 'some' } }, installations).then((results) => { expect(Array.isArray(results)).toBe(true); // 2x iOS, 1x android, 1x osx, 1x tvos @@ -347,7 +349,7 @@ describe('ParsePushAdapter', () => { } }) done(); - }).catch((err) => { + }).catch((err) => { fail('Should not fail'); done(); }) diff --git a/src/APNS.js b/src/APNS.js index eb42dbec..ac0d2862 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -1,262 +1,246 @@ -"use strict"; - -// TODO: apn does not support the new HTTP/2 protocal. It is fine to use it in V1, -// but probably we will replace it in the future. +'use strict'; import apn from 'apn'; import Parse from 'parse'; import log from 'npmlog'; const LOG_PREFIX = 'parse-server-push-adapter APNS'; -/** - * Create a new connection to the APN service. - * @constructor - * @param {Object|Array} args An argument or a list of arguments to config APNS connection - * @param {String} args.cert The filename of the connection certificate to load from disk - * @param {String} args.key The filename of the connection key to load from disk - * @param {String} args.pfx The filename for private key, certificate and CA certs in PFX or PKCS12 format, it will overwrite cert and key - * @param {String} args.passphrase The passphrase for the connection key, if required - * @param {String} args.bundleId The bundleId for cert - * @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox - */ -export default function APNS(args) { - // typePushConfig can be an array. - let apnsArgsList = []; - if (Array.isArray(args)) { - apnsArgsList = apnsArgsList.concat(args); - } else if (typeof args === 'object') { - apnsArgsList.push(args); - } else { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'APNS Configuration is invalid'); - } - - this.conns = []; - for (let apnsArgs of apnsArgsList) { - let conn = new apn.Connection(apnsArgs); - if (!apnsArgs.bundleId) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'BundleId is mssing for %j', apnsArgs); - } - conn.bundleId = apnsArgs.bundleId; - // Set the priority of the conns, prod cert has higher priority - if (apnsArgs.production) { - conn.priority = 0; +export class APNS { + + /** + * Create a new provider for the APN service. + * @constructor + * @param {Object|Array} args An argument or a list of arguments to config APNS provider + * @param {Object} args.token {Object} Configuration for Provider Authentication Tokens. (Defaults to: null i.e. fallback to Certificates) + * @param {Buffer|String} args.token.key The filename of the provider token key (as supplied by Apple) to load from disk, or a Buffer/String containing the key data. + * @param {String} args.token.keyId The ID of the key issued by Apple + * @param {String} args.token.teamId ID of the team associated with the provider token key + * @param {Buffer|String} args.cert The filename of the connection certificate to load from disk, or a Buffer/String containing the certificate data. + * @param {Buffer|String} args.key {Buffer|String} The filename of the connection key to load from disk, or a Buffer/String containing the key data. + * @param {Buffer|String} args.pfx path for private key, certificate and CA certs in PFX or PKCS12 format, or a Buffer containing the PFX data. If supplied will always be used instead of certificate and key above. + * @param {String} args.passphrase The passphrase for the provider key, if required + * @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox + * @param {String} args.topic Specififies an App-Id for this Provider + * @param {Number} args.connectionRetryLimit The maximum number of connection failures that will be tolerated before apn.Provider will "give up". (Defaults to: 3) + */ + constructor(args = []) { + // Define class members + this.providers = []; + + // Since for ios, there maybe multiple cert/key pairs, typePushConfig can be an array. + let apnsArgsList = []; + if (Array.isArray(args)) { + apnsArgsList = apnsArgsList.concat(args); + } else if (typeof args === 'object') { + apnsArgsList.push(args); } else { - conn.priority = 1; + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'APNS Configuration is invalid'); } - // Set apns client callbacks - /* istanbul ignore next */ - conn.on('connected', () => { - log.verbose(LOG_PREFIX, 'APNS Connection %d Connected', conn.index); - }); + // Create Provider from each arg-object + for (let apnsArgs of apnsArgsList) { - conn.on('transmissionError', (errCode, notification, apnDevice) => { - handleTransmissionError(this.conns, errCode, notification, apnDevice); - }); - /* istanbul ignore next */ - conn.on('timeout', () => { - log.verbose(LOG_PREFIX, 'APNS Connection %d Timeout', conn.index); - }); + // rewrite bundleId to topic for backward-compatibility + if (apnsArgs.bundleId) { + apnsArgs.topic = apnsArgs.bundleId + } - /* istanbul ignore next */ - conn.on('disconnected', () => { - log.verbose(LOG_PREFIX, 'APNS Connection %d Disconnected', conn.index); - }); + let provider = this._createProvider(apnsArgs); + this.providers.push(provider); + } - /* istanbul ignore next */ - conn.on('socketError', () => { - log.verbose(LOG_PREFIX, 'APNS Connection %d Socket Error', conn.index); + // Sort the providers based on priority ascending, high pri first + this.providers.sort((s1, s2) => { + return s1.priority - s2.priority; }); - conn.on('transmitted', function(notification, device) { - device.callback({ - notification: notification, - transmitted: true, - device: { - deviceType: device.deviceType, - deviceToken: device.token.toString('hex') - } - }); - log.verbose(LOG_PREFIX, 'APNS Connection %d Notification transmitted to %s', conn.index, device.token.toString('hex')); + // Set index-property of providers + for (let index = 0; index < this.providers.length; index++) { + this.providers[index].index = index; + } + } + + /** + * Send apns request. + * + * @param {Object} data The data we need to send, the format is the same with api request body + * @param {Array} devices An array of devices + * @returns {Object} A promise which is resolved immediately + */ + send(data, allDevices) { + let coreData = data.data; + let expirationTime = data['expiration_time']; + let allPromises = []; + + let devicesPerAppIdentifier = {}; + + // Start by clustering the devices per appIdentifier + allDevices.forEach(device => { + let appIdentifier = device.appIdentifier; + devicesPerAppIdentifier[appIdentifier] = devicesPerAppIdentifier[appIdentifier] || []; + devicesPerAppIdentifier[appIdentifier].push(device); }); - this.conns.push(conn); - } - // Sort the conn based on priority ascending, high pri first - this.conns.sort((s1, s2) => { - return s1.priority - s2.priority; - }); - // Set index of conns - for (let index = 0; index < this.conns.length; index++) { - this.conns[index].index = index; - } -} + for (let key in devicesPerAppIdentifier) { + let devices = devicesPerAppIdentifier[key]; + let appIdentifier = devices[0].appIdentifier; + let providers = this._chooseProviders(appIdentifier); -/** - * Send apns request. - * @param {Object} data The data we need to send, the format is the same with api request body - * @param {Array} devices A array of devices - * @returns {Object} A promise which is resolved immediately - */ -APNS.prototype.send = function(data, devices) { - let coreData = data.data; - let expirationTime = data['expiration_time']; - let notification = generateNotification(coreData, expirationTime); - let allPromises = []; - let devicesPerConnIndex = {}; - // Start by clustering the devices per connections - devices.forEach((device) => { - let qualifiedConnIndexs = chooseConns(this.conns, device); - if (qualifiedConnIndexs.length == 0) { - log.error(LOG_PREFIX, 'no qualified connections for %s %s', device.appIdentifier, device.deviceToken); - let promise = Promise.resolve({ - transmitted: false, - device: { - deviceToken: device.deviceToken, - deviceType: device.deviceType - }, - result: {error: 'No connection available'} - }); - allPromises.push(promise); - } else { - let apnDevice = new apn.Device(device.deviceToken); - apnDevice.deviceType = device.deviceType; - apnDevice.connIndex = qualifiedConnIndexs[0]; - /* istanbul ignore else */ - if (device.appIdentifier) { - apnDevice.appIdentifier = device.appIdentifier; + // No Providers found + if (!providers || providers.length === 0) { + let errorPromises = devices.map(device => this._createErrorPromise(device.deviceToken, 'No Provider found')); + allPromises = allPromises.concat(errorPromises); + continue; } - devicesPerConnIndex[apnDevice.connIndex] = devicesPerConnIndex[apnDevice.connIndex] || []; - devicesPerConnIndex[apnDevice.connIndex].push(apnDevice); + + let notification = this._generateNotification(coreData, expirationTime, appIdentifier); + let promise = providers[0] + .send(notification, devices.map(device => device.deviceToken)) + .then(this._handlePromise.bind(this)); + allPromises.push(promise); + }; + + return Promise.all(allPromises); + } + + /** + * Creates an Provider base on apnsArgs. + */ + _createProvider(apnsArgs) { + let provider = new apn.Provider(apnsArgs); + + // if using certificate, then topic must be defined + if ((apnsArgs.cert || apnsArgs.key || apnsArgs.pfx) && !apnsArgs.topic) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'topic is mssing for %j', apnsArgs); } - }) - allPromises = Object.keys(devicesPerConnIndex).reduce((memo, connIndex) => { - let devices = devicesPerConnIndex[connIndex]; - // Create a promise, attach the callback - let promises = devices.map((apnDevice) => { - return new Promise((resolve, reject) => { - apnDevice.callback = resolve; - }); - }); - let conn = this.conns[connIndex]; - conn.pushNotification(notification, devices); - return memo.concat(promises); - }, allPromises); + // Sets the topic on this provider + provider.topic = apnsArgs.topic; - return Promise.all(allPromises); -} + // Set the priority of the providers, prod cert has higher priority + if (apnsArgs.production) { + provider.priority = 0; + } else { + provider.priority = 1; + } -function handleTransmissionError(conns, errCode, notification, apnDevice) { - // This means the error notification is not in the cache anymore or the recepient is missing, - // we just ignore this case - if (!notification || !apnDevice) { - return + return provider; } - // If currentConn can not send the push notification, we try to use the next available conn. - // Since conns is sorted by priority, the next conn means the next low pri conn. - // If there is no conn available, we give up on sending the notification to that device. - let qualifiedConnIndexs = chooseConns(conns, apnDevice); - let currentConnIndex = apnDevice.connIndex; + /** + * Generate the apns Notification from the data we get from api request. + * @param {Object} coreData The data field under api request body + * @param {number} expirationTime The expiration time in milliseconds since Jan 1 1970 + * @param {String} topic Topic the Notification is sent to + * @returns {Object} A apns Notification + */ + _generateNotification(coreData, expirationTime, topic) { + let notification = new apn.Notification(); + let payload = {}; + for (let key in coreData) { + switch (key) { + case 'alert': + notification.setAlert(coreData.alert); + break; + case 'badge': + notification.setBadge(coreData.badge); + break; + case 'sound': + notification.setSound(coreData.sound); + break; + case 'content-available': + let isAvailable = coreData['content-available'] === 1; + notification.setContentAvailable(isAvailable); + break; + case 'mutable-content': + let isMutable = coreData['mutable-content'] === 1; + notification.setMutableContent(isMutable); + break; + case 'category': + notification.setCategory(coreData.category); + break; + default: + payload[key] = coreData[key]; + break; + } + } + notification.topic = topic; + notification.payload = payload; + notification.expiry = expirationTime / 1000; + return notification; + } - let newConnIndex = -1; - // Find the next element of currentConnIndex in qualifiedConnIndexs - for (let index = 0; index < qualifiedConnIndexs.length - 1; index++) { - if (qualifiedConnIndexs[index] === currentConnIndex) { - newConnIndex = qualifiedConnIndexs[index + 1]; - break; + /** + * Choose appropriate providers based on device appIdentifier. + * + * @param {String} appIdentifier appIdentifier for required provider + * @returns Returns Array with appropriate providers + */ + _chooseProviders(appIdentifier) { + // If the device we need to send to does not have appIdentifier, any provider could be a qualified provider + /*if (!appIdentifier || appIdentifier === '') { + return this.providers.map((provider) => provider.index); + }*/ + + // Otherwise we try to match the appIdentifier with topic on provider + let qualifiedProviders = this.providers.filter((provider) => appIdentifier === provider.topic); + + if (qualifiedProviders.length > 0) { + return qualifiedProviders; } + + // If qualifiedProviders empty, add all providers without topic + return this.providers + .filter((provider) => !provider.topic || provider.topic === ''); } - // There is no more available conns, we give up in this case - if (newConnIndex < 0 || newConnIndex >= conns.length) { - log.error(LOG_PREFIX, `cannot find vaild connection for ${apnDevice.token.toString('hex')}`); - apnDevice.callback({ - response: {error: `APNS can not find vaild connection for ${apnDevice.token.toString('hex')}`, code: errCode}, - status: errCode, - transmitted: false, - device: { - deviceType: apnDevice.deviceType, - deviceToken: apnDevice.token.toString('hex') + + _handlePromise(response) { + response.sent.forEach((token) => { + log.verbose(LOG_PREFIX, 'APNS transmitted to %s', token); + return this._createSuccesfullPromise(token); + }); + response.failed.forEach((failure) => { + if (failure.error) { + log.error(LOG_PREFIX, 'APNS error transmitting to device %s with error %s', failure.device, failure.error); + return this._createErrorPromise(failure.device, failure.error); + } else if (failure.status && failure.response && failure.response.reason) { + log.error(LOG_PREFIX, 'APNS error transmitting to device %s with status %s and reason %s', failure.device, failure.status, failure.response.reason); + return this._createErrorPromise(failure.device, failure.response.reason); } }); - return; } - let newConn = conns[newConnIndex]; - // Update device conn info - apnDevice.connIndex = newConnIndex; - // Use the new conn to send the notification - newConn.pushNotification(notification, apnDevice); -} - -function chooseConns(conns, device) { - // If device does not have appIdentifier, all conns maybe proper connections. - // Otherwise we try to match the appIdentifier with bundleId - let qualifiedConns = []; - for (let index = 0; index < conns.length; index++) { - let conn = conns[index]; - // If the device we need to send to does not have - // appIdentifier, any conn could be a qualified connection - if (!device.appIdentifier || device.appIdentifier === '') { - qualifiedConns.push(index); - continue; - } - if (device.appIdentifier === conn.bundleId) { - qualifiedConns.push(index); - } + /** + * Creates an errorPromise for return. + * + * @param {String} token Device-Token + * @param {String} errorMessage ErrrorMessage as string + */ + _createErrorPromise(token, errorMessage) { + return Promise.resolve({ + transmitted: false, + device: { + deviceToken: token, + deviceType: 'ios' + }, + result: { error: errorMessage } + }); } - return qualifiedConns; -} -/** - * Generate the apns notification from the data we get from api request. - * @param {Object} coreData The data field under api request body - * @param {number} expirationTime The expiration time in milliseconds since Jan 1 1970 - * @returns {Object} A apns notification - */ -function generateNotification(coreData, expirationTime) { - let notification = new apn.notification(); - let payload = {}; - for (let key in coreData) { - switch (key) { - case 'alert': - notification.setAlertText(coreData.alert); - break; - case 'badge': - notification.badge = coreData.badge; - break; - case 'sound': - notification.sound = coreData.sound; - break; - case 'content-available': - notification.setNewsstandAvailable(true); - let isAvailable = coreData['content-available'] === 1; - notification.setContentAvailable(isAvailable); - break; - case 'mutable-content': - let isMutable = coreData['mutable-content'] === 1; - notification.setMutableContent(isMutable); - break; - case 'category': - notification.category = coreData.category; - break; - default: - payload[key] = coreData[key]; - break; - } + /** + * Creates an successfulPromise for return. + * + * @param {String} token Device-Token + */ + _createSuccesfullPromise(token) { + return Promise.resolve({ + transmitted: true, + device: { + deviceToken: token, + deviceType: 'ios' + } + }); } - notification.payload = payload; - notification.expiry = expirationTime / 1000; - return notification; } -APNS.generateNotification = generateNotification; - -/* istanbul ignore else */ -if (process.env.TESTING) { - APNS.chooseConns = chooseConns; - APNS.handleTransmissionError = handleTransmissionError; -} +export default APNS; From 53ef3a6bf481dba1459110f21eac4b66db6d2976 Mon Sep 17 00:00:00 2001 From: Schwobaland Date: Wed, 8 Feb 2017 11:08:13 +0100 Subject: [PATCH 2/7] correcting some errors --- package.json | 4 +-- spec/APNS.spec.js | 62 ++++++++++++++++++++++++++++++++++++++--------- src/APNS.js | 42 +++++++++++++++++--------------- 3 files changed, 76 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index 3e85e5c4..49ece485 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,9 @@ "nyc": "^10.1.2" }, "dependencies": { - "apn": "^2.1.2", + "apn": "^2.1.3", "node-gcm": "^0.14.0", - "npmlog": "^2.0.3", + "npmlog": "^4.0.2", "parse": "^1.9.2" }, "engines": { diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index 6520a410..82f30919 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -5,11 +5,11 @@ describe('APNS', () => { it('can initialize with cert', (done) => { let args = { - cert: new Buffer('testCert'), + cert: '-----BEGIN CERTIFICATE-----fPEYJtQrEMXLC9JtFUJ6emXAWv2QdKu93QE+6o5htM+Eu/2oNFIEj2A71WUBu7kA-----END CERTIFICATE-----', key: new Buffer('testKey'), production: true, topic: 'topic' - } + }; let apns = new APNS(args); expect(apns.providers.length).toBe(1); @@ -99,6 +99,46 @@ describe('APNS', () => { done(); }); + it('can initialize with multiple certs', (done) => { + let args = [ + { + cert: '-----BEGIN CERTIFICATE-----fPEYJtQrEMXLC9JtFUJ6emXAWv2QdKu93QE+6o5htM+Eu/2oNFIEj2A71WUBu7kA-----END CERTIFICATE-----', + key: new Buffer('testKey'), + production: false, + topic: 'topic' + }, + { + cert: '-----BEGIN CERTIFICATE-----fPEYJtQrEMXLC9JtFUJ6emXAWv2QdKu93QE+6o5htM+Eu/2oNFIEj2A71WUBu7kA-----END CERTIFICATE-----', + key: new Buffer('testKey'), + production: true, + topic: 'topicAgain' + } + ]; + + let apns = new APNS(args); + + expect(apns.providers.length).toBe(2); + let devApnsProvider = apns.providers[1]; + expect(devApnsProvider.index).toBe(1); + expect(devApnsProvider.topic).toBe(args[0].topic); + + let devApnsOptions = devApnsProvider.client.config; + expect(devApnsOptions.cert).toBe(args[0].cert); + expect(devApnsOptions.key).toBe(args[0].key); + expect(devApnsOptions.production).toBe(args[0].production); + + let prodApnsProvider = apns.providers[0]; + expect(prodApnsProvider.index).toBe(0); + expect(prodApnsProvider.topic).toBe(args[1].topic); + + // TODO: Remove this checking onec we inject APNS + let prodApnsOptions = prodApnsProvider.client.config; + expect(prodApnsOptions.cert).toBe(args[1].cert); + expect(prodApnsOptions.key).toBe(args[1].key); + expect(prodApnsOptions.production).toBe(args[1].production); + done(); + }); + it('can generate APNS notification', (done) => { //Mock request data let data = { @@ -111,9 +151,9 @@ describe('APNS', () => { 'key': 'value', 'keyAgain': 'valueAgain' }; - let expirationTime = 1454571491354 + let expirationTime = 1454571491354; - let notification = APNS.prototype._generateNotification(data, expirationTime); + let notification = APNS._generateNotification(data, expirationTime); expect(notification.aps.alert).toEqual(data.alert); expect(notification.aps.badge).toEqual(data.badge); @@ -141,7 +181,7 @@ describe('APNS', () => { } ]; - let qualifiedProviders = APNS.prototype._chooseProviders.call({ providers: providers }, appIdentifier); + let qualifiedProviders = APNS.prototype._chooseProviders.call({providers: providers}, appIdentifier); expect(qualifiedProviders).toEqual([{ topic: 'topic' }]); @@ -160,7 +200,7 @@ describe('APNS', () => { } ]; - let qualifiedProviders = APNS.prototype._chooseProviders.call({ providers: providers }, appIdentifier); + let qualifiedProviders = APNS.prototype._chooseProviders.call({providers: providers}, appIdentifier); expect(qualifiedProviders).toEqual([]); done(); }); @@ -171,7 +211,7 @@ describe('APNS', () => { key: new Buffer('testKey'), production: true, topic: 'topic' - } + }; let apns = new APNS(args); let provider = apns.providers[0]; spyOn(provider, 'send').and.callFake((notification, devices) => { @@ -181,13 +221,13 @@ describe('APNS', () => { }) }); // Mock data - let expirationTime = 1454571491354 + let expirationTime = 1454571491354; let data = { 'expiration_time': expirationTime, 'data': { 'alert': 'alert' } - } + }; // Mock devices let mockedDevices = [ { @@ -248,13 +288,13 @@ describe('APNS', () => { }); apns.providers = [provider, providerDev]; // Mock data - let expirationTime = 1454571491354 + let expirationTime = 1454571491354; let data = { 'expiration_time': expirationTime, 'data': { 'alert': 'alert' } - } + }; // Mock devices let mockedDevices = [ { diff --git a/src/APNS.js b/src/APNS.js index ac0d2862..21b20de2 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -21,6 +21,7 @@ export class APNS { * @param {String} args.passphrase The passphrase for the provider key, if required * @param {Boolean} args.production Specifies which environment to connect to: Production (if true) or Sandbox * @param {String} args.topic Specififies an App-Id for this Provider + * @param {String} args.bundleId DEPRECATED: Specifies an App-ID for this Provider * @param {Number} args.connectionRetryLimit The maximum number of connection failures that will be tolerated before apn.Provider will "give up". (Defaults to: 3) */ constructor(args = []) { @@ -42,10 +43,11 @@ export class APNS { // rewrite bundleId to topic for backward-compatibility if (apnsArgs.bundleId) { + log.warn(LOG_PREFIX, 'bundleId is deprecated, use topic instead'); apnsArgs.topic = apnsArgs.bundleId } - let provider = this._createProvider(apnsArgs); + let provider = APNS._createProvider(apnsArgs); this.providers.push(provider); } @@ -62,9 +64,9 @@ export class APNS { /** * Send apns request. - * + * * @param {Object} data The data we need to send, the format is the same with api request body - * @param {Array} devices An array of devices + * @param {Array} allDevices An array of devices * @returns {Object} A promise which is resolved immediately */ send(data, allDevices) { @@ -88,17 +90,17 @@ export class APNS { // No Providers found if (!providers || providers.length === 0) { - let errorPromises = devices.map(device => this._createErrorPromise(device.deviceToken, 'No Provider found')); + let errorPromises = devices.map(device => APNS._createErrorPromise(device.deviceToken, 'No Provider found')); allPromises = allPromises.concat(errorPromises); continue; } - let notification = this._generateNotification(coreData, expirationTime, appIdentifier); + let notification = APNS._generateNotification(coreData, expirationTime, appIdentifier); let promise = providers[0] .send(notification, devices.map(device => device.deviceToken)) .then(this._handlePromise.bind(this)); allPromises.push(promise); - }; + } return Promise.all(allPromises); } @@ -106,10 +108,10 @@ export class APNS { /** * Creates an Provider base on apnsArgs. */ - _createProvider(apnsArgs) { + static _createProvider(apnsArgs) { let provider = new apn.Provider(apnsArgs); - // if using certificate, then topic must be defined + // if using certificate, then topic must be defined if ((apnsArgs.cert || apnsArgs.key || apnsArgs.pfx) && !apnsArgs.topic) { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'topic is mssing for %j', apnsArgs); } @@ -134,7 +136,7 @@ export class APNS { * @param {String} topic Topic the Notification is sent to * @returns {Object} A apns Notification */ - _generateNotification(coreData, expirationTime, topic) { + static _generateNotification(coreData, expirationTime, topic) { let notification = new apn.Notification(); let payload = {}; for (let key in coreData) { @@ -172,9 +174,9 @@ export class APNS { /** * Choose appropriate providers based on device appIdentifier. - * + * * @param {String} appIdentifier appIdentifier for required provider - * @returns Returns Array with appropriate providers + * @returns {Array} Returns Array with appropriate providers */ _chooseProviders(appIdentifier) { // If the device we need to send to does not have appIdentifier, any provider could be a qualified provider @@ -195,28 +197,30 @@ export class APNS { } _handlePromise(response) { + let promises = []; response.sent.forEach((token) => { - log.verbose(LOG_PREFIX, 'APNS transmitted to %s', token); - return this._createSuccesfullPromise(token); + log.verbose(LOG_PREFIX, 'APNS transmitted to %s', token.device); + promises.push(APNS._createSuccesfullPromise(token.device)); }); response.failed.forEach((failure) => { if (failure.error) { log.error(LOG_PREFIX, 'APNS error transmitting to device %s with error %s', failure.device, failure.error); - return this._createErrorPromise(failure.device, failure.error); + promises.push(PNS._createErrorPromise(failure.device, failure.error)); } else if (failure.status && failure.response && failure.response.reason) { log.error(LOG_PREFIX, 'APNS error transmitting to device %s with status %s and reason %s', failure.device, failure.status, failure.response.reason); - return this._createErrorPromise(failure.device, failure.response.reason); + promises.push(APNS._createErrorPromise(failure.device, failure.response.reason)); } }); + return Promise.all(promises); } /** * Creates an errorPromise for return. - * + * * @param {String} token Device-Token * @param {String} errorMessage ErrrorMessage as string */ - _createErrorPromise(token, errorMessage) { + static _createErrorPromise(token, errorMessage) { return Promise.resolve({ transmitted: false, device: { @@ -229,10 +233,10 @@ export class APNS { /** * Creates an successfulPromise for return. - * + * * @param {String} token Device-Token */ - _createSuccesfullPromise(token) { + static _createSuccesfullPromise(token) { return Promise.resolve({ transmitted: true, device: { From 4f39d4eeeae598110ce9935e93457cd5f15f4eda Mon Sep 17 00:00:00 2001 From: Schwobaland Date: Thu, 16 Feb 2017 08:49:57 +0100 Subject: [PATCH 3/7] fix typo --- src/APNS.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/APNS.js b/src/APNS.js index 21b20de2..206aa9a5 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -205,7 +205,7 @@ export class APNS { response.failed.forEach((failure) => { if (failure.error) { log.error(LOG_PREFIX, 'APNS error transmitting to device %s with error %s', failure.device, failure.error); - promises.push(PNS._createErrorPromise(failure.device, failure.error)); + promises.push(APNS._createErrorPromise(failure.device, failure.error)); } else if (failure.status && failure.response && failure.response.reason) { log.error(LOG_PREFIX, 'APNS error transmitting to device %s with status %s and reason %s', failure.device, failure.status, failure.response.reason); promises.push(APNS._createErrorPromise(failure.device, failure.response.reason)); From 6c0bbc53f494ba6e0651bd0f97ceede16540c47e Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sun, 19 Mar 2017 15:24:06 -0400 Subject: [PATCH 4/7] Updates tests --- spec/APNS.spec.js | 99 +++++++++++++++++++++++++---------- spec/MockAPNConnection.js | 21 -------- spec/MockAPNProvider.js | 48 +++++++++++++++++ spec/ParsePushAdapter.spec.js | 17 +++--- src/APNS.js | 43 ++++++++++----- 5 files changed, 158 insertions(+), 70 deletions(-) delete mode 100644 spec/MockAPNConnection.js create mode 100644 spec/MockAPNProvider.js diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index 82f30919..d8c52d49 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -1,5 +1,6 @@ const Parse = require('parse/node'); const APNS = require('../src/APNS').default; +const MockAPNProvider = require('./MockAPNProvider'); describe('APNS', () => { @@ -46,56 +47,72 @@ describe('APNS', () => { fail('should not be reached'); }); - xit('fails to initialize without a bundleID', (done) => { - const apnsArgs = { - production: true, - bundle: 'hello' - }; - try { - new APNS(apnsArgs); - } catch(e) { - expect(e.code).toBe(Parse.Error.PUSH_MISCONFIGURED); - done(); - return; - } - fail('should not be reached'); + it('fails to initialize without a bundleID', (done) => { + expect(() => { + new APNS({ + key: new Buffer('key'), + production: true, + bundle: 'hello' + }); + }).toThrow(); + + expect(() => { + new APNS({ + cert: 'pfx', + production: true, + bundle: 'hello' + }); + }).toThrow(); + + expect(() => { + new APNS({ + pfx: new Buffer(''), + production: true, + bundle: 'hello' + }); + }).toThrow(); done(); }); it('can initialize with multiple certs', (done) => { var args = [ { - cert: 'devCert.pem', - key: 'devKey.pem', + cert: '-----BEGIN CERTIFICATE-----fPEYJtQrEMXLC9JtFUJ6emXAWv2QdKu93QE+6o5htM+Eu/2oNFIEj2A71WUBu7kA-----END CERTIFICATE-----', + key: new Buffer('testKey'), production: false, bundleId: 'bundleId' }, { - cert: 'prodCert.pem', - key: 'prodKey.pem', + cert: '-----BEGIN CERTIFICATE-----fPEYJtQrEMXLC9JtFUJ6emXAWv2QdKu93QE+6o5htM+Eu/2oNFIEj2A71WUBu7kA-----END CERTIFICATE-----', + key: new Buffer('testKey'), production: true, bundleId: 'bundleIdAgain' } ] var apns = new APNS(args); - expect(apns.conns.length).toBe(2); - var devApnsConnection = apns.conns[1]; + expect(apns.providers.length).toBe(2); + var devApnsConnection = apns.providers[1]; expect(devApnsConnection.index).toBe(1); - var devApnsOptions = devApnsConnection.options; + var devApnsOptions = devApnsConnection.client.config; expect(devApnsOptions.cert).toBe(args[0].cert); expect(devApnsOptions.key).toBe(args[0].key); expect(devApnsOptions.production).toBe(args[0].production); - expect(devApnsConnection.bundleId).toBe(args[0].bundleId); + expect(devApnsOptions.bundleId).toBe(args[0].bundleId); + expect(devApnsOptions.topic).toBe(args[0].bundleId); + expect(devApnsConnection.topic).toBe(args[0].bundleId); - var prodApnsConnection = apns.conns[0]; + var prodApnsConnection = apns.providers[0]; expect(prodApnsConnection.index).toBe(0); + // TODO: Remove this checking onec we inject APNS - var prodApnsOptions = prodApnsConnection.options; + var prodApnsOptions = prodApnsConnection.client.config; expect(prodApnsOptions.cert).toBe(args[1].cert); expect(prodApnsOptions.key).toBe(args[1].key); expect(prodApnsOptions.production).toBe(args[1].production); expect(prodApnsOptions.bundleId).toBe(args[1].bundleId); + expect(prodApnsOptions.topic).toBe(args[1].bundleId); + expect(prodApnsConnection.topic).toBe(args[1].bundleId); done(); }); @@ -341,8 +358,8 @@ describe('APNS', () => { it('reports proper error when no conn is available', (done) => { var args = [{ - cert: 'prodCert.pem', - key: 'prodKey.pem', + cert: '-----BEGIN CERTIFICATE-----fPEYJtQrEMXLC9JtFUJ6emXAWv2QdKu93QE+6o5htM+Eu/2oNFIEj2A71WUBu7kA-----END CERTIFICATE-----', + key: new Buffer('testKey'), production: true, bundleId: 'bundleId' }]; @@ -362,11 +379,39 @@ describe('APNS', () => { expect(results.length).toBe(1); let result = results[0]; expect(result.transmitted).toBe(false); - expect(result.result.error).toBe('No connection available'); + expect(result.response.error).toBe('No Provider found'); done(); }, (err) => { fail('should not fail'); done(); }) - }) + }); + + it('properly parses errors', (done) => { + APNS._handlePushFailure({ + device: 'abcd', + status: -1, + response: { + reason: 'Something wrong happend' + } + }).then((result) => { + expect(result.transmitted).toBe(false); + expect(result.device.deviceToken).toBe('abcd'); + expect(result.device.deviceType).toBe('ios'); + expect(result.response.error).toBe('Something wrong happend'); + done(); + }) + }); + + it('properly parses errors again', (done) => { + APNS._handlePushFailure({ + device: 'abcd', + }).then((result) => { + expect(result.transmitted).toBe(false); + expect(result.device.deviceToken).toBe('abcd'); + expect(result.device.deviceType).toBe('ios'); + expect(result.response.error).toBe('Unkown status'); + done(); + }) + }); }); diff --git a/spec/MockAPNConnection.js b/spec/MockAPNConnection.js deleted file mode 100644 index 77acd43f..00000000 --- a/spec/MockAPNConnection.js +++ /dev/null @@ -1,21 +0,0 @@ -const EventEmitter = require('events'); - -module.exports = function (args) { - let emitter = new EventEmitter(); - emitter.options = args; - emitter.pushNotification = function(push, devices) { - if (!Array.isArray(devices)) { - devices = [devices]; - } - devices.forEach((device) => { - process.nextTick(() => { - if (args.shouldFailTransmissions) { - emitter.emit('transmissionError', -1, push, device); - } else { - emitter.emit('transmitted', push, device); - } - }); - }); - }; - return emitter; -} \ No newline at end of file diff --git a/spec/MockAPNProvider.js b/spec/MockAPNProvider.js new file mode 100644 index 00000000..9ede4068 --- /dev/null +++ b/spec/MockAPNProvider.js @@ -0,0 +1,48 @@ +const EventEmitter = require('events'); + +const MockAPNProvider = function (args) { + let emitter = new EventEmitter(); + emitter.options = args; + emitter.send = function(push, devices) { + if (!Array.isArray(devices)) { + devices = [devices]; + } + let sent = []; + let failed = []; + + devices.forEach((device) => { + if (args.shouldFailTransmissions) { + if (args.errorBuilder) { + failed.push() + } else { + failed.push({ + error: "Something went wrong", + status: -1, + device + }); + } + } else { + sent.push({ + device + }); + } + }) + return Promise.resolve({ sent, failed }); + }; + return emitter; +} + +const makeError = function(device) { + return { + error: "Something went wrong", + status: -1, + device + } +}; + +MockAPNProvider.makeError = makeError; +MockAPNProvider.restore = function() { + MockAPNProvider.makeError = makeError; +} + +module.exports = MockAPNProvider; \ No newline at end of file diff --git a/spec/ParsePushAdapter.spec.js b/spec/ParsePushAdapter.spec.js index cb62bf9c..693d1abe 100644 --- a/spec/ParsePushAdapter.spec.js +++ b/spec/ParsePushAdapter.spec.js @@ -3,16 +3,16 @@ var ParsePushAdapter = ParsePushAdapterPackage.ParsePushAdapter; var randomString = require('../src/PushAdapterUtils').randomString; var APNS = require('../src/APNS').default; var GCM = require('../src/GCM').default; -var MockAPNConnection = require('./MockAPNConnection'); +var MockAPNProvider = require('./MockAPNProvider'); describe('ParsePushAdapter', () => { beforeEach(() => { - jasmine.mockLibrary('apn', 'Connection', MockAPNConnection); + jasmine.mockLibrary('apn', 'Provider', MockAPNProvider); }); afterEach(() => { - jasmine.restoreLibrary('apn', 'Connection'); + jasmine.restoreLibrary('apn', 'Provider'); }); it('properly export the module', () => { @@ -278,8 +278,7 @@ describe('ParsePushAdapter', () => { cert: new Buffer('testCert'), key: new Buffer('testKey'), production: false, - bundleId: 'iosbundleId', - topic: 'topic' + topic: 'iosbundleId' } ], osx: [ @@ -287,8 +286,7 @@ describe('ParsePushAdapter', () => { cert: 'cert.cer', key: 'key.pem', production: false, - bundleId: 'osxbundleId', - topic: 'topic2' + topic: 'osxbundleId' } ] }; @@ -391,7 +389,7 @@ describe('ParsePushAdapter', () => { expect(typeof device.deviceType).toBe('string'); expect(typeof device.deviceToken).toBe('string'); expect(result.transmitted).toBe(false); - expect(result.response.error.indexOf('APNS can not find vaild connection for ')).toBe(0); + expect(typeof result.response.error).toBe('string'); done(); }).catch((err) => { fail('Should not fail'); @@ -399,7 +397,8 @@ describe('ParsePushAdapter', () => { }) }); - it('reports properly select connection', (done) => { + // Xited till we can retry on other connections + xit('reports properly select connection', (done) => { var pushConfig = { ios: [ { diff --git a/src/APNS.js b/src/APNS.js index 206aa9a5..4117317a 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -24,7 +24,7 @@ export class APNS { * @param {String} args.bundleId DEPRECATED: Specifies an App-ID for this Provider * @param {Number} args.connectionRetryLimit The maximum number of connection failures that will be tolerated before apn.Provider will "give up". (Defaults to: 3) */ - constructor(args = []) { + constructor(args) { // Define class members this.providers = []; @@ -102,20 +102,30 @@ export class APNS { allPromises.push(promise); } - return Promise.all(allPromises); + return Promise.all(allPromises).then((results) => { + // flatten all + return [].concat.apply([], results); + }); + } + + static _validateAPNArgs(apnsArgs) { + if (apnsArgs.topic) { + return true; + } + return !(apnsArgs.cert || apnsArgs.key || apnsArgs.pfx); } /** * Creates an Provider base on apnsArgs. */ static _createProvider(apnsArgs) { - let provider = new apn.Provider(apnsArgs); - // if using certificate, then topic must be defined - if ((apnsArgs.cert || apnsArgs.key || apnsArgs.pfx) && !apnsArgs.topic) { + if (!APNS._validateAPNArgs(apnsArgs)) { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, 'topic is mssing for %j', apnsArgs); } + let provider = new apn.Provider(apnsArgs); + // Sets the topic on this provider provider.topic = apnsArgs.topic; @@ -203,17 +213,24 @@ export class APNS { promises.push(APNS._createSuccesfullPromise(token.device)); }); response.failed.forEach((failure) => { - if (failure.error) { - log.error(LOG_PREFIX, 'APNS error transmitting to device %s with error %s', failure.device, failure.error); - promises.push(APNS._createErrorPromise(failure.device, failure.error)); - } else if (failure.status && failure.response && failure.response.reason) { - log.error(LOG_PREFIX, 'APNS error transmitting to device %s with status %s and reason %s', failure.device, failure.status, failure.response.reason); - promises.push(APNS._createErrorPromise(failure.device, failure.response.reason)); - } + promises.push(APNS._handlePushFailure(failure)); }); return Promise.all(promises); } + static _handlePushFailure(failure) { + if (failure.error) { + log.error(LOG_PREFIX, 'APNS error transmitting to device %s with error %s', failure.device, failure.error); + return APNS._createErrorPromise(failure.device, failure.error); + } else if (failure.status && failure.response && failure.response.reason) { + log.error(LOG_PREFIX, 'APNS error transmitting to device %s with status %s and reason %s', failure.device, failure.status, failure.response.reason); + return APNS._createErrorPromise(failure.device, failure.response.reason); + } else { + log.error(LOG_PREFIX, 'APNS error transmitting to device with unkown error'); + return APNS._createErrorPromise(failure.device, 'Unkown status'); + } + } + /** * Creates an errorPromise for return. * @@ -227,7 +244,7 @@ export class APNS { deviceToken: token, deviceType: 'ios' }, - result: { error: errorMessage } + response: { error: errorMessage } }); } From fbe235b5e15a2d97dd95cf2c84eb925c577d19ce Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sun, 19 Mar 2017 16:38:19 -0400 Subject: [PATCH 5/7] Adds support for retrying failures --- spec/ParsePushAdapter.spec.js | 4 ++-- src/APNS.js | 32 +++++++++++++++++++++++++++----- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/spec/ParsePushAdapter.spec.js b/spec/ParsePushAdapter.spec.js index 693d1abe..5c600818 100644 --- a/spec/ParsePushAdapter.spec.js +++ b/spec/ParsePushAdapter.spec.js @@ -398,7 +398,7 @@ describe('ParsePushAdapter', () => { }); // Xited till we can retry on other connections - xit('reports properly select connection', (done) => { + it('reports properly select connection', (done) => { var pushConfig = { ios: [ { @@ -441,7 +441,7 @@ describe('ParsePushAdapter', () => { expect(typeof device.deviceToken).toBe('string'); expect(result.transmitted).toBe(true); done(); - }).catch((err) => { + }).catch((err) => { fail('Should not fail'); done(); }) diff --git a/src/APNS.js b/src/APNS.js index 4117317a..6c344b2e 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -96,18 +96,40 @@ export class APNS { } let notification = APNS._generateNotification(coreData, expirationTime, appIdentifier); - let promise = providers[0] - .send(notification, devices.map(device => device.deviceToken)) - .then(this._handlePromise.bind(this)); - allPromises.push(promise); + const deviceIds = devices.map(device => device.deviceToken); + let promise = this.sendThroughProvider(notification, deviceIds, providers); + allPromises.push(promise.then(this._handlePromise.bind(this))); } - return Promise.all(allPromises).then((results) => { + return Promise.all(allPromises).then((results) => { // flatten all return [].concat.apply([], results); }); } + sendThroughProvider(notification, devices, providers) { + return providers[0] + .send(notification, devices) + .then((response) => { + if (response.failed + && response.failed.length > 0 + && providers && providers.length > 1) { + let devices = response.failed.map((failure) => { return failure.device; }); + // Reset the failures as we'll try next connection + response.failed = []; + return this.sendThroughProvider(notification, + devices, + providers.slice(1, providers.length)).then((retryResponse) => { + response.failed = response.failed.concat(retryResponse.failed); + response.sent = response.sent.concat(retryResponse.sent); + return response; + }); + } else { + return response; + } + }); + } + static _validateAPNArgs(apnsArgs) { if (apnsArgs.topic) { return true; From 1317c7a1fc1477c2fd402fcac9781d8ebe0b17af Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sun, 19 Mar 2017 16:55:41 -0400 Subject: [PATCH 6/7] v2.0.0-alpha.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 49ece485..cbe9a9af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "parse-server-push-adapter", - "version": "1.3.0", + "version": "2.0.0-alpha.1", "description": "Base parse-server-push-adapter", "main": "lib/index.js", "files": [ From 0fefba74517d1da644c3b9009ddf511f5fb3848f Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sun, 19 Mar 2017 17:47:24 -0400 Subject: [PATCH 7/7] Updates changelog --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 77e5c1fa..f8286849 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # Change Log +## [2.0.0-alpha.1](https://github.com/parse-server-modules/parse-server-push-adapter/tree/2.0.0-alpha.1) (2017-03-14) +[Full Changelog](https://github.com/parse-server-modules/parse-server-push-adapter/compare/v1.3.0...2.0.0-alpha.1) + +**What's new** + +- Adds support for APNS with HTTP/2.0 +- Improvements in testing, tooling + ## [1.3.0](https://github.com/parse-server-modules/parse-server-push-adapter/tree/1.3.0) (2017-03-14) [Full Changelog](https://github.com/parse-server-modules/parse-server-push-adapter/compare/v1.2.0...1.3.0)