From 1bcda92993b37233dbaa0dbc9d3bbd24053d7ead Mon Sep 17 00:00:00 2001 From: mathisonryan Date: Fri, 27 Aug 2021 17:05:13 -0500 Subject: [PATCH 1/2] Add rich query support Add rich query support along with list channels and records --- src/controller/db.js | 97 ++++++- src/docs/jsdoc.json | 34 +-- src/routes/agent.js | 298 ++++++++++++++++++-- src/test-helpers/db-handler.js | 12 + src/test/api.test.js | 477 ++++++++++++++++++++++++++++++++- src/test/auth.test.js | 82 +++++- src/test/bad.test.js | 51 +++- src/test/empty.test.js | 66 +++++ 8 files changed, 1066 insertions(+), 51 deletions(-) create mode 100644 src/test/empty.test.js diff --git a/src/controller/db.js b/src/controller/db.js index bf8d4fa..b6d444a 100644 --- a/src/controller/db.js +++ b/src/controller/db.js @@ -99,7 +99,7 @@ const setupClient = () => { * Commits a resource to the database * @func * @async - * @param channel {string} the name of the channel + * @param channel {string} the id of the channel * @param recordID {string} the id of the record * @param payload {object} payload to write * @param type {string} the commit type to perform @@ -139,11 +139,99 @@ const commitResource = async (channel, recordID, payload, type, ctx) => { return result; }; +/** + * List channels from the database + * @func + * @async + * @param ctx {object} the tracing context + * @returns {object} the list of channel ids + */ +const listChannels = async (ctx) => { + const span = opentracing.globalTracer() + .startSpan('MongoDB - List channels', { + childOf: ctx, + }); + // eslint-disable-next-line max-len + return client.db(CHANNEL_DB).listCollections(null, { nameOnly: true }).toArray().then((collections) => { + if (collections.length === 0) { + span.finish(); + throw new errors.NotFoundError(); + } + const collectionsArray = collections.map((item) => item.name); + span.finish(); + return collectionsArray; + }); +}; + +/** + * List records from the database + * @func + * @async + * @param channel {string} the id of the channel + * @param ctx {object} the tracing context + * @returns {object} the list of record ids + */ +const listRecords = async (channel, ctx) => { + if (channel.endsWith(AUDIT_POSTFIX)) throw new errors.ForbiddenChannelError(); + const span = opentracing.globalTracer() + .startSpan('MongoDB - List records', { + childOf: ctx, + }); + const collection = client.db(CHANNEL_DB) + .collection(channel); + + const resources = []; + const cursor = await collection.find({}, { projection: { _id: 1 } }); + if ((await cursor.count()) === 0) { + log.info('No records found'); + span.finish(); + throw new errors.NotFoundError(); + } + await cursor.forEach((resource) => { + // eslint-disable-next-line dot-notation + resources.push(resource['_id']); + }); + span.finish(); + return resources; +}; + +/** + * Query records from the database + * @func + * @async + * @param channel {string} the id of the channel + * @param ctx {object} the tracing context + * @returns {object} the list of records + */ +const queryRecords = async (channel, query, projection, limit, skip, ctx) => { + if (channel.endsWith(AUDIT_POSTFIX)) throw new errors.ForbiddenChannelError(); + const span = opentracing.globalTracer() + .startSpan('MongoDB - Query records', { + childOf: ctx, + }); + const collection = client.db(CHANNEL_DB) + .collection(channel); + + const resources = []; + const cursor = await collection.find(query, { projection, limit, skip }); + if ((await cursor.count()) === 0) { + log.info('No records found'); + span.finish(); + throw new errors.NotFoundError(); + } + await cursor.forEach((resource) => { + // eslint-disable-next-line dot-notation + resources.push(resource); + }); + span.finish(); + return resources; +}; + /** * Queries a resource from the database * @func * @async - * @param channel {string} the name of the channel + * @param channel {string} the id of the channel * @param resourceID {string} the id of the resource * @param ctx {object} the tracing context * @returns {object} the resource payload @@ -171,7 +259,7 @@ const queryResource = async (channel, resourceID, ctx) => { * Queries a the audit history of a resource from the database * @func * @async - * @param channel {string} the name of the channel + * @param channel {string} the id of the channel * @param resourceID {string} the id of the resource * @returns {object} the resource audit history */ @@ -195,6 +283,9 @@ const queryResourceAudit = async (channel, resourceID) => { module.exports = { commitResource, + listChannels, + listRecords, + queryRecords, queryResource, queryResourceAudit, makeClientFromEnv, diff --git a/src/docs/jsdoc.json b/src/docs/jsdoc.json index 02d2d76..1c8925e 100644 --- a/src/docs/jsdoc.json +++ b/src/docs/jsdoc.json @@ -1,26 +1,28 @@ { "source": { - "include": [ - "../README.md", - "app.js", - "controller/db.js", - "controller/tracer.js", - "routes/agent.js", - "routes/index.js", - "utils/logging.js" - ] + "include": [ + "../README.md", + "app.js", + "controller/db.js", + "routes/agent.js", + "routes/index.js", + "utils/environment.js", + "utils/errors.js", + "utils/logging.js", + "utils/tracer.js" + ] }, "opts": { - "destination": "docs/gen", - "recurse": true, - "pedantic": true, - "template": "./node_modules/minami" + "destination": "docs/gen", + "recurse": true, + "pedantic": true, + "template": "./node_modules/minami" }, "templates": { - "systemName": "DBoM Database Agent for Node.js", - "theme": "cosmo" + "systemName": "DBoM Database Agent for Node.js", + "theme": "cosmo" }, "tags": { - "allowUnknownTags": false + "allowUnknownTags": false } } \ No newline at end of file diff --git a/src/routes/agent.js b/src/routes/agent.js index 9a50280..3e6c59e 100644 --- a/src/routes/agent.js +++ b/src/routes/agent.js @@ -32,6 +32,22 @@ const opentracing = require('opentracing'); const db = require('../controller/db'); const jaegerHelper = require('../utils/tracer'); +const limitErrorString = 'Invalid Limit'; +const skipErrorString = 'Invalid Skip'; +const filterErrorString = 'Invalid Filter'; +const mongoErrorString = 'MongoError'; +const syntaxErrorString = 'SyntaxError'; +const invalidRequestErrorString = 'Invalid Request'; +const notFoundErrorString = 'NotFoundError'; +const noResourceErrorString = 'No Such Resource'; +const forbiddenChannelErrorString = 'ForbiddenChannelError'; +const queryNotAllowedErrorString = 'Query on this channel is not allowed'; +const commitNotAllowedErrorString = 'Commit on this channel is not allowed'; +const auditNotAllowedErrorString = 'Audit on this channel is not allowed'; +const agentQueryErrorString = 'Agent Query Failure'; +const agentCommitErrorString = 'Agent Commit Failure'; +const agentAuditErrorString = 'Agent Audit Failure'; +const recordExistsErrorString = 'Record already exists, use update'; const unauthorizedPayload = { success: false, status: 'The entity that this agent is authenticated as is not authorized to perform for this operation', @@ -39,7 +55,160 @@ const unauthorizedPayload = { router.use(jaegerHelper.injectSpanMiddleware); -/* GET home page. */ +/** + * Route serving the listing of channel ids + * @name get/ + * @async + * @function + * @memberof module:route/agent~agentRouter + * @inner + * @param {string} path - Express path + * @param {callback} middleware - Express middleware. + */ +router.get('/', async (req, res) => { + const span = opentracing.globalTracer().startSpan('List Channels', { + childOf: req.spanContext, + }); + + try { + const channels = await db.listChannels(span.context()); + res.json(channels); + } catch (e) { + log.error(`List Channels Error ${e.toString()}`); + span.setTag(opentracing.Tags.ERROR, true); + span.log({ event: 'error', message: e.toString() }); + if (e.name === mongoErrorString && e.code === 13) { + res.status(401) + .json(unauthorizedPayload); + } else if (e.name === notFoundErrorString) { + res.status(404) + .json({ + success: false, + status: noResourceErrorString, + }); + } else { + res.status(500) + .json({ + success: false, + status: agentQueryErrorString, + error: e.toString(), + }); + } + } finally { + span.finish(); + } +}); + +/** + * Route serving the listing of records ids for a channel + * @name get/:channel/records/ + * @async + * @function + * @memberof module:route/agent~agentRouter + * @inner + * @param {string} path - Express path + * @param {callback} middleware - Express middleware. + */ +router.get('/:channel/records/', async (req, res) => { + const span = opentracing.globalTracer().startSpan('List Records', { + childOf: req.spanContext, + }); + + const { channel } = req.params; + try { + const records = await db.listRecords(channel, span.context()); + res.json(records); + } catch (e) { + log.error(`List Records ${e.toString()}`); + span.setTag(opentracing.Tags.ERROR, true); + span.log({ event: 'error', message: e.toString() }); + if (e.name === mongoErrorString && e.code === 13) { + res.status(401) + .json(unauthorizedPayload); + } else if (e.name === notFoundErrorString) { + res.status(404) + .json({ + success: false, + status: noResourceErrorString, + }); + } else if (e.name === forbiddenChannelErrorString) { + res.status(403) + .json({ + success: false, + status: queryNotAllowedErrorString, + }); + } else { + res.status(500) + .json({ + success: false, + status: agentQueryErrorString, + error: e.toString(), + }); + } + } finally { + span.finish(); + } +}); + +/** + * Route serving the querying of records from a channel + * @name get/:channel/records/_query + * @async + * @function + * @memberof module:route/agent~agentRouter + * @inner + * @param {string} path - Express path + * @param {callback} middleware - Express middleware. + */ +router.get('/:channel/records/_query', async (req, res) => { + const span = opentracing.globalTracer().startSpan('Query Records - Get', { + childOf: req.spanContext, + }); + const { channel } = req.params; + const { + query, limit, skip, + } = req.query; + let { + filter, + } = req.query; + try { + if (filter !== undefined && filter !== null) filter = JSON.parse(filter); + // eslint-disable-next-line no-use-before-define, max-len + await handleQueryRecords(res, channel, JSON.parse(query), filter, limit, skip, span.context()); + } catch (e) { + res.status(400) + .json({ + success: false, + status: invalidRequestErrorString, + error: e.toString(), + }); + } + span.finish(); +}); + +/** + * Route serving the querying of records from a channel + * @name post/:channel/records/_query + * @async + * @function + * @memberof module:route/agent~agentRouter + * @inner + * @param {string} path - Express path + * @param {callback} middleware - Express middleware. + */ +router.post('/:channel/records/_query', async (req, res) => { + const span = opentracing.globalTracer().startSpan('Query Records - Post', { + childOf: req.spanContext, + }); + const { channel } = req.params; + const { + query, filter, limit, skip, + } = req.body; + // eslint-disable-next-line no-use-before-define + await handleQueryRecords(res, channel, query, filter, limit, skip, span.context()); + span.finish(); +}); + /** * Route serving the creation of a record * @name post/:channel/records/ @@ -82,31 +251,31 @@ router.post('/:channel/records/', async (req, res) => { log.error(`Commit Error ${e}`); span.setTag(opentracing.Tags.ERROR, true); span.log({ event: 'error', message: e.toString() }); - if (e.name === 'MongoError' && e.code === 11000) { + if (e.name === mongoErrorString && e.code === 11000) { res.status(409).json({ success: false, - status: 'Record already exists, use update', + status: recordExistsErrorString, }); - } else if (e.name === 'MongoError' && e.code === 13) { + } else if (e.name === mongoErrorString && e.code === 13) { res.status(401) .json(unauthorizedPayload); - } else if (e.name === 'ForbiddenChannelError') { + } else if (e.name === forbiddenChannelErrorString) { res.status(403) .json({ success: false, - status: 'You are not allowed to commit to this channel', + status: commitNotAllowedErrorString, }); - } else if (e.name === 'NotFoundError') { + } else if (e.name === notFoundErrorString) { res.status(404) .json({ success: false, - status: 'No Such Resource', + status: noResourceErrorString, }); } else { res.status(500) .json({ success: false, - status: 'Agent Commit Failure', + status: agentCommitErrorString, error: e.toString(), }); } @@ -140,26 +309,26 @@ router.get('/:channel/records/:recordID', async (req, res) => { log.error(`Query Error ${e.toString()}`); span.setTag(opentracing.Tags.ERROR, true); span.log({ event: 'error', message: e.toString() }); - if (e.name === 'MongoError' && e.code === 13) { + if (e.name === mongoErrorString && e.code === 13) { res.status(401) .json(unauthorizedPayload); - } else if (e.name === 'NotFoundError') { + } else if (e.name === notFoundErrorString) { res.status(404) .json({ success: false, - status: 'No Such Resource', + status: noResourceErrorString, }); - } else if (e.name === 'ForbiddenChannelError') { + } else if (e.name === forbiddenChannelErrorString) { res.status(403) .json({ success: false, - status: 'You are not allowed to query from this channel', + status: queryNotAllowedErrorString, }); } else { res.status(500) .json({ success: false, - status: 'Agent Query Failure', + status: agentQueryErrorString, error: e.toString(), }); } @@ -193,26 +362,26 @@ router.get('/:channel/records/:recordID/audit', async (req, res) => { }); } catch (e) { log.error(`Audit Error ${e.toString()}`); - if (e.name === 'NotFoundError') { + if (e.name === notFoundErrorString) { res.status(404) .json({ success: false, - status: 'No Such Resource', + status: noResourceErrorString, }); - } else if (e.name === 'ForbiddenChannelError') { + } else if (e.name === forbiddenChannelErrorString) { res.status(403) .json({ success: false, - status: 'You are not allowed to query audit trail from this channel', + status: auditNotAllowedErrorString, }); - } else if (e.name === 'MongoError' && e.code === 13) { + } else if (e.name === mongoErrorString && e.code === 13) { res.status(401) .json(unauthorizedPayload); } else { res.status(500) .json({ success: false, - status: 'Agent Audit Failure', + status: agentAuditErrorString, error: e.toString(), }); } @@ -220,4 +389,91 @@ router.get('/:channel/records/:recordID/audit', async (req, res) => { span.finish(); }); +/** + * Common handling of querying records from a channel + * @func + * @async + * @param res {object} the response object + * @param channel {string} the id of the channel + * @param query {object} the query to run + * @param filter {array} the records fields to return + * @param limit {number} the limit of records to return + * @param skip {number} the number of records to skip + * @param ctx {object} the tracing context + */ +async function handleQueryRecords(res, channel, query, filter, limit, skip, ctx) { + const span = opentracing.globalTracer().startSpan('Handle Query', { + childOf: ctx, + }); + + try { + const limitNumber = Number(limit); + if (limit && Number.isNaN(limitNumber)) { + throw new Error(limitErrorString); + } + const skipNumber = Number(skip); + if (skip && Number.isNaN(skipNumber)) { + throw new Error(skipErrorString); + } + let filterJSON = null; + if (filter) { + if (!Array.isArray(filter)) throw new Error(filterErrorString); + if (filter.length < 1) throw new Error(filterErrorString); + // eslint-disable-next-line no-return-assign, no-sequences + filterJSON = filter.reduce((acc, curr) => (acc[curr] = 1, acc), {}); + } else { + filterJSON = {}; + } + // eslint-disable-next-line max-len + const records = await db.queryRecords(channel, query, filterJSON, limitNumber, skipNumber, span.context()); + const recs = {}; + records.forEach((record) => { + // eslint-disable-next-line dot-notation + const id = record['_id']; + // eslint-disable-next-line no-param-reassign, dot-notation + delete record['_id']; + recs[id] = record; + }); + res.status(200); + res.json(recs); + } catch (e) { + log.error(`Query Records Error ${e.toString()}`); + span.setTag(opentracing.Tags.ERROR, true); + span.log({ event: 'error', message: e.toString() }); + if (e.name === mongoErrorString && e.code === 13) { + res.status(401) + .json(unauthorizedPayload); + // eslint-disable-next-line max-len + } else if (e.name === syntaxErrorString || e.message === filterErrorString || e.message === limitErrorString || e.message === skipErrorString) { + res.status(400) + .json({ + success: false, + status: invalidRequestErrorString, + error: e.toString(), + }); + } else if (e.name === notFoundErrorString) { + res.status(404) + .json({ + success: false, + status: noResourceErrorString, + }); + } else if (e.name === forbiddenChannelErrorString) { + res.status(403) + .json({ + success: false, + status: queryNotAllowedErrorString, + }); + } else { + res.status(500) + .json({ + success: false, + status: agentQueryErrorString, + error: e.toString(), + }); + } + } finally { + span.finish(); + } +} + module.exports = router; diff --git a/src/test-helpers/db-handler.js b/src/test-helpers/db-handler.js index d1a952d..fa6efa5 100644 --- a/src/test-helpers/db-handler.js +++ b/src/test-helpers/db-handler.js @@ -58,6 +58,18 @@ module.exports.prepareFakeAudit = async () => { }); }; +/** + * Prepare an empty db for tests + */ +module.exports.prepareEmptyDB = async () => { + await this.connect(); + process.env.MONGO_PORT = await this.getPort(); + const client = await new MongoClient(await mongod.getUri(), { + useNewUrlParser: true, + }).connect(); + await client.db('empty'); +}; + /** * Connect to the in-memory database. */ diff --git a/src/test/api.test.js b/src/test/api.test.js index d8c687d..570b188 100644 --- a/src/test/api.test.js +++ b/src/test/api.test.js @@ -33,6 +33,7 @@ let app; process.env.MONGO_URI = 'mongodb://127.0.0.1:27071'; before((done) => { + process.env.CHANNEL_DB = 'primary'; decache('../app'); // eslint-disable-next-line global-require app = require('../app'); @@ -67,6 +68,30 @@ describe('Create', () => { recordID: 'test', recordIDPayload: { test: 'test', + test2: 'test', + }, + }) + .end((err, res) => { + expect(res) + .to + .have + .status(200); + expect(res.body.success) + .to + .equals(true); + done(); + }); + }); + it('Create New 2', (done) => { + chai + .request(app) + .post('/channels/test/records') + .set('commit-type', 'CREATE') + .send({ + recordID: 'test2', + recordIDPayload: { + test: 'test2', + test2: 'test2', }, }) .end((err, res) => { @@ -184,22 +209,467 @@ describe('Get', () => { done(); }); }); - it('Get No AssetID', (done) => { + it('Get Audit Channel', (done) => { + chai + .request(app) + .get('/channels/_audit/records/test') + .end((err, res) => { + expect(res) + .to + .have + .status(403); + expect(res.body.success) + .to + .equals(false); + done(); + }); + }); +}); + +describe('List Channels', () => { + it('List Channels', (done) => { + chai + .request(app) + .get('/channels/') + .end((err, res) => { + expect(res) + .to + .have + .status(200); + expect(res.body.length) + .to + .equal(1); + expect(res.body) + .to + .contain('test'); + done(); + }); + }); +}); + +describe('List Channel Assets', () => { + it('List Channel Assets', (done) => { chai .request(app) .get('/channels/test/records/') + .end((err, res) => { + expect(res) + .to + .have + .status(200); + expect(res.body.length) + .to + .equal(2); + expect(res.body) + .to + .contain('test'); + expect(res.body) + .to + .contain('test2'); + done(); + }); + }); + it('List Bad Channel', (done) => { + chai + .request(app) + .get('/channels/bad/records') .end((err, res) => { expect(res) .to .have .status(404); + expect(res.body.success) + .to + .equals(false); done(); }); }); - it('Get Audit Channel', (done) => { + it('List Audit Channel', (done) => { chai .request(app) - .get('/channels/_audit/records/test') + .get('/channels/_audit/records') + .end((err, res) => { + expect(res) + .to + .have + .status(403); + expect(res.body.success) + .to + .equals(false); + done(); + }); + }); +}); + +describe('Query Channel Assets', () => { + it('Query Channel Assets', (done) => { + chai + .request(app) + .get('/channels/test/records/_query?query={}') + .end((err, res) => { + expect(res) + .to + .have + .status(200); + expect(Object.keys(res.body).length) + .to + .equal(2); + expect(res.body) + .to + .have + .property('test'); + expect(res.body) + .to + .have + .property('test2'); + expect(res.body.test) + .to + .have + .property('test'); + expect(res.body.test) + .to + .have + .property('test2'); + done(); + }); + }); + it('Query Channel Assets None', (done) => { + chai + .request(app) + .get('/channels/none/records/_query?query={}') + .end((err, res) => { + expect(res) + .to + .have + .status(404); + done(); + }); + }); + it('Query Channel Assets Limit', (done) => { + chai + .request(app) + .get('/channels/test/records/_query?query={}&limit=1') + .end((err, res) => { + expect(res) + .to + .have + .status(200); + expect(Object.keys(res.body).length) + .to + .equal(1); + expect(res.body) + .to + .have + .property('test'); + done(); + }); + }); + it('Query Channel Assets Bad Limit', (done) => { + chai + .request(app) + .get('/channels/test/records/_query?query={}&limit=one') + .end((err, res) => { + expect(res) + .to + .have + .status(400); + done(); + }); + }); + it('Query Channel Assets Skip', (done) => { + chai + .request(app) + .get('/channels/test/records/_query?query={}&limit=1&skip=1') + .end((err, res) => { + expect(res) + .to + .have + .status(200); + expect(Object.keys(res.body).length) + .to + .equal(1); + expect(res.body) + .to + .not + .have + .property('test'); + expect(res.body) + .to + .have + .property('test2'); + done(); + }); + }); + it('Query Channel Assets Bad Skip', (done) => { + chai + .request(app) + .get('/channels/test/records/_query?query={}&limit=1&skip=one') + .end((err, res) => { + expect(res) + .to + .have + .status(400); + done(); + }); + }); + it('Query Channel Assets Filter', (done) => { + chai + .request(app) + .get('/channels/test/records/_query?query={}&filter=["test"]') + .end((err, res) => { + expect(res) + .to + .have + .status(200); + expect(Object.keys(res.body).length) + .to + .equal(2); + expect(res.body) + .to + .have + .property('test'); + expect(res.body.test) + .to + .have + .property('test'); + expect(res.body.test) + .to + .not + .have + .property('test2'); + done(); + }); + }); + it('Query Channel Assets Bad Filter', (done) => { + chai + .request(app) + .get('/channels/test/records/_query?query={}&filter="test"') + .end((err, res) => { + expect(res) + .to + .have + .status(400); + done(); + }); + }); + it('Query Channel Assets Bad Filter Parse', (done) => { + chai + .request(app) + .get('/channels/test/records/_query?query={}&filter={test') + .end((err, res) => { + expect(res) + .to + .have + .status(400); + done(); + }); + }); + it('Query Channel Assets Missing Filter', (done) => { + chai + .request(app) + .get('/channels/test/records/_query?query={}&filter=[]') + .end((err, res) => { + expect(res) + .to + .have + .status(400); + done(); + }); + }); + it('Query Audit Channel', (done) => { + chai + .request(app) + .get('/channels/_audit/records/_query?query={}') + .end((err, res) => { + expect(res) + .to + .have + .status(403); + expect(res.body.success) + .to + .equals(false); + done(); + }); + }); +}); + +describe('Query Channel Assets - Post', () => { + it('Query Channel Assets', (done) => { + chai + .request(app) + .post('/channels/test/records/_query') + .send({ query: {} }) + .end((err, res) => { + expect(res) + .to + .have + .status(200); + expect(Object.keys(res.body).length) + .to + .equal(2); + expect(res.body) + .to + .have + .property('test'); + expect(res.body) + .to + .have + .property('test2'); + expect(res.body.test) + .to + .have + .property('test'); + expect(res.body.test) + .to + .have + .property('test2'); + done(); + }); + }); + it('Query Channel Assets None', (done) => { + chai + .request(app) + .post('/channels/none/records/_query') + .send({ query: {} }) + .end((err, res) => { + expect(res) + .to + .have + .status(404); + done(); + }); + }); + it('Query Channel Assets Limit', (done) => { + chai + .request(app) + .post('/channels/test/records/_query') + .send({ query: {}, limit: 1 }) + .end((err, res) => { + expect(res) + .to + .have + .status(200); + expect(Object.keys(res.body).length) + .to + .equal(1); + expect(res.body) + .to + .have + .property('test'); + done(); + }); + }); + it('Query Channel Assets Bad Limit', (done) => { + chai + .request(app) + .post('/channels/test/records/_query') + .send({ query: {}, limit: 'one' }) + .end((err, res) => { + expect(res) + .to + .have + .status(400); + done(); + }); + }); + it('Query Channel Assets Skip', (done) => { + chai + .request(app) + .post('/channels/test/records/_query') + .send({ query: {}, limit: 1, skip: 1 }) + .end((err, res) => { + expect(res) + .to + .have + .status(200); + expect(Object.keys(res.body).length) + .to + .equal(1); + expect(res.body) + .to + .not + .have + .property('test'); + expect(res.body) + .to + .have + .property('test2'); + done(); + }); + }); + it('Query Channel Assets Bad Skip', (done) => { + chai + .request(app) + .post('/channels/test/records/_query') + .send({ query: {}, limit: 1, skip: 'one' }) + .end((err, res) => { + expect(res) + .to + .have + .status(400); + done(); + }); + }); + it('Query Channel Assets Filter', (done) => { + chai + .request(app) + .post('/channels/test/records/_query') + .send({ query: {}, filter: ['test'] }) + .end((err, res) => { + expect(res) + .to + .have + .status(200); + expect(Object.keys(res.body).length) + .to + .equal(2); + expect(res.body) + .to + .have + .property('test'); + expect(res.body.test) + .to + .have + .property('test'); + expect(res.body.test) + .to + .not + .have + .property('test2'); + done(); + }); + }); + it('Query Channel Assets Bad Filter', (done) => { + chai + .request(app) + .post('/channels/test/records/_query') + .send({ query: {}, filter: 'test' }) + .end((err, res) => { + expect(res) + .to + .have + .status(400); + done(); + }); + }); + it('Query Channel Assets Missing Filter', (done) => { + chai + .request(app) + .post('/channels/test/records/_query?query={}') + .send({ query: {}, filter: [] }) + .end((err, res) => { + expect(res) + .to + .have + .status(400); + done(); + }); + }); + it('Query Audit Channel', (done) => { + chai + .request(app) + .post('/channels/_audit/records/_query') + .send({ query: {} }) .end((err, res) => { expect(res) .to @@ -262,6 +732,7 @@ describe('Update', () => { describe('Audit', () => { it('Audit Existing', (done) => { + process.env.CHANNEL_DB = 'primary'; prepareFakeAudit() .then(() => { chai diff --git a/src/test/auth.test.js b/src/test/auth.test.js index 65a9325..5da9a4b 100644 --- a/src/test/auth.test.js +++ b/src/test/auth.test.js @@ -20,31 +20,39 @@ const sinonChai = require('sinon-chai'); const db = require('../controller/db'); const { expect } = chai; -const { before, after, describe, it } = require('mocha'); +const { + before, after, describe, it, +} = require('mocha'); chai.use(chaiHttp); chai.use(sinonChai); -let app = require('../app'); -describe('Bad Authorization', () => { +const app = require('../app'); +describe('Bad Authorization', () => { // minimal mock of a mongoDB authorization error const mongoDBPermissionErrorMock = { ok: 0, errmsg: 'not authorized on primary to execute command {}', code: 13, codeName: 'Unauthorized', - name: 'MongoError' + name: 'MongoError', }; before((done) => { sinon.stub(db, 'commitResource') .rejects(mongoDBPermissionErrorMock); + sinon.stub(db, 'queryRecords') + .rejects(mongoDBPermissionErrorMock); sinon.stub(db, 'queryResource') .rejects(mongoDBPermissionErrorMock); sinon.stub(db, 'queryResourceAudit') .rejects(mongoDBPermissionErrorMock); + sinon.stub(db, 'listChannels') + .rejects(mongoDBPermissionErrorMock); + sinon.stub(db, 'listRecords') + .rejects(mongoDBPermissionErrorMock); // eslint-disable-next-line global-require - //app = require('../app'); + // app = require('../app'); done(); }); @@ -74,7 +82,7 @@ describe('Bad Authorization', () => { done(); }); }); - it('when a query occurs', (done) => { + it('when a retrieve occurs', (done) => { chai .request(app) .get('/channels/test/records/test') @@ -89,6 +97,37 @@ describe('Bad Authorization', () => { done(); }); }); + it('when a query occurs', (done) => { + chai + .request(app) + .get('/channels/test/records/_query?query={}') + .end((err, res) => { + expect(res) + .to + .have + .status(401); + expect(res.body.success) + .to + .equals(false); + done(); + }); + }); + it('when a query post occurs', (done) => { + chai + .request(app) + .post('/channels/test/records/_query') + .send({ query: {} }) + .end((err, res) => { + expect(res) + .to + .have + .status(401); + expect(res.body.success) + .to + .equals(false); + done(); + }); + }); it('when a audit query occurs', (done) => { chai .request(app) @@ -104,5 +143,34 @@ describe('Bad Authorization', () => { done(); }); }); - + it('when a list channels occurs', (done) => { + chai + .request(app) + .get('/channels/') + .end((err, res) => { + expect(res) + .to + .have + .status(401); + expect(res.body.success) + .to + .equals(false); + done(); + }); + }); + it('when a list records occurs', (done) => { + chai + .request(app) + .get('/channels/test/records/') + .end((err, res) => { + expect(res) + .to + .have + .status(401); + expect(res.body.success) + .to + .equals(false); + done(); + }); + }); }); diff --git a/src/test/bad.test.js b/src/test/bad.test.js index 39069c4..40b7862 100644 --- a/src/test/bad.test.js +++ b/src/test/bad.test.js @@ -16,6 +16,7 @@ const chai = require('chai'); const chaiHttp = require('chai-http'); + const { expect } = chai; const decache = require('decache'); const { before, describe, it } = require('mocha'); @@ -79,4 +80,52 @@ describe('Bad Connection', () => { }); }); }); - +it('List Channels Bad Connection', (done) => { + chai + .request(app) + .get('/channels/') + .end((err, res) => { + expect(res) + .to + .have + .status(500); + done(); + }); +}); +it('List Records Bad Connection', (done) => { + chai + .request(app) + .get('/channels/test/records/') + .end((err, res) => { + expect(res) + .to + .have + .status(500); + done(); + }); +}); +it('Query Records Bad Connection', (done) => { + chai + .request(app) + .get('/channels/test/records/_query?query={}') + .end((err, res) => { + expect(res) + .to + .have + .status(500); + done(); + }); +}); +it('Query Records Post Bad Connection', (done) => { + chai + .request(app) + .post('/channels/test/records/_query') + .send({ query: {} }) + .end((err, res) => { + expect(res) + .to + .have + .status(500); + done(); + }); +}); diff --git a/src/test/empty.test.js b/src/test/empty.test.js new file mode 100644 index 0000000..06c2e38 --- /dev/null +++ b/src/test/empty.test.js @@ -0,0 +1,66 @@ +/* + * Copyright 2020 Unisys Corporation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const chai = require('chai'); +const chaiHttp = require('chai-http'); +const sinonChai = require('sinon-chai'); +const decache = require('decache'); +const { + before, + describe, + it, +} = require('mocha'); +const dbHandler = require('../test-helpers/db-handler'); + +const { expect } = chai; +chai.use(chaiHttp); +chai.use(sinonChai); + +let app; +const { prepareEmptyDB } = require('../test-helpers/db-handler'); + +before((done) => { + process.env.MONGO_URI = 'mongodb://127.0.0.1:27071'; + process.env.CHANNEL_DB = 'empty'; + // sinon.stub(MongoClient.Db, 'listCollections') + // .resolves([]); + decache('../app'); + dbHandler.connect(); + // eslint-disable-next-line global-require + app = require('../app'); + done(); +}); + +describe('List Channels', () => { + it('List Channels Error', (done) => { + prepareEmptyDB() + .then(() => { + chai + .request(app) + .get('/channels/') + .end((err, res) => { + expect(res) + .to + .have + .status(404); + expect(res.body.success) + .to + .equal(false); + done(); + }); + }); + }); +}); From f3ec3ed5c7ca73d26d9761cd8b36293f5be409cf Mon Sep 17 00:00:00 2001 From: mathisonryan Date: Mon, 30 Aug 2021 16:50:02 -0500 Subject: [PATCH 2/2] Handle audit channels when listing Add handling so audit channels are ignored when listing channels --- src/controller/db.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller/db.js b/src/controller/db.js index b6d444a..82d7f8d 100644 --- a/src/controller/db.js +++ b/src/controller/db.js @@ -159,7 +159,7 @@ const listChannels = async (ctx) => { } const collectionsArray = collections.map((item) => item.name); span.finish(); - return collectionsArray; + return collectionsArray.filter((collection) => !collection.endsWith(AUDIT_POSTFIX)); }); };