'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); }; const pushCompleted = async pushId => { const query = new Parse.Query('_PushStatus'); query.equalTo('objectId', pushId); let result = await query.first({ useMasterKey: true }); while (!(result && result.get('status') === 'succeeded')) { await jasmine.timeout(); result = await query.first({ useMasterKey: true }); } }; const sendPush = (body, where, config, auth, now) => { const pushController = new PushController(); return new Promise((resolve, reject) => { pushController.sendPush(body, where, config, auth, resolve, now).catch(reject); }); }; 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 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_id('01e3e1b8-fad2-4249-b664-5a3efaab8cb1')('properly increment badges', async () => { 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, }; await reconfigureServer({ push: { adapter: pushAdapter }, }); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, {}, config, auth); await pushCompleted(pushStatusId); // Check we actually sent 15 pushes. const pushStatus = await Parse.Push.getPushStatus(pushStatusId); expect(pushStatus.get('numSent')).toBe(15); // Check that the installations were actually updated. const query = new Parse.Query('_Installation'); const results = await query.find({ useMasterKey: true }); 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); } }); it_id('14afcedf-e65d-41cd-981e-07f32df84c14')('properly increment badges by more than 1', async () => { 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, }; await reconfigureServer({ push: { adapter: pushAdapter }, }); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, {}, config, auth); await pushCompleted(pushStatusId); const pushStatus = await Parse.Push.getPushStatus(pushStatusId); expect(pushStatus.get('numSent')).toBe(15); // Check that the installations were actually updated. const query = new Parse.Query('_Installation'); const results = await query.find({ useMasterKey: true }); 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); } }); it_id('758dd579-aa91-4010-9033-8d48d3463644')('properly set badges to 1', async () => { 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, }; await reconfigureServer({ push: { adapter: pushAdapter }, }); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, {}, config, auth); await pushCompleted(pushStatusId); const pushStatus = await Parse.Push.getPushStatus(pushStatusId); expect(pushStatus.get('numSent')).toBe(10); // Check that the installations were actually updated. const query = new Parse.Query('_Installation'); const results = await query.find({ useMasterKey: true }); expect(results.length).toBe(10); for (let i = 0; i < 10; i++) { const installation = results[i]; expect(installation.get('badge')).toBe(1); } }); it_id('75c39ae3-06ac-4354-b321-931e81c5a927')('properly set badges to 1 with complex query #2903 #3022', async () => { 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, }; await reconfigureServer({ push: { adapter: pushAdapter }, }); await Parse.Object.saveAll(installations); const objectIds = installations.map(installation => { return installation.id; }); const where = { objectId: { $in: objectIds.slice(0, 5) }, }; const pushStatusId = await sendPush(payload, where, config, auth); await pushCompleted(pushStatusId); expect(matchedInstallationsCount).toBe(5); const query = new Parse.Query(Parse.Installation); query.equalTo('badge', 1); const results = await query.find({ useMasterKey: true }); expect(results.length).toBe(5); }); it_id('667f31c0-b458-4f61-ab57-668c04e3cc0b')('properly creates _PushStatus', async () => { 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, }; await reconfigureServer({ push: { adapter: pushAdapter }, }); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, {}, config, auth); await pushCompleted(pushStatusId); const result = await Parse.Push.getPushStatus(pushStatusId); 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 { // Try to get it without masterKey const query = new Parse.Query('_PushStatus'); await query.find(); fail(); } catch (error) { expect(error.code).toBe(119); } 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(); let pendingCount = 0; let runningCount = 0; let succeedCount = 0; allCalls.forEach((call, index) => { expect(call.args.length).toBe(1); const object = call.args[0].object; expect(object instanceof Parse.Object).toBe(true); const pushStatus = getPushStatus(index); if (pushStatus.get('status') === 'pending') { pendingCount += 1; } if (pushStatus.get('status') === 'running') { runningCount += 1; } if (pushStatus.get('status') === 'succeeded') { succeedCount += 1; } if (pushStatus.get('status') === 'running' && pushStatus.get('numSent') > 0) { expect(pushStatus.get('numSent')).toBe(10); expect(pushStatus.get('numFailed')).toBe(5); expect(pushStatus.get('failedPerType')).toEqual({ android: 5, }); expect(pushStatus.get('sentPerType')).toEqual({ ios: 10, }); } }); expect(pendingCount).toBe(1); expect(runningCount).toBe(2); expect(succeedCount).toBe(1); }); it_id('30e0591a-56de-4720-8c60-7d72291b532a')('properly creates _PushStatus without serverURL', async () => { 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, }; await installation.save(); await reconfigureServer({ serverURL: 'http://localhost:8378/', // server with borked URL push: { adapter: pushAdapter }, }); const pushStatusId = await sendPush(payload, {}, config, auth); // it is enqueued so it can take time await jasmine.timeout(1000); Parse.serverURL = 'http://localhost:8378/1'; // GOOD url const result = await Parse.Push.getPushStatus(pushStatusId); expect(result).toBeDefined(); await pushCompleted(pushStatusId); }); it('should properly report failures in _PushStatus', async () => { const pushAdapter = { send: function (body, installations) { return installations.map(installation => { return Promise.resolve({ deviceType: installation.deviceType, }); }); }, getValidPushTypes: function () { return ['ios']; }, }; // $ins is invalid query const where = { channels: { $ins: ['Giants', 'Mets'], }, }; const payload = { data: { alert: 'Hello World!', badge: 1, }, }; const auth = { isMaster: true, }; const pushController = new PushController(); await reconfigureServer({ push: { adapter: pushAdapter }, }); const config = Config.get(Parse.applicationId); try { await pushController.sendPush(payload, where, config, auth); fail(); } catch (e) { const query = new Parse.Query('_PushStatus'); let results = await query.find({ useMasterKey: true }); while (results.length === 0) { results = await query.find({ useMasterKey: true }); } expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('status')).toBe('failed'); } }); it_id('53551fc3-b975-4774-92e6-7e5f3c05e105')('should support full RESTQuery for increment', async () => { 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'], }, }; await reconfigureServer({ push: { adapter: pushAdapter }, }); 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); } await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, where, config, auth); await pushCompleted(pushStatusId); const pushStatus = await Parse.Push.getPushStatus(pushStatusId); expect(pushStatus.get('numSent')).toBe(3); }); it('should support object type for alert', async () => { 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', }; await reconfigureServer({ push: { adapter: pushAdapter }, }); 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); } await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, where, config, auth); await pushCompleted(pushStatusId); const pushStatus = await Parse.Push.getPushStatus(pushStatusId); expect(pushStatus.get('numSent')).toBe(5); }); 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', async () => { 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); } await reconfigureServer({ push: { adapter: pushAdapter }, }); await Parse.Object.saveAll(installations); await pushController.sendPush(payload, {}, config, auth); await jasmine.timeout(1000); const query = new Parse.Query('_PushStatus'); const results = await query.find({ useMasterKey: true }); expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('status')).not.toBe('scheduled'); }); it('should schedule push when configured', async () => { 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); } await reconfigureServer({ push: { adapter: pushAdapter }, scheduledPush: true, }); const config = Config.get(Parse.applicationId); await Parse.Object.saveAll(installations); await pushController.sendPush(payload, {}, config, auth); await jasmine.timeout(1000); const query = new Parse.Query('_PushStatus'); const results = await query.find({ useMasterKey: true }); expect(results.length).toBe(1); const pushStatus = results[0]; expect(pushStatus.get('status')).toBe('scheduled'); }); it('should not enqueue push when device token is not set', async () => { 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 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); } await reconfigureServer({ push: { adapter: pushAdapter }, }); const config = Config.get(Parse.applicationId); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, {}, config, auth); await pushCompleted(pushStatusId); const pushStatus = await Parse.Push.getPushStatus(pushStatusId); expect(pushStatus.get('numSent')).toBe(5); expect(pushStatus.get('status')).toBe('succeeded'); }); it('should not mark the _PushStatus as failed when audience has no deviceToken', async () => { 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 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); } await reconfigureServer({ push: { adapter: pushAdapter }, }); const config = Config.get(Parse.applicationId); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, {}, config, auth); await pushCompleted(pushStatusId); const pushStatus = await Parse.Push.getPushStatus(pushStatusId); expect(pushStatus.get('status')).toBe('succeeded'); }); it('should support localized payload data', async () => { 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', }; 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'); spyOn(pushAdapter, 'send').and.callThrough(); await reconfigureServer({ push: { adapter: pushAdapter }, }); await Parse.Object.saveAll(installations); const pushStatusId = await sendPush(payload, where, config, auth); await pushCompleted(pushStatusId); 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 }); it_id('ef2e5569-50c3-40c2-ab49-175cdbd5f024')('should update audiences', async () => { 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', }; 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); } spyOn(pushAdapter, 'send').and.callThrough(); await reconfigureServer({ push: { adapter: pushAdapter }, }); await Parse.Object.saveAll(installations); // 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)); await Parse.Object.saveAll(audience); await query.find({ useMasterKey: true }).then(parseResults); const body = { data: { alert: 'hello' }, audience_id: audienceId, }; const pushStatusId = await sendPush(body, where, config, auth); await pushCompleted(pushStatusId); 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); // Get the audience we used above. const audienceQuery = new Parse.Query('_Audience'); audienceQuery.equalTo('objectId', audienceId); const results = await audienceQuery.find({ useMasterKey: true }); expect(results[0].get('query')).toBe(JSON.stringify(where)); expect(results[0].get('timesUsed')).toBe(timesUsed + 1); expect(results[0].get('lastUsed')).not.toBeLessThan(now); }); 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'); let expectedHour = 17 + noTimezone.getTimezoneOffset() / 60; let day = '06'; if (expectedHour >= 24) { expectedHour = expectedHour - 24; day = '07'; } expect( PushController.formatPushTime({ date: noTimezone, isLocalTime: true, }) ).toBe(`2017-09-${day}T${expectedHour.toString().padStart(2, '0')}: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', async () => { const auth = { isMaster: true }; const pushAdapter = { send(body, installations) { return successfulTransmissions(body, installations); }, getValidPushTypes() { return ['ios']; }, }; const pushTime = '2017-09-06T17:14:01.048'; let expectedHour = 17 + new Date(pushTime).getTimezoneOffset() / 60; let day = '06'; if (expectedHour >= 24) { expectedHour = expectedHour - 24; day = '07'; } const payload = { data: { alert: 'Hello World!', badge: 'Increment', }, push_time: pushTime, }; await reconfigureServer({ push: { adapter: pushAdapter }, scheduledPush: true, }); const config = Config.get(Parse.applicationId); const pushStatusId = await sendPush(payload, {}, config, auth); const pushStatus = await Parse.Push.getPushStatus(pushStatusId); expect(pushStatus.get('status')).toBe('scheduled'); expect(pushStatus.get('pushTime')).toBe( `2017-09-${day}T${expectedHour.toString().padStart(2, '0')}:14:01.048` ); }); }); describe('With expiration defined', () => { const auth = { isMaster: true }; const pushController = new PushController(); let config; 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', async () => { const now = new Date('2017-09-25T13:30:10.452Z'); const payload = { data: { alert: 'immediate push', }, expiration_interval: 20 * 60, // twenty minutes }; await reconfigureServer({ push: { adapter: pushAdapter }, }); const pushStatusId = await sendPush( payload, {}, Config.get(Parse.applicationId), auth, now ); const pushStatus = await Parse.Push.getPushStatus(pushStatusId); 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); }); }); }); });