From b490688652e9352372a4b85a58206f5b9f98d1b3 Mon Sep 17 00:00:00 2001 From: Florent Vilmart Date: Sat, 20 Feb 2016 10:49:32 -0500 Subject: [PATCH] Splits Push handling in Router and Controller - Improves tests and coverage, fix bugs --- spec/PushController.spec.js | 112 +++-------------- spec/PushRouter.spec.js | 123 ++++++++++++++++++ spec/helper.js | 8 ++ src/Controllers/PushController.js | 201 +++++++++++------------------- src/Routers/PushRouter.js | 72 +++++++++++ src/index.js | 7 +- 6 files changed, 298 insertions(+), 225 deletions(-) create mode 100644 spec/PushRouter.spec.js create mode 100644 src/Routers/PushRouter.js diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 5414eca2..6c86b011 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -3,103 +3,28 @@ var PushController = require('../src/Controllers/PushController').PushController describe('PushController', () => { it('can check valid master key of request', (done) => { // Make mock request - var request = { - info: { - masterKey: 'masterKey' - }, - config: { - masterKey: 'masterKey' - } + var auth = { + isMaster: true } expect(() => { - PushController.validateMasterKey(request); + PushController.validateMasterKey(auth); }).not.toThrow(); done(); }); it('can check invalid master key of request', (done) => { // Make mock request - var request = { - info: { - masterKey: 'masterKey' - }, - config: { - masterKey: 'masterKeyAgain' - } + var auth = { + isMaster: false } expect(() => { - PushController.validateMasterKey(request); + PushController.validateMasterKey(auth); }).toThrow(); done(); }); - it('can get query condition when channels is set', (done) => { - // Make mock request - var request = { - body: { - channels: ['Giants', 'Mets'] - } - } - - var where = PushController.getQueryCondition(request); - expect(where).toEqual({ - 'channels': { - '$in': ['Giants', 'Mets'] - } - }); - done(); - }); - - it('can get query condition when where is set', (done) => { - // Make mock request - var request = { - body: { - 'where': { - 'injuryReports': true - } - } - } - - var where = PushController.getQueryCondition(request); - expect(where).toEqual({ - 'injuryReports': true - }); - done(); - }); - - it('can get query condition when nothing is set', (done) => { - // Make mock request - var request = { - body: { - } - } - - expect(function() { - PushController.getQueryCondition(request); - }).toThrow(); - done(); - }); - - it('can throw on getQueryCondition when channels and where are set', (done) => { - // Make mock request - var request = { - body: { - 'channels': { - '$in': ['Giants', 'Mets'] - }, - 'where': { - 'injuryReports': true - } - } - } - - expect(function() { - PushController.getQueryCondition(request); - }).toThrow(); - done(); - }); it('can validate device type when no device type is set', (done) => { // Make query condition @@ -170,13 +95,11 @@ describe('PushController', () => { it('can get expiration time in string format', (done) => { // Make mock request var timeStr = '2015-03-19T22:05:08Z'; - var request = { - body: { + var body = { 'expiration_time': timeStr - } - } + } - var time = PushController.getExpirationTime(request); + var time = PushController.getExpirationTime(body); expect(time).toEqual(new Date(timeStr).valueOf()); done(); }); @@ -184,28 +107,25 @@ describe('PushController', () => { it('can get expiration time in number format', (done) => { // Make mock request var timeNumber = 1426802708; - var request = { - body: { - 'expiration_time': timeNumber - } + var body = { + 'expiration_time': timeNumber } - var time = PushController.getExpirationTime(request); + var time = PushController.getExpirationTime(body); expect(time).toEqual(timeNumber * 1000); done(); }); it('can throw on getExpirationTime in invalid format', (done) => { // Make mock request - var request = { - body: { - 'expiration_time': 'abcd' - } + var body = { + 'expiration_time': 'abcd' } expect(function(){ - PushController.getExpirationTime(request); + PushController.getExpirationTime(body); }).toThrow(); done(); }); + }); diff --git a/spec/PushRouter.spec.js b/spec/PushRouter.spec.js new file mode 100644 index 00000000..e7273dd5 --- /dev/null +++ b/spec/PushRouter.spec.js @@ -0,0 +1,123 @@ +var PushRouter = require('../src/Routers/PushRouter').PushRouter; +var request = require('request'); + +describe('PushRouter', () => { + it('can check valid master key of request', (done) => { + // Make mock request + var request = { + info: { + masterKey: 'masterKey' + }, + config: { + masterKey: 'masterKey' + } + } + + expect(() => { + PushRouter.validateMasterKey(request); + }).not.toThrow(); + done(); + }); + + it('can check invalid master key of request', (done) => { + // Make mock request + var request = { + info: { + masterKey: 'masterKey' + }, + config: { + masterKey: 'masterKeyAgain' + } + } + + expect(() => { + PushRouter.validateMasterKey(request); + }).toThrow(); + done(); + }); + + it('can get query condition when channels is set', (done) => { + // Make mock request + var request = { + body: { + channels: ['Giants', 'Mets'] + } + } + + var where = PushRouter.getQueryCondition(request); + expect(where).toEqual({ + 'channels': { + '$in': ['Giants', 'Mets'] + } + }); + done(); + }); + + it('can get query condition when where is set', (done) => { + // Make mock request + var request = { + body: { + 'where': { + 'injuryReports': true + } + } + } + + var where = PushRouter.getQueryCondition(request); + expect(where).toEqual({ + 'injuryReports': true + }); + done(); + }); + + it('can get query condition when nothing is set', (done) => { + // Make mock request + var request = { + body: { + } + } + + expect(function() { + PushRouter.getQueryCondition(request); + }).toThrow(); + done(); + }); + + it('can throw on getQueryCondition when channels and where are set', (done) => { + // Make mock request + var request = { + body: { + 'channels': { + '$in': ['Giants', 'Mets'] + }, + 'where': { + 'injuryReports': true + } + } + } + + expect(function() { + PushRouter.getQueryCondition(request); + }).toThrow(); + done(); + }); + + it('sends a push through REST', (done) => { + request.post({ + url: Parse.serverURL+"/push", + json: true, + body: { + 'channels': { + '$in': ['Giants', 'Mets'] + } + }, + headers: { + 'X-Parse-Application-Id': Parse.applicationId, + 'X-Parse-Master-Key': Parse.masterKey + } + }, function(err, res, body){ + expect(body.result).toBe(true); + done(); + }); + }); +}); \ No newline at end of file diff --git a/spec/helper.js b/spec/helper.js index c89e2dbe..7fbb0ae2 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -26,6 +26,14 @@ var defaultConfiguration = { masterKey: 'test', collectionPrefix: 'test_', fileKey: 'test', + push: { + 'ios': { + cert: 'prodCert.pem', + key: 'prodKey.pem', + production: true, + bundleId: 'bundleId' + } + }, oauth: { // Override the facebook provider facebook: mockFacebook(), myoauth: { diff --git a/src/Controllers/PushController.js b/src/Controllers/PushController.js index e87e6f76..3b73f16b 100644 --- a/src/Controllers/PushController.js +++ b/src/Controllers/PushController.js @@ -6,138 +6,85 @@ export class PushController { constructor(pushAdapter) { this._pushAdapter = pushAdapter; - } + }; - handlePOST(req) { - if (!this._pushAdapter) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Push adapter is not availabe'); + /** + * Check whether the deviceType parameter in qury condition is valid or not. + * @param {Object} where A query condition + * @param {Array} validPushTypes An array of valid push types(string) + */ + static validatePushType(where = {}, validPushTypes = []) { + var deviceTypeField = where.deviceType || {}; + var deviceTypes = []; + if (typeof deviceTypeField === 'string') { + deviceTypes.push(deviceTypeField); + } else if (typeof deviceTypeField['$in'] === 'array') { + deviceTypes.concat(deviceTypeField['$in']); } - - validateMasterKey(req); - var where = getQueryCondition(req); - var pushAdapter = this._pushAdapter; - validatePushType(where, pushAdapter.getValidPushTypes()); - // Replace the expiration_time with a valid Unix epoch milliseconds time - req.body['expiration_time'] = getExpirationTime(req); - // 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. - rest.find(req.config, req.auth, '_Installation', where).then(function(response) { - return pushAdapter.send(req.body, response.results); - }); - return Parse.Promise.as({ - response: { - 'result': true - } - }); - } - - static getExpressRouter() { - var router = new PromiseRouter(); - router.route('POST','/push', (req) => { - return req.config.pushController.handlePOST(req); - }); - return router; - } -} - -/** - * Check whether the deviceType parameter in qury condition is valid or not. - * @param {Object} where A query condition - * @param {Array} validPushTypes An array of valid push types(string) - */ -function validatePushType(where, validPushTypes) { - var where = where || {}; - var deviceTypeField = where.deviceType || {}; - var deviceTypes = []; - if (typeof deviceTypeField === 'string') { - deviceTypes.push(deviceTypeField); - } else if (typeof deviceTypeField['$in'] === 'array') { - deviceTypes.concat(deviceTypeField['$in']); - } - for (var i = 0; i < deviceTypes.length; i++) { - var deviceType = deviceTypes[i]; - if (validPushTypes.indexOf(deviceType) < 0) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - deviceType + ' is not supported push type.'); - } - } -} - -/** - * Get expiration time from the request body. - * @param {Object} request A request object - * @returns {Number|undefined} The expiration time if it exists in the request - */ -function getExpirationTime(req) { - var body = req.body || {}; - var hasExpirationTime = !!body['expiration_time']; - if (!hasExpirationTime) { - return; - } - var expirationTimeParam = body['expiration_time']; - var expirationTime; - if (typeof expirationTimeParam === 'number') { - expirationTime = new Date(expirationTimeParam * 1000); - } else if (typeof expirationTimeParam === 'string') { - expirationTime = new Date(expirationTimeParam); - } else { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - body['expiration_time'] + ' is not valid time.'); - } - // Check expirationTime is valid or not, if it is not valid, expirationTime is NaN - if (!isFinite(expirationTime)) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - body['expiration_time'] + ' is not valid time.'); - } - return expirationTime.valueOf(); -} - -/** - * Get query condition from the request body. - * @param {Object} request A request object - * @returns {Object} The query condition, the where field in a query api call - */ -function getQueryCondition(req) { - var body = req.body || {}; - var hasWhere = typeof body.where !== 'undefined'; - var hasChannels = typeof body.channels !== 'undefined'; - - var where; - if (hasWhere && hasChannels) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Channels and query can not be set at the same time.'); - } else if (hasWhere) { - where = body.where; - } else if (hasChannels) { - where = { - "channels": { - "$in": body.channels + for (var i = 0; i < deviceTypes.length; i++) { + var deviceType = deviceTypes[i]; + if (validPushTypes.indexOf(deviceType) < 0) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + deviceType + ' is not supported push type.'); } } - } else { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Channels and query should be set at least one.'); + }; + + /** + * Check whether the api call has master key or not. + * @param {Object} request A request object + */ + static validateMasterKey(auth = {}) { + if (!auth.isMaster) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Master key is invalid, you should only use master key to send push'); + } } - return where; -} -/** - * Check whether the api call has master key or not. - * @param {Object} request A request object - */ -function validateMasterKey(req) { - if (req.info.masterKey !== req.config.masterKey) { - throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, - 'Master key is invalid, you should only use master key to send push'); - } -} - -if (typeof process !== 'undefined' && process.env.NODE_ENV === 'test') { - PushController.getQueryCondition = getQueryCondition; - PushController.validateMasterKey = validateMasterKey; - PushController.getExpirationTime = getExpirationTime; - PushController.validatePushType = validatePushType; -} + sendPush(body = {}, where = {}, config, auth) { + var pushAdapter = this._pushAdapter; + if (!pushAdapter) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Push adapter is not available'); + } + PushController.validateMasterKey(auth); + + PushController.validatePushType(where, pushAdapter.getValidPushTypes()); + // Replace the expiration_time with a valid Unix epoch milliseconds time + body['expiration_time'] = PushController.getExpirationTime(body); + // 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. + rest.find(config, auth, '_Installation', where).then(function(response) { + return pushAdapter.send(body, response.results); + }); + }; + /** + * Get expiration time from the request body. + * @param {Object} request A request object + * @returns {Number|undefined} The expiration time if it exists in the request + */ + static getExpirationTime(body = {}) { + var hasExpirationTime = !!body['expiration_time']; + if (!hasExpirationTime) { + return; + } + var expirationTimeParam = body['expiration_time']; + var expirationTime; + if (typeof expirationTimeParam === 'number') { + expirationTime = new Date(expirationTimeParam * 1000); + } else if (typeof expirationTimeParam === 'string') { + expirationTime = new Date(expirationTimeParam); + } else { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + body['expiration_time'] + ' is not valid time.'); + } + // Check expirationTime is valid or not, if it is not valid, expirationTime is NaN + if (!isFinite(expirationTime)) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + body['expiration_time'] + ' is not valid time.'); + } + return expirationTime.valueOf(); + }; +}; export default PushController; diff --git a/src/Routers/PushRouter.js b/src/Routers/PushRouter.js new file mode 100644 index 00000000..f75d9998 --- /dev/null +++ b/src/Routers/PushRouter.js @@ -0,0 +1,72 @@ +import PushController from '../Controllers/PushController' +import PromiseRouter from '../PromiseRouter'; + +export class PushRouter extends PromiseRouter { + + mountRoutes() { + this.route("POST", "/push", req => { return this.handlePOST(req); }); + } + + /** + * Check whether the api call has master key or not. + * @param {Object} request A request object + */ + static validateMasterKey(req) { + if (req.info.masterKey !== req.config.masterKey) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Master key is invalid, you should only use master key to send push'); + } + } + + handlePOST(req) { + // TODO: move to middlewares when support for Promise middlewares + PushRouter.validateMasterKey(req); + + const pushController = req.config.pushController; + if (!pushController) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Push controller is not set'); + } + + var where = PushRouter.getQueryCondition(req); + + pushController.sendPush(req.body, where, req.config, req.auth); + return Promise.resolve({ + response: { + 'result': true + } + }); + } + + /** + * Get query condition from the request body. + * @param {Object} request A request object + * @returns {Object} The query condition, the where field in a query api call + */ + static getQueryCondition(req) { + var body = req.body || {}; + var hasWhere = typeof body.where !== 'undefined'; + var hasChannels = typeof body.channels !== 'undefined'; + + var where; + if (hasWhere && hasChannels) { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Channels and query can not be set at the same time.'); + } else if (hasWhere) { + where = body.where; + } else if (hasChannels) { + where = { + "channels": { + "$in": body.channels + } + } + } else { + throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED, + 'Channels and query should be set at least one.'); + } + return where; + } + +} + +export default PushRouter; diff --git a/src/index.js b/src/index.js index de63f9bb..6512f322 100644 --- a/src/index.js +++ b/src/index.js @@ -18,6 +18,7 @@ import { FilesController } from './Controllers/FilesController'; import ParsePushAdapter from './Adapters/Push/ParsePushAdapter'; import { PushController } from './Controllers/PushController'; + import { ClassesRouter } from './Routers/ClassesRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter'; import { UsersRouter } from './Routers/UsersRouter'; @@ -27,7 +28,7 @@ import { AnalyticsRouter } from './Routers/AnalyticsRouter'; import { FunctionsRouter } from './Routers/FunctionsRouter'; import { SchemasRouter } from './Routers/SchemasRouter'; import { IAPValidationRouter } from './Routers/IAPValidationRouter'; - +import { PushRouter } from './Routers/PushRouter'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { LoggerController } from './Controllers/LoggerController'; @@ -111,6 +112,7 @@ function ParseServer({ } let filesController = new FilesController(filesAdapter); + let pushController = new PushController(pushAdapter); cache.apps[appId] = { masterKey: masterKey, @@ -122,6 +124,7 @@ function ParseServer({ fileKey: fileKey, facebookAppIds: facebookAppIds, filesController: filesController, + pushController: pushController, enableAnonymousUsers: enableAnonymousUsers, oauth: oauth, }; @@ -161,7 +164,7 @@ function ParseServer({ new InstallationsRouter(), new FunctionsRouter(), new SchemasRouter(), - PushController.getExpressRouter(), + new PushRouter(), new LoggerController(loggerAdapter).getExpressRouter(), new IAPValidationRouter() ];