846 lines
33 KiB
JavaScript
846 lines
33 KiB
JavaScript
/**
|
|
* Unit testing file for ElevateSession command
|
|
*/
|
|
'use strict';
|
|
/* eslint max-nested-callbacks: ["error", 7] import/max-dependencies: ["error", {"max": 13}] */
|
|
// eslint-disable-next-line no-unused-vars
|
|
const testGlobals = require('../../tools/test/testGlobals.js');
|
|
const _ = require('lodash');
|
|
const Q = require('q');
|
|
const chai = require('chai');
|
|
const sinon = require('sinon');
|
|
const sinonChai = require('sinon-chai');
|
|
const chaiAsPromised = require('chai-as-promised');
|
|
const rewire = require('rewire');
|
|
const JsonRefs = require('json-refs');
|
|
const mongodb = require('mongodb');
|
|
|
|
const {MockRequest} = require('../../utils/test/mock-request');
|
|
const {bridgeBodyParser} = require('../api_body_middleware.js');
|
|
|
|
const utils = require('../../ComServe/utils');
|
|
|
|
/**
|
|
* Use rewire to pull in the unit under test, and then get access to the
|
|
* private variables to stub them
|
|
*/
|
|
const apiSecurityDevice = rewire('../api_security_device.js');
|
|
|
|
const authStub = apiSecurityDevice.__get__('auth');
|
|
const flagsStub = apiSecurityDevice.__get__('featureFlags');
|
|
const referencesStub = apiSecurityDevice.__get__('references');
|
|
const mainDBPStub = apiSecurityDevice.__get__('mainDBP');
|
|
|
|
/**
|
|
* Set up chai & sinon to simplify the tests
|
|
*/
|
|
const expect = chai.expect;
|
|
const sandbox = sinon.createSandbox();
|
|
|
|
chai.use(sinonChai);
|
|
chai.use(chaiAsPromised);
|
|
|
|
/**
|
|
* Make a promise-style version of the security handler for easier testing
|
|
*/
|
|
const deviceSessionP = (req, def, scopes) => Q.nfcall(
|
|
apiSecurityDevice.deviceSession,
|
|
req,
|
|
def,
|
|
scopes
|
|
);
|
|
const hmacNoSessionP = (req, def, scopes) => Q.nfcall(
|
|
apiSecurityDevice.deviceHmacNoSession,
|
|
req,
|
|
def,
|
|
scopes
|
|
);
|
|
|
|
const COLLECTION_DEVICES = 'Mock devices collection parameter';
|
|
|
|
/**
|
|
* Valid values
|
|
*/
|
|
const DEVICE_TOKEN = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop';
|
|
const SESSION_TOKEN = 'qrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUV';
|
|
const DEVICE_MONGO_ID = (new mongodb.ObjectID()).toHexString(); // New random ObjectID
|
|
const CLIENT_NAME = 'a@example.com';
|
|
const CLIENT_MONGO_ID = (new mongodb.ObjectID()).toHexString(); // New random ObjectID
|
|
const CLIENT_ID = 'A unique random value generated by us';
|
|
const CLIENT_DISPLAY_NAME = 'Display Name';
|
|
|
|
const SESSION_HEADER = DEVICE_TOKEN + ':' + SESSION_TOKEN;
|
|
const HMAC_HEADER = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
|
|
const TIMESTAMP_HEADER = new Date().toISOString();
|
|
|
|
const PROTOCOL = 'https';
|
|
const PATH = '/api/v0/devices';
|
|
const EXPECTED_FULL_URL = 'https://unittest.example.com' + PATH;
|
|
const METHOD = 'get';
|
|
|
|
const SESSION_ID = 'A session id as-if generated by express-session middleware';
|
|
|
|
const DB_FEATURE_FLAGS = ['unit-test'];
|
|
const DB_DEVICE = {
|
|
ClientID: CLIENT_ID,
|
|
DeviceToken: DEVICE_TOKEN,
|
|
DeviceStatus: utils.DeviceFullyRegistered
|
|
};
|
|
const DB_CLIENT = {
|
|
_id: CLIENT_MONGO_ID,
|
|
ClientName: CLIENT_NAME,
|
|
ClientID: CLIENT_ID,
|
|
DisplayName: CLIENT_DISPLAY_NAME,
|
|
FeatureFlags: DB_FEATURE_FLAGS,
|
|
ClientStatus: utils.ClientEmailVerifiedMask
|
|
};
|
|
|
|
const MOCK_SWAGGER_PATHNAME = '/test-api-security-device';
|
|
const MOCK_SWAGGER_FEATURE_FLAG = 'unit-test';
|
|
const MOCK_SWAGGER_DEFINITION = {
|
|
post: {
|
|
summary: 'Just a test',
|
|
description: 'Just a test',
|
|
'x-feature-flag': MOCK_SWAGGER_FEATURE_FLAG,
|
|
responses: {
|
|
200: {
|
|
description: 'Success'
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Mock request for requests that use the standard security model
|
|
*/
|
|
const MOCK_REQUEST_OPTIONS = {
|
|
headers: {
|
|
'x-bridge-device-session': SESSION_HEADER,
|
|
'x-bridge-hmac': HMAC_HEADER,
|
|
'x-bridge-timestamp': TIMESTAMP_HEADER
|
|
},
|
|
originalUrl: PATH,
|
|
protocol: PROTOCOL,
|
|
method: METHOD,
|
|
sessionID: SESSION_ID,
|
|
session: {} // Empty session as-if created by express-session
|
|
};
|
|
|
|
const MOCK_REQUEST_BODY = '{\n' +
|
|
' "test": "value",\n' +
|
|
' "other": "value2"\n' +
|
|
'}';
|
|
const MOCK_REQUEST_BODY_OPTIONS = {
|
|
mockBody: MOCK_REQUEST_BODY
|
|
};
|
|
|
|
/**
|
|
* Mock request for requests that use the "no session" security model
|
|
*/
|
|
const SWAGGER_PATH_LOGIN = '/devices/{objectId}/login';
|
|
const PATH_LOGIN = '/api/v0/devices/' + DEVICE_MONGO_ID + '/login';
|
|
const EXPECTED_FULL_URL_LOGIN = 'https://unittest.example.com' + PATH_LOGIN;
|
|
const METHOD_LOGIN = 'POST';
|
|
|
|
const MOCK_LOGIN_REQUEST_OPTIONS = {
|
|
headers: {
|
|
'x-bridge-hmac': HMAC_HEADER,
|
|
'x-bridge-timestamp': TIMESTAMP_HEADER
|
|
},
|
|
originalUrl: PATH_LOGIN,
|
|
protocol: PROTOCOL,
|
|
method: METHOD_LOGIN,
|
|
sessionID: SESSION_ID,
|
|
session: {} // Empty session as-if created by express-session
|
|
};
|
|
|
|
const MOCK_LOGIN_REQUEST_BODY = '{\n' +
|
|
' "ClientName": "' + CLIENT_NAME + '"\n' +
|
|
'}';
|
|
|
|
const MOCK_LOGIN_REQUEST_BODY_OPTIONS = {
|
|
mockBody: MOCK_LOGIN_REQUEST_BODY
|
|
};
|
|
|
|
/**
|
|
* Function to create a mock `req` objects that mimics the important parts of a
|
|
* real request object.
|
|
*
|
|
* @param {Object} resolvedSwagger - The **resolved** swagger object (i.e. all refs resolved)
|
|
* @param {Object} reqOptions - The additional fields to add to the request object
|
|
* @param {Object} bodyOptions - Additional params for creating the MockRequest (provides the body)
|
|
*/
|
|
function createMockReq(resolvedSwagger, reqOptions, bodyOptions) {
|
|
const req = new MockRequest(_.cloneDeep(bodyOptions));
|
|
_.merge(req, _.cloneDeep(reqOptions));
|
|
return req;
|
|
}
|
|
|
|
/**
|
|
* The tests
|
|
*/
|
|
describe('Device security validation', () => {
|
|
let resolvedSwagger;
|
|
let req;
|
|
const def = {};
|
|
|
|
/**
|
|
* Before we run any tests we need to resolve all the references within
|
|
* the swagger specification.
|
|
*/
|
|
before(() => {
|
|
/**
|
|
* Set some values for the collections so we can differentiate them
|
|
*/
|
|
mainDBPStub.mainDB._collectionDevice = mainDBPStub.mainDB.collectionDevice;
|
|
mainDBPStub.mainDB.collectionDevice = COLLECTION_DEVICES;
|
|
|
|
/**
|
|
* Load the swagger files and merge them back into a single file
|
|
*/
|
|
return JsonRefs
|
|
.resolveRefsAt(require.resolve('../api_swagger_def.json'))
|
|
.then((swagger) => {
|
|
/**
|
|
* Add our test path to the swagger
|
|
*/
|
|
resolvedSwagger = swagger.resolved;
|
|
resolvedSwagger.paths[MOCK_SWAGGER_PATHNAME] = MOCK_SWAGGER_DEFINITION;
|
|
|
|
//
|
|
// Add them to the default options
|
|
//
|
|
_.assign(MOCK_REQUEST_OPTIONS, {
|
|
swagger: {
|
|
swaggerObject: resolvedSwagger,
|
|
operation: resolvedSwagger.paths[MOCK_SWAGGER_PATHNAME].post
|
|
}
|
|
});
|
|
|
|
return resolvedSwagger;
|
|
});
|
|
});
|
|
|
|
after(() => {
|
|
/**
|
|
* Set the collections back
|
|
*/
|
|
mainDBPStub.mainDB.collectionDevice = mainDBPStub.mainDB._collectionDevice;
|
|
delete mainDBPStub.mainDB._collectionDevice;
|
|
});
|
|
|
|
describe('device_session security', () => {
|
|
/**
|
|
* Before each test, stub the auth functions we will be calling.
|
|
*/
|
|
beforeEach((done) => {
|
|
req = createMockReq(resolvedSwagger, MOCK_REQUEST_OPTIONS, MOCK_REQUEST_BODY_OPTIONS);
|
|
|
|
sandbox.stub(authStub, 'validateCurrentSession').resolves([DB_DEVICE, DB_CLIENT]);
|
|
sandbox.stub(authStub, 'checkHMAC').resolves();
|
|
sandbox.spy(flagsStub, 'isEnabled');
|
|
|
|
//
|
|
// Run the mock request through the middleware to get the parsed and raw bodies
|
|
//
|
|
bridgeBodyParser()(req, {}, done);
|
|
});
|
|
|
|
/**
|
|
* After each test we reset the sanbox to reset all stubs etc.
|
|
*/
|
|
afterEach(() => {
|
|
sandbox.restore();
|
|
});
|
|
|
|
describe('With validly formatted params that are correct', () => {
|
|
it('checks the device and session tokens', () => {
|
|
return deviceSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(authStub.validateCurrentSession)
|
|
.to.have.been.calledOnce
|
|
.calledWith(
|
|
DEVICE_TOKEN,
|
|
SESSION_TOKEN
|
|
);
|
|
});
|
|
});
|
|
|
|
it('checks the HMAC', () => {
|
|
return deviceSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(authStub.checkHMAC)
|
|
.to.have.been.calledOnce
|
|
.calledWith(
|
|
DB_DEVICE,
|
|
{
|
|
address: EXPECTED_FULL_URL,
|
|
method: METHOD,
|
|
body: MOCK_REQUEST_BODY + DEVICE_TOKEN + ':' + SESSION_TOKEN,
|
|
ClientName: CLIENT_NAME,
|
|
timestamp: TIMESTAMP_HEADER,
|
|
hmac: HMAC_HEADER
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
it('checks the FeatureFlags if they are required for the request', () => {
|
|
return deviceSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(flagsStub.isEnabled)
|
|
.to.have.been.calledOnce
|
|
.calledWith(
|
|
MOCK_SWAGGER_FEATURE_FLAG,
|
|
DB_CLIENT
|
|
);
|
|
});
|
|
});
|
|
|
|
it('doesn\'t check the FeatureFlags if they are NOT required for the request', () => {
|
|
delete req.swagger.operation['x-feature-flag'];
|
|
return deviceSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(flagsStub.isEnabled)
|
|
.to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
it('stores web session data + the client (as clientObj) and device (as deviceObj) in req.session.data for controller to use', () => {
|
|
return deviceSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(req.session.data)
|
|
.to.deep.equal({
|
|
//
|
|
// Existing session details for old web console requests
|
|
//
|
|
client: CLIENT_MONGO_ID,
|
|
clientID: CLIENT_ID,
|
|
displayName: CLIENT_DISPLAY_NAME,
|
|
email: CLIENT_NAME,
|
|
isMerchant: false,
|
|
isVATRegistered: false,
|
|
FeatureFlags: DB_FEATURE_FLAGS,
|
|
|
|
//
|
|
// New sessiond details for App APIs copied across
|
|
//
|
|
clientObj: DB_CLIENT,
|
|
deviceObj: DB_DEVICE,
|
|
isDeviceSession: true
|
|
});
|
|
});
|
|
});
|
|
|
|
it('clears req.sessionID so express-session doesn\'t persist the session', () => {
|
|
return deviceSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(req.sessionID)
|
|
.to.be.null;
|
|
});
|
|
});
|
|
|
|
it('passes the security tests', () => {
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER))
|
|
.to.eventually.be.fulfilled;
|
|
});
|
|
});
|
|
|
|
describe('With validly formatted params that are wrong', () => {
|
|
it('rejects when required feature flag isn\'t enabled', () => {
|
|
const NO_FEATURE_FLAG_CLIENT = {
|
|
ClientName: CLIENT_NAME
|
|
};
|
|
authStub.validateCurrentSession.resolves([DB_DEVICE, NO_FEATURE_FLAG_CLIENT]);
|
|
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('rejects when validating current session fails', () => {
|
|
authStub.validateCurrentSession.rejects();
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('rejects when checking HMAC fails', () => {
|
|
authStub.checkHMAC.rejects();
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('leaves req.sessionID alone when device session verifcation fails', () => {
|
|
authStub.checkHMAC.rejects();
|
|
return deviceSessionP(req, def, SESSION_HEADER).catch(() => {
|
|
return expect(req.sessionID)
|
|
.to.equal(SESSION_ID);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('With invalidly formatted params', () => {
|
|
describe('Rejects when x-bridge-session-device is the wrong format: ', () => {
|
|
it('missing entirely', () => {
|
|
return expect(deviceSessionP(req, def, undefined)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('device token too short', () => {
|
|
const token = DEVICE_TOKEN.slice(0, -1) + ':' + SESSION_TOKEN;
|
|
return expect(deviceSessionP(req, def, token)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('device token too long', () => {
|
|
const token = DEVICE_TOKEN + 'a:' + SESSION_TOKEN;
|
|
return expect(deviceSessionP(req, def, token)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('device token invalid char', () => {
|
|
const token = DEVICE_TOKEN.slice(0, -1) + '!:' + SESSION_TOKEN;
|
|
return expect(deviceSessionP(req, def, token)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('session token too short', () => {
|
|
const token = DEVICE_TOKEN + ':' + SESSION_TOKEN.slice(0, -1);
|
|
return expect(deviceSessionP(req, def, token)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('session token too long', () => {
|
|
const token = DEVICE_TOKEN + ':b' + SESSION_TOKEN;
|
|
return expect(deviceSessionP(req, def, token)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('session token invalid char', () => {
|
|
const token = DEVICE_TOKEN + ':?' + SESSION_TOKEN.slice(0, -1);
|
|
return expect(deviceSessionP(req, def, token)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('wrong character between tokens', () => {
|
|
const token = DEVICE_TOKEN + ';' + SESSION_TOKEN;
|
|
return expect(deviceSessionP(req, def, token)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
});
|
|
|
|
describe('Rejects when x-bridge-hmac is the wrong format: ', () => {
|
|
it('missing entirely', () => {
|
|
delete req.headers['x-bridge-hmac'];
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('too short', () => {
|
|
req.headers['x-bridge-hmac'] = HMAC_HEADER.slice(0, -1);
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('too long', () => {
|
|
req.headers['x-bridge-hmac'] = HMAC_HEADER + 'a';
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('invalid char', () => {
|
|
req.headers['x-bridge-hmac'] = HMAC_HEADER.slice(0, -1) + '!';
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
});
|
|
|
|
describe('Rejects when x-bridge-timestamp is the wrong format: ', () => {
|
|
it('missing entirely', () => {
|
|
delete req.headers['x-bridge-timestamp'];
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('not a string', () => {
|
|
req.headers['x-bridge-timestamp'] = Date.now();
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('only has date', () => {
|
|
req.headers['x-bridge-timestamp'] = new Date().toDateString();
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('only has time', () => {
|
|
req.headers['x-bridge-timestamp'] = new Date().toTimeString();
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('device_hmac_nosession security', () => {
|
|
/**
|
|
* Before each test, stub the auth functions we will be calling.
|
|
*/
|
|
beforeEach((done) => {
|
|
//
|
|
// Setup the swagger details as-if parsed from the request by the
|
|
// swagger middleware
|
|
//
|
|
_.assign(MOCK_LOGIN_REQUEST_OPTIONS, {
|
|
swagger: {
|
|
swaggerObject: resolvedSwagger,
|
|
operation: resolvedSwagger.paths[SWAGGER_PATH_LOGIN].post,
|
|
params: {
|
|
objectId: {
|
|
value: DEVICE_MONGO_ID
|
|
},
|
|
body: {
|
|
value: {
|
|
ClientName: CLIENT_NAME
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
req = createMockReq(
|
|
resolvedSwagger,
|
|
MOCK_LOGIN_REQUEST_OPTIONS,
|
|
MOCK_LOGIN_REQUEST_BODY_OPTIONS
|
|
);
|
|
|
|
sandbox.stub(authStub, 'checkHMAC').resolves();
|
|
sandbox.stub(referencesStub, 'getClientByEmail').resolves(DB_CLIENT);
|
|
sandbox.stub(mainDBPStub, 'findOneObject').resolves(DB_DEVICE);
|
|
|
|
sandbox.spy(flagsStub, 'isEnabled');
|
|
sandbox.spy(authStub, 'checkClientStatus');
|
|
sandbox.spy(authStub, 'checkDeviceStatus');
|
|
|
|
//
|
|
// Run the mock request through the middleware to get the parsed and raw bodies
|
|
//
|
|
bridgeBodyParser()(req, {}, done);
|
|
});
|
|
|
|
/**
|
|
* After each test we reset the sanbox to reset all stubs etc.
|
|
*/
|
|
afterEach(() => {
|
|
sandbox.restore();
|
|
});
|
|
|
|
describe('With validly formatted params that are correct', () => {
|
|
it('gets the client based on the ClientName in the body', () => {
|
|
return hmacNoSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(referencesStub.getClientByEmail)
|
|
.to.have.been.calledOnce
|
|
.calledWith(CLIENT_NAME);
|
|
});
|
|
});
|
|
|
|
it('gets the device based on the objectID if it is owned by the correct client', () => {
|
|
return hmacNoSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(mainDBPStub.findOneObject)
|
|
.to.have.been.calledOnce
|
|
.calledWith(
|
|
mainDBPStub.mainDB.collectionDevice,
|
|
{
|
|
_id: mongodb.ObjectID(DEVICE_MONGO_ID),
|
|
ClientID: CLIENT_ID
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
it('checks the client is in a valid state', () => {
|
|
return hmacNoSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(authStub.checkClientStatus)
|
|
.to.have.been.calledOnce
|
|
.calledWith(utils.ClientEmailVerifiedMask)
|
|
.returned(null); // Null for no errors
|
|
});
|
|
});
|
|
|
|
it('checks the device is in a valid state', () => {
|
|
return hmacNoSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(authStub.checkDeviceStatus)
|
|
.to.have.been.calledOnce
|
|
.calledWith(utils.DeviceFullyRegistered)
|
|
.returned(null); // Null for no errors
|
|
});
|
|
});
|
|
|
|
it('checks the HMAC, with the function name set to Login1.process', () => {
|
|
return hmacNoSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(authStub.checkHMAC)
|
|
.to.have.been.calledOnce
|
|
.calledWith(
|
|
DB_DEVICE,
|
|
{
|
|
address: EXPECTED_FULL_URL_LOGIN,
|
|
method: METHOD_LOGIN,
|
|
body: MOCK_LOGIN_REQUEST_BODY,
|
|
ClientName: CLIENT_NAME,
|
|
timestamp: TIMESTAMP_HEADER,
|
|
hmac: HMAC_HEADER
|
|
},
|
|
'Login1.process' // Renamed to Login1.process to match expectations
|
|
);
|
|
});
|
|
});
|
|
|
|
it('checks the FeatureFlags if they are required for the request', () => {
|
|
req.swagger.operation['x-feature-flag'] = MOCK_SWAGGER_FEATURE_FLAG;
|
|
return hmacNoSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(flagsStub.isEnabled)
|
|
.to.have.been.calledOnce
|
|
.calledWith(
|
|
MOCK_SWAGGER_FEATURE_FLAG,
|
|
DB_CLIENT
|
|
);
|
|
});
|
|
});
|
|
|
|
it('doesn\'t check the FeatureFlags if they are NOT required for the request', () => {
|
|
delete req.swagger.operation['x-feature-flag'];
|
|
return hmacNoSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(flagsStub.isEnabled)
|
|
.to.not.have.been.called;
|
|
});
|
|
});
|
|
|
|
it('stores web session data + the client (as clientObj) and device (as deviceObj) in req.session.data for controller to use', () => {
|
|
return hmacNoSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(req.session.data)
|
|
.to.deep.equal({
|
|
//
|
|
// Existing session details for old web console requests
|
|
//
|
|
client: CLIENT_MONGO_ID,
|
|
clientID: CLIENT_ID,
|
|
displayName: CLIENT_DISPLAY_NAME,
|
|
email: CLIENT_NAME,
|
|
isMerchant: false,
|
|
isVATRegistered: false,
|
|
FeatureFlags: DB_FEATURE_FLAGS,
|
|
|
|
//
|
|
// New sessiond details for App APIs copied across
|
|
//
|
|
clientObj: DB_CLIENT,
|
|
deviceObj: DB_DEVICE,
|
|
isDeviceSession: true
|
|
});
|
|
});
|
|
});
|
|
|
|
it('clears req.sessionID so express-session doesn\'t persist the session', () => {
|
|
return hmacNoSessionP(req, def, SESSION_HEADER).then(() => {
|
|
return expect(req.sessionID)
|
|
.to.be.null;
|
|
});
|
|
});
|
|
|
|
it('passes the security tests', () => {
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER))
|
|
.to.eventually.be.fulfilled;
|
|
});
|
|
});
|
|
|
|
describe('With validly formatted params that are wrong', () => {
|
|
it('rejects when required feature flag isn\'t enabled', () => {
|
|
// Fake that a feature flag is required
|
|
req.swagger.operation['x-feature-flag'] = MOCK_SWAGGER_FEATURE_FLAG;
|
|
|
|
// Return a client that doesn't have that flag
|
|
const modifiedClient = _.cloneDeep(DB_CLIENT);
|
|
modifiedClient.FeatureFlags = [];
|
|
referencesStub.getClientByEmail.resolves(modifiedClient);
|
|
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('rejects when finding the client fails', () => {
|
|
referencesStub.getClientByEmail.rejects();
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('rejects when finding the device fails', () => {
|
|
mainDBPStub.findOneObject.rejects();
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('rejects when the client isn\'t verified', () => {
|
|
const modifiedClient = _.cloneDeep(DB_CLIENT);
|
|
modifiedClient.ClientStatus = 0;
|
|
referencesStub.getClientByEmail.resolves(modifiedClient);
|
|
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('rejects when the client is barred', () => {
|
|
const modifiedClient = _.cloneDeep(DB_CLIENT);
|
|
modifiedClient.ClientStatus |= utils.ClientBarredMask;
|
|
referencesStub.getClientByEmail.resolves(modifiedClient);
|
|
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('rejects when the device isn\'t completely registered', () => {
|
|
const modifiedDevice = _.cloneDeep(DB_DEVICE);
|
|
modifiedDevice.DeviceStatus = utils.DeviceRegister2Mask;
|
|
mainDBPStub.findOneObject.resolves(modifiedDevice);
|
|
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('rejects when the device is suspended', () => {
|
|
const modifiedDevice = _.cloneDeep(DB_DEVICE);
|
|
modifiedDevice.DeviceStatus |= utils.DeviceSuspendedMask;
|
|
mainDBPStub.findOneObject.resolves(modifiedDevice);
|
|
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('rejects when the device is barred', () => {
|
|
const modifiedDevice = _.cloneDeep(DB_DEVICE);
|
|
modifiedDevice.DeviceStatus |= utils.DeviceBarredMask;
|
|
mainDBPStub.findOneObject.resolves(modifiedDevice);
|
|
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('rejects when checking HMAC fails', () => {
|
|
authStub.checkHMAC.rejects();
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER))
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('leaves req.sessionID alone when device session verifcation fails', () => {
|
|
authStub.checkHMAC.rejects();
|
|
return hmacNoSessionP(req, def, SESSION_HEADER).catch(() => {
|
|
return expect(req.sessionID)
|
|
.to.equal(SESSION_ID);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('With invalidly formatted params', () => {
|
|
describe('Rejects when x-bridge-hmac is the wrong format: ', () => {
|
|
it('missing entirely', () => {
|
|
delete req.headers['x-bridge-hmac'];
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('too short', () => {
|
|
req.headers['x-bridge-hmac'] = HMAC_HEADER.slice(0, -1);
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('too long', () => {
|
|
req.headers['x-bridge-hmac'] = HMAC_HEADER + 'a';
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('invalid char', () => {
|
|
req.headers['x-bridge-hmac'] = HMAC_HEADER.slice(0, -1) + '!';
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
});
|
|
|
|
describe('Rejects when x-bridge-timestamp is the wrong format: ', () => {
|
|
it('missing entirely', () => {
|
|
delete req.headers['x-bridge-timestamp'];
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('not a string', () => {
|
|
req.headers['x-bridge-timestamp'] = Date.now();
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('only has date', () => {
|
|
req.headers['x-bridge-timestamp'] = new Date().toDateString();
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('only has time', () => {
|
|
req.headers['x-bridge-timestamp'] = new Date().toTimeString();
|
|
return expect(deviceSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
});
|
|
|
|
describe('Rejects when objectId is the wrong format: ', () => {
|
|
it('too short', () => {
|
|
req.swagger.params.objectId.value = DEVICE_MONGO_ID.slice(0, -1);
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('too long', () => {
|
|
req.swagger.params.objectId.value = DEVICE_MONGO_ID + 'a';
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('invalid char', () => {
|
|
req.swagger.params.objectId.value = DEVICE_MONGO_ID.slice(0, -1) + 'g';
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
});
|
|
|
|
describe('Rejects when ClientName is the wrong format: ', () => {
|
|
it('too short', () => {
|
|
req.swagger.params.body.value.ClientName = 'a@b.co';
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('too long', () => {
|
|
req.swagger.params.body.value.ClientName =
|
|
'a@' +
|
|
'b'.repeat(249) +
|
|
'.com';
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('invalid char', () => {
|
|
req.swagger.params.body.value.ClientName = 'a@Bücher.example'; // No IDN support
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('missing the @', () => {
|
|
req.swagger.params.body.value.ClientName = 'example.com';
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
|
|
it('missing the tld', () => {
|
|
req.swagger.params.body.value.ClientName = 'aexample';
|
|
return expect(hmacNoSessionP(req, def, SESSION_HEADER)).to
|
|
.eventually.be.rejected;
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|