From 80df153cb09eeedeb5e6d842c5d2470d4ceb5a20 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Mon, 22 Mar 2021 19:40:35 -0700 Subject: [PATCH 01/10] Played with better config, but not worth it? --- package.json | 3 + src/lib/users.js | 56 ++ src/modules/users/controller.js | 59 +- .../rest-api/a01-auth.rest-integration.js | 102 +++ .../rest-api/a02-users.rest-integration.js | 823 ++++++++++++++++++ test/unit/biz-logic/a02-users.lib-unit.js | 44 + test/unit/rest-api/a02-users.rest-unit.js | 109 +++ test/utils/test-utils.js | 135 +++ 8 files changed, 1304 insertions(+), 27 deletions(-) create mode 100644 src/lib/users.js create mode 100644 test/integration/rest-api/a01-auth.rest-integration.js create mode 100644 test/integration/rest-api/a02-users.rest-integration.js create mode 100644 test/unit/biz-logic/a02-users.lib-unit.js create mode 100644 test/unit/rest-api/a02-users.rest-unit.js create mode 100644 test/utils/test-utils.js diff --git a/package.json b/package.json index e26d541..a849e04 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "scripts": { "start": "node index.js", "test": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/", + "test:unit:lib": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/biz-logic/", + "test:unit:rest": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/rest-api/", + "test:integration": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/integration/rest-api/", "lint": "standard --env mocha --fix", "docs": "./node_modules/.bin/apidoc -i src/ -o docs", "coverage": "export KOA_ENV=test && npm run prep-test && nyc report --reporter=text-lcov | coveralls", diff --git a/src/lib/users.js b/src/lib/users.js new file mode 100644 index 0000000..6bc8afc --- /dev/null +++ b/src/lib/users.js @@ -0,0 +1,56 @@ +/* + This library contains business-logic for dealing with users. Most of these + functions are called by the /user REST API endpoints. +*/ + +const UserModel = require('../models/users') +const wlogger = require('./wlogger') + +class UserLib { + constructor (configObj) { + // Encapsulate dependencies + this.UserModel = UserModel + } + + // Returns an array of all user models in the Mongo database. + async getAllUsers () { + try { + // Get all user models. Delete the password property from each model. + const users = await this.UserModel.find({}, '-password') + + return users + } catch (err) { + wlogger.error('Error in lib/users.js/getAllUsers()') + throw err + } + } + + // Get the model for a specific user. + async getUser (params) { + try { + const { id } = params + + const user = await this.UserModel.findById(id, '-password') + + // Throw a 404 error if the user isn't found. + if (!user) { + const err = new Error('User not found') + err.status = 404 + throw err + } + + return user + } catch (err) { + // console.log('Error in getUser: ', err) + + if (err.status === 404) throw err + + // Return 422 for any other error + err.status = 422 + err.message = 'Unprocessable Entity' + throw err + } + } +} + +module.exports = UserLib diff --git a/src/modules/users/controller.js b/src/modules/users/controller.js index 47418b6..b964dc4 100644 --- a/src/modules/users/controller.js +++ b/src/modules/users/controller.js @@ -1,10 +1,18 @@ +// User database model. const User = require('../../models/users') +// User library for business logic. +const UserLib = require('../../lib/users') + +const wlogger = require('../../lib/wlogger') + let _this class UserController { constructor () { _this = this + this.User = User + this.userLib = new UserLib() } /** @@ -50,21 +58,13 @@ class UserController { const userObj = ctx.request.body.user try { /* - * ERROR HANDLERS - * + * Input Validation */ // Required property if (!userObj.email || typeof userObj.email !== 'string') { throw new Error("Property 'email' must be a string!") } - // This validation is not permissive to different TLDs like this one: - // someone@somewhere.link. Removing it until it can be updated. - // const isEmail = await _this.validateEmail(user.email) - // if (!isEmail) { - // throw new Error("Property 'email' must be email format!") - // } - if (!userObj.password || typeof userObj.password !== 'string') { throw new Error("Property 'password' must be a string!") } @@ -74,6 +74,7 @@ class UserController { } const user = new _this.User(userObj) + // Enforce default value of 'user' user.type = 'user' @@ -125,10 +126,12 @@ class UserController { */ async getUsers (ctx) { try { - const users = await _this.User.find({}, '-password') + const users = await _this.userLib.getAllUsers() + ctx.body = { users } - } catch (error) { - ctx.throw(404) + } catch (err) { + wlogger.error('Error in users/controller.js/getUsers(): '.err) + ctx.throw(422, err.message) } } @@ -160,28 +163,15 @@ class UserController { * * @apiUse TokenError */ - async getUser (ctx, next) { try { - const user = await _this.User.findById(ctx.params.id, '-password') - if (!user) { - ctx.throw(404) - } + const user = await _this.userLib.getUser(ctx.params) ctx.body = { user } } catch (err) { - // Handle different error types. - if ( - err === 404 || - err.name === 'CastError' || - err.message.toString().includes('Not Found') - ) { - ctx.throw(404) - } - - ctx.throw(500) + _this.handleError(ctx, err) } if (next) { @@ -316,6 +306,21 @@ class UserController { } } + // DRY error handler + handleError (ctx, err) { + // If an HTTP status is specified by the buisiness logic, use that. + if (err.status) { + if (err.message) { + ctx.throw(err.status, err.message) + } else { + ctx.throw(err.status) + } + } else { + // By default use a 422 error if the HTTP status is not specified. + ctx.throw(422, err.message) + } + } + // Validate Email Format async validateEmail (email) { // eslint-disable-next-line no-useless-escape diff --git a/test/integration/rest-api/a01-auth.rest-integration.js b/test/integration/rest-api/a01-auth.rest-integration.js new file mode 100644 index 0000000..ee29367 --- /dev/null +++ b/test/integration/rest-api/a01-auth.rest-integration.js @@ -0,0 +1,102 @@ +const app = require('../../../bin/server') +const utils = require('../../utils/test-utils') +const config = require('../../../config') +const assert = require('chai').assert + +const axios = require('axios').default + +// const request = supertest.agent(app.listen()) +const context = {} + +const LOCALHOST = `http://localhost:${config.port}` + +describe('Auth', () => { + before(async () => { + // This should be the first instruction. It starts the REST API server. + await app.startServer() + + const userObj = { + email: 'test@test.com', + password: 'pass' + } + const testUser = await utils.createUser(userObj) + console.log(`TestUser : ${testUser}`) + + context.user = testUser.user + context.token = testUser.token + }) + + describe('POST /auth', () => { + it('should throw 401 if credentials are incorrect', async () => { + try { + const options = { + method: 'post', + url: `${LOCALHOST}/auth`, + data: { + email: 'test@test.com', + password: 'wrongpassword' + } + } + + const result = await axios(options) + + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + console.log( + `result stringified: ${JSON.stringify(result.data, null, 2)}` + ) + assert(false, 'Unexpected result') + } catch (err) { + assert(err.response.status === 401, 'Error code 401 expected.') + } + }) + + it('should throw 401 if email is wrong format', async () => { + try { + const options = { + method: 'post', + url: `${LOCALHOST}/auth`, + data: { + email: 'wrongEmail', + password: 'wrongpassword' + } + } + + await axios(options) + assert(false, 'Unexpected result') + } catch (err) { + assert(err.response.status === 401, 'Error code 401 expected.') + } + }) + + it('should auth user', async () => { + try { + const options = { + method: 'post', + url: `${LOCALHOST}/auth`, + data: { + email: 'test@test.com', + password: 'pass' + } + } + const result = await axios(options) + // console.log(`result: ${JSON.stringify(result.data, null, 2)}`) + + assert(result.status === 200, 'Status Code 200 expected.') + assert( + result.data.user.email === 'test@test.com', + 'Email of test expected' + ) + assert( + result.data.user.password === undefined, + 'Password expected to be omited' + ) + } catch (err) { + console.log( + 'Error authenticating test user: ' + JSON.stringify(err, null, 2) + ) + throw err + } + }) + }) +}) diff --git a/test/integration/rest-api/a02-users.rest-integration.js b/test/integration/rest-api/a02-users.rest-integration.js new file mode 100644 index 0000000..51fdff5 --- /dev/null +++ b/test/integration/rest-api/a02-users.rest-integration.js @@ -0,0 +1,823 @@ +const testUtils = require('../../utils/test-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 } + +const LOCALHOST = `http://localhost:${config.port}` + +const context = {} + +const UserController = require('../../../src/modules/users/controller') +let uut +let sandbox + +// const mockContext = require('../../unit/mocks/ctx-mock').context + +describe('Users', () => { + before(async () => { + // console.log(`config: ${JSON.stringify(config, null, 2)}`) + + // Create a second test user. + const userObj = { + email: 'test2@test.com', + password: 'pass2' + } + const testUser = await testUtils.createUser(userObj) + // console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`) + + context.user2 = testUser.user + context.token2 = testUser.token + context.id2 = testUser.user._id + + // Get the JWT used to log in as the admin 'system' user. + const adminJWT = await testUtils.getAdminJWT() + // console.log(`adminJWT: ${adminJWT}`) + context.adminJWT = adminJWT + + // const admin = await testUtils.loginAdminUser() + // context.adminJWT = admin.token + + // const admin = await adminLib.loginAdmin() + // console.log(`admin: ${JSON.stringify(admin, null, 2)}`) + }) + + beforeEach(() => { + uut = new UserController() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + describe('POST /users - Create User', () => { + it('should reject signup when data is incomplete', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/users`, + data: { + email: 'test2@test.com' + } + } + + await axios(options) + + /* console.log( + `result stringified: ${JSON.stringify(result.data, null, 2)}` + ) */ + assert(false, 'Unexpected result') + } catch (err) { + assert(err.response.status === 422, 'Error code 422 expected.') + } + }) + + it('should reject signup if no email property is provided', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/users`, + data: { + user: { + password: 'pass2' + } + } + } + await axios(options) + + assert(false, 'Unexpected result') + } catch (err) { + // console.log('err', err) + assert.equal(err.response.status, 422) + assert.include(err.response.data, "Property 'email' must be a string") + } + }) + + it('should reject signup if no password property is provided', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/users`, + data: { + user: { + email: 'test2@test.com' + } + } + } + await axios(options) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 422) + assert.include( + err.response.data, + "Property 'password' must be a string" + ) + } + }) + + it('should reject if name property property is not string', async () => { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/users`, + data: { + user: { + email: 'test322@test.com', + password: 'supersecretpassword', + name: 1234 + } + } + } + await axios(options) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 422) + assert.include(err.response.data, "Property 'name' must be a string") + } + }) + + it("should signup of type 'user' by default", async () => { + const options = { + method: 'post', + url: `${LOCALHOST}/users`, + data: { + user: { + email: 'test3@test.com', + password: 'supersecretpassword' + } + } + } + const result = await axios(options) + // console.log(`result: ${JSON.stringify(result, null, 2)}`) + + context.user = result.data.user + context.token = result.data.token + + assert(result.status === 200, 'Status Code 200 expected.') + assert( + result.data.user.email === 'test3@test.com', + 'Email of test expected' + ) + assert( + result.data.user.password === undefined, + 'Password expected to be omited' + ) + assert.property(result.data, 'token', 'Token property exists.') + assert.equal(result.data.user.type, 'user') + }) + }) + + describe('GET /users', () => { + it('should not fetch users if the authorization header is missing', async () => { + try { + const options = { + method: 'GET', + url: `${LOCALHOST}/users`, + headers: { + Accept: 'application/json' + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should not fetch users if the authorization header is missing the scheme', async () => { + try { + const options = { + method: 'GET', + url: `${LOCALHOST}/users`, + headers: { + Accept: 'application/json', + Authorization: '1' + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should not fetch users if the authorization header has invalid scheme', async () => { + const { token } = context + try { + const options = { + method: 'GET', + url: `${LOCALHOST}/users`, + headers: { + Accept: 'application/json', + Authorization: `Unknown ${token}` + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should not fetch users if token is invalid', async () => { + try { + const options = { + method: 'GET', + url: `${LOCALHOST}/users`, + headers: { + Accept: 'application/json', + Authorization: 'Bearer 1' + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should fetch all users', async () => { + const { token } = context + + const options = { + method: 'GET', + url: `${LOCALHOST}/users`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + } + const result = await axios(options) + + const users = result.data.users + // console.log(`users: ${util.inspect(users)}`) + + assert.hasAnyKeys(users[0], ['type', '_id', 'email']) + assert.isNumber(users.length) + }) + + it('should return a 422 http status if biz-logic throws an error', async () => { + try { + const { token } = context + + // Force an error + sandbox + .stub(uut.userLib, 'getAllUsers') + .rejects(new Error('test error')) + + const options = { + method: 'GET', + url: `${LOCALHOST}/users`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + } + await axios(options) + + assert.fail('Unexpected code path!') + } catch (err) { + assert.equal(err.response.status, 422) + assert.equal(err.response.data, 'test error') + } + }) + }) + + describe('GET /users/:id', () => { + it('should not fetch user if token is invalid', async () => { + try { + const options = { + method: 'GET', + url: `${LOCALHOST}/users/1`, + headers: { + Accept: 'application/json', + Authorization: 'Bearer 1' + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it("should throw 404 if user doesn't exist", async () => { + const { token } = context + + try { + const options = { + method: 'GET', + url: `${LOCALHOST}/users/5fa4bd7ee1828f5f4d8ed004`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 404) + } + }) + + it('should throw 422 for invalid input', async () => { + const { token } = context + + try { + const options = { + method: 'GET', + url: `${LOCALHOST}/users/1`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 422) + } + }) + + it('should fetch own user', async () => { + const _id = context.user._id + const token = context.token + + const options = { + method: 'GET', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + } + const result = await axios(options) + + const user = result.data.user + // console.log(`user: ${util.inspect(user)}`) + + assert.property(user, 'type') + assert.property(user, 'email') + + assert.property(user, '_id') + assert.equal(user._id, _id) + + assert.notProperty( + user, + 'password', + 'Password property should not be returned' + ) + }) + }) + + describe('PUT /users/:id', () => { + it('should not update user if token is invalid', async () => { + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/1`, + headers: { + Accept: 'application/json', + Authorization: 'Bearer 1' + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should throw 401 if non-admin updating other user', async () => { + const { token } = context + + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/1`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should not be able to update user type', async () => { + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${context.user._id.toString()}`, + headers: { + Authorization: `Bearer ${context.token}` + }, + data: { + user: { + name: 'new name', + type: 'test' + } + } + } + await axios(options) + + // console.log(`Users: ${JSON.stringify(result.data, null, 2)}`) + + // assert(result.status === 200, 'Status Code 200 expected.') + // assert(result.data.user.type === 'user', 'Type should be unchanged.') + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 422) + assert.include( + err.response.data, + "Property 'type' can only be changed by Admin user" + ) + } + }) + + it('should not be able to update other user when not admin', async () => { + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${context.user2._id.toString()}`, + headers: { + Authorization: `Bearer ${context.token}` + }, + data: { + user: { + name: 'This should not work' + } + } + } + await axios(options) + + // console.log(`result: ${JSON.stringify(result.data, null, 2)}`) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should not be able to update if name property is wrong', async () => { + try { + const _id = context.user._id + const token = context.token + + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + email: 'testToUpdate@test.com', + name: {} + } + } + } + await axios(options) + } catch (error) { + assert.equal(error.response.status, 422) + assert.include(error.response.data, "Property 'name' must be a string!") + } + }) + it('should not be able to update if password property is not string', async () => { + const { token } = context + const _id = context.user._id + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + password: 1234 + } + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 422) + assert.include( + err.response.data, + "Property 'password' must be a string!" + ) + } + }) + it('should not be able to update if project property is not array', async () => { + const { token } = context + const _id = context.user._id + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + projects: 'projects' + } + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 422) + assert.include( + err.response.data, + "Property 'projects' must be a Array!" + ) + } + }) + it('should not be able to update if email is not string', async () => { + const { token } = context + const _id = context.user._id + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + email: 1234 + } + } + } + await axios(options) + + 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!") + } + }) + it('should not be able to update if email is wrong format', async () => { + try { + const _id = context.user._id + const token = context.token + + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + email: 'badEmailFormat' + } + } + } + await axios(options) + } catch (err) { + assert.equal(err.response.status, 422) + 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 () => { + try { + const _id = context.user._id + const token = context.token + + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + type: 1 + } + } + } + await axios(options) + } catch (err) { + assert.equal(err.response.status, 422) + assert.include(err.response.data, "Property 'type' must be a string!") + } + }) + + it('should be able to update other user when admin', async () => { + const adminJWT = context.adminJWT + + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${context.user2._id.toString()}`, + headers: { + Authorization: `Bearer ${adminJWT}` + }, + data: { + user: { + name: 'This should work' + } + } + } + const result = await axios(options) + // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) + + const userName = result.data.user.name + assert.equal(userName, 'This should work') + }) + it('should update user with minimum inputs', async () => { + const _id = context.user._id + const token = context.token + + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { email: 'testToUpdate@test.com' } + } + } + + const result = await axios(options) + const user = result.data.user + // console.log(`user: ${util.inspect(user)}`) + + assert.property(user, 'type') + assert.property(user, 'email') + + assert.property(user, '_id') + assert.equal(user._id, _id) + + assert.notProperty( + user, + 'password', + 'Password property should not be returned' + ) + assert.equal(user.email, 'testToUpdate@test.com') + }) + + it('should update user with all inputs', async () => { + const _id = context.user._id + const token = context.token + + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + email: 'testToUpdate@test.com', + name: 'my name', + username: 'myUsername' + } + } + } + const result = await axios(options) + + const user = result.data.user + // console.log(`user: ${util.inspect(user)}`) + + assert.property(user, 'type') + assert.property(user, 'email') + assert.property(user, 'name') + + assert.property(user, '_id') + assert.equal(user._id, _id) + assert.notProperty( + user, + 'password', + 'Password property should not be returned' + ) + assert.equal(user.name, 'my name') + assert.equal(user.email, 'testToUpdate@test.com') + assert.equal(user.username, 'myUsername') + }) + }) + + describe('DELETE /users/:id', () => { + it('should not delete user if token is invalid', async () => { + try { + const options = { + method: 'DELETE', + url: `${LOCALHOST}/users/1`, + headers: { + Accept: 'application/json', + Authorization: 'Bearer 1' + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should throw 401 if deleting invalid user', async () => { + const { token } = context + + try { + const options = { + method: 'DELETE', + url: `${LOCALHOST}/users/1`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should not be able to delete other users unless admin', async () => { + try { + const options = { + method: 'DELETE', + url: `${LOCALHOST}/users/${context.user2._id.toString()}`, + headers: { + Authorization: `Bearer ${context.token}` + } + } + await axios(options) + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should delete own user', async () => { + const _id = context.user._id + const token = context.token + + const options = { + method: 'DELETE', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + } + const result = await axios(options) + // console.log(`result: ${util.inspect(result.data.success)}`) + + assert.equal(result.data.success, true) + }) + + it('should be able to delete other users when admin', async () => { + const id = context.id2 + const adminJWT = context.adminJWT + + const options = { + method: 'DELETE', + url: `${LOCALHOST}/users/${id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${adminJWT}` + } + } + const result = await axios(options) + // console.log(`result: ${util.inspect(result.data)}`) + + assert.equal(result.data.success, true) + }) + }) +}) diff --git a/test/unit/biz-logic/a02-users.lib-unit.js b/test/unit/biz-logic/a02-users.lib-unit.js new file mode 100644 index 0000000..12681f5 --- /dev/null +++ b/test/unit/biz-logic/a02-users.lib-unit.js @@ -0,0 +1,44 @@ +/* + Unit tests for the src/lib/users.js business logic library. +*/ + +// Public npm libraries +const mongoose = require('mongoose') +const assert = require('chai').assert + +// Local support libraries +const config = require('../../../config') + +// Unit under test (uut) +const UserLib = require('../../../src/lib/users') + +describe('#users', () => { + let uut + + before(async () => { + // Connect to the Mongo Database. + mongoose.Promise = global.Promise + mongoose.set('useCreateIndex', true) // Stop deprecation warning. + await mongoose.connect(config.database, { + useUnifiedTopology: true, + useNewUrlParser: true + }) + }) + + beforeEach(() => { + uut = new UserLib() + }) + + after(() => { + mongoose.connection.close() + }) + + describe('#getAllUsers', () => { + it('should return all users from the database', async () => { + const users = await uut.getAllUsers() + // console.log(`users: ${JSON.stringify(users, null, 2)}`) + + assert.isArray(users) + }) + }) +}) diff --git a/test/unit/rest-api/a02-users.rest-unit.js b/test/unit/rest-api/a02-users.rest-unit.js new file mode 100644 index 0000000..0345dd9 --- /dev/null +++ b/test/unit/rest-api/a02-users.rest-unit.js @@ -0,0 +1,109 @@ +// const testUtils = require('../../utils/test-utils') +const assert = require('chai').assert +const config = require('../../../config') +// const axios = require('axios').default +const sinon = require('sinon') +const mongoose = require('mongoose') + +const util = require('util') +util.inspect.defaultOptions = { depth: 1 } + +// const LOCALHOST = `http://localhost:${config.port}` + +// const context = {} + +const UserController = require('../../../src/modules/users/controller') +let uut +let sandbox + +const mockContext = require('../../unit/mocks/ctx-mock').context + +describe('Users', () => { + before(async () => { + // Connect to the Mongo Database. + mongoose.Promise = global.Promise + mongoose.set('useCreateIndex', true) // Stop deprecation warning. + await mongoose.connect(config.database, { + useUnifiedTopology: true, + useNewUrlParser: true + }) + + // console.log(`config: ${JSON.stringify(config, null, 2)}`) + + // Create a second test user. + // const userObj = { + // email: 'test2@test.com', + // password: 'pass2' + // } + // const testUser = await testUtils.createUser(userObj) + // console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`) + + // context.user2 = testUser.user + // context.token2 = testUser.token + // context.id2 = testUser.user._id + + // Get the JWT used to log in as the admin 'system' user. + // const adminJWT = await testUtils.getAdminJWT() + // // console.log(`adminJWT: ${adminJWT}`) + // context.adminJWT = adminJWT + + // const admin = await testUtils.loginAdminUser() + // context.adminJWT = admin.token + + // const admin = await adminLib.loginAdmin() + // console.log(`admin: ${JSON.stringify(admin, null, 2)}`) + }) + + beforeEach(() => { + uut = new UserController() + + sandbox = sinon.createSandbox() + }) + + afterEach(() => sandbox.restore()) + + after(() => { + mongoose.connection.close() + }) + + describe('GET /users', () => { + it('should catch and handle errors', async () => { + try { + // Force an error + sandbox + .stub(uut.userLib, 'getAllUsers') + .rejects(new Error('test error')) + + // Mock the context object. + const ctx = mockContext() + + await uut.getUsers(ctx) + + assert.fail('Unexpected result') + } catch (err) { + console.log('err: ', err) + assert.include(err.message, 'Not Found') + } + }) + }) + + describe('GET /users/:id', () => { + it('should catch and handle errors', async () => { + try { + // Force an error + sandbox + .stub(uut.userLib, 'getUser') + .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') + } + }) + }) +}) diff --git a/test/utils/test-utils.js b/test/utils/test-utils.js new file mode 100644 index 0000000..91cc79a --- /dev/null +++ b/test/utils/test-utils.js @@ -0,0 +1,135 @@ +const mongoose = require('mongoose') +const config = require('../../config') +const axios = require('axios').default + +const LOCALHOST = `http://localhost:${config.port}` + +// Remove all collections from the DB. +async function cleanDb () { + for (const collection in mongoose.connection.collections) { + const collections = mongoose.connection.collections + if (collections.collection) { + // const thisCollection = mongoose.connection.collections[collection] + // console.log(`thisCollection: ${JSON.stringify(thisCollection, null, 2)}`) + + await collection.deleteMany() + } + } +} + +// This function is used to create new users. +// userObj = { +// username, +// password +// } +async function createUser (userObj) { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/users`, + data: { + user: { + email: userObj.email, + password: userObj.password + } + } + } + + const result = await axios(options) + + const retObj = { + user: result.data.user, + token: result.data.token + } + + return retObj + } catch (err) { + console.log('Error in utils.js/createUser(): ' + JSON.stringify(err, null, 2)) + throw err + } +} + +async function loginTestUser () { + try { + const options = { + method: 'POST', + url: `${LOCALHOST}/auth`, + data: { + email: 'test@test.com', + password: 'pass' + } + } + + const result = await axios(options) + + // console.log(`result: ${JSON.stringify(result.data, null, 2)}`) + + const retObj = { + token: result.data.token, + user: result.data.user.username, + id: result.data.user._id.toString() + } + + return retObj + } catch (err) { + console.log('Error authenticating test user: ' + JSON.stringify(err, null, 2)) + throw err + } +} + +async function loginAdminUser () { + try { + const FILENAME = `../../config/system-user-${config.env}.json` + const adminUserData = require(FILENAME) + console.log(`adminUserData: ${JSON.stringify(adminUserData, null, 2)}`) + + const options = { + method: 'POST', + url: `${LOCALHOST}/auth`, + data: { + email: adminUserData.email, + password: adminUserData.password + } + } + + const result = await axios(options) + + // console.log(`result: ${JSON.stringify(result.data, null, 2)}`) + + const retObj = { + token: result.data.token, + user: result.data.user.username, + id: result.data.user._id.toString() + } + + return retObj + } catch (err) { + console.log('Error authenticating test admin user: ' + JSON.stringify(err, null, 2)) + throw err + } +} + +// Retrieve the admin user JWT token from the JSON file it's saved at. +async function getAdminJWT () { + try { + // process.env.KOA_ENV = process.env.KOA_ENV || 'dev' + // console.log(`env: ${process.env.KOA_ENV}`) + + const FILENAME = `../../config/system-user-${config.env}.json` + const adminUserData = require(FILENAME) + // console.log(`adminUserData: ${JSON.stringify(adminUserData, null, 2)}`) + + return adminUserData.token + } catch (err) { + console.error('Error in test/utils.js/getAdminJWT()') + throw err + } +} + +module.exports = { + cleanDb, + createUser, + loginTestUser, + loginAdminUser, + getAdminJWT +} From 88df3b2e12c92e98890a287bc1d35b4b3f33b7e2 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 28 Mar 2021 08:20:58 -0700 Subject: [PATCH 02/10] Created unit tests for user REST API --- package.json | 2 +- test/e2e/automated/README.md | 5 ++ .../automated/a01-auth.rest-e2e.js} | 2 +- .../automated/a02-users.rest-e2e.js} | 0 test/unit/biz-logic/README.md | 3 + test/unit/rest-api/README.md | 9 +++ test/unit/rest-api/a02-users.rest-unit.js | 64 +++++++++++++++---- 7 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 test/e2e/automated/README.md rename test/{integration/rest-api/a01-auth.rest-integration.js => e2e/automated/a01-auth.rest-e2e.js} (98%) rename test/{integration/rest-api/a02-users.rest-integration.js => e2e/automated/a02-users.rest-e2e.js} (100%) create mode 100644 test/unit/biz-logic/README.md create mode 100644 test/unit/rest-api/README.md diff --git a/package.json b/package.json index a849e04..434ebae 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/", "test:unit:lib": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/biz-logic/", "test:unit:rest": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/rest-api/", - "test:integration": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/integration/rest-api/", + "test:e2e:auto": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/e2e/automated/", "lint": "standard --env mocha --fix", "docs": "./node_modules/.bin/apidoc -i src/ -o docs", "coverage": "export KOA_ENV=test && npm run prep-test && nyc report --reporter=text-lcov | coveralls", diff --git a/test/e2e/automated/README.md b/test/e2e/automated/README.md new file mode 100644 index 0000000..ab888ea --- /dev/null +++ b/test/e2e/automated/README.md @@ -0,0 +1,5 @@ +# Automated End-to-end Tests + +This directly contains the original boilerplate tests, which are end-to-end tests. These tests are fully automated and test the system directly by making REST API calls with axios. + +These tests function exactly the same as a normal user would. As a result, they are fine for testing internal system components like authorization and user handling. However, they are inappropriate for testing sophisticated endpoints that involve complex operations. For example, interacting with a blockchain, pinging other network systems, or writing data to a secondary database. diff --git a/test/integration/rest-api/a01-auth.rest-integration.js b/test/e2e/automated/a01-auth.rest-e2e.js similarity index 98% rename from test/integration/rest-api/a01-auth.rest-integration.js rename to test/e2e/automated/a01-auth.rest-e2e.js index ee29367..709b5a3 100644 --- a/test/integration/rest-api/a01-auth.rest-integration.js +++ b/test/e2e/automated/a01-auth.rest-e2e.js @@ -20,7 +20,7 @@ describe('Auth', () => { password: 'pass' } const testUser = await utils.createUser(userObj) - console.log(`TestUser : ${testUser}`) + console.log('TestUser: ', testUser) context.user = testUser.user context.token = testUser.token diff --git a/test/integration/rest-api/a02-users.rest-integration.js b/test/e2e/automated/a02-users.rest-e2e.js similarity index 100% rename from test/integration/rest-api/a02-users.rest-integration.js rename to test/e2e/automated/a02-users.rest-e2e.js diff --git a/test/unit/biz-logic/README.md b/test/unit/biz-logic/README.md new file mode 100644 index 0000000..8c5cdff --- /dev/null +++ b/test/unit/biz-logic/README.md @@ -0,0 +1,3 @@ +# Business Logic Unit Tests + +The unit tests in this directly are concerned with business logic libraries in the /src/lib folder. These are the methods that should be triggered by REST API endpoints. These tests are not concerned with the handling of the REST API request/response, but by the code that is triggered by those endpoints. It also tests any business logic that is not directly associated with a REST API endpoint. diff --git a/test/unit/rest-api/README.md b/test/unit/rest-api/README.md new file mode 100644 index 0000000..fe7d1d6 --- /dev/null +++ b/test/unit/rest-api/README.md @@ -0,0 +1,9 @@ +# REST API Unit Tests + +The tests in this directory are unit tests of REST API. These tests are not +concerned with the business logic behind the endpoints. They are only concerned +with the handling of the REST API endpoint. These tests answer questions like: + +- Is the endpoint responding properly to network errors? +- When returning an error, is it returning the proper HTTP response? +- When returning success, is it returning the correct payload? diff --git a/test/unit/rest-api/a02-users.rest-unit.js b/test/unit/rest-api/a02-users.rest-unit.js index 0345dd9..e6dd2e9 100644 --- a/test/unit/rest-api/a02-users.rest-unit.js +++ b/test/unit/rest-api/a02-users.rest-unit.js @@ -15,6 +15,7 @@ util.inspect.defaultOptions = { depth: 1 } const UserController = require('../../../src/modules/users/controller') let uut let sandbox +let ctx const mockContext = require('../../unit/mocks/ctx-mock').context @@ -58,6 +59,9 @@ describe('Users', () => { uut = new UserController() sandbox = sinon.createSandbox() + + // Mock the context object. + ctx = mockContext() }) afterEach(() => sandbox.restore()) @@ -67,42 +71,74 @@ describe('Users', () => { }) describe('GET /users', () => { - it('should catch and handle errors', async () => { + it('should return 422 status on arbitrary biz logic error', async () => { try { // Force an error sandbox .stub(uut.userLib, 'getAllUsers') .rejects(new Error('test error')) - // Mock the context object. - const ctx = mockContext() - await uut.getUsers(ctx) assert.fail('Unexpected result') } catch (err) { - console.log('err: ', err) - assert.include(err.message, 'Not Found') + assert.equal(err.status, 422) + assert.include(err.message, 'test error') } }) + + it('should return 200 status on success', async () => { + await uut.getUsers(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + // Assert that expected properties exist in the returned data. + assert.property(ctx.response.body, 'users') + }) }) describe('GET /users/:id', () => { - it('should catch and handle errors', async () => { + it('should return 422 status on arbitrary biz logic error', async () => { try { // Force an error - sandbox - .stub(uut.userLib, 'getUser') - .rejects(new Error('test error')) - - // Mock the context object. - const ctx = mockContext() + sandbox.stub(uut.userLib, 'getUser').rejects(new Error('test error')) await uut.getUser(ctx) assert.fail('Unexpected result') } catch (err) { - assert.include(err.message, 'Internal Server Error') + assert.equal(err.status, 422) + assert.include(err.message, 'test error') + } + }) + + it('should return 200 status on success', async () => { + // Mock dependencies + sandbox.stub(uut.userLib, 'getUser').resolves({ _id: '123' }) + + await uut.getUser(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + // Assert that expected properties exist in the returned data. + assert.property(ctx.response.body, 'user') + }) + + it('should return other error status passed by biz logic', async () => { + try { + // Mock dependencies + const testErr = new Error('test error') + testErr.status = 404 + sandbox.stub(uut.userLib, 'getUser').rejects(testErr) + + await uut.getUser(ctx) + + assert.fail('Unexpected result') + } catch (err) { + assert.equal(err.status, 404) + assert.include(err.message, 'test error') } }) }) From 6a4604344f7b92fa55920120e75905929644d82a Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 28 Mar 2021 09:39:06 -0700 Subject: [PATCH 03/10] fix(user): Added unit tests for biz logic --- src/lib/users.js | 38 ++++++ src/modules/users/controller.js | 60 ++++---- test/e2e/automated/README.md | 6 +- test/unit/biz-logic/a02-users.lib-unit.js | 159 ++++++++++++++++++++++ 4 files changed, 233 insertions(+), 30 deletions(-) diff --git a/src/lib/users.js b/src/lib/users.js index 6bc8afc..bf5d3e9 100644 --- a/src/lib/users.js +++ b/src/lib/users.js @@ -51,6 +51,44 @@ class UserLib { throw err } } + + // Create a new user model and add it to the Mongo database. + async createUser (userObj) { + try { + // Input Validation + if (!userObj.email || typeof userObj.email !== 'string') { + throw new Error("Property 'email' must be a string!") + } + if (!userObj.password || typeof userObj.password !== 'string') { + throw new Error("Property 'password' must be a string!") + } + if (!userObj.name && typeof userObj.name !== 'string') { + throw new Error("Property 'name' must be a string!") + } + + const user = new this.UserModel(userObj) + + // Enforce default value of 'user' + user.type = 'user' + + // Save the new user model to the database. + await user.save() + + // Generate a JWT token for the user. + const token = user.generateToken() + + // Convert the database model to a JSON object. + const userData = user.toJSON() + + // Delete the password property. + delete userData.password + + return { userData, token } + } catch (err) { + wlogger.error('Error in lib/users.js/createUser()') + throw err + } + } } module.exports = UserLib diff --git a/src/modules/users/controller.js b/src/modules/users/controller.js index b964dc4..1384280 100644 --- a/src/modules/users/controller.js +++ b/src/modules/users/controller.js @@ -55,44 +55,48 @@ class UserController { * } */ async createUser (ctx) { - const userObj = ctx.request.body.user try { - /* - * Input Validation - */ - // Required property - if (!userObj.email || typeof userObj.email !== 'string') { - throw new Error("Property 'email' must be a string!") - } + const userObj = ctx.request.body.user - if (!userObj.password || typeof userObj.password !== 'string') { - throw new Error("Property 'password' must be a string!") - } + // /* + // * Input Validation + // */ + // // Required property + // if (!userObj.email || typeof userObj.email !== 'string') { + // throw new Error("Property 'email' must be a string!") + // } + // + // if (!userObj.password || typeof userObj.password !== 'string') { + // throw new Error("Property 'password' must be a string!") + // } + // + // if (userObj.name && typeof userObj.name !== 'string') { + // throw new Error("Property 'name' must be a string!") + // } + // + // const user = new _this.User(userObj) + // + // // Enforce default value of 'user' + // user.type = 'user' + // + // await user.save() + // + // const token = user.generateToken() + // const response = user.toJSON() + // + // delete response.password - if (userObj.name && typeof userObj.name !== 'string') { - throw new Error("Property 'name' must be a string!") - } - - const user = new _this.User(userObj) - - // Enforce default value of 'user' - user.type = 'user' - - await user.save() - - const token = user.generateToken() - const response = user.toJSON() - - delete response.password + const { userData, token } = this.userLib.createUser(userObj) ctx.body = { - user: response, + user: userData, token } } catch (err) { // console.log(`err.message: ${err.message}`) // console.log('err: ', err) - ctx.throw(422, err.message) + // ctx.throw(422, err.message) + _this.handleError(ctx, err) } } diff --git a/test/e2e/automated/README.md b/test/e2e/automated/README.md index ab888ea..283c815 100644 --- a/test/e2e/automated/README.md +++ b/test/e2e/automated/README.md @@ -1,5 +1,7 @@ # Automated End-to-end Tests -This directly contains the original boilerplate tests, which are end-to-end tests. These tests are fully automated and test the system directly by making REST API calls with axios. +This contains the original boilerplate tests, which are end-to-end tests. These tests are fully automated and test the system directly by making REST API calls with axios. -These tests function exactly the same as a normal user would. As a result, they are fine for testing internal system components like authorization and user handling. However, they are inappropriate for testing sophisticated endpoints that involve complex operations. For example, interacting with a blockchain, pinging other network systems, or writing data to a secondary database. +These tests function exactly the same as a normal user would, by making real REST API calls to the software. As a result, they are fine for testing internal system components like authorization and user handling. However, they are inappropriate for testing sophisticated endpoints that involve complex operations. For example, interacting with a blockchain, pinging other network systems, or writing data to a secondary database. + +There is some redundancy between these tests and the unit tests. The focus is on *how* the tests are executed. The unit tests call the libraries directly (internally). These e2e tests use the REST API (externally). diff --git a/test/unit/biz-logic/a02-users.lib-unit.js b/test/unit/biz-logic/a02-users.lib-unit.js index 12681f5..a06fe69 100644 --- a/test/unit/biz-logic/a02-users.lib-unit.js +++ b/test/unit/biz-logic/a02-users.lib-unit.js @@ -5,6 +5,7 @@ // Public npm libraries const mongoose = require('mongoose') const assert = require('chai').assert +const sinon = require('sinon') // Local support libraries const config = require('../../../config') @@ -14,6 +15,7 @@ const UserLib = require('../../../src/lib/users') describe('#users', () => { let uut + let sandbox before(async () => { // Connect to the Mongo Database. @@ -26,13 +28,113 @@ describe('#users', () => { }) beforeEach(() => { + sandbox = sinon.createSandbox() + uut = new UserLib() }) + afterEach(() => sandbox.restore()) + after(() => { mongoose.connection.close() }) + describe('#createUser', () => { + it('should throw an error if no input is given', async () => { + try { + await uut.createUser() + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + // assert.equal(err.status, 422) + assert.include(err.message, 'Cannot read property') + } + }) + + it('should throw an error if email is not provided', async () => { + try { + await uut.createUser({}) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, "Property 'email' must be a string!") + } + }) + + it('should throw an error if password is not provided', async () => { + try { + const usrObj = { + email: 'test@test.com' + } + + await uut.createUser(usrObj) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, "Property 'password' must be a string!") + } + }) + + it('should throw an error if name is not provided', async () => { + try { + const usrObj = { + email: 'test@test.com', + password: 'password' + } + + await uut.createUser(usrObj) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, "Property 'name' must be a string!") + } + }) + + it('should catch and throw DB errors', async () => { + try { + // Force an error with the database. + sandbox.stub(uut, 'UserModel').throws(new Error('test error')) + + const usrObj = { + email: 'test@test.com', + password: 'password', + name: 'test' + } + + await uut.createUser(usrObj) + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + + it('should create a new user in the DB', async () => { + // Note: The user created in this test is used by the getUser, update, + // and delete tests. + + const usrObj = { + email: 'test01@test.com', + password: 'test', + name: 'test01' + } + + const { userData, token } = await uut.createUser(usrObj) + + // Assert that the user model has the expected properties with expected values. + assert.property(userData, 'type') + assert.equal(userData.type, 'user') + assert.property(userData, '_id') + assert.property(userData, 'email') + assert.property(userData, 'name') + + // Assert that the JWT token was generated for this user. + assert.isString(token) + assert.include(token, 'eyJ') + }) + }) + describe('#getAllUsers', () => { it('should return all users from the database', async () => { const users = await uut.getAllUsers() @@ -40,5 +142,62 @@ describe('#users', () => { assert.isArray(users) }) + + it('should catch and throw an error', async () => { + try { + // Force an error. + sandbox.stub(uut.UserModel, 'find').rejects(new Error('test error')) + + await uut.getAllUsers() + + assert.fail('Unexpected code path') + } catch (err) { + assert.include(err.message, 'test error') + } + }) + }) + + describe('#getUser', () => { + it('should throw 422 if no id given.', async () => { + try { + await uut.getUser() + + assert.fail('Unexpected code path.') + } catch (err) { + // console.log(err) + assert.equal(err.status, 422) + assert.include(err.message, 'Unprocessable Entity') + } + }) + + it('should throw 422 for malformed id', async () => { + try { + const params = { id: 1 } + await uut.getUser(params) + + assert.fail('Unexpected code path.') + } catch (err) { + // console.log(err) + assert.equal(err.status, 422) + assert.include(err.message, 'Unprocessable Entity') + } + }) + + it('should throw 404 if user is not found', async () => { + try { + const params = { id: '5fa4bd7ee1828f5f4d3ed004' } + await uut.getUser(params) + + assert.fail('Unexpected code path.') + } catch (err) { + // console.log(err) + assert.equal(err.status, 404) + assert.include(err.message, 'User not found') + } + }) + + it('should return the user model', async () => { + console.log('TODO') + }) }) }) From 05441e153e0a2fa14a345744160e2311568e83e2 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 28 Mar 2021 09:47:23 -0700 Subject: [PATCH 04/10] Got create, get all, and get ID for user lib unit tests --- test/unit/biz-logic/a02-users.lib-unit.js | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/unit/biz-logic/a02-users.lib-unit.js b/test/unit/biz-logic/a02-users.lib-unit.js index a06fe69..81c47f3 100644 --- a/test/unit/biz-logic/a02-users.lib-unit.js +++ b/test/unit/biz-logic/a02-users.lib-unit.js @@ -16,6 +16,7 @@ const UserLib = require('../../../src/lib/users') describe('#users', () => { let uut let sandbox + let testUserId = '' before(async () => { // Connect to the Mongo Database. @@ -122,6 +123,8 @@ describe('#users', () => { const { userData, token } = await uut.createUser(usrObj) + testUserId = userData._id + // Assert that the user model has the expected properties with expected values. assert.property(userData, 'type') assert.equal(userData.type, 'user') @@ -197,7 +200,15 @@ describe('#users', () => { }) it('should return the user model', async () => { - console.log('TODO') + const params = { id: testUserId } + const result = await uut.getUser(params) + // console.log('result: ', result) + + // Assert that the expected properties for the user model exist. + assert.property(result, 'type') + assert.property(result, '_id') + assert.property(result, 'email') + assert.property(result, 'name') }) }) }) From 21763d3fd253aa5a5ac3877499a94fe5408e7185 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 28 Mar 2021 11:23:25 -0700 Subject: [PATCH 05/10] Got npm test working again --- package.json | 15 +- src/lib/admin.js | 6 +- src/lib/users.js | 77 +- src/modules/users/controller.js | 37 +- test/e2e/automated/a01-auth.rest-e2e.js | 26 +- test/e2e/automated/a02-users.rest-e2e.js | 887 +++++++++++----------- test/unit/biz-logic/a02-users.lib-unit.js | 4 + test/unit/rest-api/README.md | 2 +- test/unit/rest-api/a02-users.rest-unit.js | 53 +- test/utils/test-utils.js | 49 +- 10 files changed, 613 insertions(+), 543 deletions(-) diff --git a/package.json b/package.json index 434ebae..31b2952 100644 --- a/package.json +++ b/package.json @@ -5,15 +5,16 @@ "main": "index.js", "scripts": { "start": "node index.js", - "test": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/", - "test:unit:lib": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/biz-logic/", - "test:unit:rest": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/rest-api/", - "test:e2e:auto": "export KOA_ENV=test && npm run prep-test && nyc --reporter=text mocha --exit --timeout 15000 test/e2e/automated/", + "test": "npm run test:all", + "test:all": "npm run set-env && nyc --reporter=text mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/rest-api/ test/e2e/automated/", + "test:unit:lib": "npm run set-env && mocha --exit --timeout 15000 test/unit/biz-logic/", + "test:unit:rest": "npm run set-env && mocha --exit --timeout 15000 test/unit/rest-api/", + "test:e2e:auto": "npm run set-env && mocha --exit --timeout 15000 test/e2e/automated/", + "set-env": "export KOA_ENV=test", "lint": "standard --env mocha --fix", "docs": "./node_modules/.bin/apidoc -i src/ -o docs", - "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 --timeout 15000 test/unit/", - "prep-test": "node util/users/delete-all-test-users.js" + "coverage": "nyc report --reporter=text-lcov | coveralls", + "coverage:report": "npm run set-env && nyc --reporter=html mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/rest-api/ test/e2e/automated/" }, "keywords": [ "koa-api-boilerplate", diff --git a/src/lib/admin.js b/src/lib/admin.js index e6bfdc8..fbd0964 100644 --- a/src/lib/admin.js +++ b/src/lib/admin.js @@ -49,11 +49,14 @@ class Admin { data: { user: { email: 'system@system.com', - password: context.password + password: context.password, + name: 'admin' } } } const result = await _this.axios.request(options) + // console.log('admin.data: ', result.data) + context.email = result.data.user.email context.id = result.data.user._id context.token = result.data.token @@ -81,6 +84,7 @@ class Admin { // Handle existing system user. if (err.response.status === 422) { try { + console.log('ping03') // Delete the existing user await _this.deleteExistingSystemUser() diff --git a/src/lib/users.js b/src/lib/users.js index bf5d3e9..111aeb1 100644 --- a/src/lib/users.js +++ b/src/lib/users.js @@ -12,6 +12,45 @@ class UserLib { this.UserModel = UserModel } + // Create a new user model and add it to the Mongo database. + async createUser (userObj) { + try { + // Input Validation + if (!userObj.email || typeof userObj.email !== 'string') { + throw new Error("Property 'email' must be a string!") + } + if (!userObj.password || typeof userObj.password !== 'string') { + throw new Error("Property 'password' must be a string!") + } + if (!userObj.name || typeof userObj.name !== 'string') { + throw new Error("Property 'name' must be a string!") + } + + const user = new this.UserModel(userObj) + + // Enforce default value of 'user' + user.type = 'user' + + // Save the new user model to the database. + await user.save() + + // Generate a JWT token for the user. + const token = user.generateToken() + + // Convert the database model to a JSON object. + const userData = user.toJSON() + + // Delete the password property. + delete userData.password + + return { userData, token } + } catch (err) { + // console.log('createUser() error: ', err) + wlogger.error('Error in lib/users.js/createUser()') + throw err + } + } + // Returns an array of all user models in the Mongo database. async getAllUsers () { try { @@ -51,44 +90,6 @@ class UserLib { throw err } } - - // Create a new user model and add it to the Mongo database. - async createUser (userObj) { - try { - // Input Validation - if (!userObj.email || typeof userObj.email !== 'string') { - throw new Error("Property 'email' must be a string!") - } - if (!userObj.password || typeof userObj.password !== 'string') { - throw new Error("Property 'password' must be a string!") - } - if (!userObj.name && typeof userObj.name !== 'string') { - throw new Error("Property 'name' must be a string!") - } - - const user = new this.UserModel(userObj) - - // Enforce default value of 'user' - user.type = 'user' - - // Save the new user model to the database. - await user.save() - - // Generate a JWT token for the user. - const token = user.generateToken() - - // Convert the database model to a JSON object. - const userData = user.toJSON() - - // Delete the password property. - delete userData.password - - return { userData, token } - } catch (err) { - wlogger.error('Error in lib/users.js/createUser()') - throw err - } - } } module.exports = UserLib diff --git a/src/modules/users/controller.js b/src/modules/users/controller.js index 1384280..8c5b481 100644 --- a/src/modules/users/controller.js +++ b/src/modules/users/controller.js @@ -9,10 +9,11 @@ const wlogger = require('../../lib/wlogger') let _this class UserController { constructor () { - _this = this - + // Encapsulate dependencies this.User = User this.userLib = new UserLib() + + _this = this } /** @@ -58,35 +59,9 @@ class UserController { try { const userObj = ctx.request.body.user - // /* - // * Input Validation - // */ - // // Required property - // if (!userObj.email || typeof userObj.email !== 'string') { - // throw new Error("Property 'email' must be a string!") - // } - // - // if (!userObj.password || typeof userObj.password !== 'string') { - // throw new Error("Property 'password' must be a string!") - // } - // - // if (userObj.name && typeof userObj.name !== 'string') { - // throw new Error("Property 'name' must be a string!") - // } - // - // const user = new _this.User(userObj) - // - // // Enforce default value of 'user' - // user.type = 'user' - // - // await user.save() - // - // const token = user.generateToken() - // const response = user.toJSON() - // - // delete response.password - - const { userData, token } = this.userLib.createUser(userObj) + const { userData, token } = await _this.userLib.createUser(userObj) + // console.log('userData: ', userData) + // console.log('token: ', token) ctx.body = { user: userData, diff --git a/test/e2e/automated/a01-auth.rest-e2e.js b/test/e2e/automated/a01-auth.rest-e2e.js index 709b5a3..a6a877f 100644 --- a/test/e2e/automated/a01-auth.rest-e2e.js +++ b/test/e2e/automated/a01-auth.rest-e2e.js @@ -1,10 +1,18 @@ -const app = require('../../../bin/server') -const utils = require('../../utils/test-utils') -const config = require('../../../config') -const assert = require('chai').assert +/* + End-to-end tests for /auth endpoints. + This test sets up the environment for other e2e tests. +*/ + +// Public npm libraries +const assert = require('chai').assert const axios = require('axios').default +// Local support libraries +const config = require('../../../config') +const app = require('../../../bin/server') +const testUtils = require('../../utils/test-utils') + // const request = supertest.agent(app.listen()) const context = {} @@ -15,12 +23,16 @@ describe('Auth', () => { // This should be the first instruction. It starts the REST API server. await app.startServer() + // Delete all previous users in the database. + await testUtils.deleteAllUsers() + const userObj = { email: 'test@test.com', - password: 'pass' + password: 'pass', + name: 'test' } - const testUser = await utils.createUser(userObj) - console.log('TestUser: ', testUser) + const testUser = await testUtils.createUser(userObj) + // console.log('TestUser: ', testUser) context.user = testUser.user context.token = testUser.token diff --git a/test/e2e/automated/a02-users.rest-e2e.js b/test/e2e/automated/a02-users.rest-e2e.js index 51fdff5..d239866 100644 --- a/test/e2e/automated/a02-users.rest-e2e.js +++ b/test/e2e/automated/a02-users.rest-e2e.js @@ -24,7 +24,8 @@ describe('Users', () => { // Create a second test user. const userObj = { email: 'test2@test.com', - password: 'pass2' + password: 'pass2', + name: 'test2' } const testUser = await testUtils.createUser(userObj) // console.log(`testUser2: ${JSON.stringify(testUser, null, 2)}`) @@ -134,8 +135,9 @@ describe('Users', () => { } await axios(options) - assert(false, 'Unexpected result') + assert.fail('Unexpected result') } catch (err) { + // console.log(err) assert.equal(err.response.status, 422) assert.include(err.response.data, "Property 'name' must be a string") } @@ -148,7 +150,8 @@ describe('Users', () => { data: { user: { email: 'test3@test.com', - password: 'supersecretpassword' + password: 'supersecretpassword', + name: 'test3' } } } @@ -381,443 +384,443 @@ describe('Users', () => { ) }) }) - - describe('PUT /users/:id', () => { - it('should not update user if token is invalid', async () => { - try { - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/1`, - headers: { - Accept: 'application/json', - Authorization: 'Bearer 1' - } - } - await axios(options) - - assert.equal(true, false, 'Unexpected behavior') - } catch (err) { - assert.equal(err.response.status, 401) - } - }) - - it('should throw 401 if non-admin updating other user', async () => { - const { token } = context - - try { - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/1`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` - } - } - await axios(options) - - assert.equal(true, false, 'Unexpected behavior') - } catch (err) { - assert.equal(err.response.status, 401) - } - }) - - it('should not be able to update user type', async () => { - try { - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/${context.user._id.toString()}`, - headers: { - Authorization: `Bearer ${context.token}` - }, - data: { - user: { - name: 'new name', - type: 'test' - } - } - } - await axios(options) - - // console.log(`Users: ${JSON.stringify(result.data, null, 2)}`) - - // assert(result.status === 200, 'Status Code 200 expected.') - // assert(result.data.user.type === 'user', 'Type should be unchanged.') - assert.equal(true, false, 'Unexpected behavior') - } catch (err) { - assert.equal(err.response.status, 422) - assert.include( - err.response.data, - "Property 'type' can only be changed by Admin user" - ) - } - }) - - it('should not be able to update other user when not admin', async () => { - try { - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/${context.user2._id.toString()}`, - headers: { - Authorization: `Bearer ${context.token}` - }, - data: { - user: { - name: 'This should not work' - } - } - } - await axios(options) - - // console.log(`result: ${JSON.stringify(result.data, null, 2)}`) - - assert(false, 'Unexpected result') - } catch (err) { - assert.equal(err.response.status, 401) - } - }) - - it('should not be able to update if name property is wrong', async () => { - try { - const _id = context.user._id - const token = context.token - - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/${_id}`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` - }, - data: { - user: { - email: 'testToUpdate@test.com', - name: {} - } - } - } - await axios(options) - } catch (error) { - assert.equal(error.response.status, 422) - assert.include(error.response.data, "Property 'name' must be a string!") - } - }) - it('should not be able to update if password property is not string', async () => { - const { token } = context - const _id = context.user._id - try { - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/${_id}`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` - }, - data: { - user: { - password: 1234 - } - } - } - await axios(options) - - assert.equal(true, false, 'Unexpected behavior') - } catch (err) { - assert.equal(err.response.status, 422) - assert.include( - err.response.data, - "Property 'password' must be a string!" - ) - } - }) - it('should not be able to update if project property is not array', async () => { - const { token } = context - const _id = context.user._id - try { - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/${_id}`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` - }, - data: { - user: { - projects: 'projects' - } - } - } - await axios(options) - - assert.equal(true, false, 'Unexpected behavior') - } catch (err) { - assert.equal(err.response.status, 422) - assert.include( - err.response.data, - "Property 'projects' must be a Array!" - ) - } - }) - it('should not be able to update if email is not string', async () => { - const { token } = context - const _id = context.user._id - try { - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/${_id}`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` - }, - data: { - user: { - email: 1234 - } - } - } - await axios(options) - - 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!") - } - }) - it('should not be able to update if email is wrong format', async () => { - try { - const _id = context.user._id - const token = context.token - - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/${_id}`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` - }, - data: { - user: { - email: 'badEmailFormat' - } - } - } - await axios(options) - } catch (err) { - assert.equal(err.response.status, 422) - 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 () => { - try { - const _id = context.user._id - const token = context.token - - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/${_id}`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` - }, - data: { - user: { - type: 1 - } - } - } - await axios(options) - } catch (err) { - assert.equal(err.response.status, 422) - assert.include(err.response.data, "Property 'type' must be a string!") - } - }) - - it('should be able to update other user when admin', async () => { - const adminJWT = context.adminJWT - - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/${context.user2._id.toString()}`, - headers: { - Authorization: `Bearer ${adminJWT}` - }, - data: { - user: { - name: 'This should work' - } - } - } - const result = await axios(options) - // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) - - const userName = result.data.user.name - assert.equal(userName, 'This should work') - }) - it('should update user with minimum inputs', async () => { - const _id = context.user._id - const token = context.token - - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/${_id}`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` - }, - data: { - user: { email: 'testToUpdate@test.com' } - } - } - - const result = await axios(options) - const user = result.data.user - // console.log(`user: ${util.inspect(user)}`) - - assert.property(user, 'type') - assert.property(user, 'email') - - assert.property(user, '_id') - assert.equal(user._id, _id) - - assert.notProperty( - user, - 'password', - 'Password property should not be returned' - ) - assert.equal(user.email, 'testToUpdate@test.com') - }) - - it('should update user with all inputs', async () => { - const _id = context.user._id - const token = context.token - - const options = { - method: 'PUT', - url: `${LOCALHOST}/users/${_id}`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` - }, - data: { - user: { - email: 'testToUpdate@test.com', - name: 'my name', - username: 'myUsername' - } - } - } - const result = await axios(options) - - const user = result.data.user - // console.log(`user: ${util.inspect(user)}`) - - assert.property(user, 'type') - assert.property(user, 'email') - assert.property(user, 'name') - - assert.property(user, '_id') - assert.equal(user._id, _id) - assert.notProperty( - user, - 'password', - 'Password property should not be returned' - ) - assert.equal(user.name, 'my name') - assert.equal(user.email, 'testToUpdate@test.com') - assert.equal(user.username, 'myUsername') - }) - }) - - describe('DELETE /users/:id', () => { - it('should not delete user if token is invalid', async () => { - try { - const options = { - method: 'DELETE', - url: `${LOCALHOST}/users/1`, - headers: { - Accept: 'application/json', - Authorization: 'Bearer 1' - } - } - await axios(options) - - assert.equal(true, false, 'Unexpected behavior') - } catch (err) { - assert.equal(err.response.status, 401) - } - }) - - it('should throw 401 if deleting invalid user', async () => { - const { token } = context - - try { - const options = { - method: 'DELETE', - url: `${LOCALHOST}/users/1`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` - } - } - await axios(options) - - assert.equal(true, false, 'Unexpected behavior') - } catch (err) { - assert.equal(err.response.status, 401) - } - }) - - it('should not be able to delete other users unless admin', async () => { - try { - const options = { - method: 'DELETE', - url: `${LOCALHOST}/users/${context.user2._id.toString()}`, - headers: { - Authorization: `Bearer ${context.token}` - } - } - await axios(options) - } catch (err) { - assert.equal(err.response.status, 401) - } - }) - - it('should delete own user', async () => { - const _id = context.user._id - const token = context.token - - const options = { - method: 'DELETE', - url: `${LOCALHOST}/users/${_id}`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${token}` - } - } - const result = await axios(options) - // console.log(`result: ${util.inspect(result.data.success)}`) - - assert.equal(result.data.success, true) - }) - - it('should be able to delete other users when admin', async () => { - const id = context.id2 - const adminJWT = context.adminJWT - - const options = { - method: 'DELETE', - url: `${LOCALHOST}/users/${id}`, - headers: { - Accept: 'application/json', - Authorization: `Bearer ${adminJWT}` - } - } - const result = await axios(options) - // console.log(`result: ${util.inspect(result.data)}`) - - assert.equal(result.data.success, true) - }) - }) + // + // describe('PUT /users/:id', () => { + // it('should not update user if token is invalid', async () => { + // try { + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/1`, + // headers: { + // Accept: 'application/json', + // Authorization: 'Bearer 1' + // } + // } + // await axios(options) + // + // assert.equal(true, false, 'Unexpected behavior') + // } catch (err) { + // assert.equal(err.response.status, 401) + // } + // }) + // + // it('should throw 401 if non-admin updating other user', async () => { + // const { token } = context + // + // try { + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/1`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${token}` + // } + // } + // await axios(options) + // + // assert.equal(true, false, 'Unexpected behavior') + // } catch (err) { + // assert.equal(err.response.status, 401) + // } + // }) + // + // it('should not be able to update user type', async () => { + // try { + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/${context.user._id.toString()}`, + // headers: { + // Authorization: `Bearer ${context.token}` + // }, + // data: { + // user: { + // name: 'new name', + // type: 'test' + // } + // } + // } + // await axios(options) + // + // // console.log(`Users: ${JSON.stringify(result.data, null, 2)}`) + // + // // assert(result.status === 200, 'Status Code 200 expected.') + // // assert(result.data.user.type === 'user', 'Type should be unchanged.') + // assert.equal(true, false, 'Unexpected behavior') + // } catch (err) { + // assert.equal(err.response.status, 422) + // assert.include( + // err.response.data, + // "Property 'type' can only be changed by Admin user" + // ) + // } + // }) + // + // it('should not be able to update other user when not admin', async () => { + // try { + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/${context.user2._id.toString()}`, + // headers: { + // Authorization: `Bearer ${context.token}` + // }, + // data: { + // user: { + // name: 'This should not work' + // } + // } + // } + // await axios(options) + // + // // console.log(`result: ${JSON.stringify(result.data, null, 2)}`) + // + // assert(false, 'Unexpected result') + // } catch (err) { + // assert.equal(err.response.status, 401) + // } + // }) + // + // it('should not be able to update if name property is wrong', async () => { + // try { + // const _id = context.user._id + // const token = context.token + // + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/${_id}`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${token}` + // }, + // data: { + // user: { + // email: 'testToUpdate@test.com', + // name: {} + // } + // } + // } + // await axios(options) + // } catch (error) { + // assert.equal(error.response.status, 422) + // assert.include(error.response.data, "Property 'name' must be a string!") + // } + // }) + // it('should not be able to update if password property is not string', async () => { + // const { token } = context + // const _id = context.user._id + // try { + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/${_id}`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${token}` + // }, + // data: { + // user: { + // password: 1234 + // } + // } + // } + // await axios(options) + // + // assert.equal(true, false, 'Unexpected behavior') + // } catch (err) { + // assert.equal(err.response.status, 422) + // assert.include( + // err.response.data, + // "Property 'password' must be a string!" + // ) + // } + // }) + // it('should not be able to update if project property is not array', async () => { + // const { token } = context + // const _id = context.user._id + // try { + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/${_id}`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${token}` + // }, + // data: { + // user: { + // projects: 'projects' + // } + // } + // } + // await axios(options) + // + // assert.equal(true, false, 'Unexpected behavior') + // } catch (err) { + // assert.equal(err.response.status, 422) + // assert.include( + // err.response.data, + // "Property 'projects' must be a Array!" + // ) + // } + // }) + // it('should not be able to update if email is not string', async () => { + // const { token } = context + // const _id = context.user._id + // try { + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/${_id}`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${token}` + // }, + // data: { + // user: { + // email: 1234 + // } + // } + // } + // await axios(options) + // + // 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!") + // } + // }) + // it('should not be able to update if email is wrong format', async () => { + // try { + // const _id = context.user._id + // const token = context.token + // + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/${_id}`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${token}` + // }, + // data: { + // user: { + // email: 'badEmailFormat' + // } + // } + // } + // await axios(options) + // } catch (err) { + // assert.equal(err.response.status, 422) + // 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 () => { + // try { + // const _id = context.user._id + // const token = context.token + // + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/${_id}`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${token}` + // }, + // data: { + // user: { + // type: 1 + // } + // } + // } + // await axios(options) + // } catch (err) { + // assert.equal(err.response.status, 422) + // assert.include(err.response.data, "Property 'type' must be a string!") + // } + // }) + // + // it('should be able to update other user when admin', async () => { + // const adminJWT = context.adminJWT + // + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/${context.user2._id.toString()}`, + // headers: { + // Authorization: `Bearer ${adminJWT}` + // }, + // data: { + // user: { + // name: 'This should work' + // } + // } + // } + // const result = await axios(options) + // // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) + // + // const userName = result.data.user.name + // assert.equal(userName, 'This should work') + // }) + // it('should update user with minimum inputs', async () => { + // const _id = context.user._id + // const token = context.token + // + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/${_id}`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${token}` + // }, + // data: { + // user: { email: 'testToUpdate@test.com' } + // } + // } + // + // const result = await axios(options) + // const user = result.data.user + // // console.log(`user: ${util.inspect(user)}`) + // + // assert.property(user, 'type') + // assert.property(user, 'email') + // + // assert.property(user, '_id') + // assert.equal(user._id, _id) + // + // assert.notProperty( + // user, + // 'password', + // 'Password property should not be returned' + // ) + // assert.equal(user.email, 'testToUpdate@test.com') + // }) + // + // it('should update user with all inputs', async () => { + // const _id = context.user._id + // const token = context.token + // + // const options = { + // method: 'PUT', + // url: `${LOCALHOST}/users/${_id}`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${token}` + // }, + // data: { + // user: { + // email: 'testToUpdate@test.com', + // name: 'my name', + // username: 'myUsername' + // } + // } + // } + // const result = await axios(options) + // + // const user = result.data.user + // // console.log(`user: ${util.inspect(user)}`) + // + // assert.property(user, 'type') + // assert.property(user, 'email') + // assert.property(user, 'name') + // + // assert.property(user, '_id') + // assert.equal(user._id, _id) + // assert.notProperty( + // user, + // 'password', + // 'Password property should not be returned' + // ) + // assert.equal(user.name, 'my name') + // assert.equal(user.email, 'testToUpdate@test.com') + // assert.equal(user.username, 'myUsername') + // }) + // }) + // + // describe('DELETE /users/:id', () => { + // it('should not delete user if token is invalid', async () => { + // try { + // const options = { + // method: 'DELETE', + // url: `${LOCALHOST}/users/1`, + // headers: { + // Accept: 'application/json', + // Authorization: 'Bearer 1' + // } + // } + // await axios(options) + // + // assert.equal(true, false, 'Unexpected behavior') + // } catch (err) { + // assert.equal(err.response.status, 401) + // } + // }) + // + // it('should throw 401 if deleting invalid user', async () => { + // const { token } = context + // + // try { + // const options = { + // method: 'DELETE', + // url: `${LOCALHOST}/users/1`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${token}` + // } + // } + // await axios(options) + // + // assert.equal(true, false, 'Unexpected behavior') + // } catch (err) { + // assert.equal(err.response.status, 401) + // } + // }) + // + // it('should not be able to delete other users unless admin', async () => { + // try { + // const options = { + // method: 'DELETE', + // url: `${LOCALHOST}/users/${context.user2._id.toString()}`, + // headers: { + // Authorization: `Bearer ${context.token}` + // } + // } + // await axios(options) + // } catch (err) { + // assert.equal(err.response.status, 401) + // } + // }) + // + // it('should delete own user', async () => { + // const _id = context.user._id + // const token = context.token + // + // const options = { + // method: 'DELETE', + // url: `${LOCALHOST}/users/${_id}`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${token}` + // } + // } + // const result = await axios(options) + // // console.log(`result: ${util.inspect(result.data.success)}`) + // + // assert.equal(result.data.success, true) + // }) + // + // it('should be able to delete other users when admin', async () => { + // const id = context.id2 + // const adminJWT = context.adminJWT + // + // const options = { + // method: 'DELETE', + // url: `${LOCALHOST}/users/${id}`, + // headers: { + // Accept: 'application/json', + // Authorization: `Bearer ${adminJWT}` + // } + // } + // const result = await axios(options) + // // console.log(`result: ${util.inspect(result.data)}`) + // + // assert.equal(result.data.success, true) + // }) + // }) }) diff --git a/test/unit/biz-logic/a02-users.lib-unit.js b/test/unit/biz-logic/a02-users.lib-unit.js index 81c47f3..c58aa96 100644 --- a/test/unit/biz-logic/a02-users.lib-unit.js +++ b/test/unit/biz-logic/a02-users.lib-unit.js @@ -9,6 +9,7 @@ const sinon = require('sinon') // Local support libraries const config = require('../../../config') +const testUtils = require('../../utils/test-utils') // Unit under test (uut) const UserLib = require('../../../src/lib/users') @@ -26,6 +27,9 @@ describe('#users', () => { useUnifiedTopology: true, useNewUrlParser: true }) + + // Delete all previous users in the database. + await testUtils.deleteAllUsers() }) beforeEach(() => { diff --git a/test/unit/rest-api/README.md b/test/unit/rest-api/README.md index fe7d1d6..eeab4e9 100644 --- a/test/unit/rest-api/README.md +++ b/test/unit/rest-api/README.md @@ -4,6 +4,6 @@ The tests in this directory are unit tests of REST API. These tests are not concerned with the business logic behind the endpoints. They are only concerned with the handling of the REST API endpoint. These tests answer questions like: -- Is the endpoint responding properly to network errors? +- Is the endpoint responding properly when the business logic throws an error? - When returning an error, is it returning the proper HTTP response? - When returning success, is it returning the correct payload? diff --git a/test/unit/rest-api/a02-users.rest-unit.js b/test/unit/rest-api/a02-users.rest-unit.js index e6dd2e9..985c2fe 100644 --- a/test/unit/rest-api/a02-users.rest-unit.js +++ b/test/unit/rest-api/a02-users.rest-unit.js @@ -1,16 +1,15 @@ -// const testUtils = require('../../utils/test-utils') +/* + Unit tests for the REST API handler for the /users endpoints. +*/ + +// Public npm libraries const assert = require('chai').assert -const config = require('../../../config') -// const axios = require('axios').default const sinon = require('sinon') const mongoose = require('mongoose') -const util = require('util') -util.inspect.defaultOptions = { depth: 1 } - -// const LOCALHOST = `http://localhost:${config.port}` - -// const context = {} +// Local support libraries +const config = require('../../../config') +const testUtils = require('../../utils/test-utils') const UserController = require('../../../src/modules/users/controller') let uut @@ -29,6 +28,9 @@ describe('Users', () => { useNewUrlParser: true }) + // Delete all previous users in the database. + await testUtils.deleteAllUsers() + // console.log(`config: ${JSON.stringify(config, null, 2)}`) // Create a second test user. @@ -70,6 +72,39 @@ describe('Users', () => { mongoose.connection.close() }) + describe('#POST /users', () => { + it('should return 422 status on biz logic error', async () => { + try { + await uut.createUser(ctx) + + assert.fail('Unexpected result') + } catch (err) { + // console.log(err) + assert.equal(err.status, 422) + assert.include(err.message, 'Cannot read property') + } + }) + + it('should return 200 status on success', async () => { + ctx.request.body = { + user: { + email: 'test02@test.com', + password: 'test', + name: 'test02' + } + } + + await uut.createUser(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + // Assert that expected properties exist in the returned data. + assert.property(ctx.response.body, 'user') + assert.property(ctx.response.body, 'token') + }) + }) + describe('GET /users', () => { it('should return 422 status on arbitrary biz logic error', async () => { try { diff --git a/test/utils/test-utils.js b/test/utils/test-utils.js index 91cc79a..31b26b0 100644 --- a/test/utils/test-utils.js +++ b/test/utils/test-utils.js @@ -1,7 +1,15 @@ +/* + Utility functions used to prepare the environment for tests. +*/ + +// Public NPM libraries const mongoose = require('mongoose') -const config = require('../../config') const axios = require('axios').default +// Local libraries +const config = require('../../config') +const User = require('../../src/models/users') + const LOCALHOST = `http://localhost:${config.port}` // Remove all collections from the DB. @@ -17,6 +25,24 @@ async function cleanDb () { } } +// Delete all users in the database. This ensures there is no previous state +// to confuse tests. +async function deleteAllUsers () { + try { + // Get all the users in the DB. + const users = await User.find({}, '-password') + // console.log(`users: ${JSON.stringify(users, null, 2)}`) + + // Delete each user. + for (let i = 0; i < users.length; i++) { + const thisUser = users[i] + await thisUser.remove() + } + } catch (err) { + console.error('Error in test-utils.js/deleteAllUsers()') + } +} + // This function is used to create new users. // userObj = { // username, @@ -30,7 +56,8 @@ async function createUser (userObj) { data: { user: { email: userObj.email, - password: userObj.password + password: userObj.password, + name: userObj.name } } } @@ -44,7 +71,9 @@ async function createUser (userObj) { return retObj } catch (err) { - console.log('Error in utils.js/createUser(): ' + JSON.stringify(err, null, 2)) + console.log( + 'Error in utils.js/createUser(): ' + JSON.stringify(err, null, 2) + ) throw err } } @@ -72,7 +101,9 @@ async function loginTestUser () { return retObj } catch (err) { - console.log('Error authenticating test user: ' + JSON.stringify(err, null, 2)) + console.log( + 'Error authenticating test user: ' + JSON.stringify(err, null, 2) + ) throw err } } @@ -88,7 +119,8 @@ async function loginAdminUser () { url: `${LOCALHOST}/auth`, data: { email: adminUserData.email, - password: adminUserData.password + password: adminUserData.password, + name: 'admin' } } @@ -104,7 +136,9 @@ async function loginAdminUser () { return retObj } catch (err) { - console.log('Error authenticating test admin user: ' + JSON.stringify(err, null, 2)) + console.log( + 'Error authenticating test admin user: ' + JSON.stringify(err, null, 2) + ) throw err } } @@ -131,5 +165,6 @@ module.exports = { createUser, loginTestUser, loginAdminUser, - getAdminJWT + getAdminJWT, + deleteAllUsers } From e48372e1255dbcb4c437d4029e6e2f77922f462e Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 28 Mar 2021 12:51:52 -0700 Subject: [PATCH 06/10] Almost done refactoring user update() --- src/lib/users.js | 45 ++ src/modules/users/controller.js | 49 +- test/e2e/automated/a02-users.rest-e2e.js | 657 ++++++++++------------ test/unit/biz-logic/a02-users.lib-unit.js | 122 +++- test/unit/rest-api/a02-users.rest-unit.js | 46 ++ 5 files changed, 521 insertions(+), 398 deletions(-) diff --git a/src/lib/users.js b/src/lib/users.js index 111aeb1..aad7fa0 100644 --- a/src/lib/users.js +++ b/src/lib/users.js @@ -90,6 +90,51 @@ class UserLib { throw err } } + + async updateUser (existingUser, newData) { + try { + // Input Validation + if (!newData.email || typeof newData.email !== 'string') { + throw new Error("Property 'email' must be a string!") + } + if (!newData.password || typeof newData.password !== 'string') { + throw new Error("Property 'password' must be a string!") + } + if (!newData.name || typeof newData.name !== 'string') { + throw new Error("Property 'name' must be a string!") + } + + // Save a copy of the original user type. + const userType = existingUser.type + console.log('userType: ', userType) + + // If user 'type' property is sent by the client + if (newData.type) { + if (typeof newData.type !== 'string') { + throw new Error("Property 'type' must be a string!") + } + + // Unless the calling user is an admin, they can not change the user type. + if (userType !== 'admin') { + throw new Error("Property 'type' can only be changed by Admin user") + } + } + + // Overwrite any existing data with the new data. + Object.assign(existingUser, newData) + + // Save the changes to the database. + await existingUser.save() + + // Delete the password property. + delete existingUser.password + + return existingUser + } catch (err) { + wlogger.error('Error in lib/users.js/updateUser()') + throw err + } + } } module.exports = UserLib diff --git a/src/modules/users/controller.js b/src/modules/users/controller.js index 8c5b481..9ef4f79 100644 --- a/src/modules/users/controller.js +++ b/src/modules/users/controller.js @@ -200,54 +200,11 @@ class UserController { * @apiUse TokenError */ async updateUser (ctx) { - // Values obtain from user request. - // This variable is intended to validate the properties - // sent by the client - const userObj = ctx.request.body.user - - const user = ctx.body.user try { - /* - * ERROR HANDLERS - * - */ - // Required property - if (userObj.email && typeof userObj.email !== 'string') { - throw new Error("Property 'email' must be a string!") - } + const existingUser = ctx.body.user + const newData = ctx.request.body.user - const isEmail = await _this.validateEmail(userObj.email) - if (userObj.email && !isEmail) { - throw new Error("Property 'email' must be email format!") - } - if (userObj.password && typeof userObj.password !== 'string') { - throw new Error("Property 'password' must be a string!") - } - if (userObj.name && typeof userObj.name !== 'string') { - throw new Error("Property 'name' must be a string!") - } - if (userObj.projects && !Array.isArray(userObj.projects)) { - throw new Error("Property 'projects' must be a Array!") - } - // Save a copy of the original user type. - const userType = user.type - - // If user type property is sent by the client - if (userObj.type) { - if (typeof userObj.type !== 'string') { - throw new Error("Property 'type' must be a string!") - } - // TODO: Here we can validate the user types allowed - - // Unless the calling user is an admin, they can not change the user type. - if (userType !== 'admin') { - throw new Error("Property 'type' can only be changed by Admin user") - } - } - - Object.assign(user, ctx.request.body.user) - - await user.save() + const user = await _this.userLib.updateUser(existingUser, newData) ctx.body = { user diff --git a/test/e2e/automated/a02-users.rest-e2e.js b/test/e2e/automated/a02-users.rest-e2e.js index d239866..96519f5 100644 --- a/test/e2e/automated/a02-users.rest-e2e.js +++ b/test/e2e/automated/a02-users.rest-e2e.js @@ -384,355 +384,314 @@ describe('Users', () => { ) }) }) - // - // describe('PUT /users/:id', () => { - // it('should not update user if token is invalid', async () => { - // try { - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/1`, - // headers: { - // Accept: 'application/json', - // Authorization: 'Bearer 1' - // } - // } - // await axios(options) - // - // assert.equal(true, false, 'Unexpected behavior') - // } catch (err) { - // assert.equal(err.response.status, 401) - // } - // }) - // - // it('should throw 401 if non-admin updating other user', async () => { - // const { token } = context - // - // try { - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/1`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${token}` - // } - // } - // await axios(options) - // - // assert.equal(true, false, 'Unexpected behavior') - // } catch (err) { - // assert.equal(err.response.status, 401) - // } - // }) - // - // it('should not be able to update user type', async () => { - // try { - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/${context.user._id.toString()}`, - // headers: { - // Authorization: `Bearer ${context.token}` - // }, - // data: { - // user: { - // name: 'new name', - // type: 'test' - // } - // } - // } - // await axios(options) - // - // // console.log(`Users: ${JSON.stringify(result.data, null, 2)}`) - // - // // assert(result.status === 200, 'Status Code 200 expected.') - // // assert(result.data.user.type === 'user', 'Type should be unchanged.') - // assert.equal(true, false, 'Unexpected behavior') - // } catch (err) { - // assert.equal(err.response.status, 422) - // assert.include( - // err.response.data, - // "Property 'type' can only be changed by Admin user" - // ) - // } - // }) - // - // it('should not be able to update other user when not admin', async () => { - // try { - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/${context.user2._id.toString()}`, - // headers: { - // Authorization: `Bearer ${context.token}` - // }, - // data: { - // user: { - // name: 'This should not work' - // } - // } - // } - // await axios(options) - // - // // console.log(`result: ${JSON.stringify(result.data, null, 2)}`) - // - // assert(false, 'Unexpected result') - // } catch (err) { - // assert.equal(err.response.status, 401) - // } - // }) - // - // it('should not be able to update if name property is wrong', async () => { - // try { - // const _id = context.user._id - // const token = context.token - // - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/${_id}`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${token}` - // }, - // data: { - // user: { - // email: 'testToUpdate@test.com', - // name: {} - // } - // } - // } - // await axios(options) - // } catch (error) { - // assert.equal(error.response.status, 422) - // assert.include(error.response.data, "Property 'name' must be a string!") - // } - // }) - // it('should not be able to update if password property is not string', async () => { - // const { token } = context - // const _id = context.user._id - // try { - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/${_id}`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${token}` - // }, - // data: { - // user: { - // password: 1234 - // } - // } - // } - // await axios(options) - // - // assert.equal(true, false, 'Unexpected behavior') - // } catch (err) { - // assert.equal(err.response.status, 422) - // assert.include( - // err.response.data, - // "Property 'password' must be a string!" - // ) - // } - // }) - // it('should not be able to update if project property is not array', async () => { - // const { token } = context - // const _id = context.user._id - // try { - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/${_id}`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${token}` - // }, - // data: { - // user: { - // projects: 'projects' - // } - // } - // } - // await axios(options) - // - // assert.equal(true, false, 'Unexpected behavior') - // } catch (err) { - // assert.equal(err.response.status, 422) - // assert.include( - // err.response.data, - // "Property 'projects' must be a Array!" - // ) - // } - // }) - // it('should not be able to update if email is not string', async () => { - // const { token } = context - // const _id = context.user._id - // try { - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/${_id}`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${token}` - // }, - // data: { - // user: { - // email: 1234 - // } - // } - // } - // await axios(options) - // - // 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!") - // } - // }) - // it('should not be able to update if email is wrong format', async () => { - // try { - // const _id = context.user._id - // const token = context.token - // - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/${_id}`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${token}` - // }, - // data: { - // user: { - // email: 'badEmailFormat' - // } - // } - // } - // await axios(options) - // } catch (err) { - // assert.equal(err.response.status, 422) - // 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 () => { - // try { - // const _id = context.user._id - // const token = context.token - // - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/${_id}`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${token}` - // }, - // data: { - // user: { - // type: 1 - // } - // } - // } - // await axios(options) - // } catch (err) { - // assert.equal(err.response.status, 422) - // assert.include(err.response.data, "Property 'type' must be a string!") - // } - // }) - // - // it('should be able to update other user when admin', async () => { - // const adminJWT = context.adminJWT - // - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/${context.user2._id.toString()}`, - // headers: { - // Authorization: `Bearer ${adminJWT}` - // }, - // data: { - // user: { - // name: 'This should work' - // } - // } - // } - // const result = await axios(options) - // // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) - // - // const userName = result.data.user.name - // assert.equal(userName, 'This should work') - // }) - // it('should update user with minimum inputs', async () => { - // const _id = context.user._id - // const token = context.token - // - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/${_id}`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${token}` - // }, - // data: { - // user: { email: 'testToUpdate@test.com' } - // } - // } - // - // const result = await axios(options) - // const user = result.data.user - // // console.log(`user: ${util.inspect(user)}`) - // - // assert.property(user, 'type') - // assert.property(user, 'email') - // - // assert.property(user, '_id') - // assert.equal(user._id, _id) - // - // assert.notProperty( - // user, - // 'password', - // 'Password property should not be returned' - // ) - // assert.equal(user.email, 'testToUpdate@test.com') - // }) - // - // it('should update user with all inputs', async () => { - // const _id = context.user._id - // const token = context.token - // - // const options = { - // method: 'PUT', - // url: `${LOCALHOST}/users/${_id}`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${token}` - // }, - // data: { - // user: { - // email: 'testToUpdate@test.com', - // name: 'my name', - // username: 'myUsername' - // } - // } - // } - // const result = await axios(options) - // - // const user = result.data.user - // // console.log(`user: ${util.inspect(user)}`) - // - // assert.property(user, 'type') - // assert.property(user, 'email') - // assert.property(user, 'name') - // - // assert.property(user, '_id') - // assert.equal(user._id, _id) - // assert.notProperty( - // user, - // 'password', - // 'Password property should not be returned' - // ) - // assert.equal(user.name, 'my name') - // assert.equal(user.email, 'testToUpdate@test.com') - // assert.equal(user.username, 'myUsername') - // }) - // }) - // + + describe('PUT /users/:id', () => { + it('should not update user if token is invalid', async () => { + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/1`, + headers: { + Accept: 'application/json', + Authorization: 'Bearer 1' + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should throw 401 if non-admin updating other user', async () => { + const { token } = context + + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/1`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should not be able to update user type', async () => { + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${context.user._id.toString()}`, + headers: { + Authorization: `Bearer ${context.token}` + }, + data: { + user: { + email: 'test@test.com', + password: 'password', + name: 'new name', + type: 'test' + } + } + } + await axios(options) + + // console.log(`Users: ${JSON.stringify(result.data, null, 2)}`) + + // assert(result.status === 200, 'Status Code 200 expected.') + // assert(result.data.user.type === 'user', 'Type should be unchanged.') + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 422) + assert.include( + err.response.data, + "Property 'type' can only be changed by Admin user" + ) + } + }) + + it('should not be able to update other user when not admin', async () => { + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${context.user2._id.toString()}`, + headers: { + Authorization: `Bearer ${context.token}` + }, + data: { + user: { + name: 'This should not work' + } + } + } + await axios(options) + + // console.log(`result: ${JSON.stringify(result.data, null, 2)}`) + + assert(false, 'Unexpected result') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should not be able to update if name property is wrong', async () => { + try { + const _id = context.user._id + const token = context.token + + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + email: 'testToUpdate@test.com', + name: {}, + password: 'password' + } + } + } + await axios(options) + } catch (error) { + assert.equal(error.response.status, 422) + assert.include(error.response.data, "Property 'name' must be a string!") + } + }) + + it('should not be able to update if password property is not string', async () => { + const { token } = context + const _id = context.user._id + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + password: 1234, + email: 'test@test.com', + name: 'test' + } + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 422) + assert.include( + err.response.data, + "Property 'password' must be a string!" + ) + } + }) + + it('should not be able to update if email is not string', async () => { + const { token } = context + const _id = context.user._id + try { + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + email: 1234 + } + } + } + await axios(options) + + 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!") + } + }) + + it('should not be able to update type property if is not string', async () => { + try { + const _id = context.user._id + const token = context.token + + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + type: 1, + email: 'test@test.com', + name: 'test', + password: 'password' + } + } + } + await axios(options) + } catch (err) { + assert.equal(err.response.status, 422) + assert.include(err.response.data, "Property 'type' must be a string!") + } + }) + + it('should be able to update other user when admin', async () => { + const adminJWT = context.adminJWT + + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${context.user2._id.toString()}`, + headers: { + Authorization: `Bearer ${adminJWT}` + }, + data: { + user: { + name: 'This should work', + email: 'test@test.com' + // password: 'password' + } + } + } + const result = await axios(options) + // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) + + const userName = result.data.user.name + assert.equal(userName, 'This should work') + }) + + it('should update user with minimum inputs', async () => { + const _id = context.user._id + const token = context.token + + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { email: 'testToUpdate@test.com' } + } + } + + const result = await axios(options) + const user = result.data.user + // console.log(`user: ${util.inspect(user)}`) + + assert.property(user, 'type') + assert.property(user, 'email') + + assert.property(user, '_id') + assert.equal(user._id, _id) + + assert.notProperty( + user, + 'password', + 'Password property should not be returned' + ) + assert.equal(user.email, 'testToUpdate@test.com') + }) + + it('should update user with all inputs', async () => { + const _id = context.user._id + const token = context.token + + const options = { + method: 'PUT', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + }, + data: { + user: { + email: 'testToUpdate@test.com', + name: 'my name', + username: 'myUsername' + } + } + } + const result = await axios(options) + + const user = result.data.user + // console.log(`user: ${util.inspect(user)}`) + + assert.property(user, 'type') + assert.property(user, 'email') + assert.property(user, 'name') + + assert.property(user, '_id') + assert.equal(user._id, _id) + assert.notProperty( + user, + 'password', + 'Password property should not be returned' + ) + assert.equal(user.name, 'my name') + assert.equal(user.email, 'testToUpdate@test.com') + assert.equal(user.username, 'myUsername') + }) + }) + // describe('DELETE /users/:id', () => { // it('should not delete user if token is invalid', async () => { // try { diff --git a/test/unit/biz-logic/a02-users.lib-unit.js b/test/unit/biz-logic/a02-users.lib-unit.js index c58aa96..0f70da4 100644 --- a/test/unit/biz-logic/a02-users.lib-unit.js +++ b/test/unit/biz-logic/a02-users.lib-unit.js @@ -1,5 +1,7 @@ /* Unit tests for the src/lib/users.js business logic library. + + TODO: verify that an admin can change the type of a user */ // Public npm libraries @@ -17,7 +19,7 @@ const UserLib = require('../../../src/lib/users') describe('#users', () => { let uut let sandbox - let testUserId = '' + let testUser = {} before(async () => { // Connect to the Mongo Database. @@ -127,7 +129,7 @@ describe('#users', () => { const { userData, token } = await uut.createUser(usrObj) - testUserId = userData._id + testUser = userData // Assert that the user model has the expected properties with expected values. assert.property(userData, 'type') @@ -204,10 +206,14 @@ describe('#users', () => { }) it('should return the user model', async () => { - const params = { id: testUserId } + const params = { id: testUser._id } const result = await uut.getUser(params) // console.log('result: ', result) + // Replace the JSON model with an actual Mongoos model. Used by later + // test cases. + testUser = result + // Assert that the expected properties for the user model exist. assert.property(result, 'type') assert.property(result, '_id') @@ -215,4 +221,114 @@ describe('#users', () => { assert.property(result, 'name') }) }) + + describe('#updateUser', () => { + it('should throw an error if no input is given', async () => { + try { + await uut.updateUser() + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, 'Cannot read property') + } + }) + + it('should throw an error if no email given', async () => { + try { + await uut.updateUser(testUser, {}) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'email' must be a string!") + } + }) + + it('should throw an error if no password given', async () => { + try { + const newData = { + email: 'test@test.com' + } + + await uut.updateUser(testUser, newData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'password' must be a string!") + } + }) + + it('should throw an error if no name given', async () => { + try { + const newData = { + email: 'test@test.com', + password: 'password' + } + + await uut.updateUser(testUser, newData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'name' must be a string!") + } + }) + + it('should throw an error for malformed type given', async () => { + try { + const newData = { + email: 'test@test.com', + password: 'password', + name: 'test', + type: 1234 + } + + await uut.updateUser(testUser, newData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'type' must be a string!") + } + }) + + it('should throw an error if normal user tries to change themselves into an admin', async () => { + try { + const newData = { + email: 'test@test.com', + password: 'password', + name: 'test', + type: 'admin' + } + + await uut.updateUser(testUser, newData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'type' can only be changed by Admin user") + } + }) + + it('should update the user model', async () => { + const newData = { + email: 'test@test.com', + password: 'password', + name: 'testy tester' + } + + const result = await uut.updateUser(testUser, newData) + + // Assert that expected properties and values exist. + assert.property(result, '_id') + assert.property(result, 'email') + assert.equal(result.email, 'test@test.com') + assert.property(result, 'name') + assert.equal(result.name, 'testy tester') + }) + + // TODO: verify that an admin can change the type of a user + }) }) diff --git a/test/unit/rest-api/a02-users.rest-unit.js b/test/unit/rest-api/a02-users.rest-unit.js index 985c2fe..4386874 100644 --- a/test/unit/rest-api/a02-users.rest-unit.js +++ b/test/unit/rest-api/a02-users.rest-unit.js @@ -10,6 +10,7 @@ const mongoose = require('mongoose') // Local support libraries const config = require('../../../config') const testUtils = require('../../utils/test-utils') +const User = require('../../../src/models/users') const UserController = require('../../../src/modules/users/controller') let uut @@ -19,6 +20,8 @@ let ctx const mockContext = require('../../unit/mocks/ctx-mock').context describe('Users', () => { + let testUser = {} + before(async () => { // Connect to the Mongo Database. mongoose.Promise = global.Promise @@ -102,6 +105,10 @@ describe('Users', () => { // Assert that expected properties exist in the returned data. assert.property(ctx.response.body, 'user') assert.property(ctx.response.body, 'token') + + // Used by downstream tests. + testUser = ctx.response.body.user + // console.log('testUser: ', testUser) }) }) @@ -177,4 +184,43 @@ describe('Users', () => { } }) }) + + describe('PUT /users/:id', () => { + it('should return 422 if no input data given', async () => { + try { + await uut.updateUser(ctx) + + assert.fail('Unexpected result') + } catch (err) { + // console.log(err) + assert.equal(err.status, 422) + assert.include(err.message, 'Cannot read property') + } + }) + + it('should return 200 on success', async () => { + // Prep the testUser data. + // console.log('testUser: ', testUser) + testUser.password = 'password' + delete testUser.type + + // Replace the testUser variable with an actual model from the DB. + const existingUser = await User.findById(testUser._id) + + ctx.body = { + user: existingUser + } + ctx.request.body = { + user: testUser + } + + await uut.updateUser(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + + // Assert that expected properties exist in the returned data. + assert.property(ctx.response.body, 'user') + }) + }) }) From 8c21029c7cbfb40e42d17c904d4d3c14aeef9b3c Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 28 Mar 2021 14:42:11 -0700 Subject: [PATCH 07/10] Finished adding unit tests for user update --- package.json | 9 +++-- src/lib/users.js | 11 +++--- src/middleware/validators.js | 1 + test/e2e/automated/a01-auth.rest-e2e.js | 5 +++ test/e2e/automated/a02-users.rest-e2e.js | 7 ++-- test/unit/biz-logic/a02-users.lib-unit.js | 44 ++++++++++++----------- 6 files changed, 44 insertions(+), 33 deletions(-) diff --git a/package.json b/package.json index 31b2952..3b3b6e0 100644 --- a/package.json +++ b/package.json @@ -7,14 +7,13 @@ "start": "node index.js", "test": "npm run test:all", "test:all": "npm run set-env && nyc --reporter=text mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/rest-api/ test/e2e/automated/", - "test:unit:lib": "npm run set-env && mocha --exit --timeout 15000 test/unit/biz-logic/", - "test:unit:rest": "npm run set-env && mocha --exit --timeout 15000 test/unit/rest-api/", - "test:e2e:auto": "npm run set-env && mocha --exit --timeout 15000 test/e2e/automated/", - "set-env": "export KOA_ENV=test", + "test:unit:lib": "export KOA_ENV=test && mocha --exit --timeout 15000 test/unit/biz-logic/", + "test:unit:rest": "export KOA_ENV=test && mocha --exit --timeout 15000 test/unit/rest-api/", + "test:e2e:auto": "export KOA_ENV=test && mocha --exit --timeout 15000 test/e2e/automated/", "lint": "standard --env mocha --fix", "docs": "./node_modules/.bin/apidoc -i src/ -o docs", "coverage": "nyc report --reporter=text-lcov | coveralls", - "coverage:report": "npm run set-env && nyc --reporter=html mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/rest-api/ test/e2e/automated/" + "coverage:report": "export KOA_ENV=test && nyc --reporter=html mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/rest-api/ test/e2e/automated/" }, "keywords": [ "koa-api-boilerplate", diff --git a/src/lib/users.js b/src/lib/users.js index aad7fa0..6693f46 100644 --- a/src/lib/users.js +++ b/src/lib/users.js @@ -94,15 +94,16 @@ class UserLib { async updateUser (existingUser, newData) { try { // Input Validation - if (!newData.email || typeof newData.email !== 'string') { + // Optional inputs, but they must be strings if included. + if (newData.email && typeof newData.email !== 'string') { throw new Error("Property 'email' must be a string!") } - if (!newData.password || typeof newData.password !== 'string') { - throw new Error("Property 'password' must be a string!") - } - if (!newData.name || typeof newData.name !== 'string') { + if (newData.name && typeof newData.name !== 'string') { throw new Error("Property 'name' must be a string!") } + if (newData.password && typeof newData.password !== 'string') { + throw new Error("Property 'password' must be a string!") + } // Save a copy of the original user type. const userType = existingUser.type diff --git a/src/middleware/validators.js b/src/middleware/validators.js index 2fb4e15..9e9dc86 100644 --- a/src/middleware/validators.js +++ b/src/middleware/validators.js @@ -120,6 +120,7 @@ class Validators { // console.log(`Err: Could not find user.`) ctx.throw(401) } + // console.log('ctx.state.user: ', ctx.state.user) // console.log(`ctx.state.user: ${JSON.stringify(ctx.state.user, null, 2)}`) // Ensure the calling user and the target user are the same. diff --git a/test/e2e/automated/a01-auth.rest-e2e.js b/test/e2e/automated/a01-auth.rest-e2e.js index a6a877f..52099ee 100644 --- a/test/e2e/automated/a01-auth.rest-e2e.js +++ b/test/e2e/automated/a01-auth.rest-e2e.js @@ -12,6 +12,8 @@ const axios = require('axios').default const config = require('../../../config') const app = require('../../../bin/server') const testUtils = require('../../utils/test-utils') +const AdminLib = require('../../../src/lib/admin') +const adminLib = new AdminLib() // const request = supertest.agent(app.listen()) const context = {} @@ -26,6 +28,9 @@ describe('Auth', () => { // Delete all previous users in the database. await testUtils.deleteAllUsers() + // Create a new admin user. + await adminLib.createSystemUser() + const userObj = { email: 'test@test.com', password: 'pass', diff --git a/test/e2e/automated/a02-users.rest-e2e.js b/test/e2e/automated/a02-users.rest-e2e.js index 96519f5..9b0aa9c 100644 --- a/test/e2e/automated/a02-users.rest-e2e.js +++ b/test/e2e/automated/a02-users.rest-e2e.js @@ -36,7 +36,7 @@ describe('Users', () => { // Get the JWT used to log in as the admin 'system' user. const adminJWT = await testUtils.getAdminJWT() - // console.log(`adminJWT: ${adminJWT}`) + console.log(`adminJWT: ${adminJWT}`) context.adminJWT = adminJWT // const admin = await testUtils.loginAdminUser() @@ -605,11 +605,12 @@ describe('Users', () => { data: { user: { name: 'This should work', - email: 'test@test.com' - // password: 'password' + email: 'test4@test.com', + password: 'password' } } } + const result = await axios(options) // console.log(`result stringified: ${JSON.stringify(result, null, 2)}`) diff --git a/test/unit/biz-logic/a02-users.lib-unit.js b/test/unit/biz-logic/a02-users.lib-unit.js index 0f70da4..e5f9e0c 100644 --- a/test/unit/biz-logic/a02-users.lib-unit.js +++ b/test/unit/biz-logic/a02-users.lib-unit.js @@ -23,6 +23,7 @@ describe('#users', () => { before(async () => { // Connect to the Mongo Database. + console.log(`Connecting to database: ${config.database}`) mongoose.Promise = global.Promise mongoose.set('useCreateIndex', true) // Stop deprecation warning. await mongoose.connect(config.database, { @@ -234,9 +235,11 @@ describe('#users', () => { } }) - it('should throw an error if no email given', async () => { + it('should throw an error if email is not a string', async () => { try { - await uut.updateUser(testUser, {}) + await uut.updateUser(testUser, { + email: 1234 + }) assert.fail('Unexpected code path') } catch (err) { @@ -245,26 +248,10 @@ describe('#users', () => { } }) - it('should throw an error if no password given', async () => { + it('should throw an error if name is not a string', async () => { try { const newData = { - email: 'test@test.com' - } - - await uut.updateUser(testUser, newData) - - assert.fail('Unexpected code path') - } catch (err) { - // console.log(err) - assert.include(err.message, "Property 'password' must be a string!") - } - }) - - it('should throw an error if no name given', async () => { - try { - const newData = { - email: 'test@test.com', - password: 'password' + name: 1234 } await uut.updateUser(testUser, newData) @@ -276,6 +263,23 @@ describe('#users', () => { } }) + it('should throw an error if non-string password given', async () => { + try { + const newData = { + email: 'test@test.com', + name: 'test', + password: 1234 + } + + await uut.updateUser(testUser, newData) + + assert.fail('Unexpected code path') + } catch (err) { + // console.log(err) + assert.include(err.message, "Property 'password' must be a string!") + } + }) + it('should throw an error for malformed type given', async () => { try { const newData = { From 345c55c1502b6d18f6f4b930e9447f6d7289c377 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 28 Mar 2021 15:01:37 -0700 Subject: [PATCH 08/10] feat(refactor): Completed refactoring auth and user libs --- package.json | 2 +- src/lib/admin.js | 1 - src/lib/users.js | 11 +- src/modules/users/controller.js | 21 ++- test/e2e/automated/a02-users.rest-e2e.js | 182 +++++++++++----------- test/unit/biz-logic/a02-users.lib-unit.js | 19 +++ test/unit/rest-api/a02-users.rest-unit.js | 28 ++++ 7 files changed, 163 insertions(+), 101 deletions(-) diff --git a/package.json b/package.json index 3b3b6e0..a098d15 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "start": "node index.js", "test": "npm run test:all", - "test:all": "npm run set-env && nyc --reporter=text mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/rest-api/ test/e2e/automated/", + "test:all": "export KOA_ENV=test && nyc --reporter=text mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/rest-api/ test/e2e/automated/", "test:unit:lib": "export KOA_ENV=test && mocha --exit --timeout 15000 test/unit/biz-logic/", "test:unit:rest": "export KOA_ENV=test && mocha --exit --timeout 15000 test/unit/rest-api/", "test:e2e:auto": "export KOA_ENV=test && mocha --exit --timeout 15000 test/e2e/automated/", diff --git a/src/lib/admin.js b/src/lib/admin.js index fbd0964..7b08455 100644 --- a/src/lib/admin.js +++ b/src/lib/admin.js @@ -84,7 +84,6 @@ class Admin { // Handle existing system user. if (err.response.status === 422) { try { - console.log('ping03') // Delete the existing user await _this.deleteExistingSystemUser() diff --git a/src/lib/users.js b/src/lib/users.js index 6693f46..ee55d6c 100644 --- a/src/lib/users.js +++ b/src/lib/users.js @@ -107,7 +107,7 @@ class UserLib { // Save a copy of the original user type. const userType = existingUser.type - console.log('userType: ', userType) + // console.log('userType: ', userType) // If user 'type' property is sent by the client if (newData.type) { @@ -136,6 +136,15 @@ class UserLib { throw err } } + + async deleteUser (user) { + try { + await user.remove() + } catch (err) { + wlogger.error('Error in lib/users.js/deleteUser()') + throw err + } + } } module.exports = UserLib diff --git a/src/modules/users/controller.js b/src/modules/users/controller.js index 9ef4f79..e1dbb5f 100644 --- a/src/modules/users/controller.js +++ b/src/modules/users/controller.js @@ -209,8 +209,8 @@ class UserController { ctx.body = { user } - } catch (error) { - ctx.throw(422, error.message) + } catch (err) { + ctx.throw(422, err.message) } } @@ -234,11 +234,18 @@ class UserController { * @apiUse TokenError */ async deleteUser (ctx) { - const user = ctx.body.user - await user.remove() - ctx.status = 200 - ctx.body = { - success: true + try { + const user = ctx.body.user + + // await user.remove() + await _this.userLib.deleteUser(user) + + ctx.status = 200 + ctx.body = { + success: true + } + } catch (err) { + ctx.throw(422, err.message) } } diff --git a/test/e2e/automated/a02-users.rest-e2e.js b/test/e2e/automated/a02-users.rest-e2e.js index 9b0aa9c..5ecea89 100644 --- a/test/e2e/automated/a02-users.rest-e2e.js +++ b/test/e2e/automated/a02-users.rest-e2e.js @@ -36,7 +36,7 @@ describe('Users', () => { // Get the JWT used to log in as the admin 'system' user. const adminJWT = await testUtils.getAdminJWT() - console.log(`adminJWT: ${adminJWT}`) + // console.log(`adminJWT: ${admi nJWT}`) context.adminJWT = adminJWT // const admin = await testUtils.loginAdminUser() @@ -693,94 +693,94 @@ describe('Users', () => { }) }) - // describe('DELETE /users/:id', () => { - // it('should not delete user if token is invalid', async () => { - // try { - // const options = { - // method: 'DELETE', - // url: `${LOCALHOST}/users/1`, - // headers: { - // Accept: 'application/json', - // Authorization: 'Bearer 1' - // } - // } - // await axios(options) - // - // assert.equal(true, false, 'Unexpected behavior') - // } catch (err) { - // assert.equal(err.response.status, 401) - // } - // }) - // - // it('should throw 401 if deleting invalid user', async () => { - // const { token } = context - // - // try { - // const options = { - // method: 'DELETE', - // url: `${LOCALHOST}/users/1`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${token}` - // } - // } - // await axios(options) - // - // assert.equal(true, false, 'Unexpected behavior') - // } catch (err) { - // assert.equal(err.response.status, 401) - // } - // }) - // - // it('should not be able to delete other users unless admin', async () => { - // try { - // const options = { - // method: 'DELETE', - // url: `${LOCALHOST}/users/${context.user2._id.toString()}`, - // headers: { - // Authorization: `Bearer ${context.token}` - // } - // } - // await axios(options) - // } catch (err) { - // assert.equal(err.response.status, 401) - // } - // }) - // - // it('should delete own user', async () => { - // const _id = context.user._id - // const token = context.token - // - // const options = { - // method: 'DELETE', - // url: `${LOCALHOST}/users/${_id}`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${token}` - // } - // } - // const result = await axios(options) - // // console.log(`result: ${util.inspect(result.data.success)}`) - // - // assert.equal(result.data.success, true) - // }) - // - // it('should be able to delete other users when admin', async () => { - // const id = context.id2 - // const adminJWT = context.adminJWT - // - // const options = { - // method: 'DELETE', - // url: `${LOCALHOST}/users/${id}`, - // headers: { - // Accept: 'application/json', - // Authorization: `Bearer ${adminJWT}` - // } - // } - // const result = await axios(options) - // // console.log(`result: ${util.inspect(result.data)}`) - // - // assert.equal(result.data.success, true) - // }) - // }) + describe('DELETE /users/:id', () => { + it('should not delete user if token is invalid', async () => { + try { + const options = { + method: 'DELETE', + url: `${LOCALHOST}/users/1`, + headers: { + Accept: 'application/json', + Authorization: 'Bearer 1' + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should throw 401 if deleting invalid user', async () => { + const { token } = context + + try { + const options = { + method: 'DELETE', + url: `${LOCALHOST}/users/1`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + } + await axios(options) + + assert.equal(true, false, 'Unexpected behavior') + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should not be able to delete other users unless admin', async () => { + try { + const options = { + method: 'DELETE', + url: `${LOCALHOST}/users/${context.user2._id.toString()}`, + headers: { + Authorization: `Bearer ${context.token}` + } + } + await axios(options) + } catch (err) { + assert.equal(err.response.status, 401) + } + }) + + it('should delete own user', async () => { + const _id = context.user._id + const token = context.token + + const options = { + method: 'DELETE', + url: `${LOCALHOST}/users/${_id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${token}` + } + } + const result = await axios(options) + // console.log(`result: ${util.inspect(result.data.success)}`) + + assert.equal(result.data.success, true) + }) + + it('should be able to delete other users when admin', async () => { + const id = context.id2 + const adminJWT = context.adminJWT + + const options = { + method: 'DELETE', + url: `${LOCALHOST}/users/${id}`, + headers: { + Accept: 'application/json', + Authorization: `Bearer ${adminJWT}` + } + } + const result = await axios(options) + // console.log(`result: ${util.inspect(result.data)}`) + + assert.equal(result.data.success, true) + }) + }) }) diff --git a/test/unit/biz-logic/a02-users.lib-unit.js b/test/unit/biz-logic/a02-users.lib-unit.js index e5f9e0c..2885e2f 100644 --- a/test/unit/biz-logic/a02-users.lib-unit.js +++ b/test/unit/biz-logic/a02-users.lib-unit.js @@ -335,4 +335,23 @@ describe('#users', () => { // TODO: verify that an admin can change the type of a user }) + + describe('#deleteUser', () => { + it('should throw error if no user provided', async () => { + try { + await uut.deleteUser() + + assert.fail('Unexpected code path.') + } catch (err) { + // console.log(err) + assert.include(err.message, 'Cannot read property') + } + }) + + it('should delete the user from the database', async () => { + await uut.deleteUser(testUser) + + assert.isOk('Not throwing an error is a pass!') + }) + }) }) diff --git a/test/unit/rest-api/a02-users.rest-unit.js b/test/unit/rest-api/a02-users.rest-unit.js index 4386874..0ec57b2 100644 --- a/test/unit/rest-api/a02-users.rest-unit.js +++ b/test/unit/rest-api/a02-users.rest-unit.js @@ -223,4 +223,32 @@ describe('Users', () => { assert.property(ctx.response.body, 'user') }) }) + + describe('DELETE /users/:id', () => { + it('should return 422 if no input data given', async () => { + try { + await uut.deleteUser(ctx) + + assert.fail('Unexpected result') + } catch (err) { + // console.log(err) + assert.equal(err.status, 422) + assert.include(err.message, 'Cannot read property') + } + }) + + it('should return 200 status on success', async () => { + // Replace the testUser variable with an actual model from the DB. + const existingUser = await User.findById(testUser._id) + + ctx.body = { + user: existingUser + } + + await uut.deleteUser(ctx) + + // Assert the expected HTTP response + assert.equal(ctx.status, 200) + }) + }) }) From 17f0472d59eca45b15b1ac3b8cadf762f2e2a839 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 28 Mar 2021 15:16:10 -0700 Subject: [PATCH 09/10] Moved old tests to old-tests directory --- test/unit/README.md | 11 ++++++++--- test/unit/{ => old-tests}/a01-auth.spec.js | 0 test/unit/{ => old-tests}/a02-users.spec.js | 0 test/unit/{ => old-tests}/a03-nodemailer.spec.js | 0 test/unit/{ => old-tests}/a04-contact.spec.js | 0 test/unit/{ => old-tests}/a05-passport.spec.js | 0 test/unit/{ => old-tests}/a06-logapi.spec.js | 0 test/unit/{ => old-tests}/a07-json-files.spec.js | 0 test/unit/{ => old-tests}/a08-validators.spec.js | 0 test/unit/{ => old-tests}/a09-admin.spec.js | 0 test/unit/{ => old-tests}/utils.js | 0 11 files changed, 8 insertions(+), 3 deletions(-) rename test/unit/{ => old-tests}/a01-auth.spec.js (100%) rename test/unit/{ => old-tests}/a02-users.spec.js (100%) rename test/unit/{ => old-tests}/a03-nodemailer.spec.js (100%) rename test/unit/{ => old-tests}/a04-contact.spec.js (100%) rename test/unit/{ => old-tests}/a05-passport.spec.js (100%) rename test/unit/{ => old-tests}/a06-logapi.spec.js (100%) rename test/unit/{ => old-tests}/a07-json-files.spec.js (100%) rename test/unit/{ => old-tests}/a08-validators.spec.js (100%) rename test/unit/{ => old-tests}/a09-admin.spec.js (100%) rename test/unit/{ => old-tests}/utils.js (100%) diff --git a/test/unit/README.md b/test/unit/README.md index 8cddb82..acd1a9d 100644 --- a/test/unit/README.md +++ b/test/unit/README.md @@ -1,3 +1,8 @@ -The files in this directory are named so as to control the order in which -the files are executed by mocha. a01... runs first. The order is important, -as some tests downstream depend on tests upstream. +# Unit Tests +Unit tests are defined as testing the smallest possible unit of a function. They also do not make any live network calls. + +Unit tests are broken up by directory: + +- [biz-logic](./biz-logic) tests the business logic libraries. +- [rest-api](./rest-api) tests the REST API specific handling of the router. +- json-rpc (coming soon) tests the JSON-RPC routing using ipfs-coord library. diff --git a/test/unit/a01-auth.spec.js b/test/unit/old-tests/a01-auth.spec.js similarity index 100% rename from test/unit/a01-auth.spec.js rename to test/unit/old-tests/a01-auth.spec.js diff --git a/test/unit/a02-users.spec.js b/test/unit/old-tests/a02-users.spec.js similarity index 100% rename from test/unit/a02-users.spec.js rename to test/unit/old-tests/a02-users.spec.js diff --git a/test/unit/a03-nodemailer.spec.js b/test/unit/old-tests/a03-nodemailer.spec.js similarity index 100% rename from test/unit/a03-nodemailer.spec.js rename to test/unit/old-tests/a03-nodemailer.spec.js diff --git a/test/unit/a04-contact.spec.js b/test/unit/old-tests/a04-contact.spec.js similarity index 100% rename from test/unit/a04-contact.spec.js rename to test/unit/old-tests/a04-contact.spec.js diff --git a/test/unit/a05-passport.spec.js b/test/unit/old-tests/a05-passport.spec.js similarity index 100% rename from test/unit/a05-passport.spec.js rename to test/unit/old-tests/a05-passport.spec.js diff --git a/test/unit/a06-logapi.spec.js b/test/unit/old-tests/a06-logapi.spec.js similarity index 100% rename from test/unit/a06-logapi.spec.js rename to test/unit/old-tests/a06-logapi.spec.js diff --git a/test/unit/a07-json-files.spec.js b/test/unit/old-tests/a07-json-files.spec.js similarity index 100% rename from test/unit/a07-json-files.spec.js rename to test/unit/old-tests/a07-json-files.spec.js diff --git a/test/unit/a08-validators.spec.js b/test/unit/old-tests/a08-validators.spec.js similarity index 100% rename from test/unit/a08-validators.spec.js rename to test/unit/old-tests/a08-validators.spec.js diff --git a/test/unit/a09-admin.spec.js b/test/unit/old-tests/a09-admin.spec.js similarity index 100% rename from test/unit/a09-admin.spec.js rename to test/unit/old-tests/a09-admin.spec.js diff --git a/test/unit/utils.js b/test/unit/old-tests/utils.js similarity index 100% rename from test/unit/utils.js rename to test/unit/old-tests/utils.js From 14145bb4023e8e7eed0f7d0f480f73499e0e79b1 Mon Sep 17 00:00:00 2001 From: Chris Troutner Date: Sun, 28 Mar 2021 15:18:23 -0700 Subject: [PATCH 10/10] Minor edits to nodemailer --- src/lib/nodemailer.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/nodemailer.js b/src/lib/nodemailer.js index 4835bfe..426fae6 100644 --- a/src/lib/nodemailer.js +++ b/src/lib/nodemailer.js @@ -20,6 +20,7 @@ class NodeMailer { _this.transporter = _this.createTransporter() } + // Define an email server 'transport' for nodemailer createTransporter () { const transporter = _this.nodemailer.createTransport({ host: _this.config.emailServer, @@ -78,7 +79,7 @@ class NodeMailer { const subject = data.subject const to = data.to - const emailUser = data.email // email from the user who initiated the sharing email + // const emailUser = data.email // email from the user who initiated the sharing email const payload = data.payloadTitle const bodyJson = data @@ -103,9 +104,7 @@ class NodeMailer { }) // This paragraph just be added to the message for // emails that are not password reset ones - const paragraphTag = `

${emailUser} would like to share the document ${payload} with you through - LaunchpadIP.net. -

` + const paragraphTag = 'This is a test email' const htmlMsg = `

${subject}:

${payload === 'Email reset' ? '' : paragraphTag}