Splits Push handling in Router and Controller

- Improves tests and coverage, fix bugs
This commit is contained in:
Florent Vilmart
2016-02-20 10:49:32 -05:00
parent eb0340585f
commit b490688652
6 changed files with 298 additions and 225 deletions

View File

@@ -3,103 +3,28 @@ var PushController = require('../src/Controllers/PushController').PushController
describe('PushController', () => { describe('PushController', () => {
it('can check valid master key of request', (done) => { it('can check valid master key of request', (done) => {
// Make mock request // Make mock request
var request = { var auth = {
info: { isMaster: true
masterKey: 'masterKey'
},
config: {
masterKey: 'masterKey'
}
} }
expect(() => { expect(() => {
PushController.validateMasterKey(request); PushController.validateMasterKey(auth);
}).not.toThrow(); }).not.toThrow();
done(); done();
}); });
it('can check invalid master key of request', (done) => { it('can check invalid master key of request', (done) => {
// Make mock request // Make mock request
var request = { var auth = {
info: { isMaster: false
masterKey: 'masterKey'
},
config: {
masterKey: 'masterKeyAgain'
}
} }
expect(() => { expect(() => {
PushController.validateMasterKey(request); PushController.validateMasterKey(auth);
}).toThrow(); }).toThrow();
done(); 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) => { it('can validate device type when no device type is set', (done) => {
// Make query condition // Make query condition
@@ -170,13 +95,11 @@ describe('PushController', () => {
it('can get expiration time in string format', (done) => { it('can get expiration time in string format', (done) => {
// Make mock request // Make mock request
var timeStr = '2015-03-19T22:05:08Z'; var timeStr = '2015-03-19T22:05:08Z';
var request = { var body = {
body: {
'expiration_time': timeStr 'expiration_time': timeStr
} }
}
var time = PushController.getExpirationTime(request); var time = PushController.getExpirationTime(body);
expect(time).toEqual(new Date(timeStr).valueOf()); expect(time).toEqual(new Date(timeStr).valueOf());
done(); done();
}); });
@@ -184,28 +107,25 @@ describe('PushController', () => {
it('can get expiration time in number format', (done) => { it('can get expiration time in number format', (done) => {
// Make mock request // Make mock request
var timeNumber = 1426802708; var timeNumber = 1426802708;
var request = { var body = {
body: {
'expiration_time': timeNumber 'expiration_time': timeNumber
} }
}
var time = PushController.getExpirationTime(request); var time = PushController.getExpirationTime(body);
expect(time).toEqual(timeNumber * 1000); expect(time).toEqual(timeNumber * 1000);
done(); done();
}); });
it('can throw on getExpirationTime in invalid format', (done) => { it('can throw on getExpirationTime in invalid format', (done) => {
// Make mock request // Make mock request
var request = { var body = {
body: {
'expiration_time': 'abcd' 'expiration_time': 'abcd'
} }
}
expect(function(){ expect(function(){
PushController.getExpirationTime(request); PushController.getExpirationTime(body);
}).toThrow(); }).toThrow();
done(); done();
}); });
}); });

123
spec/PushRouter.spec.js Normal file
View File

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

View File

@@ -26,6 +26,14 @@ var defaultConfiguration = {
masterKey: 'test', masterKey: 'test',
collectionPrefix: 'test_', collectionPrefix: 'test_',
fileKey: 'test', fileKey: 'test',
push: {
'ios': {
cert: 'prodCert.pem',
key: 'prodKey.pem',
production: true,
bundleId: 'bundleId'
}
},
oauth: { // Override the facebook provider oauth: { // Override the facebook provider
facebook: mockFacebook(), facebook: mockFacebook(),
myoauth: { myoauth: {

View File

@@ -6,48 +6,14 @@ export class PushController {
constructor(pushAdapter) { constructor(pushAdapter) {
this._pushAdapter = pushAdapter; this._pushAdapter = pushAdapter;
} };
handlePOST(req) {
if (!this._pushAdapter) {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Push adapter is not availabe');
}
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. * Check whether the deviceType parameter in qury condition is valid or not.
* @param {Object} where A query condition * @param {Object} where A query condition
* @param {Array} validPushTypes An array of valid push types(string) * @param {Array} validPushTypes An array of valid push types(string)
*/ */
function validatePushType(where, validPushTypes) { static validatePushType(where = {}, validPushTypes = []) {
var where = where || {};
var deviceTypeField = where.deviceType || {}; var deviceTypeField = where.deviceType || {};
var deviceTypes = []; var deviceTypes = [];
if (typeof deviceTypeField === 'string') { if (typeof deviceTypeField === 'string') {
@@ -62,15 +28,42 @@ function validatePushType(where, validPushTypes) {
deviceType + ' is not supported push type.'); deviceType + ' is not supported push type.');
} }
} }
};
/**
* 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');
}
} }
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. * Get expiration time from the request body.
* @param {Object} request A request object * @param {Object} request A request object
* @returns {Number|undefined} The expiration time if it exists in the request * @returns {Number|undefined} The expiration time if it exists in the request
*/ */
function getExpirationTime(req) { static getExpirationTime(body = {}) {
var body = req.body || {};
var hasExpirationTime = !!body['expiration_time']; var hasExpirationTime = !!body['expiration_time'];
if (!hasExpirationTime) { if (!hasExpirationTime) {
return; return;
@@ -91,53 +84,7 @@ function getExpirationTime(req) {
body['expiration_time'] + ' is not valid time.'); body['expiration_time'] + ' is not valid time.');
} }
return expirationTime.valueOf(); 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
}
}
} else {
throw new Parse.Error(Parse.Error.PUSH_MISCONFIGURED,
'Channels and query should be set at least one.');
}
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;
}
export default PushController; export default PushController;

72
src/Routers/PushRouter.js Normal file
View File

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

View File

@@ -18,6 +18,7 @@ import { FilesController } from './Controllers/FilesController';
import ParsePushAdapter from './Adapters/Push/ParsePushAdapter'; import ParsePushAdapter from './Adapters/Push/ParsePushAdapter';
import { PushController } from './Controllers/PushController'; import { PushController } from './Controllers/PushController';
import { ClassesRouter } from './Routers/ClassesRouter'; import { ClassesRouter } from './Routers/ClassesRouter';
import { InstallationsRouter } from './Routers/InstallationsRouter'; import { InstallationsRouter } from './Routers/InstallationsRouter';
import { UsersRouter } from './Routers/UsersRouter'; import { UsersRouter } from './Routers/UsersRouter';
@@ -27,7 +28,7 @@ import { AnalyticsRouter } from './Routers/AnalyticsRouter';
import { FunctionsRouter } from './Routers/FunctionsRouter'; import { FunctionsRouter } from './Routers/FunctionsRouter';
import { SchemasRouter } from './Routers/SchemasRouter'; import { SchemasRouter } from './Routers/SchemasRouter';
import { IAPValidationRouter } from './Routers/IAPValidationRouter'; import { IAPValidationRouter } from './Routers/IAPValidationRouter';
import { PushRouter } from './Routers/PushRouter';
import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter'; import { FileLoggerAdapter } from './Adapters/Logger/FileLoggerAdapter';
import { LoggerController } from './Controllers/LoggerController'; import { LoggerController } from './Controllers/LoggerController';
@@ -111,6 +112,7 @@ function ParseServer({
} }
let filesController = new FilesController(filesAdapter); let filesController = new FilesController(filesAdapter);
let pushController = new PushController(pushAdapter);
cache.apps[appId] = { cache.apps[appId] = {
masterKey: masterKey, masterKey: masterKey,
@@ -122,6 +124,7 @@ function ParseServer({
fileKey: fileKey, fileKey: fileKey,
facebookAppIds: facebookAppIds, facebookAppIds: facebookAppIds,
filesController: filesController, filesController: filesController,
pushController: pushController,
enableAnonymousUsers: enableAnonymousUsers, enableAnonymousUsers: enableAnonymousUsers,
oauth: oauth, oauth: oauth,
}; };
@@ -161,7 +164,7 @@ function ParseServer({
new InstallationsRouter(), new InstallationsRouter(),
new FunctionsRouter(), new FunctionsRouter(),
new SchemasRouter(), new SchemasRouter(),
PushController.getExpressRouter(), new PushRouter(),
new LoggerController(loggerAdapter).getExpressRouter(), new LoggerController(loggerAdapter).getExpressRouter(),
new IAPValidationRouter() new IAPValidationRouter()
]; ];