diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js index b22645f6..98f346be 100644 --- a/spec/APNS.spec.js +++ b/spec/APNS.spec.js @@ -35,6 +35,17 @@ describe('APNS', () => { fail('should not be reached'); }); + it('fails to initialize with no options', (done) => { + try { + new APNS(); + } 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) => { const apnsArgs = { production: true, @@ -431,4 +442,35 @@ describe('APNS', () => { }); done(); }); + + it('reports proper error when no conn is available', (done) => { + var args = [{ + cert: 'prodCert.pem', + key: 'prodKey.pem', + production: true, + bundleId: 'bundleId' + }]; + var data = { + 'data': { + 'alert': 'alert' + } + } + var devices = [ + { + deviceToken: '112233', + appIdentifier: 'invalidBundleId' + }, + ] + var apns = new APNS(args); + apns.send(data, devices).then((results) => { + expect(results.length).toBe(1); + let result = results[0]; + expect(result.transmitted).toBe(false); + expect(result.result.error).toBe('No connection available'); + done(); + }, (err) => { + fail('should not fail'); + done(); + }) + }) }); diff --git a/spec/GCM.spec.js b/spec/GCM.spec.js index daefcea3..7f53f296 100644 --- a/spec/GCM.spec.js +++ b/spec/GCM.spec.js @@ -1,5 +1,35 @@ var GCM = require('../src/GCM'); +function mockSender(gcm) { + return spyOn(gcm.sender, 'send').and.callFake(function(message, options, timeout, cb) { + /*{ "multicast_id":7680139367771848000, + "success":0, + "failure":4, + "canonical_ids":0, + "results":[ {"error":"InvalidRegistration"}, + {"error":"InvalidRegistration"}, + {"error":"InvalidRegistration"}, + {"error":"InvalidRegistration"}] }*/ + + let tokens = options.registrationTokens; + const response = { + multicast_id: 7680139367771848000, + success: tokens.length, + failure: 0, + cannonical_ids: 0, + results: tokens.map((token, index) => { + return { + message_id: 7680139367771848000+''+index, + registration_id: token + } + }) + } + process.nextTick(() => { + cb(null, response); + }); + }); +} + describe('GCM', () => { it('can initialize', (done) => { var args = { @@ -15,6 +45,16 @@ describe('GCM', () => { expect(function() { new GCM(args); }).toThrow(); + args = { + apisKey: 'apiKey' + }; + expect(function() { + new GCM(args); + }).toThrow(); + args = undefined; + expect(function() { + new GCM(args); + }).toThrow(); done(); }); @@ -194,19 +234,77 @@ describe('GCM', () => { deviceToken: 'token4' } ]; - + mockSender(gcm); gcm.send(data, devices).then((response) => { expect(Array.isArray(response)).toBe(true); expect(response.length).toEqual(devices.length); expect(response.length).toEqual(4); response.forEach((res, index) => { - expect(res.transmitted).toEqual(false); + expect(res.transmitted).toEqual(true); expect(res.device).toEqual(devices[index]); }) done(); }) }); + it('can send GCM request with slices', (done) => { + let originalMax = GCM.GCMRegistrationTokensMax; + GCM.GCMRegistrationTokensMax = 2; + var gcm = new GCM({ + apiKey: 'apiKey' + }); + // Mock data + var expirationTime = 2454538822113; + var data = { + 'expiration_time': expirationTime, + 'data': { + 'alert': 'alert' + } + } + // Mock devices + var devices = [ + { + deviceToken: 'token' + }, + { + deviceToken: 'token2' + }, + { + deviceToken: 'token3' + }, + { + deviceToken: 'token4' + }, + { + deviceToken: 'token5' + }, + { + deviceToken: 'token6' + }, + { + deviceToken: 'token7' + }, + { + deviceToken: 'token8' + } + ]; + spyOn(gcm, 'send').and.callThrough(); + gcm.send(data, devices).then((response) => { + expect(Array.isArray(response)).toBe(true); + expect(response.length).toEqual(devices.length); + expect(response.length).toEqual(8); + response.forEach((res, index) => { + expect(res.transmitted).toEqual(false); + expect(res.device).toEqual(devices[index]); + }); + // 1 original call + // 4 calls (1 per slice of 2) + expect(gcm.send.calls.count()).toBe(1+4); + GCM.GCMRegistrationTokensMax = originalMax; + done(); + }) + }); + it('can slice devices', (done) => { // Mock devices var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)]; diff --git a/spec/MockAPNConnection.js b/spec/MockAPNConnection.js index fff8ddbf..77acd43f 100644 --- a/spec/MockAPNConnection.js +++ b/spec/MockAPNConnection.js @@ -4,9 +4,16 @@ 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(() => { - emitter.emit('transmitted', push, device); + if (args.shouldFailTransmissions) { + emitter.emit('transmissionError', -1, push, device); + } else { + emitter.emit('transmitted', push, device); + } }); }); }; diff --git a/spec/ParsePushAdapter.spec.js b/spec/ParsePushAdapter.spec.js index a38cce50..da3cf87e 100644 --- a/spec/ParsePushAdapter.spec.js +++ b/spec/ParsePushAdapter.spec.js @@ -344,6 +344,99 @@ describe('ParsePushAdapter', () => { }) }); + it('reports properly failures when all transmissions have failed', (done) => { + var pushConfig = { + ios: [ + { + cert: 'cert.cer', + key: 'key.pem', + production: false, + shouldFailTransmissions: true, + bundleId: 'iosbundleId' + } + ] + }; + var installations = [ + { + deviceType: 'ios', + deviceToken: '0d72a1baa92a2febd9a254cbd6584f750c70b2350af5fc9052d1d12584b738e6', + appIdentifier: 'iosbundleId' + } + ]; + + var parsePushAdapter = new ParsePushAdapter(pushConfig); + parsePushAdapter.send({data: {alert: 'some'}}, installations).then((results) => { + expect(Array.isArray(results)).toBe(true); + + // 2x iOS, 1x android, 1x osx, 1x tvos + expect(results.length).toBe(1); + const result = results[0]; + expect(typeof result.device).toBe('object'); + if (!result.device) { + fail('result should have device'); + return; + } + const device = result.device; + 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); + done(); + }).catch((err) => { + fail('Should not fail'); + done(); + }) + }); + + it('reports properly select connection', (done) => { + var pushConfig = { + ios: [ + { + cert: 'cert.cer', + key: 'key.pem', + production: false, + shouldFailTransmissions: true, + bundleId: 'iosbundleId' + }, + { + cert: 'cert.cer', + key: 'key.pem', + production: false, + bundleId: 'iosbundleId' + } + ] + }; + var installations = [ + { + deviceType: 'ios', + deviceToken: '0d72a1baa92a2febd9a254cbd6584f750c70b2350af5fc9052d1d12584b738e6', + appIdentifier: 'iosbundleId' + } + ]; + + var parsePushAdapter = new ParsePushAdapter(pushConfig); + parsePushAdapter.send({data: {alert: 'some'}}, installations).then((results) => { + expect(Array.isArray(results)).toBe(true); + + // 2x iOS, 1x android, 1x osx, 1x tvos + expect(results.length).toBe(1); + const result = results[0]; + expect(typeof result.device).toBe('object'); + if (!result.device) { + fail('result should have device'); + return; + } + const device = result.device; + expect(typeof device.deviceType).toBe('string'); + expect(typeof device.deviceToken).toBe('string'); + expect(result.transmitted).toBe(true); + done(); + }).catch((err) => { + fail('Should not fail'); + done(); + }) + }); + it('properly marks not transmitter when sender is missing', (done) => { var pushConfig = { android: { diff --git a/src/APNS.js b/src/APNS.js index 8799373e..d76ff027 100644 --- a/src/APNS.js +++ b/src/APNS.js @@ -71,16 +71,14 @@ function APNS(args) { }); conn.on('transmitted', function(notification, device) { - if (device.callback) { - device.callback({ - notification: notification, - transmitted: true, - device: { - deviceType: device.deviceType, - deviceToken: device.token.toString('hex') - } - }); - } + 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')); }); @@ -126,6 +124,7 @@ APNS.prototype.send = function(data, devices) { 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; } @@ -173,18 +172,16 @@ function handleTransmissionError(conns, errCode, notification, apnDevice) { } // There is no more available conns, we give up in this case if (newConnIndex < 0 || newConnIndex >= conns.length) { - if (apnDevice.callback) { - 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') - } - }); - } + 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') + } + }); return; } @@ -258,6 +255,7 @@ function generateNotification(coreData, expirationTime) { APNS.generateNotification = generateNotification; +/* istanbul ignore else */ if (process.env.TESTING) { APNS.chooseConns = chooseConns; APNS.handleTransmissionError = handleTransmissionError; diff --git a/src/GCM.js b/src/GCM.js index 79ff3111..3aa2520c 100644 --- a/src/GCM.js +++ b/src/GCM.js @@ -17,6 +17,8 @@ function GCM(args) { this.sender = new gcm.Sender(args.apiKey); } +GCM.GCMRegistrationTokensMax = GCMRegistrationTokensMax; + /** * Send gcm request. * @param {Object} data The data we need to send, the format is the same with api request body @@ -30,7 +32,7 @@ GCM.prototype.send = function(data, devices) { let timestamp = Date.now(); // For android, we can only have 1000 recepients per send, so we need to slice devices to // chunk if necessary - let slices = sliceDevices(devices, GCMRegistrationTokensMax); + let slices = sliceDevices(devices, GCM.GCMRegistrationTokensMax); if (slices.length > 1) { log.verbose(LOG_PREFIX, `the number of devices exceeds ${GCMRegistrationTokensMax}`); // Make 1 send per slice @@ -129,12 +131,13 @@ function generateGCMPayload(requestData, pushId, timeStamp, expirationTime) { push_id: pushId, time: new Date(timeStamp).toISOString() } - if (requestData.content_available) { - payload.content_available = requestData.content_available; - } - if (requestData.notification) { - payload.notification = requestData.notification; - } + const optionalKeys = ['content_available', 'notification']; + optionalKeys.forEach((key, index, array) => { + if (requestData.hasOwnProperty(key)) { + payload[key] = requestData[key]; + } + }); + if (expirationTime) { // The timeStamp and expiration is in milliseconds but gcm requires second let timeToLive = Math.floor((expirationTime - timeStamp) / 1000); @@ -165,6 +168,7 @@ function sliceDevices(devices, chunkSize) { GCM.generateGCMPayload = generateGCMPayload; +/* istanbul ignore else */ if (process.env.TESTING) { GCM.sliceDevices = sliceDevices; }