From ac2ec4c41f64a2aa72ccde5be2365021bfbc1cc1 Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 05:37:12 -0400 Subject: [PATCH 01/14] New IdentityChannelController class new file: lib/services/identity/controllers/identity-channel-controller.js --- .../identity-channel-controller.js | 88 +++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 lib/services/identity/controllers/identity-channel-controller.js diff --git a/lib/services/identity/controllers/identity-channel-controller.js b/lib/services/identity/controllers/identity-channel-controller.js new file mode 100644 index 0000000..7554426 --- /dev/null +++ b/lib/services/identity/controllers/identity-channel-controller.js @@ -0,0 +1,88 @@ +'use strict'; + +const Promise = require('bluebird'); +const _ = require('lodash'); +const Boom = require('boom'); + +const Controller = require('../../../controllers/controller'); +const IdentityItemController = require('./identity-item-controller'); + +class IdentityChannelController extends IdentityItemController { + get(req, res, next) { + const type = 'channel'; + const id = req.params.id; + const args = {type, id}; + + if (req.query.include) { + args.include = req.query.include.split(','); + } + + return this.bus.query({role: 'store', cmd: 'get', type}, args) + .then(resource => { + if (!resource) { + return Promise.reject(Boom.notFound('resource not found')); + } + + res.body = resource; + res.status(200); + next(); + return null; + }) + .catch(next); + } + + patch(req, res, next) { + const type = 'channel'; + const id = req.params.id; + const payload = req.body; + const args = {type, id}; + + return this.bus.query({role: 'store', cmd: 'get', type}, args) + .then(resource => { + if (resource) { + resource = _.merge({}, resource, payload); + resource.type = type; + resource.id = id; + + return this.bus.sendCommand({role: 'store', cmd: 'set', type}, resource); + } + + return Promise.reject(Boom.notFound(`${type} "${id}" not found`)); + }) + .then(resource => { + res.body = resource; + res.status(200); + next(); + return null; + }) + .catch(next); + } + + delete(req, res, next) { + const type = 'channel'; + const id = req.params.id; + const args = {type, id}; + + return this.bus.sendCommand({role: 'store', cmd: 'remove', type}, args) + .then(() => { + res.body = {}; + res.status(200); + next(); + return null; + }) + .catch(next); + } + + static create(spec) { + if (!spec.bus || !_.isObject(spec.bus)) { + throw new Error('IdentityChannelController spec.bus is required'); + } + + return Controller.create(new IdentityChannelController({ + bus: spec.bus, + type: 'channel' + })); + } +} + +module.exports = IdentityChannelController; From b0ecd3d26c8d36758af780d9b5c2045beeb2eaca Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 05:37:32 -0400 Subject: [PATCH 02/14] New IdentityChannelsListController class new file: lib/services/identity/controllers/identity-channels-list-controller.js --- .../identity-channels-list-controller.js | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 lib/services/identity/controllers/identity-channels-list-controller.js diff --git a/lib/services/identity/controllers/identity-channels-list-controller.js b/lib/services/identity/controllers/identity-channels-list-controller.js new file mode 100644 index 0000000..99a7879 --- /dev/null +++ b/lib/services/identity/controllers/identity-channels-list-controller.js @@ -0,0 +1,80 @@ +'use strict'; + +const Promise = require('bluebird'); +const _ = require('lodash'); +const Boom = require('boom'); + +const Controller = require('../../../controllers/controller'); +const IdentityListController = require('./identity-list-controller'); + +class IdentityChannelsListController extends IdentityListController { + get(req, res, next) { + const type = 'channel'; + const limit = parseInt(req.query.limit, 10) || 10; + const args = {type, limit}; + + return this.bus.query({role: 'store', cmd: 'scan', type}, args) + .then(resources => { + // If this is not an admin request then we filter out all channel + // resources from the response other than the one which matches the + // one the caller is authenticated for. + if (!this.isAdminRequest(req)) { + const channelId = (req.identity.channel || {}).id; + resources = resources.filter(item => { + return item.id === channelId; + }); + } + + res.status(200); + res.body = resources.slice(0, limit); + next(); + return null; + }) + .catch(next); + } + + post(req, res, next) { + const type = 'channel'; + const payload = _.cloneDeep(req.body); + payload.type = type; + + // If there is a client defined id, then we check the store for a + // conflict before moving on. + let existingResource; + if (payload.id) { + const args = {type, id: payload.id}; + existingResource = this.bus.query({role: 'store', cmd: 'get', type}, args); + } else { + existingResource = Promise.resolve(null); + } + + return existingResource + .then(resource => { + // Return a 409 error if there was a conflict in the store. + if (resource) { + return Promise.reject(Boom.conflict(`The ${type} "${payload.id}" already exists`)); + } + return this.bus.sendCommand({role: 'store', cmd: 'set', type}, payload); + }) + .then(resource => { + res.body = resource; + res.status(201); + next(); + return null; + }) + .catch(next); + } + + static create(spec) { + if (!spec.bus || !_.isObject(spec.bus)) { + throw new Error('IdentityChannelsListController spec.bus is required'); + } + + return Controller.create(new IdentityChannelsListController({ + bus: spec.bus, + type: 'channel' + })); + } +} + +module.exports = IdentityChannelsListController; From 241e233082e66d1bdcadb3eaf502d05d54423f9f Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 05:50:57 -0400 Subject: [PATCH 03/14] Deny unauthorized access to channel type. modified: lib/services/identity/controllers/identity-channel-controller.js --- .../identity-channel-controller.js | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/lib/services/identity/controllers/identity-channel-controller.js b/lib/services/identity/controllers/identity-channel-controller.js index 7554426..1653ae5 100644 --- a/lib/services/identity/controllers/identity-channel-controller.js +++ b/lib/services/identity/controllers/identity-channel-controller.js @@ -13,6 +13,10 @@ class IdentityChannelController extends IdentityItemController { const id = req.params.id; const args = {type, id}; + if (!this.checkChannelAccess(req)) { + return next(Boom.forbidden('Access to the requested channel is forbidden')); + } + if (req.query.include) { args.include = req.query.include.split(','); } @@ -37,6 +41,10 @@ class IdentityChannelController extends IdentityItemController { const payload = req.body; const args = {type, id}; + if (!this.checkChannelAccess(req)) { + return next(Boom.forbidden('Access to the requested channel is forbidden')); + } + return this.bus.query({role: 'store', cmd: 'get', type}, args) .then(resource => { if (resource) { @@ -63,6 +71,10 @@ class IdentityChannelController extends IdentityItemController { const id = req.params.id; const args = {type, id}; + if (!this.checkChannelAccess(req)) { + return next(Boom.forbidden('Access to the requested channel is forbidden')); + } + return this.bus.sendCommand({role: 'store', cmd: 'remove', type}, args) .then(() => { res.body = {}; @@ -73,6 +85,14 @@ class IdentityChannelController extends IdentityItemController { .catch(next); } + checkChannelAccess(req) { + if (this.isAdminRequest(req)) { + return true; + } + + return req.params.id === _.get(req, 'identity.channel.id'); + } + static create(spec) { if (!spec.bus || !_.isObject(spec.bus)) { throw new Error('IdentityChannelController spec.bus is required'); From 122e9e48cb1def30d7c9e6dfe009473d547965e3 Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 05:51:53 -0400 Subject: [PATCH 04/14] Remove channel specific logic from IdentityItemController modified: lib/services/identity/controllers/identity-item-controller.js --- .../controllers/identity-item-controller.js | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/lib/services/identity/controllers/identity-item-controller.js b/lib/services/identity/controllers/identity-item-controller.js index f5e168b..08df136 100644 --- a/lib/services/identity/controllers/identity-item-controller.js +++ b/lib/services/identity/controllers/identity-item-controller.js @@ -24,9 +24,7 @@ class IdentityItemController extends Controller { return this.getChannel(req) .then(channel => { - if (type !== 'channel') { - args.channel = channel.id; - } + args.channel = channel.id; if (req.query.include) { args.include = req.query.include.split(','); @@ -55,9 +53,7 @@ class IdentityItemController extends Controller { return this.getChannel(req) .then(channel => { - if (type !== 'channel') { - args.channel = channel.id; - } + args.channel = channel.id; return this.bus.query({role: 'store', cmd: 'get', type}, args); }) .then(resource => { @@ -65,10 +61,7 @@ class IdentityItemController extends Controller { resource = _.merge({}, resource, payload); resource.type = type; resource.id = id; - - if (type !== 'channel') { - resource.channel = args.channel; - } + resource.channel = args.channel; return this.bus.sendCommand({role: 'store', cmd: 'set', type}, resource); } @@ -91,9 +84,7 @@ class IdentityItemController extends Controller { return this.getChannel(req) .then(channel => { - if (type !== 'channel') { - args.channel = channel.id; - } + args.channel = channel.id; return this.bus.sendCommand({role: 'store', cmd: 'remove', type}, args); }) .then(() => { From 5d3be7cde21a85fa58cbd053e7cac06ac617a4c1 Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 05:52:13 -0400 Subject: [PATCH 05/14] Remove channel specific logic from base Controller class modified: lib/controllers/controller.js --- lib/controllers/controller.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/controllers/controller.js b/lib/controllers/controller.js index bd3b46d..5c7b5ad 100644 --- a/lib/controllers/controller.js +++ b/lib/controllers/controller.js @@ -11,10 +11,6 @@ class Controller { getChannel(req) { if (this.isAdminRequest(req)) { - if (this.type === 'channel') { - return Promise.resolve(null); - } - const channelId = (req.query || {}).channel || (req.body || {}).channel; if (channelId) { @@ -41,11 +37,6 @@ class Controller { const channel = req.identity.channel; if (channel) { - if (this.type === 'channel' && req.params.id && channel.id !== req.params.id) { - return Promise.reject(Boom.forbidden( - 'Access to the requested channel is forbidden' - )); - } return Promise.resolve(channel); } From 28e56f8fd7b432903ff89b7ba92c8863d2ffc244 Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 06:25:10 -0400 Subject: [PATCH 06/14] Factor out specific tests for channel controllers. new file: spec/services/identity/controllers/identity-channel-controller-spec.js new file: spec/services/identity/controllers/identity-channels-list-controller-spec.js modified: spec/services/identity/controllers/identity-item-controller-spec.js modified: spec/services/identity/controllers/identity-list-controller-spec.js --- .../identity-channel-controller-spec.js | 626 ++++++ .../identity-channels-list-controller-spec.js | 300 +++ .../identity-item-controller-spec.js | 1804 ++++++----------- .../identity-list-controller-spec.js | 1136 ++++------- 4 files changed, 1925 insertions(+), 1941 deletions(-) create mode 100644 spec/services/identity/controllers/identity-channel-controller-spec.js create mode 100644 spec/services/identity/controllers/identity-channels-list-controller-spec.js diff --git a/spec/services/identity/controllers/identity-channel-controller-spec.js b/spec/services/identity/controllers/identity-channel-controller-spec.js new file mode 100644 index 0000000..4794fa5 --- /dev/null +++ b/spec/services/identity/controllers/identity-channel-controller-spec.js @@ -0,0 +1,626 @@ +/* global describe, beforeAll, it, expect, spyOn */ +/* eslint prefer-arrow-callback: 0 */ +/* eslint-disable max-nested-callbacks */ +'use strict'; + +const Promise = require('bluebird'); +const _ = require('lodash'); +const IdentityChannelController = require('../../../../lib/services/identity/controllers/identity-channel-controller'); + +describe('Identity Channel Controller', function () { + const type = 'channel'; + let bus; + let handler; + + function createRequest(spec) { + const req = { + method: 'GET', + identity: {}, + params: {}, + query: {}, + body: null + }; + + return _.merge(req, spec); + } + + function createResponse(spec) { + const res = { + status(code) { + this.statusCode = code; + } + }; + + return _.merge(res, spec); + } + + beforeAll(function () { + bus = this.createBus(); + handler = IdentityChannelController.create({bus}); + + bus.queryHandler({role: 'store', cmd: 'get', type: 'channel'}, args => { + if (args.id === 'non-existent-record') { + return Promise.resolve(null); + } + return Promise.resolve({type: 'channel', id: args.id}); + }); + + bus.commandHandler({role: 'store', cmd: 'set', type: 'channel'}, args => { + return Promise.resolve(_.merge({type: 'channel'}, args)); + }); + + bus.commandHandler({role: 'store', cmd: 'remove', type: 'channel'}, () => { + return Promise.resolve(true); + }); + }); + + // Performing a GET request. + describe('GET', function () { + const method = 'GET'; + const params = Object.freeze({id: 'a-channel-id'}); + + // Performing a GET request with "platform" role. + describe('as a platform', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['platform']) + }); + + // Performing a GET request with "platform" role. + describe('when channel not in the JWT', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + + req = createRequest({method, identity, params}); + res = createResponse(); + + spyOn(bus, 'query').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not query for the channel', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); + + it('does not query for the resource', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); + + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + }); + }); + + // Performing a GET request with "platform" role. + describe('JWT channel does not match requested channel ID', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-different-channel-id'}}, IDENTITY); + req = createRequest({method, identity, params}); + res = createResponse(); + + spyOn(bus, 'query').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not query for the channel', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); + + it('does not query for the resource', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); + + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Access to the requested channel is forbidden'); + }); + }); + + // Performing a GET request with "platform" role. + describe('when resource exists', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); + req = createRequest({method, identity, params}); + res = createResponse(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); + + it('returns status code 200', function () { + expect(res.statusCode).toBe(200); + }); + + it('assigns the resource to the response body', function () { + expect(res.body).toEqual({type, id: params.id}); + }); + }); + + // Identity Item Controller with type === "channel" + // Performing a GET request with "platform" role. + // + // We don't test this case, because if the channel did not exist, + // the request would not have authenticated. + // describe('when resource does not exist', function () { + // }); + }); + + // Performing a GET request with "admin" role. + describe('as an admin', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['admin']) + }); + + // Identity Item Controller with type === "channel" + // Performing a GET request with "admin" role. + describe('when no channel is specified', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + req = createRequest({method, identity, params}); + res = createResponse(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + // An admin caller can access a channel resource without specifying it + // in the JWT or query parameters. + + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); + + it('returns status code 200', function () { + expect(res.statusCode).toBe(200); + }); + + it('assigns the resource to the response body', function () { + expect(res.body).toEqual({type, id: params.id}); + }); + }); + + // Performing a GET request with "admin" role. + describe('when resource does not exist', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + const params = {id: 'non-existent-record'}; + req = createRequest({method, identity, params}); + res = createResponse(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('returns a 404 status code', function () { + expect(error.output.payload.statusCode).toBe(404); + }); + + it('does not assign a resource to the response', function () { + expect(res.body).not.toBeDefined(); + }); + }); + }); + }); + + // Performing a PATCH request. + describe('PATCH', function () { + const method = 'PATCH'; + const params = Object.freeze({id: 'a-channel-id'}); + const BODY = Object.freeze({ + type, + id: params.id, + foo: 'bar' + }); + + // Performing a PATCH request with "platform" role. + describe('as a platform', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['platform']) + }); + + // Performing a PATCH request with "platform" role. + describe('when the channel is not in the JWT', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + const body = BODY; + req = createRequest({method, identity, params, body}); + res = createResponse(); + + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not query for the channel', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); + + it('does not query for the resource', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); + + it('does not attempt to save the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); + + it('returns a 403', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + }); + }); + + // Performing a PATCH request with "platform" role. + describe('with valid request', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); + const body = BODY; + req = createRequest({method, identity, params, body}); + res = createResponse(); + + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); + + it('queries for the resource', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type, id: params.id}); + }); + + it('saves the resource', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); + }); + + it('updates the resource', function () { + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ + type, + id: params.id, + foo: 'bar' + }); + }); + + it('returns a 200', function () { + expect(res.statusCode).toBe(200); + }); + + it('attaches the updated resource as response body', function () { + expect(res.body).toEqual({ + type, + id: params.id, + foo: 'bar' + }); + }); + }); + + // Performing a PATCH request with "platform" role. + // + // We don't test this case, because if the channel did not exist, + // the request would not have authenticated. + // describe('when resource does not exist', function () { + // }); + }); + + // Performing a PATCH request with "admin" role. + describe('as an admin', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['admin']) + }); + + // Performing a PATCH request with "admin" role. + describe('when no channel is specified', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + const body = BODY; + req = createRequest({method, identity, params, body}); + res = createResponse(); + + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + // An admin caller can access a channel resource without specifying it + // in the JWT or query parameters. + + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); + + it('queries for the resource', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type, id: params.id}); + }); + + it('saves the resource', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); + }); + + it('updates the resource', function () { + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ + type, + id: params.id, + foo: 'bar' + }); + }); + + it('returns status code 200', function () { + expect(res.statusCode).toBe(200); + }); + + it('assigns the resource to the response body', function () { + expect(res.body).toEqual({type, id: params.id, foo: 'bar'}); + }); + }); + + // Performing a PATCH request with "admin" role. + describe('when the resource does not exist', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + const params = {id: 'non-existent-record'}; + const body = BODY; + req = createRequest({method, identity, params, body}); + res = createResponse(); + + spyOn(bus, 'sendCommand').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not attempt to save the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); + + it('returns a 404 status code', function () { + expect(error.output.payload.statusCode).toBe(404); + }); + + it('does not assign a resource to the response', function () { + expect(res.body).not.toBeDefined(); + }); + }); + }); + }); + + // Performing a DELETE request. + describe('DELETE', function () { + const method = 'DELETE'; + const params = Object.freeze({id: 'a-channel-id'}); + + // Performing a DELETE request with "platform" role. + describe('as a platform', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['platform']) + }); + + // Performing a DELETE request with "platform" role. + describe('when channel not in the JWT', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + + req = createRequest({method, identity, params}); + res = createResponse(); + + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not query for the channel', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); + + it('does not remove the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); + + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + }); + }); + + // Performing a DELETE request with "platform" role. + describe('JWT channel does not match requested channel ID', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-different-channel-id'}}, IDENTITY); + req = createRequest({method, identity, params}); + res = createResponse(); + + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not query for the channel', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); + + it('does not remove the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); + + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Access to the requested channel is forbidden'); + }); + }); + + // Performing a DELETE request with "platform" role. + describe('when resource exists', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); + req = createRequest({method, identity, params}); + res = createResponse(); + + spyOn(bus, 'sendCommand').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); + + it('removes the resource', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'remove', type}); + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, id: params.id}); + }); + + it('returns status code 200', function () { + expect(res.statusCode).toBe(200); + }); + }); + + // Performing a DELETE request with "platform" role. + // + // We don't test this case, because if the channel did not exist, + // the request would not have authenticated. + // describe('when resource does not exist', function () { + // }); + }); + + // Performing a DELETE request with "admin" role. + describe('as an admin', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['admin']) + }); + + // Performing a DELETE request with "admin" role. + describe('when no channel is specified', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + req = createRequest({method, identity, params}); + res = createResponse(); + + spyOn(bus, 'sendCommand').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + // An admin caller can access a channel resource without specifying it + // in the JWT or query parameters. + it('removes the resource', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'remove', type}); + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, id: params.id}); + }); + + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); + + it('returns status code 200', function () { + expect(res.statusCode).toBe(200); + }); + }); + }); + }); +}); diff --git a/spec/services/identity/controllers/identity-channels-list-controller-spec.js b/spec/services/identity/controllers/identity-channels-list-controller-spec.js new file mode 100644 index 0000000..72116f1 --- /dev/null +++ b/spec/services/identity/controllers/identity-channels-list-controller-spec.js @@ -0,0 +1,300 @@ +/* global describe, beforeAll, it, expect, spyOn */ +/* eslint prefer-arrow-callback: 0 */ +/* eslint-disable max-nested-callbacks */ +'use strict'; + +const Promise = require('bluebird'); +const _ = require('lodash'); +const uuid = require('node-uuid'); +const IdentityChannelsListController = require('../../../../lib/services/identity/controllers/identity-channels-list-controller'); + +describe('Identity Channels List Controller', function () { + const type = 'channel'; + let handler; + let bus; + + function createRequest(spec) { + const req = { + method: 'GET', + identity: {}, + params: {}, + query: {}, + body: null + }; + + return _.merge(req, spec); + } + + function createResponse(spec) { + const res = { + status(code) { + this.statusCode = code; + } + }; + + return _.merge(res, spec); + } + + beforeAll(function () { + bus = this.createBus(); + handler = IdentityChannelsListController.create({bus, type}); + + bus.queryHandler({role: 'store', cmd: 'get', type: 'channel'}, args => { + if (args.id === 'non-existent-channel') { + return Promise.resolve(null); + } + return Promise.resolve({type: 'channel', id: args.id}); + }); + + bus.commandHandler({role: 'store', cmd: 'set', type: 'channel'}, args => { + args = _.cloneDeep(args); + args.id = args.id || uuid.v4(); + return Promise.resolve(_.merge({type: 'channel'}, args)); + }); + + bus.queryHandler({role: 'store', cmd: 'scan', type: 'channel'}, () => { + const results = _.range(11).map(() => { + return {type: 'channel', id: uuid.v4()}; + }); + + results.push({type: 'channel', id: 'jwt-channel-id'}); + return Promise.resolve(results); + }); + }); + + describe('GET', function () { + const method = 'GET'; + + // Performing a GET request with platform role. + describe('as platform', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['platform']) + }); + + // Performing a GET request with platform role. + describe('when channel not in the JWT', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + req = createRequest({method, identity}); + res = createResponse(); + + spyOn(bus, 'query').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not query for the channel', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); + + it('does not query for the resource', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); + + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + }); + }); + + // Performing a GET request with platform role. + describe('with valid request', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); + req = createRequest({method, identity}); + res = createResponse(); + + spyOn(bus, 'query').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); + + it('calls store scan', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'scan', type}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type, limit: 10}); + }); + + it('return 200 response', function () { + expect(res.statusCode).toBe(200); + }); + + it('returns an array of channel objects', function () { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBe(1); + expect(res.body[0].type).toBe('channel'); + }); + + it('only returns the channel for which this JWT has access', function () { + expect(res.body.length).toBe(1); + expect(res.body[0].id).toBe('jwt-channel-id'); + }); + }); + }); + + // Performing a GET request with admin role. + describe('as admin', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['admin']) + }); + + // Performing a GET request with admin role. + describe('when no channel is specified', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + const query = {}; + req = createRequest({method, identity, query}); + res = createResponse(); + + spyOn(bus, 'query').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); + + it('calls store scan', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'scan', type}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type, limit: 10}); + }); + + it('return 200 response', function () { + expect(res.statusCode).toBe(200); + }); + + it('returns an array of channel objects', function () { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBe(10); + res.body.forEach(item => { + expect(item.type).toBe('channel'); + }); + }); + }); + }); + }); + + describe('POST', function () { + const method = 'POST'; + const BODY = Object.freeze({ + type, + foo: 'bar' + }); + + // Performing a POST request with platform role. + // + // We don't test this case, because platform requests are not + // authorized to POST a channel via the authorize middleware. + // describe('as platform', function () { + // }); + + // Identity List Controller with type === "channel". + // Performing a POST request with admin role. + describe('as admin', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['admin']) + }); + + // Performing a POST request with platform role. + describe('when no channel is specified', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + const body = BODY; + req = createRequest({method, identity, body}); + res = createResponse(); + + spyOn(bus, 'sendCommand').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + // An admin caller can access a channel resource without specifying it + // in the JWT or query parameters. + + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); + + it('saves the resource', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, foo: 'bar'}); + }); + + it('returns status code 201', function () { + expect(res.statusCode).toBe(201); + }); + + it('assigns the resource to the response body', function () { + expect(res.body.type).toBe(type); + expect(res.body.id).toMatch(/^[0-9a-z\-]{36}$/); + expect(res.body.foo).toBe('bar'); + }); + }); + + // Performing a POST request with platform role. + describe('when the resource already exists', function () { + let req; + let res; + let error; + + beforeAll(function (done) { + const identity = IDENTITY; + const body = _.merge({}, BODY, {id: 'some-existing-channel-id'}); + req = createRequest({method, identity, body}); + res = createResponse(); + + spyOn(bus, 'sendCommand').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); + }); + }); + + it('does not set the existing resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); + + it('responds with 409', function () { + expect(error.output.payload.statusCode).toBe(409); + expect(error.output.payload.message).toBe('The channel "some-existing-channel-id" already exists'); + }); + }); + }); + }); +}); diff --git a/spec/services/identity/controllers/identity-item-controller-spec.js b/spec/services/identity/controllers/identity-item-controller-spec.js index c84e7ad..aad2c4e 100644 --- a/spec/services/identity/controllers/identity-item-controller-spec.js +++ b/spec/services/identity/controllers/identity-item-controller-spec.js @@ -9,7 +9,11 @@ const IdentityItemController = require('../../../../lib/services/identity/contro describe('Identity Item Controller', function () { const TYPES = Object.freeze(['platform', 'viewer', 'cat', 'horse']); + + const type = _.sample(TYPES); + let bus; + let handler; function createRequest(spec) { const req = { @@ -35,6 +39,7 @@ describe('Identity Item Controller', function () { beforeAll(function () { bus = this.createBus(); + handler = IdentityItemController.create({bus, type}); bus.queryHandler({role: 'store', cmd: 'get', type: 'channel'}, args => { if (args.id === 'non-existent-record') { @@ -43,14 +48,6 @@ describe('Identity Item Controller', function () { return Promise.resolve({type: 'channel', id: args.id}); }); - bus.commandHandler({role: 'store', cmd: 'set', type: 'channel'}, args => { - return Promise.resolve(_.merge({type: 'channel'}, args)); - }); - - bus.commandHandler({role: 'store', cmd: 'remove', type: 'channel'}, () => { - return Promise.resolve(true); - }); - TYPES.forEach(type => { bus.queryHandler({role: 'store', cmd: 'get', type}, args => { if (args.id === 'non-existent-record') { @@ -69,1469 +66,826 @@ describe('Identity Item Controller', function () { }); }); - // Identity Item Controller with random type definition (not "channel"). - describe('with random type', function () { - const type = _.sample(TYPES); - let handler; + // Performing a GET request. + describe('GET', function () { + const method = 'GET'; + const params = Object.freeze({id: 'record-id'}); - beforeAll(function () { - handler = IdentityItemController.create({bus, type}); - }); - - // Identity Item Controller with random type definition (not "channel") - // Performing a GET request. - describe('GET', function () { - const method = 'GET'; - const params = Object.freeze({id: 'record-id'}); + // Performing a GET request with "platform" role. + describe('as a platform', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['platform']) + }); - // Identity Item Controller with random type definition (not "channel") // Performing a GET request with "platform" role. - describe('as a platform', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['platform']) - }); - - // Identity Item Controller with random type definition (not "channel") - // Performing a GET request with "platform" role. - describe('when channel not in the JWT', function () { - let req; - let res; - let error; + describe('when channel not in the JWT', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const identity = IDENTITY; + beforeAll(function (done) { + const identity = IDENTITY; - req = createRequest({method, identity, params}); - res = createResponse(); + req = createRequest({method, identity, params}); + res = createResponse(); - spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'query').and.callThrough(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('does not query for the channel', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + it('does not query for the channel', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); - it('does not query for the resource', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + it('does not query for the resource', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); - }); + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); }); + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a GET request with "platform" role. - describe('when resource exists', function () { - let req; - let res; - let error; + // Performing a GET request with "platform" role. + describe('when resource exists', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); - req = createRequest({method, identity, params}); - res = createResponse(); + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); + req = createRequest({method, identity, params}); + res = createResponse(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - it('returns status code 200', function () { - expect(res.statusCode).toBe(200); - }); + it('returns status code 200', function () { + expect(res.statusCode).toBe(200); + }); - it('assigns the resource to the response body', function () { - expect(res.body).toEqual({type, id: params.id, channel: 'a-channel-id'}); - }); + it('assigns the resource to the response body', function () { + expect(res.body).toEqual({type, id: params.id, channel: 'a-channel-id'}); }); + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a GET request with "platform" role. - describe('when resource does not exist', function () { - let req; - let res; - let error; + // Performing a GET request with "platform" role. + describe('when resource does not exist', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); - const params = {id: 'non-existent-record'}; - req = createRequest({method, identity, params}); - res = createResponse(); + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); + const params = {id: 'non-existent-record'}; + req = createRequest({method, identity, params}); + res = createResponse(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('returns a 404 status code', function () { - expect(error.output.payload.statusCode).toBe(404); - }); + it('returns a 404 status code', function () { + expect(error.output.payload.statusCode).toBe(404); + }); - it('does not assign a resource to the response', function () { - expect(res.body).not.toBeDefined(); - }); + it('does not assign a resource to the response', function () { + expect(res.body).not.toBeDefined(); }); }); + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a GET request with "admin" role. - describe('as an admin', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['admin']) - }); + // Performing a GET request with "admin" role. + describe('as an admin', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['admin']) + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a GET request with "admin" role. - describe('with channel in query parameter and JWT', function () { - let req; - let res; - let error; + // Performing a GET request with "admin" role. + describe('with channel in query parameter and JWT', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const query = {channel: 'query-channel-id'}; - const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); + beforeAll(function (done) { + const query = {channel: 'query-channel-id'}; + const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - req = createRequest({method, identity, params, query}); - res = createResponse(); + req = createRequest({method, identity, params, query}); + res = createResponse(); - spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'query').and.callThrough(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - it('queries for the channel in the query parameter', function () { - expect(bus.query).toHaveBeenCalledTimes(2); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type: 'channel'}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type: 'channel', id: 'query-channel-id'}); - }); + it('queries for the channel in the query parameter', function () { + expect(bus.query).toHaveBeenCalledTimes(2); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type: 'channel'}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type: 'channel', id: 'query-channel-id'}); }); + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a GET request with "admin" role. - describe('with channel in JWT only (not query parameter)', function () { - let req; - let res; - let error; + // Performing a GET request with "admin" role. + describe('with channel in JWT only (not query parameter)', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); + beforeAll(function (done) { + const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - req = createRequest({method, identity, params}); - res = createResponse(); + req = createRequest({method, identity, params}); + res = createResponse(); - spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'query').and.callThrough(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - it('queries for the channel in the JWT', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type, id: params.id, channel: 'jwt-channel-id'}); - }); + it('queries for the channel in the JWT', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type, id: params.id, channel: 'jwt-channel-id'}); }); + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a GET request with "admin" role. - describe('when no channel is specified', function () { - let req; - let res; - let error; + // Performing a GET request with "admin" role. + describe('when no channel is specified', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const identity = IDENTITY; - req = createRequest({method, identity, params}); - res = createResponse(); + beforeAll(function (done) { + const identity = IDENTITY; + req = createRequest({method, identity, params}); + res = createResponse(); - spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'query').and.callThrough(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('does not query for the resource', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + it('does not query for the resource', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); - it('return a 400 error', function () { - expect(error.output.payload.statusCode).toBe(400); - expect(error.output.payload.message).toBe('The "channel" query parameter is required'); - }); + it('return a 400 error', function () { + expect(error.output.payload.statusCode).toBe(400); + expect(error.output.payload.message).toBe('The "channel" query parameter is required'); }); + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a GET request with "admin" role. - describe('when the specified channel does not exist', function () { - let req; - let res; - let error; + // Performing a GET request with "admin" role. + describe('when the specified channel does not exist', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const query = {channel: 'query-channel-id'}; - const identity = IDENTITY; + beforeAll(function (done) { + const query = {channel: 'query-channel-id'}; + const identity = IDENTITY; - req = createRequest({method, identity, params, query}); - res = createResponse(); + req = createRequest({method, identity, params, query}); + res = createResponse(); - spyOn(bus, 'query').and.returnValue(Promise.resolve(null)); + spyOn(bus, 'query').and.returnValue(Promise.resolve(null)); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Channel "query-channel-id" does not exist'); - }); + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Channel "query-channel-id" does not exist'); + }); - it('does not qeury for the resource', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - }); + it('does not qeury for the resource', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + }); - it('does not attach the resource to the response body', function () { - expect(res.body).not.toBeDefined(); - }); + it('does not attach the resource to the response body', function () { + expect(res.body).not.toBeDefined(); }); }); }); + }); + + // Performing a PATCH request. + describe('PATCH', function () { + const method = 'PATCH'; + const params = Object.freeze({id: 'record-id'}); + const BODY = Object.freeze({ + type, + id: params.id, + foo: 'bar' + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a PATCH request. - describe('PATCH', function () { - const method = 'PATCH'; - const params = Object.freeze({id: 'record-id'}); - const BODY = Object.freeze({ - type, - id: params.id, - foo: 'bar' + // Performing a PATCH request with "platform" role. + describe('as a platform', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['platform']) }); - // Identity Item Controller with random type definition (not "channel") // Performing a PATCH request with "platform" role. - describe('as a platform', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['platform']) - }); - - // Identity Item Controller with random type definition (not "channel") - // Performing a PATCH request with "platform" role. - describe('when the channel is not in the JWT', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - const body = _.merge({channel: 'a-channel-id'}, BODY); - req = createRequest({method, identity, params, body}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not query for the channel', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + describe('when the channel is not in the JWT', function () { + let req; + let res; + let error; - it('does not query for the resource', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + beforeAll(function (done) { + const identity = IDENTITY; + const body = _.merge({channel: 'a-channel-id'}, BODY); + req = createRequest({method, identity, params, body}); + res = createResponse(); - it('does not attempt to save the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); - it('returns a 403', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity Item Controller with random type definition (not "channel") - // Performing a PATCH request with "platform" role. - describe('when the resource does not exist', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); - const params = {id: 'non-existent-record'}; - const body = _.merge({channel: identity.channel.id}, BODY); - req = createRequest({method, identity, params, body}); - res = createResponse(); - - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not attempt to save the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); - - it('returns a 404 status code', function () { - expect(error.output.payload.statusCode).toBe(404); - }); - - it('does not assign a resource to the response', function () { - expect(res.body).not.toBeDefined(); - }); + it('does not query for the channel', function () { + expect(bus.query).not.toHaveBeenCalled(); }); - // Identity Item Controller with random type definition (not "channel") - // Performing a PATCH request with "platform" role. - describe('with valid request', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); - const body = _.merge({channel: identity.channel.id}, BODY); - req = createRequest({method, identity, params, body}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); + it('does not query for the resource', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); - handler(req, res, err => { - error = err; - done(); - }); - }); + it('does not attempt to save the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + it('returns a 403', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + }); + }); - it('queries for the resource', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type, id: params.id, channel: 'a-channel-id'}); - }); + // Performing a PATCH request with "platform" role. + describe('when the resource does not exist', function () { + let req; + let res; + let error; - it('saves the resource', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); - }); + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); + const params = {id: 'non-existent-record'}; + const body = _.merge({channel: identity.channel.id}, BODY); + req = createRequest({method, identity, params, body}); + res = createResponse(); - it('updates the resource', function () { - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ - type, - id: params.id, - channel: 'a-channel-id', - foo: 'bar' - }); - }); + spyOn(bus, 'sendCommand').and.callThrough(); - it('returns a 200', function () { - expect(res.statusCode).toBe(200); + handler(req, res, err => { + error = err; + done(); }); + }); - it('attaches the updated resource as response body', function () { - expect(res.body).toEqual({ - type, - id: params.id, - channel: 'a-channel-id', - foo: 'bar' - }); - }); + it('does not attempt to save the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); }); - // Identity Item Controller with random type definition (not "channel") - // Performing a PATCH request with "platform" role. - describe('when channel is not included in the payload', function () { - let req; - let res; - let error; + it('returns a 404 status code', function () { + expect(error.output.payload.statusCode).toBe(404); + }); - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); - const body = BODY; - req = createRequest({method, identity, params, body}); - res = createResponse(); + it('does not assign a resource to the response', function () { + expect(res.body).not.toBeDefined(); + }); + }); - spyOn(bus, 'sendCommand').and.callThrough(); + // Performing a PATCH request with "platform" role. + describe('with valid request', function () { + let req; + let res; + let error; - handler(req, res, err => { - error = err; - done(); - }); - }); + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); + const body = _.merge({channel: identity.channel.id}, BODY); + req = createRequest({method, identity, params, body}); + res = createResponse(); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); - it('updates the resource with the channel ID', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ - type, - id: params.id, - channel: 'a-channel-id', - foo: 'bar' - }); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity Item Controller with random type definition (not "channel") - // Performing a PATCH request with "platform" role. - describe('when attempting to update channel, type, or id attributes', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); - const body = _.merge({}, BODY, { - type: 'pluto', - id: 'some-other-id', - channel: 'another-channel-id' - }); - req = createRequest({method, identity, params, body}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); - - it('does not update channel, type, or id', function () { - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ - type, - id: params.id, - channel: 'a-channel-id', - foo: 'bar' - }); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); }); - }); - // Identity Item Controller with random type definition (not "channel") - // Performing a PATCH request with "admin" role. - describe('as an admin', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['admin']) + it('queries for the resource', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type, id: params.id, channel: 'a-channel-id'}); }); - // Identity Item Controller with random type definition (not "channel") - // Performing a PATCH request with "admin" role. - describe('with channel in body and JWT', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const channelId = 'attribute-channel-id'; - const body = _.merge({channel: channelId}, BODY); - const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - - req = createRequest({method, identity, params, body}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); + it('saves the resource', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); + }); - handler(req, res, err => { - error = err; - done(); - }); + it('updates the resource', function () { + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ + type, + id: params.id, + channel: 'a-channel-id', + foo: 'bar' }); + }); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + it('returns a 200', function () { + expect(res.statusCode).toBe(200); + }); - it('queries for the channel in the query parameter', function () { - expect(bus.query).toHaveBeenCalledTimes(2); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type: 'channel'}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type: 'channel', id: 'attribute-channel-id'}); + it('attaches the updated resource as response body', function () { + expect(res.body).toEqual({ + type, + id: params.id, + channel: 'a-channel-id', + foo: 'bar' }); }); + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a PATCH request with "admin" role. - describe('with channel in JWT only (not body)', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - const body = BODY; + // Performing a PATCH request with "platform" role. + describe('when channel is not included in the payload', function () { + let req; + let res; + let error; - req = createRequest({method, identity, params, body}); - res = createResponse(); + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); + const body = BODY; + req = createRequest({method, identity, params, body}); + res = createResponse(); - spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - it('queries for the channel in the JWT', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type, id: params.id, channel: 'jwt-channel-id'}); + it('updates the resource with the channel ID', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ + type, + id: params.id, + channel: 'a-channel-id', + foo: 'bar' }); }); + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a PATCH request with "admin" role. - describe('when no channel is specified', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - const body = BODY; - - req = createRequest({method, identity, params, body}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); + // Performing a PATCH request with "platform" role. + describe('when attempting to update channel, type, or id attributes', function () { + let req; + let res; + let error; - it('does not query for the resource', function () { - expect(bus.query).not.toHaveBeenCalled(); + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); + const body = _.merge({}, BODY, { + type: 'pluto', + id: 'some-other-id', + channel: 'another-channel-id' }); + req = createRequest({method, identity, params, body}); + res = createResponse(); - it('does not save the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); - it('return a 422 error', function () { - expect(error.output.payload.statusCode).toBe(422); - expect(error.output.payload.message).toBe('The "channel" attribute is required'); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity Item Controller with random type definition (not "channel") - // Performing a PATCH request with "admin" role. - describe('when the channel does not exist', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const channelId = 'attribute-channel-id'; - const body = _.merge({channel: channelId}, BODY); - const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - - req = createRequest({method, identity, params, body}); - res = createResponse(); - - spyOn(bus, 'query').and.returnValue(Promise.resolve(null)); - spyOn(bus, 'sendCommand').and.returnValue(Promise.resolve(null)); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not query for the resource', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type: 'channel'}); - }); - - it('does not save the the source', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Channel "attribute-channel-id" does not exist'); + it('does not update channel, type, or id', function () { + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ + type, + id: params.id, + channel: 'a-channel-id', + foo: 'bar' }); }); }); }); - // Identity Item Controller with random type definition (not "channel") - // Performing a DELETE request. - describe('DELETE', function () { - const method = 'DELETE'; - const params = Object.freeze({id: 'record-id'}); - - // Identity Item Controller with random type definition (not "channel") - // Performing a DELETE request. - describe('as a platform', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['platform']) - }); - - // Identity Item Controller with random type definition (not "channel") - // Performing a DELETE request. - describe('when channel not in the JWT', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - - req = createRequest({method, identity, params}); - res = createResponse(); + // Performing a PATCH request with "admin" role. + describe('as an admin', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['admin']) + }); - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); + // Performing a PATCH request with "admin" role. + describe('with channel in body and JWT', function () { + let req; + let res; + let error; - handler(req, res, err => { - error = err; - done(); - }); - }); + beforeAll(function (done) { + const channelId = 'attribute-channel-id'; + const body = _.merge({channel: channelId}, BODY); + const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - it('does not query for the channel', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + req = createRequest({method, identity, params, body}); + res = createResponse(); - it('does not remove the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + spyOn(bus, 'query').and.callThrough(); - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity Item Controller with random type definition (not "channel") - // Performing a DELETE request with "platform" role. - describe('when resource exists', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); - req = createRequest({method, identity, params}); - res = createResponse(); - - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); - - it('removes the resource', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'remove', type}); - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, id: params.id, channel: 'a-channel-id'}); - }); - - it('returns status code 200', function () { - expect(res.statusCode).toBe(200); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); }); - }); - // Identity Item Controller with random type definition (not "channel") - // Performing a DELETE request. - describe('as an admin', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['admin']) + it('queries for the channel in the query parameter', function () { + expect(bus.query).toHaveBeenCalledTimes(2); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type: 'channel'}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type: 'channel', id: 'attribute-channel-id'}); }); + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a DELETE request with "admin" role. - describe('with channel in query parameter and JWT', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const query = {channel: 'query-channel-id'}; - const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - - req = createRequest({method, identity, params, query}); - res = createResponse(); + // Performing a PATCH request with "admin" role. + describe('with channel in JWT only (not body)', function () { + let req; + let res; + let error; - spyOn(bus, 'query').and.callThrough(); + beforeAll(function (done) { + const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); + const body = BODY; - handler(req, res, err => { - error = err; - done(); - }); - }); + req = createRequest({method, identity, params, body}); + res = createResponse(); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + spyOn(bus, 'query').and.callThrough(); - it('queries for the channel in the query parameter', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type: 'channel'}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type: 'channel', id: 'query-channel-id'}); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity Item Controller with random type definition (not "channel") - // Performing a DELETE request with "admin" role. - describe('with channel in JWT only (not query parameter)', function () { - let req; - let res; - let error; + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - beforeAll(function (done) { - const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); + it('queries for the channel in the JWT', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type, id: params.id, channel: 'jwt-channel-id'}); + }); + }); - req = createRequest({method, identity, params}); - res = createResponse(); + // Performing a PATCH request with "admin" role. + describe('when no channel is specified', function () { + let req; + let res; + let error; - spyOn(bus, 'sendCommand').and.callThrough(); + beforeAll(function (done) { + const identity = IDENTITY; + const body = BODY; - handler(req, res, err => { - error = err; - done(); - }); - }); + req = createRequest({method, identity, params, body}); + res = createResponse(); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); - it('removes the resource using channel in JWT', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'remove', type}); - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, id: params.id, channel: 'jwt-channel-id'}); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity Item Controller with random type definition (not "channel") - // Performing a DELETE request with "admin" role. - describe('when no channel is specified', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - req = createRequest({method, identity, params}); - res = createResponse(); - - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); + it('does not query for the resource', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); - it('does not remove the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + it('does not save the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); - it('return a 400 error', function () { - expect(error.output.payload.statusCode).toBe(400); - expect(error.output.payload.message).toBe('The "channel" query parameter is required'); - }); + it('return a 422 error', function () { + expect(error.output.payload.statusCode).toBe(422); + expect(error.output.payload.message).toBe('The "channel" attribute is required'); }); + }); - // Identity Item Controller with random type definition (not "channel") - // Performing a DELETE request with "admin" role. - describe('when the specified channel does not exist', function () { - let req; - let res; - let error; + // Performing a PATCH request with "admin" role. + describe('when the channel does not exist', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const query = {channel: 'query-channel-id'}; - const identity = IDENTITY; + beforeAll(function (done) { + const channelId = 'attribute-channel-id'; + const body = _.merge({channel: channelId}, BODY); + const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - req = createRequest({method, identity, params, query}); - res = createResponse(); + req = createRequest({method, identity, params, body}); + res = createResponse(); - spyOn(bus, 'query').and.returnValue(Promise.resolve(null)); - spyOn(bus, 'sendCommand').and.callThrough(); + spyOn(bus, 'query').and.returnValue(Promise.resolve(null)); + spyOn(bus, 'sendCommand').and.returnValue(Promise.resolve(null)); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Channel "query-channel-id" does not exist'); - }); + it('does not query for the resource', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type: 'channel'}); + }); - it('does not remove the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + it('does not save the the source', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); - it('does not attach the resource to the response body', function () { - expect(res.body).not.toBeDefined(); - }); + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Channel "attribute-channel-id" does not exist'); }); }); }); }); - describe('with type === channel', function () { - const type = 'channel'; - let handler; - - beforeAll(function () { - handler = IdentityItemController.create({bus, type}); - }); - - // Identity Item Controller with type === "channel" - // Performing a GET request. - describe('GET', function () { - const method = 'GET'; - const params = Object.freeze({id: 'a-channel-id'}); - - // Identity Item Controller with type === "channel" - // Performing a GET request with "platform" role. - describe('as a platform', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['platform']) - }); - - // Identity Item Controller with type === "channel" - // Performing a GET request with "platform" role. - describe('when channel not in the JWT', function () { - let req; - let res; - let error; + // Performing a DELETE request. + describe('DELETE', function () { + const method = 'DELETE'; + const params = Object.freeze({id: 'record-id'}); - beforeAll(function (done) { - const identity = IDENTITY; - - req = createRequest({method, identity, params}); - res = createResponse(); + // Performing a DELETE request. + describe('as a platform', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['platform']) + }); - spyOn(bus, 'query').and.callThrough(); + // Performing a DELETE request. + describe('when channel not in the JWT', function () { + let req; + let res; + let error; - handler(req, res, err => { - error = err; - done(); - }); - }); + beforeAll(function (done) { + const identity = IDENTITY; - it('does not query for the channel', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + req = createRequest({method, identity, params}); + res = createResponse(); - it('does not query for the resource', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity Item Controller with type === "channel" - // Performing a GET request with "platform" role. - describe('JWT channel does not match requested channel ID', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-different-channel-id'}}, IDENTITY); - req = createRequest({method, identity, params}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not query for the channel', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); - - it('does not query for the resource', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); - - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Access to the requested channel is forbidden'); - }); + it('does not query for the channel', function () { + expect(bus.query).not.toHaveBeenCalled(); }); - // Identity Item Controller with type === "channel" - // Performing a GET request with "platform" role. - describe('when resource exists', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); - req = createRequest({method, identity, params}); - res = createResponse(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); - - it('returns status code 200', function () { - expect(res.statusCode).toBe(200); - }); - - it('assigns the resource to the response body', function () { - expect(res.body).toEqual({type, id: params.id}); - }); + it('does not remove the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); }); - // Identity Item Controller with type === "channel" - // Performing a GET request with "platform" role. - // - // We don't test this case, because if the channel did not exist, - // the request would not have authenticated. - // describe('when resource does not exist', function () { - // }); + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + }); }); - // Identity Item Controller with type === "channel" - // Performing a GET request with "admin" role. - describe('as an admin', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['admin']) - }); - - // Identity Item Controller with type === "channel" - // Performing a GET request with "admin" role. - describe('when no channel is specified', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - req = createRequest({method, identity, params}); - res = createResponse(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - // An admin caller can access a channel resource without specifying it - // in the JWT or query parameters. + // Performing a DELETE request with "platform" role. + describe('when resource exists', function () { + let req; + let res; + let error; - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + beforeAll(function (done) { + const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); + req = createRequest({method, identity, params}); + res = createResponse(); - it('returns status code 200', function () { - expect(res.statusCode).toBe(200); - }); + spyOn(bus, 'sendCommand').and.callThrough(); - it('assigns the resource to the response body', function () { - expect(res.body).toEqual({type, id: params.id}); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity Item Controller with type === "channel" - // Performing a GET request with "admin" role. - describe('when resource does not exist', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - const params = {id: 'non-existent-record'}; - req = createRequest({method, identity, params}); - res = createResponse(); - - handler(req, res, err => { - error = err; - done(); - }); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - it('returns a 404 status code', function () { - expect(error.output.payload.statusCode).toBe(404); - }); + it('removes the resource', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'remove', type}); + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, id: params.id, channel: 'a-channel-id'}); + }); - it('does not assign a resource to the response', function () { - expect(res.body).not.toBeDefined(); - }); + it('returns status code 200', function () { + expect(res.statusCode).toBe(200); }); }); }); - // Identity Item Controller with type === "channel" - // Performing a PATCH request. - describe('PATCH', function () { - const method = 'PATCH'; - const params = Object.freeze({id: 'a-channel-id'}); - const BODY = Object.freeze({ - type, - id: params.id, - foo: 'bar' + // Performing a DELETE request. + describe('as an admin', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['admin']) }); - // Identity Item Controller with type === "channel" - // Performing a PATCH request with "platform" role. - describe('as a platform', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['platform']) - }); - - // Identity Item Controller with type === "channel" - // Performing a PATCH request with "platform" role. - describe('when the channel is not in the JWT', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - const body = BODY; - req = createRequest({method, identity, params, body}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); + // Performing a DELETE request with "admin" role. + describe('with channel in query parameter and JWT', function () { + let req; + let res; + let error; - it('does not query for the channel', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + beforeAll(function (done) { + const query = {channel: 'query-channel-id'}; + const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - it('does not query for the resource', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + req = createRequest({method, identity, params, query}); + res = createResponse(); - it('does not attempt to save the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + spyOn(bus, 'query').and.callThrough(); - it('returns a 403', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity Item Controller with type === "channel" - // Performing a PATCH request with "platform" role. - describe('with valid request', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); - const body = BODY; - req = createRequest({method, identity, params, body}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); - - it('queries for the resource', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type, id: params.id}); - }); - - it('saves the resource', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); - }); - - it('updates the resource', function () { - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ - type, - id: params.id, - foo: 'bar' - }); - }); - - it('returns a 200', function () { - expect(res.statusCode).toBe(200); - }); - - it('attaches the updated resource as response body', function () { - expect(res.body).toEqual({ - type, - id: params.id, - foo: 'bar' - }); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); }); - // Identity Item Controller with type === "channel" - // Performing a PATCH request with "platform" role. - // - // We don't test this case, because if the channel did not exist, - // the request would not have authenticated. - // describe('when resource does not exist', function () { - // }); + it('queries for the channel in the query parameter', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type: 'channel'}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type: 'channel', id: 'query-channel-id'}); + }); }); - // Identity Item Controller with type === "channel" - // Performing a PATCH request with "admin" role. - describe('as an admin', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['admin']) - }); - - // Identity Item Controller with type === "channel" - // Performing a PATCH request with "admin" role. - describe('when no channel is specified', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - const body = BODY; - req = createRequest({method, identity, params, body}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - // An admin caller can access a channel resource without specifying it - // in the JWT or query parameters. - - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); - - it('queries for the resource', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'get', type}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type, id: params.id}); - }); + // Performing a DELETE request with "admin" role. + describe('with channel in JWT only (not query parameter)', function () { + let req; + let res; + let error; - it('saves the resource', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); - }); + beforeAll(function (done) { + const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - it('updates the resource', function () { - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ - type, - id: params.id, - foo: 'bar' - }); - }); + req = createRequest({method, identity, params}); + res = createResponse(); - it('returns status code 200', function () { - expect(res.statusCode).toBe(200); - }); + spyOn(bus, 'sendCommand').and.callThrough(); - it('assigns the resource to the response body', function () { - expect(res.body).toEqual({type, id: params.id, foo: 'bar'}); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity Item Controller with type === "channel" - // Performing a PATCH request with "admin" role. - describe('when the resource does not exist', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - const params = {id: 'non-existent-record'}; - const body = BODY; - req = createRequest({method, identity, params, body}); - res = createResponse(); - - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not attempt to save the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); - - it('returns a 404 status code', function () { - expect(error.output.payload.statusCode).toBe(404); - }); - - it('does not assign a resource to the response', function () { - expect(res.body).not.toBeDefined(); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); }); - }); - }); - // Identity Item Controller with type === "channel" - // Performing a DELETE request. - describe('DELETE', function () { - const method = 'DELETE'; - const params = Object.freeze({id: 'a-channel-id'}); - - // Identity Item Controller with type === "channel" - // Performing a DELETE request with "platform" role. - describe('as a platform', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['platform']) + it('removes the resource using channel in JWT', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'remove', type}); + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, id: params.id, channel: 'jwt-channel-id'}); }); + }); - // Identity Item Controller with type === "channel" - // Performing a DELETE request with "platform" role. - describe('when channel not in the JWT', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - - req = createRequest({method, identity, params}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); + // Performing a DELETE request with "admin" role. + describe('when no channel is specified', function () { + let req; + let res; + let error; - it('does not query for the channel', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + beforeAll(function (done) { + const identity = IDENTITY; + req = createRequest({method, identity, params}); + res = createResponse(); - it('does not remove the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + spyOn(bus, 'sendCommand').and.callThrough(); - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity Item Controller with type === "channel" - // Performing a DELETE request with "platform" role. - describe('JWT channel does not match requested channel ID', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-different-channel-id'}}, IDENTITY); - req = createRequest({method, identity, params}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not query for the channel', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); - - it('does not remove the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); - - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Access to the requested channel is forbidden'); - }); + it('does not remove the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); }); - // Identity Item Controller with type === "channel" - // Performing a DELETE request with "platform" role. - describe('when resource exists', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {type: 'channel', id: 'a-channel-id'}}, IDENTITY); - req = createRequest({method, identity, params}); - res = createResponse(); - - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); - - it('removes the resource', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'remove', type}); - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, id: params.id}); - }); - - it('returns status code 200', function () { - expect(res.statusCode).toBe(200); - }); + it('return a 400 error', function () { + expect(error.output.payload.statusCode).toBe(400); + expect(error.output.payload.message).toBe('The "channel" query parameter is required'); }); - - // Identity Item Controller with type === "channel" - // Performing a DELETE request with "platform" role. - // - // We don't test this case, because if the channel did not exist, - // the request would not have authenticated. - // describe('when resource does not exist', function () { - // }); }); - // Identity Item Controller with type === "channel" // Performing a DELETE request with "admin" role. - describe('as an admin', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['admin']) - }); + describe('when the specified channel does not exist', function () { + let req; + let res; + let error; - // Identity Item Controller with type === "channel" - // Performing a DELETE request with "admin" role. - describe('when no channel is specified', function () { - let req; - let res; - let error; + beforeAll(function (done) { + const query = {channel: 'query-channel-id'}; + const identity = IDENTITY; - beforeAll(function (done) { - const identity = IDENTITY; - req = createRequest({method, identity, params}); - res = createResponse(); + req = createRequest({method, identity, params, query}); + res = createResponse(); - spyOn(bus, 'sendCommand').and.callThrough(); + spyOn(bus, 'query').and.returnValue(Promise.resolve(null)); + spyOn(bus, 'sendCommand').and.callThrough(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - // An admin caller can access a channel resource without specifying it - // in the JWT or query parameters. - it('removes the resource', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'remove', type}); - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, id: params.id}); - }); + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Channel "query-channel-id" does not exist'); + }); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + it('does not remove the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); - it('returns status code 200', function () { - expect(res.statusCode).toBe(200); - }); + it('does not attach the resource to the response body', function () { + expect(res.body).not.toBeDefined(); }); }); }); diff --git a/spec/services/identity/controllers/identity-list-controller-spec.js b/spec/services/identity/controllers/identity-list-controller-spec.js index 423cce0..d761c11 100644 --- a/spec/services/identity/controllers/identity-list-controller-spec.js +++ b/spec/services/identity/controllers/identity-list-controller-spec.js @@ -10,7 +10,9 @@ const IdentityListController = require('../../../../lib/services/identity/contro describe('Identity List Controller', function () { const TYPES = Object.freeze(['platform', 'viewer', 'cat', 'horse']); + const type = _.sample(TYPES); let bus; + let handler; function createRequest(spec) { const req = { @@ -36,6 +38,7 @@ describe('Identity List Controller', function () { beforeAll(function () { bus = this.createBus(); + handler = IdentityListController.create({bus, type}); bus.queryHandler({role: 'store', cmd: 'get', type: 'channel'}, args => { if (args.id === 'non-existent-channel') { @@ -44,21 +47,6 @@ describe('Identity List Controller', function () { return Promise.resolve({type: 'channel', id: args.id}); }); - bus.commandHandler({role: 'store', cmd: 'set', type: 'channel'}, args => { - args = _.cloneDeep(args); - args.id = args.id || uuid.v4(); - return Promise.resolve(_.merge({type: 'channel'}, args)); - }); - - bus.queryHandler({role: 'store', cmd: 'scan', type: 'channel'}, () => { - const results = _.range(11).map(() => { - return {type: 'channel', id: uuid.v4()}; - }); - - results.push({type: 'channel', id: 'jwt-channel-id'}); - return Promise.resolve(results); - }); - TYPES.forEach(type => { bus.queryHandler({role: 'store', cmd: 'get', type}, args => { if (args.id === 'non-existent-record') { @@ -80,864 +68,580 @@ describe('Identity List Controller', function () { }); }); - // Identity List Controller with random type definition (not "channel"). - describe('with random type', function () { - const type = _.sample(TYPES); - let handler; + describe('GET', function () { + const method = 'GET'; - beforeAll(function () { - handler = IdentityListController.create({bus, type}); - }); - - describe('GET', function () { - const method = 'GET'; + // Performing a GET request with platform role. + describe('as platform', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['platform']) + }); - // Identity List Controller with random type definition (not "channel"). // Performing a GET request with platform role. - describe('as platform', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['platform']) - }); - - // Identity List Controller with random type definition (not "channel"). - // Performing a GET request with platform role. - describe('when channel not in the JWT', function () { - let req; - let res; - let error; + describe('when channel not in the JWT', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const identity = IDENTITY; - req = createRequest({method, identity}); - res = createResponse(); + beforeAll(function (done) { + const identity = IDENTITY; + req = createRequest({method, identity}); + res = createResponse(); - spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'query').and.callThrough(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('does not query for the channel', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + it('does not query for the channel', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); - it('does not query for the resource', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + it('does not query for the resource', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); - }); + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); }); + }); - // Identity List Controller with random type definition (not "channel"). - // Performing a GET request with platform role. - describe('with valid request', function () { - let req; - let res; - let error; + // Performing a GET request with platform role. + describe('with valid request', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - const query = {}; - req = createRequest({method, identity, query}); - res = createResponse(); + beforeAll(function (done) { + const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); + const query = {}; + req = createRequest({method, identity, query}); + res = createResponse(); - spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'query').and.callThrough(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - it('calls store scan', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'scan', type}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type, limit: 10, channel: 'jwt-channel-id'}); - }); + it('calls store scan', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'scan', type}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type, limit: 10, channel: 'jwt-channel-id'}); + }); - it('return 200 response', function () { - expect(res.statusCode).toBe(200); - }); + it('return 200 response', function () { + expect(res.statusCode).toBe(200); + }); - it('returns an array of resource objects', function () { - expect(Array.isArray(res.body)).toBe(true); - expect(res.body.length).toBe(10); - res.body.forEach(item => { - expect(item.type).toBe(type); - expect(item.channel).toBe('jwt-channel-id'); - }); + it('returns an array of resource objects', function () { + expect(Array.isArray(res.body)).toBe(true); + expect(res.body.length).toBe(10); + res.body.forEach(item => { + expect(item.type).toBe(type); + expect(item.channel).toBe('jwt-channel-id'); }); }); }); + }); + + // Performing a GET request with admin role. + describe('as admin', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['admin']) + }); - // Identity List Controller with random type definition (not "channel"). // Performing a GET request with admin role. - describe('as admin', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['admin']) - }); - - // Identity List Controller with random type definition (not "channel"). - // Performing a GET request with admin role. - describe('with channel in query parameter and JWT', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - const query = {channel: 'query-channel-id'}; - req = createRequest({method, identity, query}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); + describe('with channel in query parameter and JWT', function () { + let req; + let res; + let error; - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + beforeAll(function (done) { + const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); + const query = {channel: 'query-channel-id'}; + req = createRequest({method, identity, query}); + res = createResponse(); - it('queries for the resource using the channel in the query parameter', function () { - expect(bus.query).toHaveBeenCalledTimes(2); - expect(bus.query.calls.argsFor(1)[0]).toEqual({role: 'store', cmd: 'scan', type}); - expect(bus.query.calls.argsFor(1)[1]).toEqual({type, limit: 10, channel: 'query-channel-id'}); + spyOn(bus, 'query').and.callThrough(); + + handler(req, res, err => { + error = err; + done(); }); }); - // Identity List Controller with random type definition (not "channel"). - // Performing a GET request with admin role. - describe('with channel in JWT only (not query parameter)', function () { - let req; - let res; - let error; + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - beforeAll(function (done) { - const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); - const query = {}; - req = createRequest({method, identity, query}); - res = createResponse(); + it('queries for the resource using the channel in the query parameter', function () { + expect(bus.query).toHaveBeenCalledTimes(2); + expect(bus.query.calls.argsFor(1)[0]).toEqual({role: 'store', cmd: 'scan', type}); + expect(bus.query.calls.argsFor(1)[1]).toEqual({type, limit: 10, channel: 'query-channel-id'}); + }); + }); - spyOn(bus, 'query').and.callThrough(); + // Performing a GET request with admin role. + describe('with channel in JWT only (not query parameter)', function () { + let req; + let res; + let error; - handler(req, res, err => { - error = err; - done(); - }); - }); + beforeAll(function (done) { + const identity = _.merge({}, {channel: {type: 'channel', id: 'jwt-channel-id'}}, IDENTITY); + const query = {}; + req = createRequest({method, identity, query}); + res = createResponse(); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + spyOn(bus, 'query').and.callThrough(); - it('queries for the resources using the channel in the JWT', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'scan', type}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type, limit: 10, channel: 'jwt-channel-id'}); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity List Controller with random type definition (not "channel"). - // Performing a GET request with admin role. - describe('no channel is specified', function () { - let req; - let res; - let error; + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - beforeAll(function (done) { - const identity = IDENTITY; - const query = {}; - req = createRequest({method, identity, query}); - res = createResponse(); + it('queries for the resources using the channel in the JWT', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'scan', type}); + expect(bus.query.calls.argsFor(0)[1]).toEqual({type, limit: 10, channel: 'jwt-channel-id'}); + }); + }); - spyOn(bus, 'query').and.callThrough(); + // Performing a GET request with admin role. + describe('no channel is specified', function () { + let req; + let res; + let error; - handler(req, res, err => { - error = err; - done(); - }); - }); + beforeAll(function (done) { + const identity = IDENTITY; + const query = {}; + req = createRequest({method, identity, query}); + res = createResponse(); - it('does not query for the resource', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + spyOn(bus, 'query').and.callThrough(); - it('return a 400 error', function () { - expect(error.output.payload.statusCode).toBe(400); - expect(error.output.payload.message).toBe('The "channel" query parameter is required'); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity List Controller with random type definition (not "channel"). - // Performing a GET request with admin role. - describe('when the specified channel does not exist', function () { - let req; - let res; - let error; + it('does not query for the resource', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); - beforeAll(function (done) { - const identity = IDENTITY; - const query = {channel: 'query-channel-id'}; - req = createRequest({method, identity, query}); - res = createResponse(); + it('return a 400 error', function () { + expect(error.output.payload.statusCode).toBe(400); + expect(error.output.payload.message).toBe('The "channel" query parameter is required'); + }); + }); - spyOn(bus, 'query').and.returnValue(Promise.resolve(null)); + // Performing a GET request with admin role. + describe('when the specified channel does not exist', function () { + let req; + let res; + let error; - handler(req, res, err => { - error = err; - done(); - }); - }); + beforeAll(function (done) { + const identity = IDENTITY; + const query = {channel: 'query-channel-id'}; + req = createRequest({method, identity, query}); + res = createResponse(); - it('does not qeury for the resource', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - }); + spyOn(bus, 'query').and.returnValue(Promise.resolve(null)); - it('does not attach the resource to the response body', function () { - expect(res.body).not.toBeDefined(); + handler(req, res, err => { + error = err; + done(); }); + }); - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Channel "query-channel-id" does not exist'); - }); + it('does not qeury for the resource', function () { + expect(bus.query).toHaveBeenCalledTimes(1); + }); + + it('does not attach the resource to the response body', function () { + expect(res.body).not.toBeDefined(); + }); + + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Channel "query-channel-id" does not exist'); }); }); }); + }); + + describe('POST', function () { + const method = 'POST'; + const BODY = Object.freeze({ + type, + foo: 'bar' + }); - describe('POST', function () { - const method = 'POST'; - const BODY = Object.freeze({ - type, - foo: 'bar' + // Performing a POST request with platform role. + describe('as platform', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['platform']) }); - // Identity List Controller with random type definition (not "channel"). // Performing a POST request with platform role. - describe('as platform', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['platform']) - }); - - // Identity List Controller with random type definition (not "channel"). - // Performing a POST request with platform role. - describe('when channel not in the JWT', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - const body = BODY; - req = createRequest({method, identity, body}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); + describe('when channel not in the JWT', function () { + let req; + let res; + let error; - it('does not query for the channel', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + beforeAll(function (done) { + const identity = IDENTITY; + const body = BODY; + req = createRequest({method, identity, body}); + res = createResponse(); - it('does not attempt to save the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); - it('returns a 403', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity List Controller with random type definition (not "channel"). - // Performing a POST request with platform role. - describe('with valid request', function () { - let req; - let res; - let error; + it('does not query for the channel', function () { + expect(bus.query).not.toHaveBeenCalled(); + }); - beforeAll(function (done) { - const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); - const body = BODY; - req = createRequest({method, identity, body}); - res = createResponse(); + it('does not attempt to save the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); + it('returns a 403', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + }); + }); - handler(req, res, err => { - error = err; - done(); - }); - }); + // Performing a POST request with platform role. + describe('with valid request', function () { + let req; + let res; + let error; - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + beforeAll(function (done) { + const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); + const body = BODY; + req = createRequest({method, identity, body}); + res = createResponse(); - it('saves the resource', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ - type, - channel: 'jwt-channel-id', - foo: 'bar' - }); - }); + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); - it('returns a 201', function () { - expect(res.statusCode).toBe(201); + handler(req, res, err => { + error = err; + done(); }); + }); - it('attaches the new resource as the response body', function () { - expect(res.body).toEqual({ - type, - channel: 'jwt-channel-id', - foo: 'bar' - }); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); }); - // Identity List Controller with random type definition (not "channel"). - // Performing a POST request with platform role. - describe('with client defined UID', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); - const body = _.merge({}, BODY, { - id: 'non-existent-record' - }); - req = createRequest({method, identity, body}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); + it('saves the resource', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ + type, + channel: 'jwt-channel-id', + foo: 'bar' }); + }); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + it('returns a 201', function () { + expect(res.statusCode).toBe(201); + }); - it('uses the client defined id', function () { - const resource = bus.sendCommand.calls.argsFor(0)[1]; - expect(resource.id).toBe('non-existent-record'); + it('attaches the new resource as the response body', function () { + expect(res.body).toEqual({ + type, + channel: 'jwt-channel-id', + foo: 'bar' }); }); + }); - // Identity List Controller with random type definition (not "channel"). - // Performing a POST request with platform role. - describe('when channel is not included in the payload', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); - const body = BODY; - req = createRequest({method, identity, body}); - res = createResponse(); - - spyOn(bus, 'sendCommand').and.callThrough(); + // Performing a POST request with platform role. + describe('with client defined UID', function () { + let req; + let res; + let error; - handler(req, res, err => { - error = err; - done(); - }); + beforeAll(function (done) { + const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); + const body = _.merge({}, BODY, { + id: 'non-existent-record' }); + req = createRequest({method, identity, body}); + res = createResponse(); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + spyOn(bus, 'query').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); - it('saves the resource with the channel ID', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ - type, - channel: 'jwt-channel-id', - foo: 'bar' - }); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity List Controller with random type definition (not "channel"). - // Performing a POST request with platform role. - describe('when attempting to override channel or type', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); - const body = _.merge({}, BODY, { - type: 'pluto', - channel: 'some-other-channel' - }); - req = createRequest({method, identity, body}); - res = createResponse(); - - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); - - it('uses pre-determined channel and type', function () { - const resource = bus.sendCommand.calls.argsFor(0)[1]; - expect(resource.type).toBe(type); - expect(resource.channel).toBe('jwt-channel-id'); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); }); - // Identity List Controller with random type definition (not "channel"). - // Performing a POST request with platform role. - describe('when the resource already exists', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); - const body = _.merge({id: 'some-resource-id'}, BODY); - req = createRequest({method, identity, body}); - res = createResponse(); + it('uses the client defined id', function () { + const resource = bus.sendCommand.calls.argsFor(0)[1]; + expect(resource.id).toBe('non-existent-record'); + }); + }); - spyOn(bus, 'sendCommand').and.callThrough(); + // Performing a POST request with platform role. + describe('when channel is not included in the payload', function () { + let req; + let res; + let error; - handler(req, res, err => { - error = err; - done(); - }); - }); + beforeAll(function (done) { + const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); + const body = BODY; + req = createRequest({method, identity, body}); + res = createResponse(); - it('does not set the existing resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + spyOn(bus, 'sendCommand').and.callThrough(); - it('responds with 409', function () { - expect(error.output.payload.statusCode).toBe(409); - expect(error.output.payload.message).toBe(`The ${type} "some-resource-id" already exists`); + handler(req, res, err => { + error = err; + done(); }); }); - }); - // Identity List Controller with random type definition (not "channel"). - // Performing a POST request with admin role. - describe('as admin', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['admin']) - }); - - // Identity List Controller with random type definition (not "channel"). - // Performing a POST request with admin role. - describe('with channel in the body and JWT', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); - const body = _.merge({channel: 'attribute-channel-id'}, BODY); - req = createRequest({method, identity, body}); - res = createResponse(); - - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - it('saves the resource with the channel in the query parameter', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({channel: 'attribute-channel-id', type, foo: 'bar'}); + it('saves the resource with the channel ID', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({ + type, + channel: 'jwt-channel-id', + foo: 'bar' }); }); + }); - // Identity List Controller with random type definition (not "channel"). - // Performing a POST request with admin role. - describe('with channel in the JWT only (not body)', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); - const body = BODY; - req = createRequest({method, identity, body}); - res = createResponse(); - - spyOn(bus, 'sendCommand').and.callThrough(); + // Performing a POST request with platform role. + describe('when attempting to override channel or type', function () { + let req; + let res; + let error; - handler(req, res, err => { - error = err; - done(); - }); + beforeAll(function (done) { + const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); + const body = _.merge({}, BODY, { + type: 'pluto', + channel: 'some-other-channel' }); + req = createRequest({method, identity, body}); + res = createResponse(); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + spyOn(bus, 'sendCommand').and.callThrough(); - it('saves the resource with the channel in the JWT', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, foo: 'bar', channel: 'jwt-channel-id'}); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity List Controller with random type definition (not "channel"). - // Performing a POST request with admin role. - describe('when no channel is specified', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - const body = BODY; - req = createRequest({method, identity, body}); - res = createResponse(); - - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not save the resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - it('return a 422 error', function () { - expect(error.output.payload.statusCode).toBe(422); - expect(error.output.payload.message).toBe('The "channel" attribute is required'); - }); + it('uses pre-determined channel and type', function () { + const resource = bus.sendCommand.calls.argsFor(0)[1]; + expect(resource.type).toBe(type); + expect(resource.channel).toBe('jwt-channel-id'); }); + }); - // Identity List Controller with random type definition (not "channel"). - // Performing a POST request with admin role. - describe('when the channel does not exist', function () { - let req; - let res; - let error; + // Performing a POST request with platform role. + describe('when the resource already exists', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const identity = IDENTITY; - const body = _.merge({channel: 'non-existent-channel'}, BODY); - req = createRequest({method, identity, body}); - res = createResponse(); + beforeAll(function (done) { + const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); + const body = _.merge({id: 'some-resource-id'}, BODY); + req = createRequest({method, identity, body}); + res = createResponse(); - spyOn(bus, 'sendCommand').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('does not save the the source', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + it('does not set the existing resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Channel "non-existent-channel" does not exist'); - }); + it('responds with 409', function () { + expect(error.output.payload.statusCode).toBe(409); + expect(error.output.payload.message).toBe(`The ${type} "some-resource-id" already exists`); }); }); }); - }); - // Identity List Controller with type === "channel". - describe('with type === channel', function () { - const type = 'channel'; - let handler; - - beforeAll(function () { - handler = IdentityListController.create({bus, type}); - }); - - describe('GET', function () { - const method = 'GET'; - - // Identity List Controller with type === "channel". - // Performing a GET request with platform role. - describe('as platform', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['platform']) - }); - - // Identity List Controller with type === "channel". - // Performing a GET request with platform role. - describe('when channel not in the JWT', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - req = createRequest({method, identity}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); + // Performing a POST request with admin role. + describe('as admin', function () { + const IDENTITY = Object.freeze({ + audience: Object.freeze(['admin']) + }); - handler(req, res, err => { - error = err; - done(); - }); - }); + // Performing a POST request with admin role. + describe('with channel in the body and JWT', function () { + let req; + let res; + let error; - it('does not query for the channel', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + beforeAll(function (done) { + const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); + const body = _.merge({channel: 'attribute-channel-id'}, BODY); + req = createRequest({method, identity, body}); + res = createResponse(); - it('does not query for the resource', function () { - expect(bus.query).not.toHaveBeenCalled(); - }); + spyOn(bus, 'sendCommand').and.callThrough(); - it('returns a 403 error', function () { - expect(error.output.payload.statusCode).toBe(403); - expect(error.output.payload.message).toBe('Non admin callers must have a channel embedded in the JSON Web Token'); + handler(req, res, err => { + error = err; + done(); }); }); - // Identity List Controller with type === "channel". - // Performing a GET request with platform role. - describe('with valid request', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); - req = createRequest({method, identity}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); - - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); - - it('calls store scan', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'scan', type}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type, limit: 10}); - }); - - it('return 200 response', function () { - expect(res.statusCode).toBe(200); - }); - - it('returns an array of channel objects', function () { - expect(Array.isArray(res.body)).toBe(true); - expect(res.body.length).toBe(1); - expect(res.body[0].type).toBe('channel'); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); + }); - it('only returns the channel for which this JWT has access', function () { - expect(res.body.length).toBe(1); - expect(res.body[0].id).toBe('jwt-channel-id'); - }); + it('saves the resource with the channel in the query parameter', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({channel: 'attribute-channel-id', type, foo: 'bar'}); }); }); - // Identity List Controller with type === "channel". - // Performing a GET request with admin role. - describe('as admin', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['admin']) - }); - - // Identity List Controller with type === "channel". - // Performing a GET request with admin role. - describe('when no channel is specified', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - const query = {}; - req = createRequest({method, identity, query}); - res = createResponse(); - - spyOn(bus, 'query').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); + // Performing a POST request with admin role. + describe('with channel in the JWT only (not body)', function () { + let req; + let res; + let error; - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + beforeAll(function (done) { + const identity = _.merge({channel: {id: 'jwt-channel-id'}}, IDENTITY); + const body = BODY; + req = createRequest({method, identity, body}); + res = createResponse(); - it('calls store scan', function () { - expect(bus.query).toHaveBeenCalledTimes(1); - expect(bus.query.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'scan', type}); - expect(bus.query.calls.argsFor(0)[1]).toEqual({type, limit: 10}); - }); + spyOn(bus, 'sendCommand').and.callThrough(); - it('return 200 response', function () { - expect(res.statusCode).toBe(200); + handler(req, res, err => { + error = err; + done(); }); + }); - it('returns an array of channel objects', function () { - expect(Array.isArray(res.body)).toBe(true); - expect(res.body.length).toBe(10); - res.body.forEach(item => { - expect(item.type).toBe('channel'); - }); - }); + it('does not return an error', function () { + expect(error).not.toBeDefined(); }); - }); - }); - describe('POST', function () { - const method = 'POST'; - const BODY = Object.freeze({ - type, - foo: 'bar' + it('saves the resource with the channel in the JWT', function () { + expect(bus.sendCommand).toHaveBeenCalledTimes(1); + expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); + expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, foo: 'bar', channel: 'jwt-channel-id'}); + }); }); - // Identity List Controller with type === "channel". - // Performing a POST request with platform role. - // - // We don't test this case, because platform requests are not - // authorized to POST a channel via the authorize middleware. - // describe('as platform', function () { - // }); - - // Identity List Controller with type === "channel". // Performing a POST request with admin role. - describe('as admin', function () { - const IDENTITY = Object.freeze({ - audience: Object.freeze(['admin']) - }); - - // Identity List Controller with type === "channel". - // Performing a POST request with platform role. - describe('when no channel is specified', function () { - let req; - let res; - let error; - - beforeAll(function (done) { - const identity = IDENTITY; - const body = BODY; - req = createRequest({method, identity, body}); - res = createResponse(); - - spyOn(bus, 'sendCommand').and.callThrough(); - - handler(req, res, err => { - error = err; - done(); - }); - }); + describe('when no channel is specified', function () { + let req; + let res; + let error; - // An admin caller can access a channel resource without specifying it - // in the JWT or query parameters. + beforeAll(function (done) { + const identity = IDENTITY; + const body = BODY; + req = createRequest({method, identity, body}); + res = createResponse(); - it('does not return an error', function () { - expect(error).not.toBeDefined(); - }); + spyOn(bus, 'sendCommand').and.callThrough(); - it('saves the resource', function () { - expect(bus.sendCommand).toHaveBeenCalledTimes(1); - expect(bus.sendCommand.calls.argsFor(0)[0]).toEqual({role: 'store', cmd: 'set', type}); - expect(bus.sendCommand.calls.argsFor(0)[1]).toEqual({type, foo: 'bar'}); + handler(req, res, err => { + error = err; + done(); }); + }); - it('returns status code 201', function () { - expect(res.statusCode).toBe(201); - }); + it('does not save the resource', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); - it('assigns the resource to the response body', function () { - expect(res.body.type).toBe(type); - expect(res.body.id).toMatch(/^[0-9a-z\-]{36}$/); - expect(res.body.foo).toBe('bar'); - }); + it('return a 422 error', function () { + expect(error.output.payload.statusCode).toBe(422); + expect(error.output.payload.message).toBe('The "channel" attribute is required'); }); + }); - // Identity List Controller with type === "channel". - // Performing a POST request with platform role. - describe('when the resource already exists', function () { - let req; - let res; - let error; + // Performing a POST request with admin role. + describe('when the channel does not exist', function () { + let req; + let res; + let error; - beforeAll(function (done) { - const identity = IDENTITY; - const body = _.merge({}, BODY, {id: 'some-existing-channel-id'}); - req = createRequest({method, identity, body}); - res = createResponse(); + beforeAll(function (done) { + const identity = IDENTITY; + const body = _.merge({channel: 'non-existent-channel'}, BODY); + req = createRequest({method, identity, body}); + res = createResponse(); - spyOn(bus, 'sendCommand').and.callThrough(); + spyOn(bus, 'sendCommand').and.callThrough(); - handler(req, res, err => { - error = err; - done(); - }); + handler(req, res, err => { + error = err; + done(); }); + }); - it('does not set the existing resource', function () { - expect(bus.sendCommand).not.toHaveBeenCalled(); - }); + it('does not save the the source', function () { + expect(bus.sendCommand).not.toHaveBeenCalled(); + }); - it('responds with 409', function () { - expect(error.output.payload.statusCode).toBe(409); - expect(error.output.payload.message).toBe('The channel "some-existing-channel-id" already exists'); - }); + it('returns a 403 error', function () { + expect(error.output.payload.statusCode).toBe(403); + expect(error.output.payload.message).toBe('Channel "non-existent-channel" does not exist'); }); }); }); From d4a927e44b6c4f7944cd64e9171f1548ffd313fe Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 06:25:31 -0400 Subject: [PATCH 07/14] Fix bugs in channel controllers. modified: lib/services/identity/controllers/identity-channel-controller.js modified: lib/services/identity/controllers/identity-channels-list-controller.js --- .../identity-channel-controller.js | 28 +++++++++++++------ .../identity-channels-list-controller.js | 18 ++++++++++++ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/lib/services/identity/controllers/identity-channel-controller.js b/lib/services/identity/controllers/identity-channel-controller.js index 1653ae5..88c70bb 100644 --- a/lib/services/identity/controllers/identity-channel-controller.js +++ b/lib/services/identity/controllers/identity-channel-controller.js @@ -13,8 +13,9 @@ class IdentityChannelController extends IdentityItemController { const id = req.params.id; const args = {type, id}; - if (!this.checkChannelAccess(req)) { - return next(Boom.forbidden('Access to the requested channel is forbidden')); + const err = this.checkChannelAccess(req); + if (err) { + return next(err); } if (req.query.include) { @@ -41,8 +42,9 @@ class IdentityChannelController extends IdentityItemController { const payload = req.body; const args = {type, id}; - if (!this.checkChannelAccess(req)) { - return next(Boom.forbidden('Access to the requested channel is forbidden')); + const err = this.checkChannelAccess(req); + if (err) { + return next(err); } return this.bus.query({role: 'store', cmd: 'get', type}, args) @@ -71,8 +73,9 @@ class IdentityChannelController extends IdentityItemController { const id = req.params.id; const args = {type, id}; - if (!this.checkChannelAccess(req)) { - return next(Boom.forbidden('Access to the requested channel is forbidden')); + const err = this.checkChannelAccess(req); + if (err) { + return next(err); } return this.bus.sendCommand({role: 'store', cmd: 'remove', type}, args) @@ -87,10 +90,19 @@ class IdentityChannelController extends IdentityItemController { checkChannelAccess(req) { if (this.isAdminRequest(req)) { - return true; + return null; } - return req.params.id === _.get(req, 'identity.channel.id'); + const channelId = _.get(req, 'identity.channel.id'); + if (!channelId) { + return Boom.forbidden('Non admin callers must have a channel embedded in the JSON Web Token'); + } + + if (req.params.id !== channelId) { + return Boom.forbidden('Access to the requested channel is forbidden'); + } + + return null; } static create(spec) { diff --git a/lib/services/identity/controllers/identity-channels-list-controller.js b/lib/services/identity/controllers/identity-channels-list-controller.js index 99a7879..731273f 100644 --- a/lib/services/identity/controllers/identity-channels-list-controller.js +++ b/lib/services/identity/controllers/identity-channels-list-controller.js @@ -13,6 +13,11 @@ class IdentityChannelsListController extends IdentityListController { const limit = parseInt(req.query.limit, 10) || 10; const args = {type, limit}; + const err = this.checkChannelAccess(req); + if (err) { + return next(err); + } + return this.bus.query({role: 'store', cmd: 'scan', type}, args) .then(resources => { // If this is not an admin request then we filter out all channel @@ -65,6 +70,19 @@ class IdentityChannelsListController extends IdentityListController { .catch(next); } + checkChannelAccess(req) { + if (this.isAdminRequest(req)) { + return null; + } + + const channelId = _.get(req, 'identity.channel.id'); + if (!channelId) { + return Boom.forbidden('Non admin callers must have a channel embedded in the JSON Web Token'); + } + + return null; + } + static create(spec) { if (!spec.bus || !_.isObject(spec.bus)) { throw new Error('IdentityChannelsListController spec.bus is required'); From 84117eb78d01dfa26ee9d864ad225cf508970761 Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 06:28:12 -0400 Subject: [PATCH 08/14] Force Identity Controller tests to fail on viewer type modified: spec/services/identity/controllers/identity-item-controller-spec.js modified: spec/services/identity/controllers/identity-list-controller-spec.js --- .../identity/controllers/identity-item-controller-spec.js | 4 +++- .../identity/controllers/identity-list-controller-spec.js | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/services/identity/controllers/identity-item-controller-spec.js b/spec/services/identity/controllers/identity-item-controller-spec.js index aad2c4e..ad9c685 100644 --- a/spec/services/identity/controllers/identity-item-controller-spec.js +++ b/spec/services/identity/controllers/identity-item-controller-spec.js @@ -8,7 +8,9 @@ const _ = require('lodash'); const IdentityItemController = require('../../../../lib/services/identity/controllers/identity-item-controller'); describe('Identity Item Controller', function () { - const TYPES = Object.freeze(['platform', 'viewer', 'cat', 'horse']); + // TODO: Undo this @kixxauth + // const TYPES = Object.freeze(['platform', 'viewer', 'cat', 'horse']); + const TYPES = Object.freeze(['viewer']); const type = _.sample(TYPES); diff --git a/spec/services/identity/controllers/identity-list-controller-spec.js b/spec/services/identity/controllers/identity-list-controller-spec.js index d761c11..4eb6388 100644 --- a/spec/services/identity/controllers/identity-list-controller-spec.js +++ b/spec/services/identity/controllers/identity-list-controller-spec.js @@ -9,7 +9,9 @@ const uuid = require('node-uuid'); const IdentityListController = require('../../../../lib/services/identity/controllers/identity-list-controller'); describe('Identity List Controller', function () { - const TYPES = Object.freeze(['platform', 'viewer', 'cat', 'horse']); + // TODO: Undo this @kixxauth + // const TYPES = Object.freeze(['platform', 'viewer', 'cat', 'horse']); + const TYPES = Object.freeze(['viewer']); const type = _.sample(TYPES); let bus; let handler; From 72985c5a86401b5310ca39124d7a623b11b0cfdf Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 06:42:36 -0400 Subject: [PATCH 09/14] New IdentityViewerController new file: lib/services/identity/controllers/identity-viewer-controller.js --- .../controllers/identity-viewer-controller.js | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 lib/services/identity/controllers/identity-viewer-controller.js diff --git a/lib/services/identity/controllers/identity-viewer-controller.js b/lib/services/identity/controllers/identity-viewer-controller.js new file mode 100644 index 0000000..22462d7 --- /dev/null +++ b/lib/services/identity/controllers/identity-viewer-controller.js @@ -0,0 +1,62 @@ +'use strict'; + +const _ = require('lodash'); +const Boom = require('boom'); + +const Controller = require('../../../controllers/controller'); +const IdentityItemController = require('./identity-item-controller'); + +class IdentityViewerController extends IdentityItemController { + get(req, res, next) { + const err = this.checkViewerAccess(req); + if (err) { + return next(err); + } + + return super.get(req, res, next); + } + + patch(req, res, next) { + const err = this.checkViewerAccess(req); + if (err) { + return next(err); + } + + return super.patch(req, res, next); + } + + delete(req, res, next) { + const err = this.checkViewerAccess(req); + if (err) { + return next(err); + } + + return super.delete(req, res, next); + } + + checkViewerAccess(req) { + if (this.isAdminRequest(req)) { + return null; + } + + const viewerId = _.get(req, 'identity.viewer.id'); + if (req.params.id !== viewerId) { + return Boom.unauthorized('Viewer specified in JWT does not match requested viewer.'); + } + + return null; + } + + static create(spec) { + if (!spec.bus || !_.isObject(spec.bus)) { + throw new Error('IdentityViewerController spec.bus is required'); + } + + return Controller.create(new IdentityViewerController({ + bus: spec.bus, + type: 'viewer' + })); + } +} + +module.exports = IdentityViewerController; From 41220fa977b0d25431878640011c2a30a4a094fc Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 06:56:23 -0400 Subject: [PATCH 10/14] New IdentityViewersListController class new file: lib/services/identity/controllers/identity-viewers-list-controller.js --- .../identity-viewers-list-controller.js | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 lib/services/identity/controllers/identity-viewers-list-controller.js diff --git a/lib/services/identity/controllers/identity-viewers-list-controller.js b/lib/services/identity/controllers/identity-viewers-list-controller.js new file mode 100644 index 0000000..e4471d6 --- /dev/null +++ b/lib/services/identity/controllers/identity-viewers-list-controller.js @@ -0,0 +1,75 @@ +'use strict'; + +const _ = require('lodash'); +const Boom = require('boom'); + +const Controller = require('../../../controllers/controller'); +const IdentityListController = require('./identity-list-controller'); + +class IdentityViewersListController extends IdentityListController { + constructor(spec) { + super(); + this.bus = spec.bus; + this.type = spec.type; + } + + get(req, res, next) { + const type = 'viewer'; + const limit = parseInt(req.query.limit, 10) || 10; + const args = {type, limit}; + + const err = this.checkViewerAccess(req); + if (err) { + return next(err); + } + + return this.getChannel(req) + .then(channel => { + args.channel = channel.id; + return this.bus.query({role: 'store', cmd: 'scan', type}, args); + }) + .then(resources => { + // If this is not an admin request then we filter out all viewer + // resources from the response other than the one which matches the + // one the caller is authenticated for. + if (!this.isAdminRequest(req)) { + const viewerId = (req.identity.viewer || {}).id; + resources = resources.filter(item => { + return item.id === viewerId; + }); + } + + res.status(200); + res.body = resources.slice(0, limit); + next(); + return null; + }) + .catch(next); + } + + // POST just proxies to the parent class. + // post(req, res, next) { + // } + + checkChannelAccess(req) { + if (this.isAdminRequest(req)) { + return null; + } + + if (!_.get(req, 'identity.viewer.id')) { + return Boom.forbidden('Non admin callers must have a viewer embedded in the JSON Web Token'); + } + + return null; + } + + static create(spec) { + if (!spec.bus || !_.isObject(spec.bus)) { + throw new Error('IdentityViewersListController spec.bus is required'); + } + + return Controller.create(new IdentityViewersListController(spec)); + } +} + +module.exports = IdentityViewersListController; From 9ca7e41b9c8bbcfde270203898d0ee857521aab5 Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 06:58:03 -0400 Subject: [PATCH 11/14] Remove Viewer logic from generic identity controllers modified: lib/services/identity/controllers/identity-channels-list-controller.js modified: lib/services/identity/controllers/identity-item-controller.js modified: lib/services/identity/controllers/identity-list-controller.js --- .../identity-channels-list-controller.js | 3 +-- .../controllers/identity-item-controller.js | 4 --- .../controllers/identity-list-controller.js | 27 +++---------------- 3 files changed, 4 insertions(+), 30 deletions(-) diff --git a/lib/services/identity/controllers/identity-channels-list-controller.js b/lib/services/identity/controllers/identity-channels-list-controller.js index 731273f..2da501b 100644 --- a/lib/services/identity/controllers/identity-channels-list-controller.js +++ b/lib/services/identity/controllers/identity-channels-list-controller.js @@ -75,8 +75,7 @@ class IdentityChannelsListController extends IdentityListController { return null; } - const channelId = _.get(req, 'identity.channel.id'); - if (!channelId) { + if (!_.get(req, 'identity.channel.id')) { return Boom.forbidden('Non admin callers must have a channel embedded in the JSON Web Token'); } diff --git a/lib/services/identity/controllers/identity-item-controller.js b/lib/services/identity/controllers/identity-item-controller.js index 08df136..12d6608 100644 --- a/lib/services/identity/controllers/identity-item-controller.js +++ b/lib/services/identity/controllers/identity-item-controller.js @@ -14,10 +14,6 @@ class IdentityItemController extends Controller { } get(req, res, next) { - if (this.type === 'viewer' && !this.isAdminRequest(req) && req.params.id !== req.identity.viewer.id) { - return next(Boom.unauthorized('Viewer specified in JWT does not match requested viewer.')); - } - const type = this.type; const id = req.params.id; const args = {type, id}; diff --git a/lib/services/identity/controllers/identity-list-controller.js b/lib/services/identity/controllers/identity-list-controller.js index 34999d3..6637315 100644 --- a/lib/services/identity/controllers/identity-list-controller.js +++ b/lib/services/identity/controllers/identity-list-controller.js @@ -20,25 +20,10 @@ class IdentityListController extends Controller { return this.getChannel(req) .then(channel => { - // If this is not a channel resource, then it must belong to a channel - // which we reference here with the .channel attribute. - if (type !== 'channel') { - args.channel = channel.id; - } - + args.channel = channel.id; return this.bus.query({role: 'store', cmd: 'scan', type}, args); }) .then(resources => { - // If we're querying for channel resources and this is not an admin request, - // then we filter out all channel resources from the response other than the - // one which matches the one the caller is authenticated for. - if (type === 'channel' && !this.isAdminRequest(req)) { - const channelId = (req.identity.channel || {}).id; - resources = resources.filter(item => { - return item.id === channelId; - }); - } - res.status(200); res.body = resources.slice(0, limit); next(); @@ -54,20 +39,14 @@ class IdentityListController extends Controller { return this.getChannel(req) .then(channel => { - // If this is not a channel resource, then it must belong to a channel - // which we reference here with the .channel attribute. - if (type !== 'channel') { - payload.channel = channel.id; - } + payload.channel = channel.id; // If there is a client defined id, then we check the store for a // conflict before moving on. if (payload.id) { const args = {type, id: payload.id}; - if (type !== 'channel') { - args.channel = channel.id; - } + args.channel = channel.id; return this.bus.query({role: 'store', cmd: 'get', type}, args); } From 0177d0e5352703a1049499bcd02915108fed982d Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 06:59:52 -0400 Subject: [PATCH 12/14] Unforce Identity Controller test with type viewer modified: spec/services/identity/controllers/identity-item-controller-spec.js modified: spec/services/identity/controllers/identity-list-controller-spec.js --- .../identity/controllers/identity-item-controller-spec.js | 4 +--- .../identity/controllers/identity-list-controller-spec.js | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/spec/services/identity/controllers/identity-item-controller-spec.js b/spec/services/identity/controllers/identity-item-controller-spec.js index ad9c685..aad2c4e 100644 --- a/spec/services/identity/controllers/identity-item-controller-spec.js +++ b/spec/services/identity/controllers/identity-item-controller-spec.js @@ -8,9 +8,7 @@ const _ = require('lodash'); const IdentityItemController = require('../../../../lib/services/identity/controllers/identity-item-controller'); describe('Identity Item Controller', function () { - // TODO: Undo this @kixxauth - // const TYPES = Object.freeze(['platform', 'viewer', 'cat', 'horse']); - const TYPES = Object.freeze(['viewer']); + const TYPES = Object.freeze(['platform', 'viewer', 'cat', 'horse']); const type = _.sample(TYPES); diff --git a/spec/services/identity/controllers/identity-list-controller-spec.js b/spec/services/identity/controllers/identity-list-controller-spec.js index 4eb6388..d761c11 100644 --- a/spec/services/identity/controllers/identity-list-controller-spec.js +++ b/spec/services/identity/controllers/identity-list-controller-spec.js @@ -9,9 +9,7 @@ const uuid = require('node-uuid'); const IdentityListController = require('../../../../lib/services/identity/controllers/identity-list-controller'); describe('Identity List Controller', function () { - // TODO: Undo this @kixxauth - // const TYPES = Object.freeze(['platform', 'viewer', 'cat', 'horse']); - const TYPES = Object.freeze(['viewer']); + const TYPES = Object.freeze(['platform', 'viewer', 'cat', 'horse']); const type = _.sample(TYPES); let bus; let handler; From 5d7dfea810da041c9f7c0cabb44f61724d7396be Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 07:09:38 -0400 Subject: [PATCH 13/14] Adds pending tests for Viewer Controllers. new file: spec/services/identity/controllers/identity-viewer-controller-spec.js new file: spec/services/identity/controllers/identity-viewers-list-controller-spec.js --- .../identity-viewer-controller-spec.js | 35 +++++++++++++++++++ .../identity-viewers-list-controller-spec.js | 26 ++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 spec/services/identity/controllers/identity-viewer-controller-spec.js create mode 100644 spec/services/identity/controllers/identity-viewers-list-controller-spec.js diff --git a/spec/services/identity/controllers/identity-viewer-controller-spec.js b/spec/services/identity/controllers/identity-viewer-controller-spec.js new file mode 100644 index 0000000..452941b --- /dev/null +++ b/spec/services/identity/controllers/identity-viewer-controller-spec.js @@ -0,0 +1,35 @@ +/* global describe, it */ +/* eslint prefer-arrow-callback: 0 */ +/* eslint-disable max-nested-callbacks */ +'use strict'; + +// Reference identity-item-controller-spec.js and identity-channel-controller-spec.js + +describe('Identity Viewer Controller', function () { + describe('GET', function () { + describe('as platform', function () { + it('needs to be tested'); + }); + describe('as admin', function () { + it('needs to be tested'); + }); + }); + + describe('PATCH', function () { + describe('as platform', function () { + it('needs to be tested'); + }); + describe('as admin', function () { + it('needs to be tested'); + }); + }); + + describe('DELETE', function () { + describe('as platform', function () { + it('needs to be tested'); + }); + describe('as admin', function () { + it('needs to be tested'); + }); + }); +}); diff --git a/spec/services/identity/controllers/identity-viewers-list-controller-spec.js b/spec/services/identity/controllers/identity-viewers-list-controller-spec.js new file mode 100644 index 0000000..c8d3601 --- /dev/null +++ b/spec/services/identity/controllers/identity-viewers-list-controller-spec.js @@ -0,0 +1,26 @@ +/* global describe, it */ +/* eslint prefer-arrow-callback: 0 */ +/* eslint-disable max-nested-callbacks */ +'use strict'; + +// Reference identity-list-controller-spec.js and identity-channels-list-controller-spec.js + +describe('Identity Viewers List Controller', function () { + describe('GET', function () { + describe('as platform', function () { + it('needs to be tested'); + }); + describe('as admin', function () { + it('needs to be tested'); + }); + }); + + describe('POST', function () { + describe('as platform', function () { + it('needs to be tested'); + }); + describe('as admin', function () { + it('needs to be tested'); + }); + }); +}); From 30b4ac6fb61a6fc16a85a62487a9b098335f109b Mon Sep 17 00:00:00 2001 From: Kristoffer Walker Date: Thu, 27 Oct 2016 07:19:18 -0400 Subject: [PATCH 14/14] Hook up Identity Channel and Viewer controllers to router modified: lib/services/identity/index.js --- lib/services/identity/index.js | 64 ++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 3 deletions(-) diff --git a/lib/services/identity/index.js b/lib/services/identity/index.js index 6d73962..6928754 100644 --- a/lib/services/identity/index.js +++ b/lib/services/identity/index.js @@ -5,6 +5,10 @@ const express = require('express'); const debug = require('debug')('oddworks:identity-service'); const middleware = require('../../middleware'); const initializeQueries = require('./queries/'); +const IdentityChannelController = require('./controllers/identity-channel-controller'); +const IdentityChannelsListController = require('./controllers/identity-channels-list-controller'); +const IdentityViewerController = require('./controllers/identity-viewer-controller'); +const IdentityViewersListController = require('./controllers/identity-viewers-list-controller'); const IdentityItemController = require('./controllers/identity-item-controller'); const IdentityListController = require('./controllers/identity-list-controller'); const IdentityConfigController = require('./controllers/identity-config-controller'); @@ -66,6 +70,53 @@ module.exports = function (bus, options) { const types = options.types; const router = options.router || express.Router(); // eslint-disable-line babel/new-cap, new-cap + // Hook up the channels route + // + + router.all( + 'channels', + middleware['request-authorize']({bus, audience: { + get: ['admin'], + post: ['admin'] + }}), + IdentityChannelsListController.create({bus}) + ); + + router.all( + '/channels/:id', + middleware['request-authorize']({bus, audience: { + get: ['admin'], + patch: ['admin'], + delete: ['admin'] + }}), + IdentityChannelController.create({bus}) + ); + + // Hook up the viewers route + // + + router.all( + 'viewers', + middleware['request-authorize']({bus, audience: { + get: ['admin'], + post: ['admin'] + }}), + IdentityViewersListController.create({bus}) + ); + + router.all( + '/viewers/:id', + middleware['request-authorize']({bus, audience: { + get: ['admin', 'platform'], + patch: ['admin'], + delete: ['admin'] + }}), + IdentityViewerController.create({bus}) + ); + + // Hook up all other types + // + types.forEach(type => { router.all( `/${type}s`, @@ -79,7 +130,7 @@ module.exports = function (bus, options) { router.all( `/${type}s/:id`, middleware['request-authorize']({bus, audience: { - get: ['admin', 'platform'], + get: ['admin'], patch: ['admin'], delete: ['admin'] }}), @@ -87,7 +138,9 @@ module.exports = function (bus, options) { ); }); - // Being: Viewer Relationships + // Hook up Viewer Relationships + // + ['watchlist'].forEach(relationship => { router.all( `/viewers/:id/relationships/${relationship}`, @@ -99,7 +152,9 @@ module.exports = function (bus, options) { ViewerRelationshipController.create({bus, relationship}) ); }); - // End: Viewer Relationships + + // Hook up Login + // router.all( '/login', @@ -109,6 +164,9 @@ module.exports = function (bus, options) { IdentityLoginController.create({bus}) ); + // Hook up Config + // + router.all( '/config', middleware['request-authorize']({bus, audience: {