diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index e159dd82..b6a14d32 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -138,8 +138,8 @@ describe('PushController', () => { 'push_time': timeStr } - var time = PushController.getPushTime(body); - expect(time).toEqual(new Date(timeStr)); + var { date } = PushController.getPushTime(body); + expect(date).toEqual(new Date(timeStr)); done(); }); @@ -150,8 +150,8 @@ describe('PushController', () => { 'push_time': timeNumber } - var time = PushController.getPushTime(body).valueOf(); - expect(time).toEqual(timeNumber * 1000); + var { date } = PushController.getPushTime(body); + expect(date.valueOf()).toEqual(timeNumber * 1000); done(); }); @@ -640,16 +640,36 @@ describe('PushController', () => { expect(PushController.getPushTime()).toBe(undefined); expect(PushController.getPushTime({ 'push_time': 1000 - })).toEqual(new Date(1000 * 1000)); + }).date).toEqual(new Date(1000 * 1000)); expect(PushController.getPushTime({ 'push_time': '2017-01-01' - })).toEqual(new Date('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) => { @@ -979,4 +999,86 @@ describe('PushController', () => { 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'); + + expect(PushController.formatPushTime({ + date: new Date('2017-09-06T17:14:01.048'), + isLocalTime: true, + })).toBe('2017-09-06T17: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'; + + reconfigureServer({ + push: {adapter: pushAdapter}, + scheduledPush: true + }) + .then(() => { + const config = new Config(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-06T17:14:01.048'); + }) + .then(done, done.fail); + }); + }); }); diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index 3cae892b..0a3efed4 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -14,10 +14,11 @@ export class PushController { } // Replace the expiration_time and push_time with a valid Unix epoch milliseconds time body.expiration_time = PushController.getExpirationTime(body); - const push_time = PushController.getPushTime(body); - if (typeof push_time !== 'undefined') { - body['push_time'] = push_time; + const pushTime = PushController.getPushTime(body); + if (pushTime && pushTime.date !== 'undefined') { + body['push_time'] = PushController.formatPushTime(pushTime); } + // TODO: If the req can pass the checking, we return immediately instead of waiting // pushes to be sent. We probably change this behaviour in the future. let badgeUpdate = () => { @@ -104,21 +105,53 @@ export class PushController { return; } var pushTimeParam = body['push_time']; - var pushTime; + var date; + var isLocalTime = true; + if (typeof pushTimeParam === 'number') { - pushTime = new Date(pushTimeParam * 1000); + date = new Date(pushTimeParam * 1000); } else if (typeof pushTimeParam === 'string') { - pushTime = new Date(pushTimeParam); + isLocalTime = !PushController.pushTimeHasTimezoneComponent(pushTimeParam); + date = new Date(pushTimeParam); } else { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, body['push_time'] + ' is not valid time.'); } // Check pushTime is valid or not, if it is not valid, pushTime is NaN - if (!isFinite(pushTime)) { + if (!isFinite(date)) { throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, body['push_time'] + ' is not valid time.'); } - return pushTime; + + return { + date, + isLocalTime, + }; + } + + /** + * Checks if a ISO8601 formatted date contains a timezone component + * @param pushTimeParam {string} + * @returns {boolean} + */ + static pushTimeHasTimezoneComponent(pushTimeParam: string): boolean { + const offsetPattern = /(.+)([+-])\d\d:\d\d$/; + return pushTimeParam.indexOf('Z') === pushTimeParam.length - 1 // 2007-04-05T12:30Z + || offsetPattern.test(pushTimeParam); // 2007-04-05T12:30.000+02:00, 2007-04-05T12:30.000-02:00 + } + + /** + * Converts a date to ISO format in UTC time and strips the timezone if `isLocalTime` is true + * @param date {Date} + * @param isLocalTime {boolean} + * @returns {string} + */ + static formatPushTime({ date, isLocalTime }: { date: Date, isLocalTime: boolean }) { + if (isLocalTime) { // Strip 'Z' + const isoString = date.toISOString(); + return isoString.substring(0, isoString.indexOf('Z')); + } + return date.toISOString(); } } diff --git a/src/StatusHandler.js b/src/StatusHandler.js index 62edfcf5..6da8ca8b 100644 --- a/src/StatusHandler.js +++ b/src/StatusHandler.js @@ -110,7 +110,7 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId const handler = statusHandler(PUSH_STATUS_COLLECTION, database); const setInitial = function(body = {}, where, options = {source: 'rest'}) { const now = new Date(); - let pushTime = new Date(); + let pushTime = now.toISOString(); let status = 'pending'; if (body.hasOwnProperty('push_time')) { if (config.hasPushScheduledSupport) { @@ -135,7 +135,7 @@ export function pushStatusHandler(config, objectId = newObjectId(config.objectId const object = { objectId, createdAt: now, - pushTime: pushTime.toISOString(), + pushTime, query: JSON.stringify(where), payload: payloadString, source: options.source,