From cfcfdd1d50383118f8dba12be60b8cadfa65a995 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 12 Nov 2020 06:44:01 -0800 Subject: [PATCH 1/3] fix(coverage report): Fixing script for generating coverage reprot --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5eb1541..e404d36 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "test": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text --timeout 15000 ./node_modules/.bin/mocha --exit", "lint": "standard --env mocha --fix", "docs": "./node_modules/.bin/apidoc -i src/ -o docs", - "coverage": "nyc report --reporter=text-lcov | coveralls", - "coverage:report": "nyc --reporter=html mocha --exit", + "coverage": "export KOA_ENV=test && npm run prep-test && nyc report --reporter=text-lcov | coveralls", + "coverage:report": "export KOA_ENV=test && npm run prep-test && nyc --reporter=html mocha --exit", "prep-test": "node util/users/delete-all-test-users.js" }, "keywords": [ From ba440706507d7949be8645cd19ca788f6ad76992 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 12 Nov 2020 07:44:14 -0800 Subject: [PATCH 2/3] fix(mocking): Added mock for koa context object --- package-lock.json | 119 +++++++++++++++++++++++++++++++++++++++++ package.json | 1 + test/a02-users.spec.js | 47 ++++++++++++---- test/mocks/ctx-mock.js | 50 +++++++++++++++++ 4 files changed, 208 insertions(+), 9 deletions(-) create mode 100644 test/mocks/ctx-mock.js diff --git a/package-lock.json b/package-lock.json index c883fe1..e699e8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -781,6 +781,51 @@ "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==" }, + "@sinonjs/commons": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", + "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@sinonjs/formatio": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/formatio/-/formatio-5.0.1.tgz", + "integrity": "sha512-KaiQ5pBf1MpS09MuA0kp6KBQt2JUOQycqVG1NZXvzeaXe5LGFqAKueIS0bw4w0P9r7KuBSVdUk5QjXsUdu2CxQ==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1", + "@sinonjs/samsam": "^5.0.2" + } + }, + "@sinonjs/samsam": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-5.2.0.tgz", + "integrity": "sha512-CaIcyX5cDsjcW/ab7HposFWzV1kC++4HNsfnEdFJa7cP1QIuILAKV+BgfeqRXhcnSAc76r/Rh/O5C+300BwUIw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.6.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } + }, + "@sinonjs/text-encoding": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", + "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", + "dev": true + }, "@szmarczak/http-timer": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", @@ -5199,6 +5244,12 @@ "object.assign": "^4.1.0" } }, + "just-extend": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.1.1.tgz", + "integrity": "sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==", + "dev": true + }, "jwa": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", @@ -6298,6 +6349,36 @@ "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", "dev": true }, + "nise": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/nise/-/nise-4.0.4.tgz", + "integrity": "sha512-bTTRUNlemx6deJa+ZyoCUTRvH3liK5+N6VQZ4NIw90AgDXY6iPnsqplNFf6STcj+ePk0H/xqxnP75Lr0J0Fq3A==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0", + "@sinonjs/fake-timers": "^6.0.0", + "@sinonjs/text-encoding": "^0.7.1", + "just-extend": "^4.0.2", + "path-to-regexp": "^1.7.0" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", + "dev": true + }, + "path-to-regexp": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", + "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", + "dev": true, + "requires": { + "isarray": "0.0.1" + } + } + } + }, "node-emoji": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", @@ -11671,6 +11752,44 @@ "is-arrayish": "^0.3.1" } }, + "sinon": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-9.2.1.tgz", + "integrity": "sha512-naPfsamB5KEE1aiioaoqJ6MEhdUs/2vtI5w1hPAXX/UwvoPjXcwh1m5HiKx0HGgKR8lQSoFIgY5jM6KK8VrS9w==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.8.1", + "@sinonjs/fake-timers": "^6.0.1", + "@sinonjs/formatio": "^5.0.1", + "@sinonjs/samsam": "^5.2.0", + "diff": "^4.0.2", + "nise": "^4.0.4", + "supports-color": "^7.1.0" + }, + "dependencies": { + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", diff --git a/package.json b/package.json index e404d36..6360ce7 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "mocha": "^7.0.1", "nyc": "^15.0.0", "semantic-release": "^17.0.0", + "sinon": "^9.2.1", "standard": "^14.3.1" }, "release": { diff --git a/test/a02-users.spec.js b/test/a02-users.spec.js index 9479458..8deb76f 100644 --- a/test/a02-users.spec.js +++ b/test/a02-users.spec.js @@ -2,6 +2,7 @@ const testUtils = require('./utils') const assert = require('chai').assert const config = require('../config') const axios = require('axios').default +const sinon = require('sinon') const util = require('util') util.inspect.defaultOptions = { depth: 1 } @@ -10,6 +11,12 @@ const LOCALHOST = `http://localhost:${config.port}` const context = {} +const UserController = require('../src/modules/users/controller') +let uut +let sandbox + +const mockContext = require('./mocks/ctx-mock').context + describe('Users', () => { before(async () => { // console.log(`config: ${JSON.stringify(config, null, 2)}`) @@ -38,6 +45,14 @@ describe('Users', () => { // console.log(`admin: ${JSON.stringify(admin, null, 2)}`) }) + beforeEach(() => { + uut = new UserController() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + describe('POST /users', () => { it('should reject signup when data is incomplete', async () => { try { @@ -127,6 +142,7 @@ describe('Users', () => { ) } }) + it('should reject if name property property is not string', async () => { try { const options = { @@ -145,10 +161,7 @@ describe('Users', () => { assert(false, 'Unexpected result') } catch (err) { assert.equal(err.response.status, 422) - assert.include( - err.response.data, - "Property 'name' must be a string" - ) + assert.include(err.response.data, "Property 'name' must be a string") } }) @@ -275,6 +288,22 @@ describe('Users', () => { assert.hasAnyKeys(users[0], ['type', '_id', 'email']) assert.isNumber(users.length) }) + + it('should catch and handle errors', async () => { + try { + // Force an error + sandbox.stub(uut.User, 'find').rejects(new Error('test error')) + + // Mock the context object. + const ctx = mockContext() + + await uut.getUsers(ctx) + + assert.fail('Unexpected result') + } catch (err) { + assert.include(err.message, 'Not Found') + } + }) }) describe('GET /users/:id', () => { @@ -544,10 +573,7 @@ describe('Users', () => { assert.equal(true, false, 'Unexpected behavior') } catch (err) { assert.equal(err.response.status, 422) - assert.include( - err.response.data, - "Property 'email' must be a string!" - ) + assert.include(err.response.data, "Property 'email' must be a string!") } }) it('should not be able to update if email is wrong format', async () => { @@ -571,7 +597,10 @@ describe('Users', () => { await axios(options) } catch (err) { assert.equal(err.response.status, 422) - assert.include(err.response.data, "Property 'email' must be email format!") + assert.include( + err.response.data, + "Property 'email' must be email format!" + ) } }) it('should not be able to update type property if is not string', async () => { diff --git a/test/mocks/ctx-mock.js b/test/mocks/ctx-mock.js new file mode 100644 index 0000000..c7536bd --- /dev/null +++ b/test/mocks/ctx-mock.js @@ -0,0 +1,50 @@ +// Ripped from https://github.com/koajs/koa/blob/master/test/helpers/context.js +// Solution courtesy of user @fl0w. See: https://github.com/koajs/koa/issues/999#issuecomment-309270599 +// Take from this gist: https://gist.github.com/emmanuelnk/f1254eed8f947a81e8d715476d9cc92c + +// if you want more comprehensive Koa Context object to test stuff like Cookies etc +// then use https://www.npmjs.com/package/@shopify/jest-koa-mocks (requires Jest) + +// INSTRUCTIONS: +// Import in test file as below: +// +// const mockContext = require('./mocks/ctx-mock').context +// const ctx = mockContext() +// ... + +const Stream = require('stream') +const Koa = require('koa') + +const context = (req, res, app) => { + const socket = new Stream.Duplex() + + req = Object.assign( + { headers: {}, socket }, + Stream.Readable.prototype, + req || {} + ) + res = Object.assign( + { _headers: {}, socket }, + Stream.Writable.prototype, + res || {} + ) + req.socket.remoteAddress = req.socket.remoteAddress || '127.0.0.1' + app = app || new Koa() + res.getHeader = k => res._headers[k.toLowerCase()] + res.setHeader = (k, v) => (res._headers[k.toLowerCase()] = v) + res.removeHeader = (k, v) => delete res._headers[k.toLowerCase()] + + const retApp = app.createContext(req, res) + + return retApp +} + +const request = (req, res, app) => context(req, res, app).request + +const response = (req, res, app) => context(req, res, app).response + +module.exports = { + context, + request, + response +} From 392b1b89ca237455b5258887527d15c864d72611 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Thu, 12 Nov 2020 07:56:13 -0800 Subject: [PATCH 3/3] fix(users): Achieved 100% test coverage of users controller --- src/modules/users/controller.js | 8 +++++++- test/a02-users.spec.js | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/modules/users/controller.js b/src/modules/users/controller.js index 3f003a9..1daa1ac 100644 --- a/src/modules/users/controller.js +++ b/src/modules/users/controller.js @@ -170,12 +170,18 @@ class UserController { user } } catch (err) { - if (err === 404 || err.name === 'CastError') { + // Handle different error types. + if ( + err === 404 || + err.name === 'CastError' || + err.message.toString().includes('Not Found') + ) { ctx.throw(404) } ctx.throw(500) } + if (next) { return next() } diff --git a/test/a02-users.spec.js b/test/a02-users.spec.js index 8deb76f..fbef6e2 100644 --- a/test/a02-users.spec.js +++ b/test/a02-users.spec.js @@ -374,6 +374,40 @@ describe('Users', () => { 'Password property should not be returned' ) }) + + it('should catch and handle errors', async () => { + try { + // Force an error + sandbox.stub(uut.User, 'findById').rejects(new Error('test error')) + + // Mock the context object. + const ctx = mockContext() + + await uut.getUser(ctx) + + assert.fail('Unexpected result') + } catch (err) { + assert.include(err.message, 'Internal Server Error') + } + }) + + it('should handle user not found', async () => { + try { + // Force an error + sandbox.stub(uut.User, 'findById').resolves(false) + + // Mock the context object. + const ctx = mockContext() + ctx.params = { id: 1 } + + await uut.getUser(ctx) + + assert.fail('Unexpected result') + } catch (err) { + console.log(err) + assert.include(err.message, 'Not Found') + } + }) }) describe('PUT /users/:id', () => {