Merge pull request #101 from Permissionless-Software-Foundation/ct-unstable

Increased test coverage to 100%
This commit is contained in:
Chris Troutner
2022-09-17 15:24:26 -07:00
committed by GitHub
22 changed files with 2359 additions and 3613 deletions
+8
View File
@@ -0,0 +1,8 @@
[
{
"srcDir": "",
"destDir": "",
"files": "**/*.js",
"command": "npm run lint"
}
]
+1 -1
View File
@@ -27,7 +27,7 @@ import AdminLib from '../src/adapters/admin.js'
import errorMiddleware from '../src/controllers/rest-api/middleware/error.js' import errorMiddleware from '../src/controllers/rest-api/middleware/error.js'
import wlogger from '../src/adapters/wlogger.js' import wlogger from '../src/adapters/wlogger.js'
import Controllers from '../src/controllers/index.js' import Controllers from '../src/controllers/index.js'
import applyPassportMods from '../config/passport.js' import { applyPassportMods } from '../config/passport.js'
class Server { class Server {
constructor () { constructor () {
+7 -2
View File
@@ -5,9 +5,14 @@
/* eslint no-unneeded-ternary:0 */ /* eslint no-unneeded-ternary:0 */
// Hack to get __dirname back.
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
import * as url from 'url'
// Get the version from the package.json file. // Get the version from the package.json file.
import { readFileSync } from 'fs' import { readFileSync } from 'fs'
const pkgInfo = JSON.parse(readFileSync('package.json')) const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const pkgInfo = JSON.parse(readFileSync(`${__dirname.toString()}/../../package.json`))
const version = pkgInfo.version const version = pkgInfo.version
@@ -17,7 +22,7 @@ const ipfsCoordName = process.env.COORD_NAME
export default { export default {
// Configure TCP port. // Configure TCP port.
port: process.env.PORT || 5001, port: process.env.PORT || 5020,
// Password for HTML UI that displays logs. // Password for HTML UI that displays logs.
logPass: 'test', logPass: 'test',
+2 -1
View File
@@ -5,11 +5,12 @@ import production from './env/production.js'
import test from './env/test.js' import test from './env/test.js'
const env = process.env.SVC_ENV || 'development' const env = process.env.SVC_ENV || 'development'
console.log(`Loading config for this environment: ${env}`)
let config = development let config = development
if (env === 'test') { if (env === 'test') {
config = test config = test
} else if (env === 'production') { } else if (env === 'prod') {
config = production config = production
} }
+4 -1
View File
@@ -49,8 +49,11 @@ function applyPassportMods (passport) {
passportCallback passportCallback
) )
) )
return true
} }
// For testing // For testing
// export default { passport, passportCallback }; // export default { passport, passportCallback };
export default applyPassportMods // export default applyPassportMods
export { applyPassportMods, passportCallback }
+1730 -3420
View File
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -7,14 +7,14 @@
"scripts": { "scripts": {
"start": "node index.js", "start": "node index.js",
"test": "npm run test:all", "test": "npm run test:all",
"test:all": "export SVC_ENV=test && nyc --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/", "test:all": "export SVC_ENV=test && c8 --reporter=text mocha --exit --timeout 15000 --recursive test/unit test/e2e/automated/",
"test:unit": "export SVC_ENV=test && mocha --exit --timeout 15000 --recursive test/unit/", "test:unit": "export SVC_ENV=test && mocha --exit --timeout 15000 --recursive test/unit/",
"test:e2e:auto": "export SVC_ENV=test && mocha --exit --timeout 15000 test/e2e/automated/", "test:e2e:auto": "export SVC_ENV=test && mocha --exit --timeout 15000 test/e2e/automated/",
"test:temp": "export SVC_ENV=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/", "test:temp": "export SVC_ENV=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/",
"lint": "standard --env mocha --fix", "lint": "standard --env mocha --fix",
"docs": "./node_modules/.bin/apidoc -i src/ -o docs", "docs": "./node_modules/.bin/apidoc -i src/ -o docs",
"coverage": "nyc report --reporter=text-lcov | coveralls", "coverage": "c8 report --reporter=text-lcov | coveralls",
"coverage:report": "export SVC_ENV=test && nyc --reporter=html mocha --exit --timeout 15000 --recursive test/unit/ test/e2e/automated/" "coverage:report": "export SVC_ENV=test && c8 --reporter=html mocha --exit --timeout 15000 --recursive test/unit/ test/e2e/automated/"
}, },
"author": "Chris Troutner <chris.troutner@gmail.com>", "author": "Chris Troutner <chris.troutner@gmail.com>",
"license": "MIT", "license": "MIT",
@@ -27,7 +27,7 @@
"axios": "0.27.2", "axios": "0.27.2",
"bcryptjs": "2.4.3", "bcryptjs": "2.4.3",
"glob": "7.1.6", "glob": "7.1.6",
"ipfs-coord-esm": "9.1.1", "ipfs-coord-esm": "9.1.3",
"ipfs-http-client": "58.0.0", "ipfs-http-client": "58.0.0",
"jsonrpc-lite": "2.2.0", "jsonrpc-lite": "2.2.0",
"jsonwebtoken": "8.5.1", "jsonwebtoken": "8.5.1",
@@ -44,7 +44,7 @@
"koa-static": "5.0.0", "koa-static": "5.0.0",
"koa2-ratelimit": "0.9.1", "koa2-ratelimit": "0.9.1",
"line-reader": "0.4.0", "line-reader": "0.4.0",
"minimal-slp-wallet": "5.0.3", "minimal-slp-wallet": "5.1.0",
"mongoose": "5.13.14", "mongoose": "5.13.14",
"node-fetch": "npm:@achingbrain/node-fetch@2.6.7", "node-fetch": "npm:@achingbrain/node-fetch@2.6.7",
"nodemailer": "6.7.5", "nodemailer": "6.7.5",
@@ -55,11 +55,11 @@
}, },
"devDependencies": { "devDependencies": {
"apidoc": "0.51.1", "apidoc": "0.51.1",
"c8": "7.12.0",
"chai": "4.3.0", "chai": "4.3.0",
"coveralls": "3.1.0", "coveralls": "3.1.0",
"husky": "4.3.8", "husky": "4.3.8",
"mocha": "10.0.0", "mocha": "10.0.0",
"nyc": "15.1.0",
"semantic-release": "19.0.3", "semantic-release": "19.0.3",
"sinon": "9.2.4", "sinon": "9.2.4",
"standard": "17.0.0", "standard": "17.0.0",
+5 -3
View File
@@ -25,12 +25,12 @@ import JsonFiles from '../adapters/json-files.js'
// Hack to get __dirname back. // Hack to get __dirname back.
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/ // https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
import * as url from 'url' import * as url from 'url'
const jsonFiles = new JsonFiles()
const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
const jsonFiles = new JsonFiles()
const JSON_FILE = `system-user-${config.env}.json` const JSON_FILE = `system-user-${config.env}.json`
const JSON_PATH = `${__dirname.toString()}/../../config/${JSON_FILE}` const JSON_PATH = `${__dirname.toString()}../../config/${JSON_FILE}`
const LOCALHOST = `http://localhost:${config.port}` const LOCALHOST = `http://localhost:${config.port}`
const context = {} const context = {}
@@ -90,6 +90,8 @@ class Admin {
// applications like the Task Manager and the test scripts can access. // applications like the Task Manager and the test scripts can access.
await jsonFiles.writeJSON(context, JSON_PATH) await jsonFiles.writeJSON(context, JSON_PATH)
// console.log('context: ', context)
// console.log('JSON_PATH: ', JSON_PATH)
return context return context
} catch (err) { } catch (err) {
+3
View File
@@ -51,6 +51,9 @@ class Adapters {
// Do not start these adapters if this is an e2e test. // Do not start these adapters if this is an e2e test.
if (this.config.env !== 'test') { if (this.config.env !== 'test') {
await this.ipfs.start() await this.ipfs.start()
} else {
// These lines are here to ensure code coverage hits 100%.
console.log('Not starting IPFS node since this is an e2e test.')
} }
console.log('Async Adapters have been started.') console.log('Async Adapters have been started.')
+2 -2
View File
@@ -76,5 +76,5 @@ logger.outputToConsole()
const wlogger = logger.wlogger const wlogger = logger.wlogger
// export default { wlogger, Wlogger }; export { wlogger as default, Wlogger }
export default wlogger // export default wlogger
@@ -1,5 +1,17 @@
/* /*
REST API validator middleware. REST API validator middleware.
These are a series of functions that ensure the user making a REST API
matches a user in the database (or not). In can do fine-grain user control
such as telling the difference between an admin, a normal user, and an
anonymous user.
This middleware is used to gatekeep access to different REST API resources.
CT 9/17/22:
This library was lightly refactored to make it work with the new unit tests.
This not and the commented code below can be deleted once it is verified that
this refactor did not result in any breaking changes.
*/ */
import User from '../../../adapters/localdb/models/users.js' import User from '../../../adapters/localdb/models/users.js'
@@ -21,12 +33,10 @@ class Validators {
async ensureUser (ctx, next) { async ensureUser (ctx, next) {
try { try {
// console.log(`getToken: ${typeof (getToken)}`)
const token = _this.getToken(ctx) const token = _this.getToken(ctx)
if (!token) { if (!token) {
// console.log(`Err: Token not provided.`) throw new Error('Token could not be retrieved from header')
ctx.throw(401)
} }
let decoded = null let decoded = null
@@ -35,20 +45,23 @@ class Validators {
// console.log(`config: ${JSON.stringify(config, null, 2)}`) // console.log(`config: ${JSON.stringify(config, null, 2)}`)
decoded = _this.jwt.verify(token, config.token) decoded = _this.jwt.verify(token, config.token)
} catch (err) { } catch (err) {
// console.log(`Err: Token could not be decoded: ${err}`) throw new Error('Could not verify JWT')
ctx.throw(401)
} }
ctx.state.user = await _this.User.findById(decoded.id, '-password') ctx.state.user = await _this.User.findById(decoded.id, '-password')
if (!ctx.state.user) { if (!ctx.state.user) {
// console.log(`Err: Could not find user.`) // console.log('Err: Could not find user.')
ctx.throw(401) throw new Error('Could not find user')
} }
// return next() // return next()
return true return true
} catch (error) { } catch (error) {
ctx.throw(401) // console.log('Ensure user error: ', error)
// console.log('ctx: ', ctx)
ctx.status = 401
ctx.throw(401, error.message)
} }
} }
@@ -61,7 +74,8 @@ class Validators {
if (!token) { if (!token) {
// console.log(`Err: Token not provided.`) // console.log(`Err: Token not provided.`)
ctx.throw(401) // ctx.throw(401)
throw new Error('Token could not be retrieved from header')
} }
let decoded = null let decoded = null
@@ -71,22 +85,26 @@ class Validators {
decoded = _this.jwt.verify(token, config.token) decoded = _this.jwt.verify(token, config.token)
} catch (err) { } catch (err) {
// console.log(`Err: Token could not be decoded: ${err}`) // console.log(`Err: Token could not be decoded: ${err}`)
ctx.throw(401) // ctx.throw(401)
throw new Error('Could not verify JWT')
} }
ctx.state.user = await _this.User.findById(decoded.id, '-password') ctx.state.user = await _this.User.findById(decoded.id, '-password')
if (!ctx.state.user) { if (!ctx.state.user) {
// console.log(`Err: Could not find user.`) // console.log(`Err: Could not find user.`)
ctx.throw(401) // ctx.throw(401)
throw new Error('Could not find user')
} }
if (ctx.state.user.type !== 'admin') { if (ctx.state.user.type !== 'admin') {
ctx.throw(401, 'not admin') // ctx.throw(401, 'not admin')
throw new Error('User is not an admin')
} }
// return next() // return next()
return true return true
} catch (error) { } catch (error) {
ctx.status = 401
ctx.throw(401, error.message) ctx.throw(401, error.message)
} }
} }
@@ -102,7 +120,8 @@ class Validators {
if (!token) { if (!token) {
// console.log(`Err: Token not provided.`) // console.log(`Err: Token not provided.`)
ctx.throw(401) // ctx.throw(401)
throw new Error('Token could not be retrieved from header')
} }
// The user ID targeted in this API call. // The user ID targeted in this API call.
@@ -115,14 +134,16 @@ class Validators {
// console.log(`config: ${JSON.stringify(config, null, 2)}`) // console.log(`config: ${JSON.stringify(config, null, 2)}`)
decoded = _this.jwt.verify(token, config.token) decoded = _this.jwt.verify(token, config.token)
} catch (err) { } catch (err) {
// console.log(`Err: Token could not be decoded: ${err}`) console.log(`Err: Token could not be decoded: ${err}`)
ctx.throw(401) // ctx.throw(401)
throw new Error('Could not verify JWT')
} }
ctx.state.user = await _this.User.findById(decoded.id, '-password') ctx.state.user = await _this.User.findById(decoded.id, '-password')
if (!ctx.state.user) { if (!ctx.state.user) {
// console.log(`Err: Could not find user.`) // console.log(`Err: Could not find user.`)
ctx.throw(401) // ctx.throw(401)
throw new Error('Could not find user')
} }
// console.log('ctx.state.user: ', ctx.state.user) // console.log('ctx.state.user: ', ctx.state.user)
@@ -136,7 +157,8 @@ class Validators {
// If they don't match, then the calling user better be an admin. // If they don't match, then the calling user better be an admin.
if (ctx.state.user.type !== 'admin') { if (ctx.state.user.type !== 'admin') {
ctx.throw(401, 'not admin') // ctx.throw(401, 'not admin')
throw new Error('User is not an admin')
} else { } else {
wlogger.verbose("It's ok. The user is an admin.") wlogger.verbose("It's ok. The user is an admin.")
} }
@@ -145,6 +167,8 @@ class Validators {
// return next() // return next()
return true return true
} catch (error) { } catch (error) {
// console.log('Error in ensureTargetUserOrAdmin(): ', error)
ctx.status = 401
ctx.throw(401, error.message) ctx.throw(401, error.message)
} }
} }
+2 -2
View File
@@ -36,7 +36,7 @@ class UserRESTControllerLib {
* @apiGroup REST Users * @apiGroup REST Users
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -H "Content-Type: application/json" -X POST -d '{ "user": { "email": "email@format.com", "name": "my name", "password": "secretpasas" } }' localhost:5001/users * curl -H "Content-Type: application/json" -X POST -d '{ "user": { "email": "email@format.com", "name": "my name", "password": "secretpasas" } }' localhost:5010/users
* *
* @apiParam {Object} user User object (required) * @apiParam {Object} user User object (required)
* @apiParam {String} user.email Email * @apiParam {String} user.email Email
@@ -136,7 +136,7 @@ class UserRESTControllerLib {
* @apiGroup REST Users * @apiGroup REST Users
* *
* @apiExample Example usage: * @apiExample Example usage:
* curl -H "Content-Type: application/json" -X GET localhost:5000/users/56bd1da600a526986cf65c80 * curl -H "Content-Type: application/json" -X GET localhost:5010/users/56bd1da600a526986cf65c80
* *
* @apiSuccess {Object} users User object * @apiSuccess {Object} users User object
* @apiSuccess {ObjectId} users._id User id * @apiSuccess {ObjectId} users._id User id
+11 -13
View File
@@ -1,20 +1,19 @@
/* import testUtils from '../../utils/test-utils.js'
import testUtils from '../../utils/test-utils.js'; import { assert } from 'chai'
import { assert } from 'chai'; import config from '../../../config/index.js'
import config from '../../../config/index.js'; import axios from 'axios'
import axios from 'axios'; import sinon from 'sinon'
import sinon from 'sinon'; import util from 'util'
import util from 'util';
import UserController from '../../../src/controllers/rest-api/users/controller.js'
import Adapters from '../../../src/adapters/index.js'
import UseCases from '../../../src/use-cases/index.js'
util.inspect.defaultOptions = { depth: 1 } util.inspect.defaultOptions = { depth: 1 }
const LOCALHOST = `http://localhost:${config.port}` const LOCALHOST = `http://localhost:${config.port}`
const context = {} const context = {}
import UserController from '../../../src/controllers/rest-api/users/controller.js';
import Adapters from '../../../src/adapters/index.js';
const adapters = new Adapters() const adapters = new Adapters()
import UseCases from '../../../src/use-cases/index.js';
let uut let uut
let sandbox let sandbox
@@ -39,7 +38,7 @@ describe('Users', () => {
// Get the JWT used to log in as the admin 'system' user. // Get the JWT used to log in as the admin 'system' user.
const adminJWT = await testUtils.getAdminJWT() const adminJWT = await testUtils.getAdminJWT()
// console.log(`adminJWT: ${admi nJWT}`) console.log(`adminJWT: ${adminJWT}`)
context.adminJWT = adminJWT context.adminJWT = adminJWT
// const admin = await testUtils.loginAdminUser() // const admin = await testUtils.loginAdminUser()
@@ -786,4 +785,3 @@ describe('Users', () => {
}) })
}) })
}) })
*/
+16 -1
View File
@@ -52,7 +52,7 @@ describe('#IPFS', () => {
it('should get the public IP address if this node is a Circuit Relay', async () => { it('should get the public IP address if this node is a Circuit Relay', async () => {
// Mock dependencies. // Mock dependencies.
uut.IpfsCoord = IPFSCoordMock uut.IpfsCoord = IPFSCoordMock
sandbox.stub(uut.publicIp, 'v4').returns('123') sandbox.stub(uut.publicIp, 'v4').resolves('123')
// Force Circuit Relay // Force Circuit Relay
uut.config.isCircuitRelay = true uut.config.isCircuitRelay = true
@@ -62,6 +62,21 @@ describe('#IPFS', () => {
assert.equal(result, true) assert.equal(result, true)
}) })
it('should exit quietly if this node is a Circuit Relay and there is an issue getting the IP address', async () => {
// Mock dependencies.
uut.IpfsCoord = IPFSCoordMock
sandbox.stub(uut.publicIp, 'v4').rejects(new Error('test error'))
// Force Circuit Relay
uut.config.isCircuitRelay = true
const result = await uut.start()
// console.log('result: ', result)
assert.equal(result, true)
})
it('should return a promise that resolves into an instance of IPFS in production mode', async () => { it('should return a promise that resolves into an instance of IPFS in production mode', async () => {
uut.config.isProduction = true uut.config.isProduction = true
// Mock dependencies. // Mock dependencies.
+39 -39
View File
@@ -1,39 +1,39 @@
// import { assert } from 'chai'; import { assert } from 'chai'
// import { Wlogger } from '../../../src/adapters/wlogger.js'; import { Wlogger } from '../../../src/adapters/wlogger.js'
// import sinon from 'sinon'; import sinon from 'sinon'
//
// let uut let uut
// let sandbox let sandbox
//
// describe('#wlogger', () => { describe('#wlogger', () => {
// beforeEach(() => { beforeEach(() => {
// sandbox = sinon.createSandbox() sandbox = sinon.createSandbox()
// }) })
//
// afterEach(() => { afterEach(() => {
// sandbox.restore() sandbox.restore()
//
// uut = new Wlogger() uut = new Wlogger()
// }) })
//
// describe('#constructor', () => { describe('#constructor', () => {
// it('should create a new wlogger instance', () => { it('should create a new wlogger instance', () => {
// uut = new Wlogger() uut = new Wlogger()
// // console.log('uut: ', uut) // console.log('uut: ', uut)
//
// assert.property(uut, 'transport') assert.property(uut, 'transport')
// }) })
// }) })
//
// describe('#notifyRotation', () => { describe('#notifyRotation', () => {
// it('should notify of a log rotation', () => { it('should notify of a log rotation', () => {
// uut.notifyRotation() uut.notifyRotation()
// }) })
// }) })
//
// describe('#envronment', () => { describe('#envronment', () => {
// it('should write to console in non-test environment', () => { it('should write to console in non-test environment', () => {
// uut.outputToConsole() uut.outputToConsole()
// }) })
// }) })
// }) })
@@ -0,0 +1,306 @@
/*
Unit tests for the REST API middleware that validates users.
*/
// Public npm libraries
import { assert } from 'chai'
import sinon from 'sinon'
// Local libraries
import Validators from '../../../../../src/controllers/rest-api/middleware/validators.js'
import { context as mockContext } from '../../../../unit/mocks/ctx-mock.js'
describe('#Validators', () => {
let uut
let ctx
let sandbox
beforeEach(() => {
uut = new Validators()
// Mock the context object.
ctx = mockContext()
sandbox = sinon.createSandbox()
})
afterEach(() => sandbox.restore())
describe('#getToken', () => {
it('should return null if no header is provided', () => {
const result = uut.getToken(ctx)
assert.equal(result, null)
})
it('should return null if header is not in two parts', () => {
ctx.request.header.authorization = 'Bearer'
const result = uut.getToken(ctx)
assert.equal(result, null)
})
it('should return null if first part of header does not container the word bearer', () => {
ctx.request.header.authorization = 'some thing'
const result = uut.getToken(ctx)
assert.equal(result, null)
})
it('should return the JWT token from the header', () => {
const jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYzMjNiNTUwNzgxYWYzNTc4YzI0ZmU5YiIsImlhdCI6MTY2MzQ0NDczNCwiZXhwIjoxNjYzNTMxMTM0fQ.BY5sOfXc4z5axS98CdTfyqnO9y2wijOlwnv52rcvxHA'
ctx.request.header.authorization = `Bearer ${jwt}`
const result = uut.getToken(ctx)
assert.equal(result, jwt)
})
})
describe('#ensureUser', () => {
it('should throw error if token is not provided', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns()
await uut.ensureUser(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should throw error if token can not be verified', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').throws(new Error('test error'))
await uut.ensureUser(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should throw error if user can not be found in database', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').returns({})
sandbox.stub(uut.User, 'findById').resolves(false)
await uut.ensureUser(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should return true if the user is verified', async () => {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').returns({})
sandbox.stub(uut.User, 'findById').resolves({ user: 'alice' })
const result = await uut.ensureUser(ctx)
assert.equal(result, true)
})
})
describe('#ensureUser', () => {
it('should throw error if token is not provided', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns()
await uut.ensureAdmin(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should throw error if token can not be verified', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').throws(new Error('test error'))
await uut.ensureAdmin(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should throw error if user can not be found in database', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').returns({})
sandbox.stub(uut.User, 'findById').resolves(false)
await uut.ensureAdmin(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should throw error if user is not an admin', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').returns({})
sandbox.stub(uut.User, 'findById').resolves({ type: 'user' })
await uut.ensureAdmin(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should return true if the user is an admin', async () => {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').returns({})
sandbox.stub(uut.User, 'findById').resolves({ type: 'admin' })
const result = await uut.ensureAdmin(ctx)
assert.equal(result, true)
})
})
describe('#ensureTargetUserOrAdmin', () => {
it('should throw error if token is not provided', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns()
await uut.ensureTargetUserOrAdmin(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should throw error if token can not be verified', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').throws(new Error('test error'))
ctx.params = {
id: '456'
}
await uut.ensureTargetUserOrAdmin(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should throw error if user can not be found in database', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').returns({})
sandbox.stub(uut.User, 'findById').resolves(false)
ctx.params = {
id: '456'
}
await uut.ensureTargetUserOrAdmin(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should throw error if user is not an admin', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').returns({})
sandbox.stub(uut.User, 'findById').resolves({ type: 'user' })
ctx.params = {
id: '456'
}
await uut.ensureTargetUserOrAdmin(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should throw error is user is not admin or target user', async () => {
try {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').returns({})
sandbox.stub(uut.User, 'findById').resolves({ type: 'user', _id: '123' })
ctx.params = {
id: '456'
}
await uut.ensureTargetUserOrAdmin(ctx)
assert.fail('Unexpected result')
} catch (err) {
assert.equal(ctx.status, 401)
}
})
it('should return true if the user is an admin', async () => {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').returns({})
sandbox.stub(uut.User, 'findById').resolves({ type: 'admin', _id: '123' })
ctx.params = {
id: '456'
}
const result = await uut.ensureTargetUserOrAdmin(ctx)
assert.equal(result, true)
})
it('should return true if the user is the target user', async () => {
// Mock dependencies and force desired code path
sandbox.stub(uut, 'getToken').returns('fake-jwt')
sandbox.stub(uut.jwt, 'verify').returns({})
sandbox.stub(uut.User, 'findById').resolves({ type: 'user', _id: '123' })
ctx.params = {
id: '123'
}
const result = await uut.ensureTargetUserOrAdmin(ctx)
assert.equal(result, true)
})
})
})
@@ -4,12 +4,10 @@
// Public npm libraries // Public npm libraries
import { assert } from 'chai' import { assert } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
// Local support libraries // Local support libraries
import adapters from '../../../mocks/adapters/index.js' import adapters from '../../../mocks/adapters/index.js'
import UseCasesMock from '../../../mocks/use-cases/index.js' import UseCasesMock from '../../../mocks/use-cases/index.js'
import UserController from '../../../../../src/controllers/rest-api/users/controller.js' import UserController from '../../../../../src/controllers/rest-api/users/controller.js'
+50
View File
@@ -0,0 +1,50 @@
/*
Unit tests for the config directory
*/
import { assert } from 'chai'
let currentEnv
describe('#config', () => {
before(() => {
// Backup the current environment setting.
currentEnv = process.env.SVC_ENV
})
after(() => {
// Restore the environment setting before starting these tests.
process.env.SVC_ENV = currentEnv
})
it('Should return development environment config by default', async () => {
const importedConfig = await import('../../../config/index.js')
const config = importedConfig.default
// console.log('config: ', config)
assert.equal(config.env, 'dev')
})
it('Should return test environment config', async () => {
// Hack to dynamically import a library multiple times:
// https://github.com/denoland/deno/issues/6946
process.env.SVC_ENV = 'test'
const importedConfig2 = await import('../../../config/index.js?foo=bar1')
const config = importedConfig2.default
// console.log('config: ', config)
assert.equal(config.env, 'test')
})
it('Should return test environment config', async () => {
process.env.SVC_ENV = 'prod'
const importedConfig3 = await import('../../../config/index.js?foo=bar2')
const config = importedConfig3.default
// console.log('config: ', config)
assert.equal(config.env, 'prod')
})
})
+93 -78
View File
@@ -1,78 +1,93 @@
// /* /*
// Unit tests for the passport library. Unit tests for the passport library.
// */ */
//
// // Public npm libraries // Public npm libraries
// // const assert = require('chai').assert import { assert } from 'chai'
// import sinon from 'sinon'; import sinon from 'sinon'
// import passport from 'koa-passport'
// // Local libraries
// import User from '../../../src/adapters/localdb/models/users.js'; // Local libraries
// import User from '../../../src/adapters/localdb/models/users.js'
// import { passport, passportCallback } from '../../../config/passport.js';
// import adaptersMock from '../mocks/adapters/index.js'; import { applyPassportMods, passportCallback } from '../../../config/passport.js'
// import adaptersMock from '../mocks/adapters/index.js'
// describe('#passport', () => {
// let sandbox describe('#passport', () => {
// let id let sandbox
// let done let id
// let done
// beforeEach(() => {
// sandbox = sinon.createSandbox() beforeEach(() => {
// sandbox = sinon.createSandbox()
// id = 'abc123'
// done = () => {} id = 'abc123'
// }) done = () => {}
// })
// afterEach(() => sandbox.restore())
// afterEach(() => sandbox.restore())
// describe('#serializeUser', () => {
// it('should serialize a user', () => { describe('#passportCallback', () => {
// const user = { it('should return if user is found', () => {
// id: 'abc123' // Mock Users model.
// } sandbox.stub(User, 'findOne').resolves({ id })
// const done = () => {}
// passportCallback(id, 'password', done)
// passport.serializeUser(user, done) })
// })
// }) it('should return if password is validated', () => {
// // Mock Users model.
// describe('#deserializeUser', () => { sandbox.stub(User, 'findOne').resolves(new adaptersMock.localdb.Users())
// it('should deserialize a user', () => {
// // Mock Users model. passportCallback(id, 'password', done)
// sandbox.stub(User, 'findById').resolves({ id }) })
//
// passport.deserializeUser(id, done) it('should catch a high-level error', () => {
// }) // Force an error
// sandbox.stub(User, 'findOne').rejects(new Error('test error'))
// it('should catch and handle errors', () => {
// // Force an error passportCallback(id, 'password', done)
// sandbox.stub(User, 'findById').rejects(new Error('test error')) })
// })
// passport.deserializeUser(id, done)
// }) describe('#applyPassportMods', () => {
// }) it('should apply modifications to default passport behavior', () => {
// const result = applyPassportMods(passport)
// describe('#passportCallback', () => {
// it('should return if user is found', () => { assert.equal(result, true)
// // Mock Users model. })
// sandbox.stub(User, 'findOne').resolves({ id }) })
//
// passportCallback(id, 'password', done) describe('#serializeUser', () => {
// }) it('should serialize a user', () => {
// const user = {
// it('should return if password is validated', () => { id: 'abc123'
// // Mock Users model. }
// sandbox.stub(User, 'findOne').resolves(new adaptersMock.localdb.Users()) const done = () => {}
//
// passportCallback(id, 'password', done) applyPassportMods(passport)
// })
// passport.serializeUser(user, done)
// it('should catch a high-level error', () => { })
// // Force an error })
// sandbox.stub(User, 'findOne').rejects(new Error('test error'))
// describe('#deserializeUser', () => {
// passportCallback(id, 'password', done) it('should deserialize a user', () => {
// }) // Mock Users model.
// }) sandbox.stub(User, 'findById').resolves({ id })
// })
applyPassportMods(passport)
passport.deserializeUser(id, done)
})
it('should catch and handle errors', () => {
// Force an error
sandbox.stub(User, 'findById').rejects(new Error('test error'))
applyPassportMods(passport)
passport.deserializeUser(id, done)
})
})
})
-1
View File
@@ -4,7 +4,6 @@
// Public npm libraries // Public npm libraries
import { assert } from 'chai' import { assert } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
// Local libraries // Local libraries
+17 -17
View File
@@ -6,7 +6,6 @@
// Public npm libraries // Public npm libraries
import { assert } from 'chai' import { assert } from 'chai'
import sinon from 'sinon' import sinon from 'sinon'
// Local support libraries // Local support libraries
@@ -331,22 +330,23 @@ describe('#users-use-case', () => {
} }
}) })
// it('should update the user model', async () => { it('should update the user model', async () => {
// const newData = { const newData = {
// email: 'test@test.com', email: 'test@test.com',
// password: 'password', password: 'password',
// name: 'testy tester' name: 'testy tester'
// } }
// testUser.save = async () => {}
// const result = await uut.updateUser(testUser, newData)
// const result = await uut.updateUser(testUser, newData)
// // Assert that expected properties and values exist.
// assert.property(result, '_id') // Assert that expected properties and values exist.
// assert.property(result, 'email') assert.property(result, '_id')
// assert.equal(result.email, 'test@test.com') assert.property(result, 'email')
// assert.property(result, 'name') assert.equal(result.email, 'test@test.com')
// assert.equal(result.name, 'testy tester') assert.property(result, 'name')
// }) assert.equal(result.name, 'testy tester')
})
// TODO: verify that an admin can change the type of a user // TODO: verify that an admin can change the type of a user
}) })
+16 -7
View File
@@ -4,15 +4,21 @@
// Public NPM libraries // Public NPM libraries
import mongoose from 'mongoose' import mongoose from 'mongoose'
import axios from 'axios' import axios from 'axios'
// Local libraries // Local libraries
import config from '../../config/index.js' import config from '../../config/index.js'
import User from '../../src/adapters/localdb/models/users.js' import User from '../../src/adapters/localdb/models/users.js'
import JsonFiles from '../../src/adapters/json-files.js'
// Hack to get __dirname back.
// https://blog.logrocket.com/alternatives-dirname-node-js-es-modules/
import * as url from 'url'
const jsonFiles = new JsonFiles()
const LOCALHOST = `http://localhost:${config.port}` const LOCALHOST = `http://localhost:${config.port}`
const __dirname = url.fileURLToPath(new URL('.', import.meta.url))
// Remove all collections from the DB. // Remove all collections from the DB.
async function cleanDb () { async function cleanDb () {
@@ -112,9 +118,11 @@ async function loginTestUser () {
async function loginAdminUser () { async function loginAdminUser () {
try { try {
const FILENAME = `../../config/system-user-${config.env}.json` const FILENAME = `${__dirname.toString()}../../config/system-user-${config.env}.json`
const adminUserData = require(FILENAME) // console.log('FILENAME: ', FILENAME)
console.log(`adminUserData: ${JSON.stringify(adminUserData, null, 2)}`)
const adminUserData = await jsonFiles.readJSON(FILENAME)
// console.log(`adminUserData: ${JSON.stringify(adminUserData, null, 2)}`)
const options = { const options = {
method: 'POST', method: 'POST',
@@ -151,8 +159,9 @@ async function getAdminJWT () {
// process.env.KOA_ENV = process.env.KOA_ENV || 'dev' // process.env.KOA_ENV = process.env.KOA_ENV || 'dev'
// console.log(`env: ${process.env.KOA_ENV}`) // console.log(`env: ${process.env.KOA_ENV}`)
const FILENAME = `../../config/system-user-${config.env}.json` const FILENAME = `${__dirname.toString()}../../config/system-user-${config.env}.json`
const adminUserData = require(FILENAME) // console.log('FILENAME: ', FILENAME)
const adminUserData = await jsonFiles.readJSON(FILENAME)
// console.log(`adminUserData: ${JSON.stringify(adminUserData, null, 2)}`) // console.log(`adminUserData: ${JSON.stringify(adminUserData, null, 2)}`)
return adminUserData.token return adminUserData.token