Files
kami-parse-server/spec/PushController.spec.js
Florent Vilmart 488b2ff231 Migrate to new cloud code interfaces
removes job status object, moves messasge method on req object

Adds 3.0.0 migration guide

Fixes nits about 3.0.0 documentation

Adds update guide to README
2018-08-15 10:47:21 -04:00

1417 lines
45 KiB
JavaScript

"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);
});
});
});
});