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); } 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..88c70bb --- /dev/null +++ b/lib/services/identity/controllers/identity-channel-controller.js @@ -0,0 +1,120 @@ +'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}; + + const err = this.checkChannelAccess(req); + if (err) { + return next(err); + } + + 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}; + + const err = this.checkChannelAccess(req); + if (err) { + return next(err); + } + + 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}; + + const err = this.checkChannelAccess(req); + if (err) { + return next(err); + } + + return this.bus.sendCommand({role: 'store', cmd: 'remove', type}, args) + .then(() => { + res.body = {}; + res.status(200); + next(); + return null; + }) + .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'); + } + + if (req.params.id !== channelId) { + return Boom.forbidden('Access to the requested channel is forbidden'); + } + + return null; + } + + 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; 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..2da501b --- /dev/null +++ b/lib/services/identity/controllers/identity-channels-list-controller.js @@ -0,0 +1,97 @@ +'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}; + + 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 + // 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); + } + + checkChannelAccess(req) { + if (this.isAdminRequest(req)) { + return null; + } + + if (!_.get(req, 'identity.channel.id')) { + 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'); + } + + return Controller.create(new IdentityChannelsListController({ + bus: spec.bus, + type: 'channel' + })); + } +} + +module.exports = IdentityChannelsListController; diff --git a/lib/services/identity/controllers/identity-item-controller.js b/lib/services/identity/controllers/identity-item-controller.js index f5e168b..12d6608 100644 --- a/lib/services/identity/controllers/identity-item-controller.js +++ b/lib/services/identity/controllers/identity-item-controller.js @@ -14,19 +14,13 @@ 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}; 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 +49,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 +57,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 +80,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(() => { 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); } 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; 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; 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: { 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'); }); }); }); 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'); + }); + }); +});