coco-the-bear-auth-sessiontoken
Version:
A route handler for CoCo The Bear that enforces session token authentication and authorization.
278 lines (235 loc) • 11 kB
JavaScript
const EventEmitter = require('events');
const ObjectID = require('mongodb').ObjectID;
const httpErrors = require('@dannybster/coco-the-bear-http-errors');
const systemUnderTest = require('../src/authorization');
const testUtils = require('@dannybster/coco-the-bear-test-utils');
const {
notFound,
serviceUnavailable,
} = httpErrors;
describe('authorization.js', function () {
let collection;
let req;
beforeEach(function createCommonPreConditions() {
req = {
datasource: {
db: {
collection: (name) => {
if (name === 'authorizationDocuments') {
return collection;
}
return null;
},
},
},
events: {
emitter: new EventEmitter(),
},
user: {
_id: new ObjectID().toString(),
},
};
});
describe('#createAuthorizationDocument', function () {
let expectedAuthorizationDocument;
beforeEach(function createCommonPreConditionsForCreateAuthorizationDocument() {
collection = {
insertOne: (doc) => {
collection.insertOne.parameters = { doc };
},
};
req.datasource.result = {
_id: new ObjectID(),
};
expectedAuthorizationDocument = {
documentId: req.datasource.result._id,
userId: req.user._id,
};
});
it('It should ask the mongodb database at req.datasource.db to create an authorization record.', function testCreateAuthorizationDocumentDatabaseApiCall() {
systemUnderTest
.createAuthorizationDocument(req, null, testUtils.assertCalledOnce(testUtils.noop));
collection.insertOne.parameters.should.eql({ doc: expectedAuthorizationDocument });
});
it('It should emit an app event describing the authorization document.', function testCreateAuthorizationDocumentAppEvent(done) {
req.events.emitter.once('app-event', (data) => {
data.should.eql({
message: 'Creating an authorization document. See the object for more info.',
object: expectedAuthorizationDocument,
});
done();
});
systemUnderTest
.createAuthorizationDocument(req, null, testUtils.assertCalledOnce(testUtils.noop));
});
describe('If the database driver callsback without an error', function () {
beforeEach(function setupDatabaeToCallBackWithSuccess() {
collection.insertOne = (doc, callback) => {
process.nextTick(callback.bind(null));
};
});
it('It should call next without an error.', function testCreateAuthorizationDocumentSuccessCallsNext(done) {
systemUnderTest.createAuthorizationDocument(req, null, done);
});
it('It should emit an app-event describing that the authorization record was successfully create.', function testAppEventAuthorizationRecordSuccessfullyCreate(done) {
systemUnderTest
.createAuthorizationDocument(req, null, testUtils.assertCalledOnce(testUtils.noop));
req.events.emitter.on('app-event', (data) => {
data.should.eql({
message: 'Authorization document successfully created. See the object for more info.',
object: expectedAuthorizationDocument,
});
done();
});
});
});
describe('If the database driver callsback with an error', function () {
let expectedErr;
beforeEach(function setupDatabaseToCallbackWithError() {
const cause = new Error('Database says no!!!');
const message = 'Error creating an authorization document. See the info object for more info.';
const responseBody = { message: 'Error creating authorization document.' };
expectedErr = serviceUnavailable
.createError(message, expectedAuthorizationDocument, responseBody, cause);
collection.insertOne = (doc, callback) => {
process.nextTick(callback.bind(null, cause));
};
req.events.emitter.on('error', testUtils.noop);
});
it('It should callback with a service unavailable error.', function testDatabaseErrorCallsNextWithServiceUnavailableError(done) {
systemUnderTest.createAuthorizationDocument(req, null, testUtils.assertCalledOnce((err) => {
err.should.eql(expectedErr);
done();
}));
});
it('It should emit a descriptive error event instead of the successful app-event.', function testDatabaseErrorEmitsDescriptiveErrorEvent(done) {
systemUnderTest.createAuthorizationDocument(req, null, testUtils.noop);
const emitter = req.events.emitter;
emitter.on('app-event', () => {
throw new Error('This event should not have been called under error conditions.');
});
emitter.on('error', testUtils.assertCalledOnce((err) => {
err.should.eql(expectedErr);
done();
}));
});
});
});
describe('#authorizeRequest', function () {
let expectedAuthorizationDocument;
beforeEach(function createCommonPreConditionsForAuthorizeRequest() {
collection = {
findOne: (query) => {
collection.findOne.parameters = { query };
},
};
req.params = {
identifier: new ObjectID().toString(),
};
expectedAuthorizationDocument = {
documentId: new ObjectID(req.params.identifier),
userId: req.user._id,
};
});
it('It should ask the database driver to search for an authorization document matching the identifier parameter and the current user.', function testFindAuthorizationDocumentDatabaseApiCall() {
systemUnderTest.authorizeRequest(req, null, testUtils.noop);
collection.findOne.parameters.should.eql({
query: expectedAuthorizationDocument,
});
});
it('It should emit an app event that it is looking up an authorization document.', function testEmitFindAuthorizationDocumentEvent(done) {
req.events.emitter.once('app-event', (data) => {
data.should.eql({
message: 'Looking up an authorization document. See the object for more info.',
object: expectedAuthorizationDocument,
});
done();
});
systemUnderTest.authorizeRequest(req, null, testUtils.noop);
});
describe('If the database driver calls back without an error', function () {
describe('And a result object', function () {
let foundAuthorizationDocument;
beforeEach(function createPreConditionsForFoundAuthorizationDocument() {
collection.findOne = (query, callback) => {
foundAuthorizationDocument =
Object.assign({ _id: new ObjectID() }, expectedAuthorizationDocument);
process.nextTick(callback.bind(null, null, foundAuthorizationDocument));
};
});
it('It should call next.', function testDatabaseFindsAuthorizationDocumentCallsNext(done) {
systemUnderTest.authorizeRequest(req, null, testUtils.assertCalledOnce(done));
});
it('It should emit an app-event that describes it has found an authorization document.', function testDatabaseFindsAuthorizationDocumentEmitsAppEvent(done) {
systemUnderTest.authorizeRequest(req, null, testUtils.noop);
req.events.emitter.on('app-event', testUtils.assertCalledOnce((data) => {
data.should.eql({
message: 'Successfully found an authorization document. See the object for more info.',
object: foundAuthorizationDocument,
});
done();
}));
});
});
describe('And a null result object', function () {
let expectedNotFoundError;
beforeEach(function createPreConditionsForNotFoundAuthorizationDocument() {
const message = 'Authorization document not found. See the info object for more info.';
const responseBody = { message: `A document matching ${expectedAuthorizationDocument.documentId.toString()} could not be found for the user ${req.user._id}` };
expectedNotFoundError =
notFound.createError(message, expectedAuthorizationDocument, responseBody);
req.events.emitter.on('error', testUtils.noop);
collection.findOne = (query, callback) => {
process.nextTick(callback.bind(null, null, null));
};
});
it('It should call next with a not found error.', function testAuthorizationDocumentNotFoundCallsNextWithNotFoundError(done) {
systemUnderTest.authorizeRequest(req, null, testUtils.assertCalledOnce((err) => {
err.should.eql(expectedNotFoundError);
done();
}));
});
it('It should emit a descriptive not found error event instead of a successful app-event.', function testAuthorizationDocumentNotFoundEmitsNotFoundError(done) {
systemUnderTest.authorizeRequest(req, null, testUtils.noop);
req.events.emitter.on('app-event', () => {
throw new Error('app-event should not have been called.');
});
req.events.emitter.on('error', testUtils.assertCalledOnce((err) => {
err.should.eql(expectedNotFoundError);
done();
}));
});
});
});
describe('If the database driver calls back with an error', function () {
const cause = new Error('These are not the databases you are looking for...');
let expectedServiceUnavailablError;
beforeEach(function createPreConditionsForNotFoundAuthorizationDocument() {
const message = 'Error finding an authorization document. See the info object for more info.';
const responseBody = { message: 'Error finding an authorization document.' };
expectedServiceUnavailablError =
notFound.createError(message, expectedAuthorizationDocument, responseBody, cause);
req.events.emitter.on('error', testUtils.noop);
collection.findOne = (query, callback) => {
process.nextTick(callback.bind(null, cause));
};
});
it('It should call next with a descriptive service unavailable error.', function testDatabaseDriverErrorCallsNextWithServiceUnvailableError(done) {
systemUnderTest.authorizeRequest(req, null, testUtils.assertCalledOnce((err) => {
err.should.eql(expectedServiceUnavailablError);
done();
}));
});
it('It should emit a descriptive service unavailable error event instead of a successful app-event.', function testAuthorizationDocumentNotFoundEmitsNotFoundError(done) {
systemUnderTest.authorizeRequest(req, null, testUtils.noop);
req.events.emitter.on('app-event', () => {
throw new Error('app-event should not have been called.');
});
req.events.emitter.on('error', testUtils.assertCalledOnce((err) => {
err.should.eql(expectedServiceUnavailablError);
done();
}));
});
});
});
});