diff --git a/.on-save.json b/.on-save.json new file mode 100644 index 0000000..506a153 --- /dev/null +++ b/.on-save.json @@ -0,0 +1,8 @@ +[ + { + "srcDir": "", + "destDir": "", + "files": "**/*.js", + "command": "npm run lint" + } +] diff --git a/bin/server.js b/bin/server.js index 1a7bbe3..535c238 100644 --- a/bin/server.js +++ b/bin/server.js @@ -27,7 +27,7 @@ import AdminLib from '../src/adapters/admin.js' import errorMiddleware from '../src/controllers/rest-api/middleware/error.js' import wlogger from '../src/adapters/wlogger.js' import Controllers from '../src/controllers/index.js' -import applyPassportMods from '../config/passport.js' +import { applyPassportMods } from '../config/passport.js' class Server { constructor () { diff --git a/config/env/common.js b/config/env/common.js index a015072..951f7d4 100644 --- a/config/env/common.js +++ b/config/env/common.js @@ -5,9 +5,14 @@ /* 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. 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 diff --git a/config/index.js b/config/index.js index b329fc2..25a7e5c 100644 --- a/config/index.js +++ b/config/index.js @@ -5,11 +5,12 @@ import production from './env/production.js' import test from './env/test.js' const env = process.env.SVC_ENV || 'development' +console.log(`Loading config for this environment: ${env}`) let config = development if (env === 'test') { config = test -} else if (env === 'production') { +} else if (env === 'prod') { config = production } diff --git a/config/passport.js b/config/passport.js index 6f766b7..f28c540 100644 --- a/config/passport.js +++ b/config/passport.js @@ -49,8 +49,11 @@ function applyPassportMods (passport) { passportCallback ) ) + + return true } // For testing // export default { passport, passportCallback }; -export default applyPassportMods +// export default applyPassportMods +export { applyPassportMods, passportCallback } diff --git a/package-lock.json b/package-lock.json index 4558ce0..b57711e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "axios": "0.27.2", "bcryptjs": "2.4.3", "glob": "7.1.6", - "ipfs-coord-esm": "9.1.2", + "ipfs-coord-esm": "../ipfs-coord-esm/", "ipfs-http-client": "58.0.0", "jsonrpc-lite": "2.2.0", "jsonwebtoken": "8.5.1", @@ -51,6 +51,28 @@ "uuid": "8.3.2" } }, + "../ipfs-coord-esm": { + "version": "9.0.0", + "license": "MIT", + "dependencies": { + "bch-encrypt-lib": "2.0.0", + "uuid": "9.0.0" + }, + "devDependencies": { + "@istanbuljs/esm-loader-hook": "0.2.0", + "chai": "4.3.6", + "cross-env": "7.0.3", + "lodash.clonedeep": "4.5.0", + "minimal-slp-wallet": "5.0.3", + "mocha": "10.0.0", + "nyc": "15.1.0", + "sinon": "14.0.0", + "standard": "17.0.0" + }, + "peerDependencies": { + "ipfs-http-client": ">= 58.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.18.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", @@ -1489,11 +1511,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/aes-js": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", - "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -2053,15 +2070,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/bch-encrypt-lib": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bch-encrypt-lib/-/bch-encrypt-lib-2.0.0.tgz", - "integrity": "sha512-EAfcw2vzcKMkdO004tfLmQEEpGZA9QeIgzG9sbxDweFiGr2BP6/06G5SIJdvsJhcAd9OS9Rw5bLlPAy25kX8HQ==", - "dependencies": { - "eccrypto-js": "^5.3.0", - "wif": "^2.0.6" - } - }, "node_modules/bchaddrjs-slp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/bchaddrjs-slp/-/bchaddrjs-slp-0.2.5.tgz", @@ -3550,28 +3558,6 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", "dev": true }, - "node_modules/eccrypto-js": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/eccrypto-js/-/eccrypto-js-5.4.0.tgz", - "integrity": "sha512-W4xBr0UANgpkSIu2FBHG5wjhvOR4L19HwAUm9xiA4c3bzB9gGbH8+E9hSMTNF+QbmEi+TDvt373JHKA6NnW38A==", - "dependencies": { - "aes-js": "3.1.2", - "enc-utils": "2.1.0", - "hash.js": "1.1.7", - "js-sha3": "0.8.0", - "pbkdf2": "^3.0.17", - "randombytes": "2.1.0", - "secp256k1": "3.8.0" - } - }, - "node_modules/eccrypto-js/node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -3648,16 +3634,6 @@ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, - "node_modules/enc-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/enc-utils/-/enc-utils-2.1.0.tgz", - "integrity": "sha512-VD0eunGDyzhojePzkORWDnW88gi6tIeGb5Z6QVHugux6mMAPiXyw94fb/7WdDQEWhKMSoYRyzFFUebCqeH20PA==", - "dependencies": { - "bn.js": "4.11.8", - "is-typedarray": "1.0.0", - "typedarray-to-buffer": "3.1.5" - } - }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -6072,24 +6048,8 @@ } }, "node_modules/ipfs-coord-esm": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/ipfs-coord-esm/-/ipfs-coord-esm-9.1.2.tgz", - "integrity": "sha512-tZZMtBS7TTatDJD2gWdu9EOPt5E2kxwSRr5GMeRKRPnP4z1Z4QTpP94nmDhcDDABpdD4IseVQjcwUED5fd+IgQ==", - "dependencies": { - "bch-encrypt-lib": "2.0.0", - "uuid": "9.0.0" - }, - "peerDependencies": { - "ipfs-http-client": ">= 58.0.0" - } - }, - "node_modules/ipfs-coord-esm/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } + "resolved": "../ipfs-coord-esm", + "link": true }, "node_modules/ipfs-http-client": { "version": "58.0.0", @@ -6645,7 +6605,8 @@ "node_modules/is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true }, "node_modules/is-unicode-supported": { "version": "0.1.0", @@ -6880,11 +6841,6 @@ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" }, - "node_modules/js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -14153,14 +14109,6 @@ "node": ">= 0.6" } }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, "node_modules/typeforce": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", @@ -16068,11 +16016,6 @@ "dev": true, "requires": {} }, - "aes-js": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-3.1.2.tgz", - "integrity": "sha512-e5pEa2kBnBOgR4Y/p20pskXI74UEz7de8ZGVo58asOtvSVG5YAbJeELPZxOmt+Bnz3rX753YKhfIn4X4l1PPRQ==" - }, "agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -16485,15 +16428,6 @@ } } }, - "bch-encrypt-lib": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/bch-encrypt-lib/-/bch-encrypt-lib-2.0.0.tgz", - "integrity": "sha512-EAfcw2vzcKMkdO004tfLmQEEpGZA9QeIgzG9sbxDweFiGr2BP6/06G5SIJdvsJhcAd9OS9Rw5bLlPAy25kX8HQ==", - "requires": { - "eccrypto-js": "^5.3.0", - "wif": "^2.0.6" - } - }, "bchaddrjs-slp": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/bchaddrjs-slp/-/bchaddrjs-slp-0.2.5.tgz", @@ -17669,30 +17603,6 @@ } } }, - "eccrypto-js": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/eccrypto-js/-/eccrypto-js-5.4.0.tgz", - "integrity": "sha512-W4xBr0UANgpkSIu2FBHG5wjhvOR4L19HwAUm9xiA4c3bzB9gGbH8+E9hSMTNF+QbmEi+TDvt373JHKA6NnW38A==", - "requires": { - "aes-js": "3.1.2", - "enc-utils": "2.1.0", - "hash.js": "1.1.7", - "js-sha3": "0.8.0", - "pbkdf2": "^3.0.17", - "randombytes": "2.1.0", - "secp256k1": "3.8.0" - }, - "dependencies": { - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "requires": { - "safe-buffer": "^5.1.0" - } - } - } - }, "ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -17765,16 +17675,6 @@ "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, - "enc-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/enc-utils/-/enc-utils-2.1.0.tgz", - "integrity": "sha512-VD0eunGDyzhojePzkORWDnW88gi6tIeGb5Z6QVHugux6mMAPiXyw94fb/7WdDQEWhKMSoYRyzFFUebCqeH20PA==", - "requires": { - "bn.js": "4.11.8", - "is-typedarray": "1.0.0", - "typedarray-to-buffer": "3.1.5" - } - }, "encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -19425,19 +19325,19 @@ "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==" }, "ipfs-coord-esm": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/ipfs-coord-esm/-/ipfs-coord-esm-9.1.2.tgz", - "integrity": "sha512-tZZMtBS7TTatDJD2gWdu9EOPt5E2kxwSRr5GMeRKRPnP4z1Z4QTpP94nmDhcDDABpdD4IseVQjcwUED5fd+IgQ==", + "version": "file:../ipfs-coord-esm", "requires": { + "@istanbuljs/esm-loader-hook": "0.2.0", "bch-encrypt-lib": "2.0.0", + "chai": "4.3.6", + "cross-env": "7.0.3", + "lodash.clonedeep": "4.5.0", + "minimal-slp-wallet": "5.0.3", + "mocha": "10.0.0", + "nyc": "15.1.0", + "sinon": "14.0.0", + "standard": "17.0.0", "uuid": "9.0.0" - }, - "dependencies": { - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - } } }, "ipfs-http-client": { @@ -19832,7 +19732,8 @@ "is-typedarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true }, "is-unicode-supported": { "version": "0.1.0", @@ -20021,11 +19922,6 @@ "resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.9.0.tgz", "integrity": "sha512-sga3MHh9sgQN2+pJ9VYZ+1LPwXOxuBJBA5nrR5/ofPfuiJBE2hnjsaN8se8JznOmGLN2p49Pe5U/ttafcs/apA==" }, - "js-sha3": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz", - "integrity": "sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==" - }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -25451,14 +25347,6 @@ "mime-types": "~2.1.24" } }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "requires": { - "is-typedarray": "^1.0.0" - } - }, "typeforce": { "version": "1.18.0", "resolved": "https://registry.npmjs.org/typeforce/-/typeforce-1.18.0.tgz", diff --git a/package.json b/package.json index d7892cc..85b2212 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "axios": "0.27.2", "bcryptjs": "2.4.3", "glob": "7.1.6", - "ipfs-coord-esm": "9.1.2", + "ipfs-coord-esm": "../ipfs-coord-esm/", "ipfs-http-client": "58.0.0", "jsonrpc-lite": "2.2.0", "jsonwebtoken": "8.5.1", diff --git a/src/adapters/admin.js b/src/adapters/admin.js index 6c74963..7804996 100644 --- a/src/adapters/admin.js +++ b/src/adapters/admin.js @@ -25,9 +25,9 @@ import JsonFiles from '../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 __dirname = url.fileURLToPath(new URL('.', import.meta.url)) const jsonFiles = new JsonFiles() -const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) const JSON_FILE = `system-user-${config.env}.json` const JSON_PATH = `${__dirname.toString()}/../../config/${JSON_FILE}` diff --git a/src/controllers/rest-api/middleware/validators.js b/src/controllers/rest-api/middleware/validators.js index 534c87e..8c3a88c 100644 --- a/src/controllers/rest-api/middleware/validators.js +++ b/src/controllers/rest-api/middleware/validators.js @@ -1,5 +1,17 @@ /* 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' @@ -21,12 +33,10 @@ class Validators { async ensureUser (ctx, next) { try { - // console.log(`getToken: ${typeof (getToken)}`) const token = _this.getToken(ctx) if (!token) { - // console.log(`Err: Token not provided.`) - ctx.throw(401) + throw new Error('Token could not be retrieved from header') } let decoded = null @@ -35,20 +45,23 @@ class Validators { // console.log(`config: ${JSON.stringify(config, null, 2)}`) decoded = _this.jwt.verify(token, config.token) } catch (err) { - // console.log(`Err: Token could not be decoded: ${err}`) - ctx.throw(401) + throw new Error('Could not verify JWT') } ctx.state.user = await _this.User.findById(decoded.id, '-password') + if (!ctx.state.user) { - // console.log(`Err: Could not find user.`) - ctx.throw(401) + // console.log('Err: Could not find user.') + throw new Error('Could not find user') } // return next() return true } 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) { // 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 @@ -71,22 +85,26 @@ class Validators { decoded = _this.jwt.verify(token, config.token) } catch (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') if (!ctx.state.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') { - ctx.throw(401, 'not admin') + // ctx.throw(401, 'not admin') + throw new Error('User is not an admin') } // return next() return true } catch (error) { + ctx.status = 401 ctx.throw(401, error.message) } } @@ -102,7 +120,8 @@ class Validators { if (!token) { // 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. @@ -115,14 +134,16 @@ class Validators { // console.log(`config: ${JSON.stringify(config, null, 2)}`) decoded = _this.jwt.verify(token, config.token) } catch (err) { - // console.log(`Err: Token could not be decoded: ${err}`) - ctx.throw(401) + console.log(`Err: Token could not be decoded: ${err}`) + // ctx.throw(401) + throw new Error('Could not verify JWT') } ctx.state.user = await _this.User.findById(decoded.id, '-password') if (!ctx.state.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) @@ -136,7 +157,8 @@ class Validators { // If they don't match, then the calling user better be an 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 { wlogger.verbose("It's ok. The user is an admin.") } @@ -145,6 +167,8 @@ class Validators { // return next() return true } catch (error) { + // console.log('Error in ensureTargetUserOrAdmin(): ', error) + ctx.status = 401 ctx.throw(401, error.message) } } diff --git a/src/controllers/rest-api/users/controller.js b/src/controllers/rest-api/users/controller.js index 93d4887..8c2548d 100644 --- a/src/controllers/rest-api/users/controller.js +++ b/src/controllers/rest-api/users/controller.js @@ -36,7 +36,7 @@ class UserRESTControllerLib { * @apiGroup REST Users * * @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 {String} user.email Email @@ -136,7 +136,7 @@ class UserRESTControllerLib { * @apiGroup REST Users * * @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 {ObjectId} users._id User id diff --git a/test/unit/controllers/rest-api/middleware/validators-unit.js b/test/unit/controllers/rest-api/middleware/validators-unit.js new file mode 100644 index 0000000..6a29c00 --- /dev/null +++ b/test/unit/controllers/rest-api/middleware/validators-unit.js @@ -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) + }) + }) +}) diff --git a/test/unit/controllers/rest-api/users/users.rest.controller.unit.js b/test/unit/controllers/rest-api/users/users.rest.controller.unit.js index abc60d0..45b1b81 100644 --- a/test/unit/controllers/rest-api/users/users.rest.controller.unit.js +++ b/test/unit/controllers/rest-api/users/users.rest.controller.unit.js @@ -4,12 +4,10 @@ // Public npm libraries import { assert } from 'chai' - import sinon from 'sinon' // Local support libraries import adapters from '../../../mocks/adapters/index.js' - import UseCasesMock from '../../../mocks/use-cases/index.js' import UserController from '../../../../../src/controllers/rest-api/users/controller.js' diff --git a/test/unit/misc/config.js b/test/unit/misc/config.js index 7c5c597..6a55b7e 100644 --- a/test/unit/misc/config.js +++ b/test/unit/misc/config.js @@ -2,6 +2,8 @@ Unit tests for the config directory */ +import { assert } from 'chai' + let currentEnv describe('#config', () => { @@ -15,8 +17,34 @@ describe('#config', () => { process.env.SVC_ENV = currentEnv }) - it('Should return development environment config', () => { - // import config from '../../../config/index.js' + 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') }) }) diff --git a/test/unit/misc/passport.unit.js b/test/unit/misc/passport.unit.js index 20841ab..6cd1705 100644 --- a/test/unit/misc/passport.unit.js +++ b/test/unit/misc/passport.unit.js @@ -1,78 +1,93 @@ -// /* -// Unit tests for the passport library. -// */ -// -// // Public npm libraries -// // const assert = require('chai').assert -// import sinon from 'sinon'; -// -// // 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'; -// -// describe('#passport', () => { -// let sandbox -// let id -// let done -// -// beforeEach(() => { -// sandbox = sinon.createSandbox() -// -// id = 'abc123' -// done = () => {} -// }) -// -// afterEach(() => sandbox.restore()) -// -// describe('#serializeUser', () => { -// it('should serialize a user', () => { -// const user = { -// id: 'abc123' -// } -// const done = () => {} -// -// passport.serializeUser(user, done) -// }) -// }) -// -// describe('#deserializeUser', () => { -// it('should deserialize a user', () => { -// // Mock Users model. -// sandbox.stub(User, 'findById').resolves({ id }) -// -// passport.deserializeUser(id, done) -// }) -// -// it('should catch and handle errors', () => { -// // Force an error -// sandbox.stub(User, 'findById').rejects(new Error('test error')) -// -// passport.deserializeUser(id, done) -// }) -// }) -// -// describe('#passportCallback', () => { -// it('should return if user is found', () => { -// // Mock Users model. -// sandbox.stub(User, 'findOne').resolves({ id }) -// -// passportCallback(id, 'password', done) -// }) -// -// it('should return if password is validated', () => { -// // Mock Users model. -// sandbox.stub(User, 'findOne').resolves(new adaptersMock.localdb.Users()) -// -// passportCallback(id, 'password', done) -// }) -// -// it('should catch a high-level error', () => { -// // Force an error -// sandbox.stub(User, 'findOne').rejects(new Error('test error')) -// -// passportCallback(id, 'password', done) -// }) -// }) -// }) +/* + Unit tests for the passport library. +*/ + +// Public npm libraries +import { assert } from 'chai' +import sinon from 'sinon' +import passport from 'koa-passport' + +// Local libraries +import User from '../../../src/adapters/localdb/models/users.js' + +import { applyPassportMods, passportCallback } from '../../../config/passport.js' +import adaptersMock from '../mocks/adapters/index.js' + +describe('#passport', () => { + let sandbox + let id + let done + + beforeEach(() => { + sandbox = sinon.createSandbox() + + id = 'abc123' + done = () => {} + }) + + afterEach(() => sandbox.restore()) + + describe('#passportCallback', () => { + it('should return if user is found', () => { + // Mock Users model. + sandbox.stub(User, 'findOne').resolves({ id }) + + passportCallback(id, 'password', done) + }) + + it('should return if password is validated', () => { + // Mock Users model. + sandbox.stub(User, 'findOne').resolves(new adaptersMock.localdb.Users()) + + passportCallback(id, 'password', done) + }) + + it('should catch a high-level error', () => { + // Force an error + sandbox.stub(User, 'findOne').rejects(new Error('test error')) + + passportCallback(id, 'password', done) + }) + }) + + describe('#applyPassportMods', () => { + it('should apply modifications to default passport behavior', () => { + const result = applyPassportMods(passport) + + assert.equal(result, true) + }) + }) + + describe('#serializeUser', () => { + it('should serialize a user', () => { + const user = { + id: 'abc123' + } + const done = () => {} + + applyPassportMods(passport) + + passport.serializeUser(user, done) + }) + }) + + describe('#deserializeUser', () => { + 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) + }) + }) +}) diff --git a/test/unit/misc/server-unit.js b/test/unit/misc/server-unit.js index 02dfc8d..da56b91 100644 --- a/test/unit/misc/server-unit.js +++ b/test/unit/misc/server-unit.js @@ -4,7 +4,6 @@ // Public npm libraries import { assert } from 'chai' - import sinon from 'sinon' // Local libraries