'use strict'; const PushController = require('../lib/Controllers/PushController') .PushController; const StatusHandler = require('../lib/StatusHandler'); const Config = require('../lib/Config'); const validatePushType = require('../lib/Push/utils').validatePushType; const successfulTransmissions = function(body, installations) { const promises = installations.map(device => { return Promise.resolve({ transmitted: true, device: device, }); }); return Promise.all(promises); }; const successfulIOS = function(body, installations) { const promises = installations.map(device => { return Promise.resolve({ transmitted: device.deviceType == 'ios', device: device, }); }); return Promise.all(promises); }; describe('PushController', () => { it('can validate device type when no device type is set', done => { // Make query condition const where = {}; const validPushTypes = ['ios', 'android']; expect(function() { validatePushType(where, validPushTypes); }).not.toThrow(); done(); }); it('can validate device type when single valid device type is set', done => { // Make query condition const where = { deviceType: 'ios', }; const validPushTypes = ['ios', 'android']; expect(function() { validatePushType(where, validPushTypes); }).not.toThrow(); done(); }); it('can validate device type when multiple valid device types are set', done => { // Make query condition const where = { deviceType: { $in: ['android', 'ios'], }, }; const validPushTypes = ['ios', 'android']; expect(function() { validatePushType(where, validPushTypes); }).not.toThrow(); done(); }); it('can throw on validateDeviceType when single invalid device type is set', done => { // Make query condition const where = { deviceType: 'osx', }; const validPushTypes = ['ios', 'android']; expect(function() { validatePushType(where, validPushTypes); }).toThrow(); done(); }); it('can throw on validateDeviceType when single invalid device type is set', done => { // Make query condition const where = { deviceType: 'osx', }; const validPushTypes = ['ios', 'android']; expect(function() { validatePushType(where, validPushTypes); }).toThrow(); done(); }); it('can get expiration time in string format', done => { // Make mock request const timeStr = '2015-03-19T22:05:08Z'; const body = { expiration_time: timeStr, }; const time = PushController.getExpirationTime(body); expect(time).toEqual(new Date(timeStr).valueOf()); done(); }); it('can get expiration time in number format', done => { // Make mock request const timeNumber = 1426802708; const body = { expiration_time: timeNumber, }; const time = PushController.getExpirationTime(body); expect(time).toEqual(timeNumber * 1000); done(); }); it('can throw on getExpirationTime in invalid format', done => { // Make mock request const body = { expiration_time: 'abcd', }; expect(function() { PushController.getExpirationTime(body); }).toThrow(); done(); }); it('can get push time in string format', done => { // Make mock request const timeStr = '2015-03-19T22:05:08Z'; const body = { push_time: timeStr, }; const { date } = PushController.getPushTime(body); expect(date).toEqual(new Date(timeStr)); done(); }); it('can get push time in number format', done => { // Make mock request const timeNumber = 1426802708; const body = { push_time: timeNumber, }; const { date } = PushController.getPushTime(body); expect(date.valueOf()).toEqual(timeNumber * 1000); done(); }); it('can throw on getPushTime in invalid format', done => { // Make mock request const body = { push_time: 'abcd', }; expect(function() { PushController.getPushTime(body); }).toThrow(); done(); }); it('properly increment badges', done => { const pushAdapter = { send: function(body, installations) { const badge = body.data.badge; installations.forEach(installation => { expect(installation.badge).toEqual(badge); expect(installation.originalBadge + 1).toEqual(installation.badge); }); return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ['ios', 'android']; }, }; const payload = { data: { alert: 'Hello World!', badge: 'Increment', }, }; const installations = []; while (installations.length != 10) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('deviceToken', 'device_token_' + installations.length); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } while (installations.length != 15) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('deviceToken', 'device_token_' + installations.length); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'android'); installations.push(installation); } const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const pushController = new PushController(); reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { return Parse.Object.saveAll(installations); }) .then(() => { return pushController.sendPush(payload, {}, config, auth); }) .then(() => { // Wait so the push is completed. return new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); }) .then(() => { // Check we actually sent 15 pushes. const query = new Parse.Query('_PushStatus'); return query.find({ useMasterKey: true }); }) .then(results => { expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('numSent')).toBe(15); }) .then(() => { // Check that the installations were actually updated. const query = new Parse.Query('_Installation'); return query.find({ useMasterKey: true }); }) .then(results => { expect(results.length).toBe(15); for (let i = 0; i < 15; i++) { const installation = results[i]; expect(installation.get('badge')).toBe( parseInt(installation.get('originalBadge')) + 1 ); } done(); }) .catch(err => { jfail(err); done(); }); }); it('properly increment badges by more than 1', done => { const pushAdapter = { send: function(body, installations) { const badge = body.data.badge; installations.forEach(installation => { expect(installation.badge).toEqual(badge); expect(installation.originalBadge + 3).toEqual(installation.badge); }); return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ['ios', 'android']; }, }; const payload = { data: { alert: 'Hello World!', badge: { __op: 'Increment', amount: 3 }, }, }; const installations = []; while (installations.length != 10) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('deviceToken', 'device_token_' + installations.length); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } while (installations.length != 15) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('deviceToken', 'device_token_' + installations.length); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'android'); installations.push(installation); } const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const pushController = new PushController(); reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { return Parse.Object.saveAll(installations); }) .then(() => { return pushController.sendPush(payload, {}, config, auth); }) .then(() => { // Wait so the push is completed. return new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); }) .then(() => { // Check we actually sent 15 pushes. const query = new Parse.Query('_PushStatus'); return query.find({ useMasterKey: true }); }) .then(results => { expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('numSent')).toBe(15); }) .then(() => { // Check that the installations were actually updated. const query = new Parse.Query('_Installation'); return query.find({ useMasterKey: true }); }) .then(results => { expect(results.length).toBe(15); for (let i = 0; i < 15; i++) { const installation = results[i]; expect(installation.get('badge')).toBe( parseInt(installation.get('originalBadge')) + 3 ); } done(); }) .catch(err => { jfail(err); done(); }); }); it('properly set badges to 1', done => { const pushAdapter = { send: function(body, installations) { const badge = body.data.badge; installations.forEach(installation => { expect(installation.badge).toEqual(badge); expect(1).toEqual(installation.badge); }); return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ['ios']; }, }; const payload = { data: { alert: 'Hello World!', badge: 1, }, }; const installations = []; while (installations.length != 10) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('deviceToken', 'device_token_' + installations.length); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const pushController = new PushController(); reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { return Parse.Object.saveAll(installations); }) .then(() => { return pushController.sendPush(payload, {}, config, auth); }) .then(() => { // Wait so the push is completed. return new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); }) .then(() => { // Check we actually sent the pushes. const query = new Parse.Query('_PushStatus'); return query.find({ useMasterKey: true }); }) .then(results => { expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('numSent')).toBe(10); }) .then(() => { // Check that the installations were actually updated. const query = new Parse.Query('_Installation'); return query.find({ useMasterKey: true }); }) .then(results => { expect(results.length).toBe(10); for (let i = 0; i < 10; i++) { const installation = results[i]; expect(installation.get('badge')).toBe(1); } done(); }) .catch(err => { jfail(err); done(); }); }); it('properly set badges to 1 with complex query #2903 #3022', done => { const payload = { data: { alert: 'Hello World!', badge: 1, }, }; const installations = []; while (installations.length != 10) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('deviceToken', 'device_token_' + installations.length); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } let matchedInstallationsCount = 0; const pushAdapter = { send: function(body, installations) { matchedInstallationsCount += installations.length; const badge = body.data.badge; installations.forEach(installation => { expect(installation.badge).toEqual(badge); expect(1).toEqual(installation.badge); }); return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ['ios']; }, }; const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const pushController = new PushController(); reconfigureServer({ push: { adapter: pushAdapter, }, }) .then(() => { return Parse.Object.saveAll(installations); }) .then(installations => { const objectIds = installations.map(installation => { return installation.id; }); const where = { objectId: { $in: objectIds.slice(0, 5) }, }; return pushController.sendPush(payload, where, config, auth); }) .then(() => { return new Promise(res => { setTimeout(res, 300); }); }) .then(() => { expect(matchedInstallationsCount).toBe(5); const query = new Parse.Query(Parse.Installation); query.equalTo('badge', 1); return query.find({ useMasterKey: true }); }) .then(installations => { expect(installations.length).toBe(5); done(); }) .catch(() => { fail('should not fail'); done(); }); }); it('properly creates _PushStatus', done => { const pushStatusAfterSave = { handler: function() {}, }; const spy = spyOn(pushStatusAfterSave, 'handler').and.callThrough(); Parse.Cloud.afterSave('_PushStatus', pushStatusAfterSave.handler); const installations = []; while (installations.length != 10) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('deviceToken', 'device_token_' + installations.length); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } while (installations.length != 15) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('deviceToken', 'device_token_' + installations.length); installation.set('deviceType', 'android'); installations.push(installation); } const payload = { data: { alert: 'Hello World!', badge: 1, }, }; const pushAdapter = { send: function(body, installations) { return successfulIOS(body, installations); }, getValidPushTypes: function() { return ['ios']; }, }; const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const pushController = new PushController(); reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { return Parse.Object.saveAll(installations); }) .then(() => { return pushController.sendPush(payload, {}, config, auth); }) .then(() => { // it is enqueued so it can take time return new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); }) .then(() => { const query = new Parse.Query('_PushStatus'); return query.find({ useMasterKey: true }); }) .then(results => { expect(results.length).toBe(1); const result = results[0]; expect(result.createdAt instanceof Date).toBe(true); expect(result.updatedAt instanceof Date).toBe(true); expect(result.id.length).toBe(10); expect(result.get('source')).toEqual('rest'); expect(result.get('query')).toEqual(JSON.stringify({})); expect(typeof result.get('payload')).toEqual('string'); expect(JSON.parse(result.get('payload'))).toEqual(payload.data); expect(result.get('status')).toEqual('succeeded'); expect(result.get('numSent')).toEqual(10); expect(result.get('sentPerType')).toEqual({ ios: 10, // 10 ios }); expect(result.get('numFailed')).toEqual(5); expect(result.get('failedPerType')).toEqual({ android: 5, // android }); // Try to get it without masterKey const query = new Parse.Query('_PushStatus'); return query.find(); }) .catch(error => { expect(error.code).toBe(119); }) .then(() => { function getPushStatus(callIndex) { return spy.calls.all()[callIndex].args[0].object; } expect(spy).toHaveBeenCalled(); expect(spy.calls.count()).toBe(4); const allCalls = spy.calls.all(); allCalls.forEach(call => { expect(call.args.length).toBe(1); const object = call.args[0].object; expect(object instanceof Parse.Object).toBe(true); }); expect(getPushStatus(0).get('status')).toBe('pending'); expect(getPushStatus(1).get('status')).toBe('running'); expect(getPushStatus(1).get('numSent')).toBe(0); expect(getPushStatus(2).get('status')).toBe('running'); expect(getPushStatus(2).get('numSent')).toBe(10); expect(getPushStatus(2).get('numFailed')).toBe(5); // Those are updated from a nested . operation, this would // not render correctly before expect(getPushStatus(2).get('failedPerType')).toEqual({ android: 5, }); expect(getPushStatus(2).get('sentPerType')).toEqual({ ios: 10, }); expect(getPushStatus(3).get('status')).toBe('succeeded'); }) .then(done) .catch(done.fail); }); it('properly creates _PushStatus without serverURL', done => { const pushStatusAfterSave = { handler: function() {}, }; Parse.Cloud.afterSave('_PushStatus', pushStatusAfterSave.handler); const installation = new Parse.Object('_Installation'); installation.set('installationId', 'installation'); installation.set('deviceToken', 'device_token'); installation.set('badge', 0); installation.set('originalBadge', 0); installation.set('deviceType', 'ios'); const payload = { data: { alert: 'Hello World!', badge: 1, }, }; const pushAdapter = { send: function(body, installations) { return successfulIOS(body, installations); }, getValidPushTypes: function() { return ['ios']; }, }; const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const pushController = new PushController(); return installation .save() .then(() => { return reconfigureServer({ serverURL: 'http://localhost:8378/', // server with borked URL push: { adapter: pushAdapter }, }); }) .then(() => { return pushController.sendPush(payload, {}, config, auth); }) .then(() => { // it is enqueued so it can take time return new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); }) .then(() => { Parse.serverURL = 'http://localhost:8378/1'; // GOOD url const query = new Parse.Query('_PushStatus'); return query.find({ useMasterKey: true }); }) .then(results => { expect(results.length).toBe(1); }) .then(done) .catch(done.fail); }); it('should properly report failures in _PushStatus', done => { const pushAdapter = { send: function(body, installations) { return installations.map(installation => { return Promise.resolve({ deviceType: installation.deviceType, }); }); }, getValidPushTypes: function() { return ['ios']; }, }; const where = { channels: { $ins: ['Giants', 'Mets'], }, }; const payload = { data: { alert: 'Hello World!', badge: 1, }, }; const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const pushController = new PushController(); reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { return pushController.sendPush(payload, where, config, auth); }) .then(() => { fail('should not succeed'); done(); }) .catch(() => { const query = new Parse.Query('_PushStatus'); query.find({ useMasterKey: true }).then(results => { expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('status')).toBe('failed'); done(); }); }); }); it('should support full RESTQuery for increment', done => { const payload = { data: { alert: 'Hello World!', badge: 'Increment', }, }; const pushAdapter = { send: function(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ['ios']; }, }; const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const where = { deviceToken: { $in: ['device_token_0', 'device_token_1', 'device_token_2'], }, }; const pushController = new PushController(); reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { const installations = []; while (installations.length != 5) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set( 'deviceToken', 'device_token_' + installations.length ); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } return Parse.Object.saveAll(installations); }) .then(() => { return pushController.sendPush(payload, where, config, auth); }) .then(() => { // Wait so the push is completed. return new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); }) .then(() => { const query = new Parse.Query('_PushStatus'); return query.find({ useMasterKey: true }); }) .then(results => { expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('numSent')).toBe(3); done(); }) .catch(err => { jfail(err); done(); }); }); it('should support object type for alert', done => { const payload = { data: { alert: { 'loc-key': 'hello_world', }, }, }; const pushAdapter = { send: function(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ['ios']; }, }; const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const where = { deviceType: 'ios', }; const pushController = new PushController(); reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { const installations = []; while (installations.length != 5) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set( 'deviceToken', 'device_token_' + installations.length ); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } return Parse.Object.saveAll(installations); }) .then(() => { return pushController.sendPush(payload, where, config, auth); }) .then(() => { // Wait so the push is completed. return new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); }) .then(() => { const query = new Parse.Query('_PushStatus'); return query.find({ useMasterKey: true }); }) .then(results => { expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('numSent')).toBe(5); done(); }) .catch(() => { fail('should not fail'); done(); }); }); it('should flatten', () => { const res = StatusHandler.flatten([1, [2], [[3, 4], 5], [[[6]]]]); expect(res).toEqual([1, 2, 3, 4, 5, 6]); }); it('properly transforms push time', () => { expect(PushController.getPushTime()).toBe(undefined); expect( PushController.getPushTime({ push_time: 1000, }).date ).toEqual(new Date(1000 * 1000)); expect( PushController.getPushTime({ push_time: '2017-01-01', }).date ).toEqual(new Date('2017-01-01')); expect(() => { PushController.getPushTime({ push_time: 'gibberish-time', }); }).toThrow(); expect(() => { PushController.getPushTime({ push_time: Number.NaN, }); }).toThrow(); expect( PushController.getPushTime({ push_time: '2017-09-06T13:42:48.369Z', }) ).toEqual({ date: new Date('2017-09-06T13:42:48.369Z'), isLocalTime: false, }); expect( PushController.getPushTime({ push_time: '2007-04-05T12:30-02:00', }) ).toEqual({ date: new Date('2007-04-05T12:30-02:00'), isLocalTime: false, }); expect( PushController.getPushTime({ push_time: '2007-04-05T12:30', }) ).toEqual({ date: new Date('2007-04-05T12:30'), isLocalTime: true, }); }); it('should not schedule push when not configured', done => { const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const pushAdapter = { send: function(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ['ios']; }, }; const pushController = new PushController(); const payload = { data: { alert: 'hello', }, push_time: new Date().getTime(), }; const installations = []; while (installations.length != 10) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('deviceToken', 'device_token_' + installations.length); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { return Parse.Object.saveAll(installations) .then(() => { return pushController.sendPush(payload, {}, config, auth); }) .then(() => new Promise(resolve => setTimeout(resolve, 300))); }) .then(() => { const query = new Parse.Query('_PushStatus'); return query.find({ useMasterKey: true }).then(results => { expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('status')).not.toBe('scheduled'); done(); }); }) .catch(err => { console.error(err); fail('should not fail'); done(); }); }); it('should schedule push when configured', done => { const auth = { isMaster: true, }; const pushAdapter = { send: function(body, installations) { const promises = installations.map(device => { if (!device.deviceToken) { // Simulate error when device token is not set return Promise.reject(); } return Promise.resolve({ transmitted: true, device: device, }); }); return Promise.all(promises); }, getValidPushTypes: function() { return ['ios']; }, }; const pushController = new PushController(); const payload = { data: { alert: 'hello', }, push_time: new Date().getTime() / 1000, }; const installations = []; while (installations.length != 10) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('deviceToken', 'device_token_' + installations.length); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } reconfigureServer({ push: { adapter: pushAdapter }, scheduledPush: true, }) .then(() => { const config = Config.get(Parse.applicationId); return Parse.Object.saveAll(installations) .then(() => { return pushController.sendPush(payload, {}, config, auth); }) .then(() => new Promise(resolve => setTimeout(resolve, 300))); }) .then(() => { const query = new Parse.Query('_PushStatus'); return query.find({ useMasterKey: true }).then(results => { expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('status')).toBe('scheduled'); }); }) .then(done) .catch(done.err); }); it('should not enqueue push when device token is not set', done => { const auth = { isMaster: true, }; const pushAdapter = { send: function(body, installations) { const promises = installations.map(device => { if (!device.deviceToken) { // Simulate error when device token is not set return Promise.reject(); } return Promise.resolve({ transmitted: true, device: device, }); }); return Promise.all(promises); }, getValidPushTypes: function() { return ['ios']; }, }; const pushController = new PushController(); const payload = { data: { alert: 'hello', }, push_time: new Date().getTime() / 1000, }; const installations = []; while (installations.length != 5) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('deviceToken', 'device_token_' + installations.length); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } while (installations.length != 15) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { const config = Config.get(Parse.applicationId); return Parse.Object.saveAll(installations) .then(() => { return pushController.sendPush(payload, {}, config, auth); }) .then(() => new Promise(resolve => setTimeout(resolve, 100))); }) .then(() => { const query = new Parse.Query('_PushStatus'); return query.find({ useMasterKey: true }).then(results => { expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('numSent')).toBe(5); expect(pushStatus.get('status')).toBe('succeeded'); done(); }); }) .catch(err => { console.error(err); fail('should not fail'); done(); }); }); it('should not mark the _PushStatus as failed when audience has no deviceToken', done => { const auth = { isMaster: true, }; const pushAdapter = { send: function(body, installations) { const promises = installations.map(device => { if (!device.deviceToken) { // Simulate error when device token is not set return Promise.reject(); } return Promise.resolve({ transmitted: true, device: device, }); }); return Promise.all(promises); }, getValidPushTypes: function() { return ['ios']; }, }; const pushController = new PushController(); const payload = { data: { alert: 'hello', }, push_time: new Date().getTime() / 1000, }; const installations = []; while (installations.length != 5) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { const config = Config.get(Parse.applicationId); return Parse.Object.saveAll(installations) .then(() => { return pushController.sendPush(payload, {}, config, auth); }) .then(() => new Promise(resolve => setTimeout(resolve, 100))); }) .then(() => { const query = new Parse.Query('_PushStatus'); return query.find({ useMasterKey: true }).then(results => { expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('numSent')).toBe(0); expect(pushStatus.get('status')).toBe('succeeded'); done(); }); }) .catch(err => { console.error(err); fail('should not fail'); done(); }); }); it('should support localized payload data', done => { const payload = { data: { alert: 'Hello!', 'alert-fr': 'Bonjour', 'alert-es': 'Ola', }, }; const pushAdapter = { send: function(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ['ios']; }, }; const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; const where = { deviceType: 'ios', }; spyOn(pushAdapter, 'send').and.callThrough(); const pushController = new PushController(); reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { const installations = []; while (installations.length != 5) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set( 'deviceToken', 'device_token_' + installations.length ); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } installations[0].set('localeIdentifier', 'fr-CA'); installations[1].set('localeIdentifier', 'fr-FR'); installations[2].set('localeIdentifier', 'en-US'); return Parse.Object.saveAll(installations); }) .then(() => { return pushController.sendPush(payload, where, config, auth); }) .then(() => { // Wait so the push is completed. return new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); }) .then(() => { expect(pushAdapter.send.calls.count()).toBe(2); const firstCall = pushAdapter.send.calls.first(); expect(firstCall.args[0].data).toEqual({ alert: 'Hello!', }); expect(firstCall.args[1].length).toBe(3); // 3 installations const lastCall = pushAdapter.send.calls.mostRecent(); expect(lastCall.args[0].data).toEqual({ alert: 'Bonjour', }); expect(lastCall.args[1].length).toBe(2); // 2 installations // No installation is in es so only 1 call for fr, and another for default done(); }) .catch(done.fail); }); it('should update audiences', done => { const pushAdapter = { send: function(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes: function() { return ['ios']; }, }; const config = Config.get(Parse.applicationId); const auth = { isMaster: true, }; let audienceId = null; const now = new Date(); let timesUsed = 0; const where = { deviceType: 'ios', }; spyOn(pushAdapter, 'send').and.callThrough(); const pushController = new PushController(); reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { const installations = []; while (installations.length != 5) { const installation = new Parse.Object('_Installation'); installation.set( 'installationId', 'installation_' + installations.length ); installation.set( 'deviceToken', 'device_token_' + installations.length ); installation.set('badge', installations.length); installation.set('originalBadge', installations.length); installation.set('deviceType', 'ios'); installations.push(installation); } return Parse.Object.saveAll(installations); }) .then(() => { // Create an audience const query = new Parse.Query('_Audience'); query.descending('createdAt'); query.equalTo('query', JSON.stringify(where)); const parseResults = results => { if (results.length > 0) { audienceId = results[0].id; timesUsed = results[0].get('timesUsed'); if (!isFinite(timesUsed)) { timesUsed = 0; } } }; const audience = new Parse.Object('_Audience'); audience.set('name', 'testAudience'); audience.set('query', JSON.stringify(where)); return Parse.Object.saveAll(audience).then(() => { return query.find({ useMasterKey: true }).then(parseResults); }); }) .then(() => { const body = { data: { alert: 'hello' }, audience_id: audienceId, }; return pushController.sendPush(body, where, config, auth); }) .then(() => { // Wait so the push is completed. return new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); }) .then(() => { expect(pushAdapter.send.calls.count()).toBe(1); const firstCall = pushAdapter.send.calls.first(); expect(firstCall.args[0].data).toEqual({ alert: 'hello', }); expect(firstCall.args[1].length).toBe(5); }) .then(() => { // Get the audience we used above. const query = new Parse.Query('_Audience'); query.equalTo('objectId', audienceId); return query.find({ useMasterKey: true }); }) .then(results => { const audience = results[0]; expect(audience.get('query')).toBe(JSON.stringify(where)); expect(audience.get('timesUsed')).toBe(timesUsed + 1); expect(audience.get('lastUsed')).not.toBeLessThan(now); }) .then(() => { done(); }) .catch(done.fail); }); describe('pushTimeHasTimezoneComponent', () => { it('should be accurate', () => { expect( PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048Z') ).toBe(true, 'UTC time'); expect( PushController.pushTimeHasTimezoneComponent('2007-04-05T12:30-02:00') ).toBe(true, 'Timezone offset'); expect( PushController.pushTimeHasTimezoneComponent( '2007-04-05T12:30:00.000Z-02:00' ) ).toBe(true, 'Seconds + Milliseconds + Timezone offset'); expect( PushController.pushTimeHasTimezoneComponent('2017-09-06T17:14:01.048') ).toBe(false, 'No timezone'); expect(PushController.pushTimeHasTimezoneComponent('2017-09-06')).toBe( false, 'YY-MM-DD' ); }); }); describe('formatPushTime', () => { it('should format as ISO string', () => { expect( PushController.formatPushTime({ date: new Date('2017-09-06T17:14:01.048Z'), isLocalTime: false, }) ).toBe('2017-09-06T17:14:01.048Z', 'UTC time'); expect( PushController.formatPushTime({ date: new Date('2007-04-05T12:30-02:00'), isLocalTime: false, }) ).toBe('2007-04-05T14:30:00.000Z', 'Timezone offset'); const noTimezone = new Date('2017-09-06T17:14:01.048'); const expectedHour = 17 + noTimezone.getTimezoneOffset() / 60; expect( PushController.formatPushTime({ date: noTimezone, isLocalTime: true, }) ).toBe(`2017-09-06T${expectedHour}:14:01.048`, 'No timezone'); expect( PushController.formatPushTime({ date: new Date('2017-09-06'), isLocalTime: true, }) ).toBe('2017-09-06T00:00:00.000', 'YY-MM-DD'); }); }); describe('Scheduling pushes in local time', () => { it('should preserve the push time', done => { const auth = { isMaster: true }; const pushAdapter = { send(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes() { return ['ios']; }, }; const pushTime = '2017-09-06T17:14:01.048'; const expectedHour = 17 + new Date(pushTime).getTimezoneOffset() / 60; reconfigureServer({ push: { adapter: pushAdapter }, scheduledPush: true, }) .then(() => { const config = Config.get(Parse.applicationId); return new Promise((resolve, reject) => { const pushController = new PushController(); pushController .sendPush( { data: { alert: 'Hello World!', badge: 'Increment', }, push_time: pushTime, }, {}, config, auth, resolve ) .catch(reject); }); }) .then(pushStatusId => { const q = new Parse.Query('_PushStatus'); return q.get(pushStatusId, { useMasterKey: true }); }) .then(pushStatus => { expect(pushStatus.get('status')).toBe('scheduled'); expect(pushStatus.get('pushTime')).toBe( `2017-09-06T${expectedHour}:14:01.048` ); }) .then(done, done.fail); }); }); describe('With expiration defined', () => { const auth = { isMaster: true }; const pushController = new PushController(); let config = Config.get(Parse.applicationId); const pushes = []; const pushAdapter = { send(body, installations) { pushes.push(body); return successfulTransmissions(body, installations); }, getValidPushTypes() { return ['ios']; }, }; beforeEach(done => { reconfigureServer({ push: { adapter: pushAdapter }, }) .then(() => { config = Config.get(Parse.applicationId); }) .then(done, done.fail); }); it('should throw if both expiration_time and expiration_interval are set', () => { expect(() => pushController.sendPush( { expiration_time: '2017-09-25T13:21:20.841Z', expiration_interval: 1000, }, {}, config, auth ) ).toThrow(); }); it('should throw on invalid expiration_interval', () => { expect(() => pushController.sendPush( { expiration_interval: -1, }, {}, config, auth ) ).toThrow(); expect(() => pushController.sendPush( { expiration_interval: '', }, {}, config, auth ) ).toThrow(); expect(() => pushController.sendPush( { expiration_time: {}, }, {}, config, auth ) ).toThrow(); }); describe('For immediate pushes', () => { it('should transform the expiration_interval into an absolute time', done => { const now = new Date('2017-09-25T13:30:10.452Z'); reconfigureServer({ push: { adapter: pushAdapter }, }) .then( () => new Promise(resolve => { pushController.sendPush( { data: { alert: 'immediate push', }, expiration_interval: 20 * 60, // twenty minutes }, {}, Config.get(Parse.applicationId), auth, resolve, now ); }) ) .then(pushStatusId => { const p = new Parse.Object('_PushStatus'); p.id = pushStatusId; return p.fetch({ useMasterKey: true }); }) .then(pushStatus => { expect(pushStatus.get('expiry')).toBeDefined('expiry must be set'); expect(pushStatus.get('expiry')).toEqual( new Date('2017-09-25T13:50:10.452Z').valueOf() ); expect(pushStatus.get('expiration_interval')).toBeDefined( 'expiration_interval must be defined' ); expect(pushStatus.get('expiration_interval')).toBe(20 * 60); }) .then(done, done.fail); }); }); }); });