diff --git a/package.json b/package.json index 21d0990c77..d8b879b824 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "parse": "^1.8.0", "parse-server-fs-adapter": "^1.0.0", "parse-server-gcs-adapter": "^1.0.0", + "parse-server-push-adapter": "^1.0.0", "parse-server-s3-adapter": "^1.0.0", "parse-server-simple-mailgun-adapter": "^1.0.0", "redis": "^2.5.0-1", diff --git a/spec/APNS.spec.js b/spec/APNS.spec.js deleted file mode 100644 index c56e35d550..0000000000 --- a/spec/APNS.spec.js +++ /dev/null @@ -1,307 +0,0 @@ -var APNS = require('../src/APNS'); - -describe('APNS', () => { - - it('can initialize with single cert', (done) => { - var args = { - cert: 'prodCert.pem', - key: 'prodKey.pem', - production: true, - bundleId: 'bundleId' - } - var 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); - // TODO: Remove this checking onec we inject APNS - var prodApnsOptions = apnsConnection.options; - expect(prodApnsOptions.cert).toBe(args.cert); - expect(prodApnsOptions.key).toBe(args.key); - expect(prodApnsOptions.production).toBe(args.production); - done(); - }); - - it('can initialize with multiple certs', (done) => { - var args = [ - { - cert: 'devCert.pem', - key: 'devKey.pem', - production: false, - bundleId: 'bundleId' - }, - { - cert: 'prodCert.pem', - key: 'prodKey.pem', - production: true, - bundleId: 'bundleIdAgain' - } - ] - - var apns = new APNS(args); - expect(apns.conns.length).toBe(2); - var devApnsConnection = apns.conns[1]; - expect(devApnsConnection.index).toBe(1); - var devApnsOptions = devApnsConnection.options; - 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); - - var prodApnsConnection = apns.conns[0]; - expect(prodApnsConnection.index).toBe(0); - // TODO: Remove this checking onec we inject APNS - var prodApnsOptions = prodApnsConnection.options; - 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); - done(); - }); - - it('can generate APNS notification', (done) => { - //Mock request data - var data = { - 'alert': 'alert', - 'badge': 100, - 'sound': 'test', - 'content-available': 1, - 'category': 'INVITE_CATEGORY', - 'key': 'value', - 'keyAgain': 'valueAgain' - }; - var expirationTime = 1454571491354 - - var notification = APNS.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.category).toEqual(data.category); - expect(notification.payload).toEqual({ - 'key': 'value', - 'keyAgain': 'valueAgain' - }); - expect(notification.expiry).toEqual(expirationTime); - 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]); - done(); - }); - - it('can choose conns for device with invalid appIdentifier', (done) => { - // Mock conns - var conns = [ - { - bundleId: 'bundleId' - }, - { - bundleId: 'bundleIdAgain' - } - ]; - // 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(); - 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' - }, - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId1' - }, - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId2' - }, - { - pushNotification: jasmine.createSpy('pushNotification'), - bundleId: 'bundleId1' - } - ]; - 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(); - done(); - }); - - it('can send APNS notification', (done) => { - var args = { - cert: 'prodCert.pem', - key: 'prodKey.pem', - production: true, - bundleId: 'bundleId' - } - var apns = new APNS(args); - var conn = { - pushNotification: jasmine.createSpy('send'), - bundleId: 'bundleId' - }; - apns.conns = [ conn ]; - // Mock data - var expirationTime = 1454571491354 - var data = { - 'expiration_time': expirationTime, - 'data': { - 'alert': 'alert' - } - } - // Mock devices - var devices = [ - { - deviceToken: '112233', - appIdentifier: 'bundleId' - } - ]; - - 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']); - var apnDevice = args[1] - expect(apnDevice.connIndex).toEqual(0); - expect(apnDevice.appIdentifier).toEqual('bundleId'); - done(); - }); -}); diff --git a/spec/AdapterLoader.spec.js b/spec/AdapterLoader.spec.js index d76b0f2d2b..81e057c51f 100644 --- a/spec/AdapterLoader.spec.js +++ b/spec/AdapterLoader.spec.js @@ -3,7 +3,7 @@ var loadAdapter = require("../src/Adapters/AdapterLoader").loadAdapter; var FilesAdapter = require("parse-server-fs-adapter").default; var S3Adapter = require("parse-server-s3-adapter").default; var GCSAdapter = require("parse-server-gcs-adapter").default; -var ParsePushAdapter = require("../src/Adapters/Push/ParsePushAdapter"); +var ParsePushAdapter = require("parse-server-push-adapter").default; describe("AdapterLoader", ()=>{ diff --git a/spec/GCM.spec.js b/spec/GCM.spec.js deleted file mode 100644 index 0ddfe27fa0..0000000000 --- a/spec/GCM.spec.js +++ /dev/null @@ -1,199 +0,0 @@ -var GCM = require('../src/GCM'); - -describe('GCM', () => { - it('can initialize', (done) => { - var args = { - apiKey: 'apiKey' - }; - var gcm = new GCM(args); - expect(gcm.sender.key).toBe(args.apiKey); - done(); - }); - - it('can throw on initializing with invalid args', (done) => { - var args = 123 - expect(function() { - new GCM(args); - }).toThrow(); - done(); - }); - - it('can generate GCM Payload without expiration time', (done) => { - //Mock request data - var data = { - 'alert': 'alert' - }; - var pushId = 'pushId'; - var timeStamp = 1454538822113; - var timeStampISOStr = new Date(timeStamp).toISOString(); - - var payload = GCM.generateGCMPayload(data, pushId, timeStamp); - - expect(payload.priority).toEqual('normal'); - expect(payload.timeToLive).toEqual(undefined); - var dataFromPayload = payload.data; - expect(dataFromPayload.time).toEqual(timeStampISOStr); - expect(dataFromPayload['push_id']).toEqual(pushId); - var dataFromUser = JSON.parse(dataFromPayload.data); - expect(dataFromUser).toEqual(data); - done(); - }); - - it('can generate GCM Payload with valid expiration time', (done) => { - //Mock request data - var data = { - 'alert': 'alert' - }; - var pushId = 'pushId'; - var timeStamp = 1454538822113; - var timeStampISOStr = new Date(timeStamp).toISOString(); - var expirationTime = 1454538922113 - - var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime); - - expect(payload.priority).toEqual('normal'); - expect(payload.timeToLive).toEqual(Math.floor((expirationTime - timeStamp) / 1000)); - var dataFromPayload = payload.data; - expect(dataFromPayload.time).toEqual(timeStampISOStr); - expect(dataFromPayload['push_id']).toEqual(pushId); - var dataFromUser = JSON.parse(dataFromPayload.data); - expect(dataFromUser).toEqual(data); - done(); - }); - - it('can generate GCM Payload with too early expiration time', (done) => { - //Mock request data - var data = { - 'alert': 'alert' - }; - var pushId = 'pushId'; - var timeStamp = 1454538822113; - var timeStampISOStr = new Date(timeStamp).toISOString(); - var expirationTime = 1454538822112; - - var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime); - - expect(payload.priority).toEqual('normal'); - expect(payload.timeToLive).toEqual(0); - var dataFromPayload = payload.data; - expect(dataFromPayload.time).toEqual(timeStampISOStr); - expect(dataFromPayload['push_id']).toEqual(pushId); - var dataFromUser = JSON.parse(dataFromPayload.data); - expect(dataFromUser).toEqual(data); - done(); - }); - - it('can generate GCM Payload with too late expiration time', (done) => { - //Mock request data - var data = { - 'alert': 'alert' - }; - var pushId = 'pushId'; - var timeStamp = 1454538822113; - var timeStampISOStr = new Date(timeStamp).toISOString(); - var expirationTime = 2454538822113; - - var payload = GCM.generateGCMPayload(data, pushId, timeStamp, expirationTime); - - expect(payload.priority).toEqual('normal'); - // Four week in second - expect(payload.timeToLive).toEqual(4 * 7 * 24 * 60 * 60); - var dataFromPayload = payload.data; - expect(dataFromPayload.time).toEqual(timeStampISOStr); - expect(dataFromPayload['push_id']).toEqual(pushId); - var dataFromUser = JSON.parse(dataFromPayload.data); - expect(dataFromUser).toEqual(data); - done(); - }); - - it('can send GCM request', (done) => { - var gcm = new GCM({ - apiKey: 'apiKey' - }); - // Mock gcm sender - var sender = { - send: jasmine.createSpy('send') - }; - gcm.sender = sender; - // Mock data - var expirationTime = 2454538822113; - var data = { - 'expiration_time': expirationTime, - 'data': { - 'alert': 'alert' - } - } - // Mock devices - var devices = [ - { - deviceToken: 'token' - } - ]; - - gcm.send(data, devices); - expect(sender.send).toHaveBeenCalled(); - var args = sender.send.calls.first().args; - // It is too hard to verify message of gcm library, we just verify tokens and retry times - expect(args[1].registrationTokens).toEqual(['token']); - expect(args[2]).toEqual(5); - done(); - }); - - it('can send GCM request', (done) => { - 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' - } - ]; - - 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.device).toEqual(devices[index]); - }) - done(); - }) - }); - - it('can slice devices', (done) => { - // Mock devices - var devices = [makeDevice(1), makeDevice(2), makeDevice(3), makeDevice(4)]; - - var chunkDevices = GCM.sliceDevices(devices, 3); - expect(chunkDevices).toEqual([ - [makeDevice(1), makeDevice(2), makeDevice(3)], - [makeDevice(4)] - ]); - done(); - }); - - function makeDevice(deviceToken) { - return { - deviceToken: deviceToken - }; - } -}); diff --git a/spec/OneSignalPushAdapter.spec.js b/spec/OneSignalPushAdapter.spec.js deleted file mode 100644 index 77b958c5b4..0000000000 --- a/spec/OneSignalPushAdapter.spec.js +++ /dev/null @@ -1,243 +0,0 @@ -'use strict'; - -var OneSignalPushAdapter = require('../src/Adapters/Push/OneSignalPushAdapter'); -var classifyInstallations = require('../src/Adapters/Push/PushAdapterUtils').classifyInstallations; - -// Make mock config -var pushConfig = { - oneSignalAppId:"APP ID", - oneSignalApiKey:"API KEY" -}; - -describe('OneSignalPushAdapter', () => { - it('can be initialized', (done) => { - - var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig); - - var senderMap = oneSignalPushAdapter.senderMap; - - expect(senderMap.ios instanceof Function).toBe(true); - expect(senderMap.android instanceof Function).toBe(true); - done(); - }); - - it('cannot be initialized if options are missing', (done) => { - - expect(() => { - new OneSignalPushAdapter(); - }).toThrow("Trying to initialize OneSignalPushAdapter without oneSignalAppId or oneSignalApiKey"); - done(); - }); - - it('can get valid push types', (done) => { - var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig); - - expect(oneSignalPushAdapter.getValidPushTypes()).toEqual(['ios', 'android']); - done(); - }); - - it('can classify installation', (done) => { - // Mock installations - var validPushTypes = ['ios', 'android']; - var installations = [ - { - deviceType: 'android', - deviceToken: 'androidToken' - }, - { - deviceType: 'ios', - deviceToken: 'iosToken' - }, - { - deviceType: 'win', - deviceToken: 'winToken' - }, - { - deviceType: 'android', - deviceToken: undefined - } - ]; - - var deviceMap = OneSignalPushAdapter.classifyInstallations(installations, validPushTypes); - expect(deviceMap['android']).toEqual([makeDevice('androidToken')]); - expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]); - expect(deviceMap['win']).toBe(undefined); - done(); - }); - - - it('can send push notifications', (done) => { - var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig); - - // Mock android ios senders - var androidSender = jasmine.createSpy('send') - var iosSender = jasmine.createSpy('send') - - var senderMap = { - ios: iosSender, - android: androidSender - }; - oneSignalPushAdapter.senderMap = senderMap; - - // Mock installations - var installations = [ - { - deviceType: 'android', - deviceToken: 'androidToken' - }, - { - deviceType: 'ios', - deviceToken: 'iosToken' - }, - { - deviceType: 'win', - deviceToken: 'winToken' - }, - { - deviceType: 'android', - deviceToken: undefined - } - ]; - var data = {}; - - oneSignalPushAdapter.send(data, installations); - // Check android sender - expect(androidSender).toHaveBeenCalled(); - var args = androidSender.calls.first().args; - expect(args[0]).toEqual(data); - expect(args[1]).toEqual([ - makeDevice('androidToken') - ]); - // Check ios sender - expect(iosSender).toHaveBeenCalled(); - args = iosSender.calls.first().args; - expect(args[0]).toEqual(data); - expect(args[1]).toEqual([ - makeDevice('iosToken') - ]); - done(); - }); - - it("can send iOS notifications", (done) => { - var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig); - var sendToOneSignal = jasmine.createSpy('sendToOneSignal'); - oneSignalPushAdapter.sendToOneSignal = sendToOneSignal; - - oneSignalPushAdapter.sendToAPNS({'data':{ - 'badge': 1, - 'alert': "Example content", - 'sound': "Example sound", - 'content-available': 1, - 'misc-data': 'Example Data' - }},[{'deviceToken':'iosToken1'},{'deviceToken':'iosToken2'}]) - - expect(sendToOneSignal).toHaveBeenCalled(); - var args = sendToOneSignal.calls.first().args; - expect(args[0]).toEqual({ - 'ios_badgeType':'SetTo', - 'ios_badgeCount':1, - 'contents': { 'en':'Example content'}, - 'ios_sound': 'Example sound', - 'content_available':true, - 'data':{'misc-data':'Example Data'}, - 'include_ios_tokens':['iosToken1','iosToken2'] - }) - done(); - }); - - it("can send Android notifications", (done) => { - var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig); - var sendToOneSignal = jasmine.createSpy('sendToOneSignal'); - oneSignalPushAdapter.sendToOneSignal = sendToOneSignal; - - oneSignalPushAdapter.sendToGCM({'data':{ - 'title': 'Example title', - 'alert': 'Example content', - 'misc-data': 'Example Data' - }},[{'deviceToken':'androidToken1'},{'deviceToken':'androidToken2'}]) - - expect(sendToOneSignal).toHaveBeenCalled(); - var args = sendToOneSignal.calls.first().args; - expect(args[0]).toEqual({ - 'contents': { 'en':'Example content'}, - 'title': {'en':'Example title'}, - 'data':{'misc-data':'Example Data'}, - 'include_android_reg_ids': ['androidToken1','androidToken2'] - }) - done(); - }); - - it("can post the correct data", (done) => { - - var oneSignalPushAdapter = new OneSignalPushAdapter(pushConfig); - - var write = jasmine.createSpy('write'); - oneSignalPushAdapter.https = { - 'request': function(a,b) { - return { - 'end':function(){}, - 'on':function(a,b){}, - 'write':write - } - } - }; - - var installations = [ - { - deviceType: 'android', - deviceToken: 'androidToken' - }, - { - deviceType: 'ios', - deviceToken: 'iosToken' - }, - { - deviceType: 'win', - deviceToken: 'winToken' - }, - { - deviceType: 'android', - deviceToken: undefined - } - ]; - - oneSignalPushAdapter.send({'data':{ - 'title': 'Example title', - 'alert': 'Example content', - 'content-available':1, - 'misc-data': 'Example Data' - }}, installations); - - expect(write).toHaveBeenCalled(); - - // iOS - let args = write.calls.first().args; - expect(args[0]).toEqual(JSON.stringify({ - 'contents': { 'en':'Example content'}, - 'content_available':true, - 'data':{'title':'Example title','misc-data':'Example Data'}, - 'include_ios_tokens':['iosToken'], - 'app_id':'APP ID' - })); - - // Android - args = write.calls.mostRecent().args; - expect(args[0]).toEqual(JSON.stringify({ - 'contents': { 'en':'Example content'}, - 'title': {'en':'Example title'}, - 'data':{"content-available":1,'misc-data':'Example Data'}, - 'include_android_reg_ids':['androidToken'], - 'app_id':'APP ID' - })); - - done(); - }); - - function makeDevice(deviceToken, appIdentifier) { - return { - deviceToken: deviceToken, - appIdentifier: appIdentifier - }; - } - -}); diff --git a/spec/ParsePushAdapter.spec.js b/spec/ParsePushAdapter.spec.js deleted file mode 100644 index e21a9dbb21..0000000000 --- a/spec/ParsePushAdapter.spec.js +++ /dev/null @@ -1,150 +0,0 @@ -var ParsePushAdapter = require('../src/Adapters/Push/ParsePushAdapter'); -var APNS = require('../src/APNS'); -var GCM = require('../src/GCM'); - -describe('ParsePushAdapter', () => { - it('can be initialized', (done) => { - // Make mock config - var pushConfig = { - android: { - senderId: 'senderId', - apiKey: 'apiKey' - }, - ios: [ - { - cert: 'prodCert.pem', - key: 'prodKey.pem', - production: true, - bundleId: 'bundleId' - }, - { - cert: 'devCert.pem', - key: 'devKey.pem', - production: false, - bundleId: 'bundleIdAgain' - } - ] - }; - - var parsePushAdapter = new ParsePushAdapter(pushConfig); - // Check ios - var iosSender = parsePushAdapter.senderMap['ios']; - expect(iosSender instanceof APNS).toBe(true); - // Check android - var androidSender = parsePushAdapter.senderMap['android']; - expect(androidSender instanceof GCM).toBe(true); - done(); - }); - - it('can throw on initializing with unsupported push type', (done) => { - // Make mock config - var pushConfig = { - win: { - senderId: 'senderId', - apiKey: 'apiKey' - } - }; - - expect(function() { - new ParsePushAdapter(pushConfig); - }).toThrow(); - done(); - }); - - it('can get valid push types', (done) => { - var parsePushAdapter = new ParsePushAdapter(); - - expect(parsePushAdapter.getValidPushTypes()).toEqual(['ios', 'android']); - done(); - }); - - it('can classify installation', (done) => { - // Mock installations - var validPushTypes = ['ios', 'android']; - var installations = [ - { - deviceType: 'android', - deviceToken: 'androidToken' - }, - { - deviceType: 'ios', - deviceToken: 'iosToken' - }, - { - deviceType: 'win', - deviceToken: 'winToken' - }, - { - deviceType: 'android', - deviceToken: undefined - } - ]; - - var deviceMap = ParsePushAdapter.classifyInstallations(installations, validPushTypes); - expect(deviceMap['android']).toEqual([makeDevice('androidToken')]); - expect(deviceMap['ios']).toEqual([makeDevice('iosToken')]); - expect(deviceMap['win']).toBe(undefined); - done(); - }); - - - it('can send push notifications', (done) => { - var parsePushAdapter = new ParsePushAdapter(); - // Mock android ios senders - var androidSender = { - send: jasmine.createSpy('send') - }; - var iosSender = { - send: jasmine.createSpy('send') - }; - var senderMap = { - ios: iosSender, - android: androidSender - }; - parsePushAdapter.senderMap = senderMap; - // Mock installations - var installations = [ - { - deviceType: 'android', - deviceToken: 'androidToken' - }, - { - deviceType: 'ios', - deviceToken: 'iosToken' - }, - { - deviceType: 'win', - deviceToken: 'winToken' - }, - { - deviceType: 'android', - deviceToken: undefined - } - ]; - var data = {}; - - parsePushAdapter.send(data, installations); - // Check android sender - expect(androidSender.send).toHaveBeenCalled(); - var args = androidSender.send.calls.first().args; - expect(args[0]).toEqual(data); - expect(args[1]).toEqual([ - makeDevice('androidToken') - ]); - // Check ios sender - expect(iosSender.send).toHaveBeenCalled(); - args = iosSender.send.calls.first().args; - expect(args[0]).toEqual(data); - expect(args[1]).toEqual([ - makeDevice('iosToken') - ]); - done(); - }); - - function makeDevice(deviceToken, appIdentifier) { - return { - deviceToken: deviceToken, - appIdentifier: appIdentifier - }; - } -}); diff --git a/src/APNS.js b/src/APNS.js deleted file mode 100644 index 69389ce8f7..0000000000 --- a/src/APNS.js +++ /dev/null @@ -1,227 +0,0 @@ -"use strict"; - -const Parse = require('parse/node').Parse; -// 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. -const apn = require('apn'); - -/** - * 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 - */ -function APNS(args) { - // 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 { - 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; - } else { - conn.priority = 1; - } - - // Set apns client callbacks - conn.on('connected', () => { - console.log('APNS Connection %d Connected', conn.index); - }); - - conn.on('transmissionError', (errCode, notification, apnDevice) => { - handleTransmissionError(this.conns, errCode, notification, apnDevice); - }); - - conn.on('timeout', () => { - console.log('APNS Connection %d Timeout', conn.index); - }); - - conn.on('disconnected', () => { - console.log('APNS Connection %d Disconnected', conn.index); - }); - - conn.on('socketError', () => { - console.log('APNS Connection %d Socket Error', conn.index); - }); - - conn.on('transmitted', function(notification, device) { - if (device.callback) { - device.callback({ - notification: notification, - transmitted: true, - device: device - }); - } - console.log('APNS Connection %d Notification transmitted to %s', conn.index, device.token.toString('hex')); - }); - - 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; - } -} - -/** - * 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 promises = devices.map((device) => { - let qualifiedConnIndexs = chooseConns(this.conns, device); - // We can not find a valid conn, just ignore this device - if (qualifiedConnIndexs.length == 0) { - return Promise.resolve({ - transmitted: false, - result: {error: 'No connection available'} - }); - } - let conn = this.conns[qualifiedConnIndexs[0]]; - let apnDevice = new apn.Device(device.deviceToken); - apnDevice.connIndex = qualifiedConnIndexs[0]; - // Add additional appIdentifier info to apn device instance - if (device.appIdentifier) { - apnDevice.appIdentifier = device.appIdentifier; - } - return new Promise((resolve, reject) => { - apnDevice.callback = resolve; - conn.pushNotification(notification, apnDevice); - }); - }); - return Parse.Promise.when(promises); -} - -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 - } - - // 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; - - 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; - } - } - // There is no more available conns, we give up in this case - if (newConnIndex < 0 || newConnIndex >= conns.length) { - if (apnDevice.callback) { - apnDevice.callback({ - response: {error: `APNS can not find vaild connection for ${apnDevice.token}`, code: errCode}, - status: errCode, - transmitted: false - }); - } - 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); - } - } - return qualifiedConns; -} - -/** - * Generate the apns notification from the data we get from api request. - * @param {Object} coreData The data field under api request body - * @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 'category': - notification.category = coreData.category; - break; - default: - payload[key] = coreData[key]; - break; - } - } - notification.payload = payload; - notification.expiry = expirationTime; - return notification; -} - -if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { - APNS.generateNotification = generateNotification; - APNS.chooseConns = chooseConns; - APNS.handleTransmissionError = handleTransmissionError; -} -module.exports = APNS; diff --git a/src/Adapters/Push/OneSignalPushAdapter.js b/src/Adapters/Push/OneSignalPushAdapter.js deleted file mode 100644 index 7c4d606280..0000000000 --- a/src/Adapters/Push/OneSignalPushAdapter.js +++ /dev/null @@ -1,208 +0,0 @@ -"use strict"; -// ParsePushAdapter is the default implementation of -// PushAdapter, it uses GCM for android push and APNS -// for ios push. - -import { classifyInstallations } from './PushAdapterUtils'; - -const Parse = require('parse/node').Parse; -var deepcopy = require('deepcopy'); -import PushAdapter from './PushAdapter'; - -export class OneSignalPushAdapter extends PushAdapter { - - constructor(pushConfig = {}) { - super(pushConfig); - this.https = require('https'); - - this.validPushTypes = ['ios', 'android']; - this.senderMap = {}; - this.OneSignalConfig = {}; - const { oneSignalAppId, oneSignalApiKey } = pushConfig; - if (!oneSignalAppId || !oneSignalApiKey) { - throw "Trying to initialize OneSignalPushAdapter without oneSignalAppId or oneSignalApiKey"; - } - this.OneSignalConfig['appId'] = pushConfig['oneSignalAppId']; - this.OneSignalConfig['apiKey'] = pushConfig['oneSignalApiKey']; - - this.senderMap['ios'] = this.sendToAPNS.bind(this); - this.senderMap['android'] = this.sendToGCM.bind(this); - } - - send(data, installations) { - let deviceMap = classifyInstallations(installations, this.validPushTypes); - - let sendPromises = []; - for (let pushType in deviceMap) { - let sender = this.senderMap[pushType]; - if (!sender) { - console.log('Can not find sender for push type %s, %j', pushType, data); - continue; - } - let devices = deviceMap[pushType]; - - if(devices.length > 0) { - sendPromises.push(sender(data, devices)); - } - } - return Parse.Promise.when(sendPromises); - } - - static classifyInstallations(installations, validTypes) { - return classifyInstallations(installations, validTypes) - } - - getValidPushTypes() { - return this.validPushTypes; - } - - sendToAPNS(data,tokens) { - - data= deepcopy(data['data']); - - var post = {}; - if(data['badge']) { - if(data['badge'] == "Increment") { - post['ios_badgeType'] = 'Increase'; - post['ios_badgeCount'] = 1; - } else { - post['ios_badgeType'] = 'SetTo'; - post['ios_badgeCount'] = data['badge']; - } - delete data['badge']; - } - if(data['alert']) { - post['contents'] = {en: data['alert']}; - delete data['alert']; - } - if(data['sound']) { - post['ios_sound'] = data['sound']; - delete data['sound']; - } - if(data['content-available'] == 1) { - post['content_available'] = true; - delete data['content-available']; - } - post['data'] = data; - - let promise = new Parse.Promise(); - - var chunk = 2000 // OneSignal can process 2000 devices at a time - var tokenlength=tokens.length; - var offset = 0 - // handle onesignal response. Start next batch if there's not an error. - let handleResponse = function(wasSuccessful) { - if (!wasSuccessful) { - return promise.reject("OneSignal Error"); - } - - if(offset >= tokenlength) { - promise.resolve() - } else { - this.sendNext(); - } - }.bind(this) - - this.sendNext = function() { - post['include_ios_tokens'] = []; - tokens.slice(offset,offset+chunk).forEach(function(i) { - post['include_ios_tokens'].push(i['deviceToken']) - }) - offset+=chunk; - this.sendToOneSignal(post, handleResponse); - }.bind(this) - - this.sendNext() - - return promise; - } - - sendToGCM(data,tokens) { - data= deepcopy(data['data']); - - var post = {}; - - if(data['alert']) { - post['contents'] = {en: data['alert']}; - delete data['alert']; - } - if(data['title']) { - post['title'] = {en: data['title']}; - delete data['title']; - } - if(data['uri']) { - post['url'] = data['uri']; - } - - post['data'] = data; - - let promise = new Parse.Promise(); - - var chunk = 2000 // OneSignal can process 2000 devices at a time - var tokenlength=tokens.length; - var offset = 0 - // handle onesignal response. Start next batch if there's not an error. - let handleResponse = function(wasSuccessful) { - if (!wasSuccessful) { - return promise.reject("OneSIgnal Error"); - } - - if(offset >= tokenlength) { - promise.resolve() - } else { - this.sendNext(); - } - }.bind(this); - - this.sendNext = function() { - post['include_android_reg_ids'] = []; - tokens.slice(offset,offset+chunk).forEach(function(i) { - post['include_android_reg_ids'].push(i['deviceToken']) - }) - offset+=chunk; - this.sendToOneSignal(post, handleResponse); - }.bind(this) - - - this.sendNext(); - return promise; - } - - sendToOneSignal(data, cb) { - let headers = { - "Content-Type": "application/json", - "Authorization": "Basic "+this.OneSignalConfig['apiKey'] - }; - let options = { - host: "onesignal.com", - port: 443, - path: "/api/v1/notifications", - method: "POST", - headers: headers - }; - data['app_id'] = this.OneSignalConfig['appId']; - - let request = this.https.request(options, function(res) { - if(res.statusCode < 299) { - cb(true); - } else { - console.log('OneSignal Error'); - res.on('data', function(chunk) { - console.log(chunk.toString()) - }); - cb(false) - } - }); - request.on('error', function(e) { - console.log("Error connecting to OneSignal") - console.log(e); - cb(false); - }); - request.write(JSON.stringify(data)) - request.end(); - } -} - - -export default OneSignalPushAdapter; -module.exports = OneSignalPushAdapter; diff --git a/src/Adapters/Push/ParsePushAdapter.js b/src/Adapters/Push/ParsePushAdapter.js deleted file mode 100644 index 72cd57ed1b..0000000000 --- a/src/Adapters/Push/ParsePushAdapter.js +++ /dev/null @@ -1,70 +0,0 @@ -"use strict"; -// ParsePushAdapter is the default implementation of -// PushAdapter, it uses GCM for android push and APNS -// for ios push. - -const Parse = require('parse/node').Parse; -const GCM = require('../../GCM'); -const APNS = require('../../APNS'); -import PushAdapter from './PushAdapter'; -import { classifyInstallations } from './PushAdapterUtils'; - -export class ParsePushAdapter extends PushAdapter { - - supportsPushTracking = true; - - constructor(pushConfig = {}) { - super(pushConfig); - this.validPushTypes = ['ios', 'android']; - this.senderMap = {}; - // used in PushController for Dashboard Features - this.feature = { - immediatePush: true - }; - let pushTypes = Object.keys(pushConfig); - - for (let pushType of pushTypes) { - if (this.validPushTypes.indexOf(pushType) < 0) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Push to ' + pushTypes + ' is not supported'); - } - switch (pushType) { - case 'ios': - this.senderMap[pushType] = new APNS(pushConfig[pushType]); - break; - case 'android': - this.senderMap[pushType] = new GCM(pushConfig[pushType]); - break; - } - } - } - - getValidPushTypes() { - return this.validPushTypes; - } - - static classifyInstallations(installations, validTypes) { - return classifyInstallations(installations, validTypes) - } - - send(data, installations) { - let deviceMap = classifyInstallations(installations, this.validPushTypes); - let sendPromises = []; - for (let pushType in deviceMap) { - let sender = this.senderMap[pushType]; - if (!sender) { - sendPromises.push(Promise.resolve({ - transmitted: false, - response: {'error': `Can not find sender for push type ${pushType}, ${data}`} - })) - } else { - let devices = deviceMap[pushType]; - sendPromises.push(sender.send(data, devices)); - } - } - return Parse.Promise.when(sendPromises); - } -} - -export default ParsePushAdapter; -module.exports = ParsePushAdapter; diff --git a/src/Adapters/Push/PushAdapterUtils.js b/src/Adapters/Push/PushAdapterUtils.js deleted file mode 100644 index 6a9216ec31..0000000000 --- a/src/Adapters/Push/PushAdapterUtils.js +++ /dev/null @@ -1,27 +0,0 @@ -/**g - * Classify the device token of installations based on its device type. - * @param {Object} installations An array of installations - * @param {Array} validPushTypes An array of valid push types(string) - * @returns {Object} A map whose key is device type and value is an array of device - */ -export function classifyInstallations(installations, validPushTypes) { - // Init deviceTokenMap, create a empty array for each valid pushType - let deviceMap = {}; - for (let validPushType of validPushTypes) { - deviceMap[validPushType] = []; - } - for (let installation of installations) { - // No deviceToken, ignore - if (!installation.deviceToken) { - continue; - } - let pushType = installation.deviceType; - if (deviceMap[pushType]) { - deviceMap[pushType].push({ - deviceToken: installation.deviceToken, - appIdentifier: installation.appIdentifier - }); - } - } - return deviceMap; -} diff --git a/src/GCM.js b/src/GCM.js deleted file mode 100644 index 8068f9d7dd..0000000000 --- a/src/GCM.js +++ /dev/null @@ -1,154 +0,0 @@ -"use strict"; - -const Parse = require('parse/node').Parse; -const gcm = require('node-gcm'); -const cryptoUtils = require('./cryptoUtils'); - -const GCMTimeToLiveMax = 4 * 7 * 24 * 60 * 60; // GCM allows a max of 4 weeks -const GCMRegistrationTokensMax = 1000; - -function GCM(args) { - if (typeof args !== 'object' || !args.apiKey) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'GCM Configuration is invalid'); - } - this.sender = new gcm.Sender(args.apiKey); -} - -/** - * Send gcm 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 after we get results from gcm - */ -GCM.prototype.send = function(data, devices) { - let pushId = cryptoUtils.newObjectId(); - // Make a new array - devices = new Array(...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); - if (slices.length > 1) { - // Make 1 send per slice - let promises = slices.reduce((memo, slice) => { - let promise = this.send(data, slice, timestamp); - memo.push(promise); - return memo; - }, []) - return Parse.Promise.when(promises).then((results) => { - let allResults = results.reduce((memo, result) => { - return memo.concat(result); - }, []); - return Parse.Promise.as(allResults); - }); - } - // get the devices back... - devices = slices[0]; - - let expirationTime; - // We handle the expiration_time convertion in push.js, so expiration_time is a valid date - // in Unix epoch time in milliseconds here - if (data['expiration_time']) { - expirationTime = data['expiration_time']; - } - // Generate gcm payload - // PushId is not a formal field of GCM, but Parse Android SDK uses this field to deduplicate push notifications - let gcmPayload = generateGCMPayload(data.data, pushId, timestamp, expirationTime); - // Make and send gcm request - let message = new gcm.Message(gcmPayload); - - // Build a device map - let devicesMap = devices.reduce((memo, device) => { - memo[device.deviceToken] = device; - return memo; - }, {}); - - let deviceTokens = Object.keys(devicesMap); - - let promises = deviceTokens.map(() => new Parse.Promise()); - let registrationTokens = deviceTokens; - this.sender.send(message, { registrationTokens: registrationTokens }, 5, (error, response) => { - // example response: - /* - { "multicast_id":7680139367771848000, - "success":0, - "failure":4, - "canonical_ids":0, - "results":[ {"error":"InvalidRegistration"}, - {"error":"InvalidRegistration"}, - {"error":"InvalidRegistration"}, - {"error":"InvalidRegistration"}] } - */ - let { results, multicast_id } = response || {}; - registrationTokens.forEach((token, index) => { - let promise = promises[index]; - let result = results ? results[index] : undefined; - let device = devicesMap[token]; - let resolution = { - device, - multicast_id, - response: error || result, - }; - if (!result || result.error) { - resolution.transmitted = false; - } else { - resolution.transmitted = true; - } - promise.resolve(resolution); - }); - }); - return Parse.Promise.when(promises); -} - -/** - * Generate the gcm payload from the data we get from api request. - * @param {Object} coreData The data field under api request body - * @param {String} pushId A random string - * @param {Number} timeStamp A number whose format is the Unix Epoch - * @param {Number|undefined} expirationTime A number whose format is the Unix Epoch or undefined - * @returns {Object} A promise which is resolved after we get results from gcm - */ -function generateGCMPayload(coreData, pushId, timeStamp, expirationTime) { - let payloadData = { - 'time': new Date(timeStamp).toISOString(), - 'push_id': pushId, - 'data': JSON.stringify(coreData) - } - let payload = { - priority: 'normal', - data: payloadData - }; - if (expirationTime) { - // The timeStamp and expiration is in milliseconds but gcm requires second - let timeToLive = Math.floor((expirationTime - timeStamp) / 1000); - if (timeToLive < 0) { - timeToLive = 0; - } - if (timeToLive >= GCMTimeToLiveMax) { - timeToLive = GCMTimeToLiveMax; - } - payload.timeToLive = timeToLive; - } - return payload; -} - -/** - * Slice a list of devices to several list of devices with fixed chunk size. - * @param {Array} devices An array of devices - * @param {Number} chunkSize The size of the a chunk - * @returns {Array} An array which contaisn several arries of devices with fixed chunk size - */ -function sliceDevices(devices, chunkSize) { - let chunkDevices = []; - while (devices.length > 0) { - chunkDevices.push(devices.splice(0, chunkSize)); - } - return chunkDevices; -} - -if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { - GCM.generateGCMPayload = generateGCMPayload; - GCM.sliceDevices = sliceDevices; -} -module.exports = GCM; diff --git a/src/ParseServer.js b/src/ParseServer.js index cf33b616ca..3148adb942 100644 --- a/src/ParseServer.js +++ b/src/ParseServer.js @@ -14,7 +14,6 @@ var batch = require('./batch'), import cache from './cache'; import Config from './Config'; import parseServerPackage from '../package.json'; -import ParsePushAdapter from './Adapters/Push/ParsePushAdapter'; import PromiseRouter from './PromiseRouter'; import requiredParameter from './requiredParameter'; import { AnalyticsRouter } from './Routers/AnalyticsRouter'; @@ -46,6 +45,7 @@ import { setFeature } from './features'; import { UserController } from './Controllers/UserController'; import { UsersRouter } from './Routers/UsersRouter'; +import ParsePushAdapter from 'parse-server-push-adapter'; // Mutate the Parse object to add the Cloud Code handlers addParseCloud();