Support local time for scheduled pushes (#4137)

* Handle local push time

* PR feedback

* Improve timezone detection with regex

* Use indexOf instead of endsWith

* Add documentation

* Add end to end test for scheduled pushes in local time

* Revert changes to npm-git script

* clean up
This commit is contained in:
marvelm
2017-09-11 09:31:46 -04:00
committed by Florent Vilmart
parent 21f4411571
commit bc3cef2cd9
3 changed files with 151 additions and 16 deletions

View File

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

View File

@@ -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();
}
}

View File

@@ -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,