From dc462a3d5395e9c4e47488625da05ec9f239061e Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Sat, 3 Jul 2021 13:56:53 +0000 Subject: [PATCH] Huge update and cleanup on dependencies and test environment among other stuff --- .babelrc | 5 - .circleci/config.yml | 23 +- config.js => api/config.mjs | 25 +- api/defaults.js | 20 -- api/defaults.mjs | 34 ++ api/{error.js => error.mjs} | 0 api/jwt.js | 19 - api/jwt/base64urlsafe.mjs | 21 ++ api/jwt/decode.mjs | 169 +++++++++ api/jwt/encode.mjs | 43 +++ api/log.mjs | 29 ++ api/media/{multer.js => multer.mjs} | 5 +- api/media/{routes.js => routes.mjs} | 5 +- api/media/{security.js => security.mjs} | 8 +- api/{router.js => router.mjs} | 5 +- api/server.mjs | 18 + api/test/{routes.js => routes.mjs} | 6 +- index.js | 3 - package.json | 26 +- server.js | 15 - test/defaults.test.js | 32 -- test/defaults.test.mjs | 31 ++ test/error.test.js | 54 --- test/helper.client.js | 89 ----- test/helper.client.mjs | 123 +++++++ test/{helper.server.js => helper.server.mjs} | 12 +- test/jwt.test.js | 61 ---- test/jwt/decode.test.mjs | 72 ++++ test/jwt/encode.test.mjs | 21 ++ test/jwt/jwt.test.mjs | 343 +++++++++++++++++++ test/server.test.js | 22 -- test/server.test.mjs | 28 ++ 32 files changed, 984 insertions(+), 383 deletions(-) delete mode 100644 .babelrc rename config.js => api/config.mjs (62%) delete mode 100644 api/defaults.js create mode 100644 api/defaults.mjs rename api/{error.js => error.mjs} (100%) delete mode 100644 api/jwt.js create mode 100644 api/jwt/base64urlsafe.mjs create mode 100644 api/jwt/decode.mjs create mode 100644 api/jwt/encode.mjs create mode 100644 api/log.mjs rename api/media/{multer.js => multer.mjs} (93%) rename api/media/{routes.js => routes.mjs} (63%) rename api/media/{security.js => security.mjs} (72%) rename api/{router.js => router.mjs} (55%) create mode 100644 api/server.mjs rename api/test/{routes.js => routes.mjs} (59%) delete mode 100644 index.js delete mode 100644 server.js delete mode 100644 test/defaults.test.js create mode 100644 test/defaults.test.mjs delete mode 100644 test/error.test.js delete mode 100644 test/helper.client.js create mode 100644 test/helper.client.mjs rename test/{helper.server.js => helper.server.mjs} (50%) delete mode 100644 test/jwt.test.js create mode 100644 test/jwt/decode.test.mjs create mode 100644 test/jwt/encode.test.mjs create mode 100644 test/jwt/jwt.test.mjs delete mode 100644 test/server.test.js create mode 100644 test/server.test.mjs diff --git a/.babelrc b/.babelrc deleted file mode 100644 index f7a8b05..0000000 --- a/.babelrc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "plugins": [ - "transform-es2015-modules-commonjs" - ] -} diff --git a/.circleci/config.yml b/.circleci/config.yml index c14c849..c7e265c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,31 +2,26 @@ version: 2 jobs: build: docker: - - image: docker:latest + - image: circleci/node:latest environment: - di: "nfpis/storage-upload" - dtag: "latest" working_directory: ~/storage-upload steps: - - run: - name: Update and install SSH & Git - command: apk update && apk upgrade && apk add --no-cache bash git openssh - checkout - - setup_remote_docker - run: - name: Build docker image + name: Install dependencies command: | - docker build -t nfp-test . - docker build --build-arg NODE=production -t ${di}:build_${CIRCLE_BUILD_NUM} -t ${di}:${CIRCLE_SHA1} -t ${di}:${dtag} . + npm install - run: name: Run tests command: | - docker run nfp-test npm run test - - deploy: - name: Push to docker - command: | - docker login -u $DOCKER_USER -p $DOCKER_PASS - docker push ${di} + npm test +# - deploy: +# name: Push to docker +# command: | +# docker login -u $DOCKER_USER -p $DOCKER_PASS +# docker push ${di} workflows: version: 2 diff --git a/config.js b/api/config.mjs similarity index 62% rename from config.js rename to api/config.mjs index 8e4bcbe..c659397 100644 --- a/config.js +++ b/api/config.mjs @@ -1,6 +1,12 @@ -'use strict' +import fs from 'fs' +import path from 'path' +import { fileURLToPath } from 'url' +import Nconf from 'nconf-lite' + +const nconf = new Nconf() +const __dirname = path.dirname(fileURLToPath(import.meta.url)) +let pckg = JSON.parse(fs.readFileSync(path.resolve(path.join(__dirname, '../package.json')))) -const nconf = require('nconf') // Helper method for global usage. nconf.inTest = () => nconf.get('NODE_ENV') === 'test' @@ -12,9 +18,6 @@ nconf.inTest = () => nconf.get('NODE_ENV') === 'test' // 4. config/config.default.json -// Load package.json for name and such -let pckg = require('./package.json') - pckg = { name: pckg.name, version: pckg.version, @@ -34,22 +37,22 @@ nconf.env() // Load any overrides from the appropriate config file -let configFile = 'config/config.json' +let configFile = '../config/config.json' /* istanbul ignore else */ if (nconf.get('NODE_ENV') === 'test') { - configFile = 'config/config.test.json' + configFile = '../config/config.test.json' } /* istanbul ignore if */ if (nconf.get('NODE_ENV') === 'production') { - configFile = 'config/config.production.json' + configFile = '../config/config.production.json' } -nconf.file('main', configFile) +nconf.file('main', path.resolve(path.join(__dirname, configFile))) // Load defaults -nconf.file('default', 'config/config.default.json') +nconf.file('default', path.resolve(path.join(__dirname, '../config/config.default.json'))) // Final sanity checks @@ -61,4 +64,4 @@ if (typeof global.it === 'function' & !nconf.inTest()) { } -module.exports = nconf +export default nconf diff --git a/api/defaults.js b/api/defaults.js deleted file mode 100644 index c807b6c..0000000 --- a/api/defaults.js +++ /dev/null @@ -1,20 +0,0 @@ - -function defaults(options, def) { - options = options || {} - - Object.keys(def).forEach(function(key) { - if (typeof options[key] === 'undefined') { - // No need to do clone since we mostly deal with - // flat objects - options[key] = def[key] - } - else if (typeof options[key] === 'object' && - typeof def[key] === 'object') { - options[key] = defaults(options[key], def[key]) - } - }) - - return options -} - -export default defaults diff --git a/api/defaults.mjs b/api/defaults.mjs new file mode 100644 index 0000000..a2ef666 --- /dev/null +++ b/api/defaults.mjs @@ -0,0 +1,34 @@ + +// taken from isobject npm library +function isObject(val) { + return val != null && typeof val === 'object' && Array.isArray(val) === false +} + +export default function defaults(options, def) { + let out = { } + + if (options) { + Object.keys(options || {}).forEach(key => { + out[key] = options[key] + + if (Array.isArray(out[key])) { + out[key] = out[key].map(item => { + if (isObject(item)) return defaults(item) + return item + }) + } else if (out[key] && typeof out[key] === 'object') { + out[key] = defaults(options[key], def && def[key]) + } + }) + } + + if (def) { + Object.keys(def).forEach(function(key) { + if (typeof out[key] === 'undefined') { + out[key] = def[key] + } + }) + } + + return out +} diff --git a/api/error.js b/api/error.mjs similarity index 100% rename from api/error.js rename to api/error.mjs diff --git a/api/jwt.js b/api/jwt.js deleted file mode 100644 index 3857021..0000000 --- a/api/jwt.js +++ /dev/null @@ -1,19 +0,0 @@ -import jwt from 'jsonwebtoken' - -export function sign(value, secret) { - return jwt.sign(value, secret) -} - -export function verify(token, secret) { - return new Promise((resolve, reject) => - jwt.verify(token, secret, (err, res) => { - if (err) return reject(err) - - resolve(res) - }) - ) -} - -export function decode(token) { - return jwt.decode(token) -} diff --git a/api/jwt/base64urlsafe.mjs b/api/jwt/base64urlsafe.mjs new file mode 100644 index 0000000..e770c70 --- /dev/null +++ b/api/jwt/base64urlsafe.mjs @@ -0,0 +1,21 @@ + +export function encode(buffer) { + return buffer + .toString('base64') + .replace(/\+/g, '-') // Convert '+' to '-' + .replace(/\//g, '_') // Convert '/' to '_' + .replace(/=+$/, '') // Remove ending '=' +} + +export function decode(base64StringUrlSafe) { + let base64String = base64StringUrlSafe.replace(/-/g, '+').replace(/_/g, '/') + switch (base64String.length % 4) { + case 2: + base64String += '==' + break + case 3: + base64String += '=' + break + } + return Buffer.from(base64String, 'base64') +} diff --git a/api/jwt/decode.mjs b/api/jwt/decode.mjs new file mode 100644 index 0000000..716f49a --- /dev/null +++ b/api/jwt/decode.mjs @@ -0,0 +1,169 @@ +import crypto from 'crypto' +import defaults from '../defaults.mjs' +import * as base64UrlSafe from './base64urlsafe.mjs' + +const defaultOptions = { + expiresSkew: 0, + expiresMax: 0, + nbfIatSkew: 300, + fixup: null +} + +/** + * + * @param {string} jwt + * @param {Object} publicKeys + * @param {Array} audiences + * @param {Object} [options] + * @param {Object} [options.expiresSkew=0] + * @param {Object} [options.expiresMax=0] + * @param {Object} [options.nbfIatSkew=300] + * @param {Function} [options.fixup] + */ + export default function decode(jwt, publicKeys, audiences, options = defaultOptions) { + if (typeof jwt !== 'string') { + throw new Error('jwt needs to a string') + } + + if (typeof publicKeys !== 'object' || Array.isArray(publicKeys)) { + throw new Error( + 'publicKeys needs to be a map of { issuer: { keyid: "PEM encoded key" }' + ) + } + + if (!Array.isArray(audiences)) { + throw new Error('audiences needs to be an array of allowed audiences') + } + + if (typeof options !== 'object' || Array.isArray(publicKeys)) { + throw new Error('options needs to a map of { nbfIatSkew: 300, ... }') + } + + let parts = jwt.split(/\./) + if (parts.length !== 3) { + throw new Error('JWT does not contain 3 dots') + } + + let header = JSON.parse(base64UrlSafe.decode(parts[0]).toString('utf8')) + let body = JSON.parse(base64UrlSafe.decode(parts[1]).toString('utf8')) + if (typeof options.fixup === 'function') { + options.fixup(header, body) + } + + let hmacAlgo = null + switch (header.alg) { + case 'HS256': + hmacAlgo = 'sha256' + break + case 'HS384': + hmacAlgo = 'sha384' + break + case 'HS512': + hmacAlgo = 'sha512' + break + default: + throw new Error( + 'Only alg HS256, HS384 and HS512 are supported' + ) + } + + if (!body.iss) { + throw new Error('No issuer set') + } + + let issuer = publicKeys[body.iss] + if (!issuer) { + throw new Error(`Unknown issuer '${body.iss}'`) + } + + // Find public key + let pubkeyOrSharedKey = + typeof header.kid === 'string' + ? issuer[`${header.kid}@${header.alg}`] + : issuer[`default@${header.alg}`] + let overrideOptions = {} + + if (!pubkeyOrSharedKey) { + throw new Error( + `Unknown pubkey id '${header.kid}' for this issuer` + ) + } else if ((typeof(pubkeyOrSharedKey) !== 'object' || Array.isArray(pubkeyOrSharedKey)) && typeof(pubkeyOrSharedKey) !== 'string') { + throw new Error( + `Pubkey of '${header.kid || 'default'}' for '${header.alg}' for this issuer is misconfigured` + ) + } + if (typeof(pubkeyOrSharedKey) === 'object') { + if (typeof(pubkeyOrSharedKey.key) !== 'string') { + throw new Error( + `Pubkey of '${header.kid || 'default'}' for '${header.alg}' for this issuer is misconfigured` + ) + } + overrideOptions = pubkeyOrSharedKey + pubkeyOrSharedKey = pubkeyOrSharedKey.key + } + + let signatureOrHash = base64UrlSafe.decode(parts[2]) + const hmac = crypto.createHmac(hmacAlgo, pubkeyOrSharedKey) + hmac.update(`${parts[0]}.${parts[1]}`, 'utf8') + let signatureBuffer = hmac.digest() + + if (signatureOrHash.length !== signatureBuffer.length || !crypto.timingSafeEqual(signatureOrHash, signatureBuffer)) { + throw new Error(`Verification failed with alg '${header.alg}'`) + } + + let unixNow = Math.floor(Date.now() / 1000) + + let validators = defaults(options.validators, { + aud: validateAudience, + exp: validateExpires, + iat: validateIssuedAt, + nbf: validateNotBefore + }) + + let validationOptions = defaults(overrideOptions, options) + + validators.aud(body, audiences, validationOptions) + validators.iat(body, unixNow, validationOptions) + validators.nbf(body, unixNow, validationOptions) + validators.exp(body, unixNow, validationOptions) + + return body +} + +function validateNotBefore(body, unixNow, options) { + if (body.nbf && body.nbf > unixNow + options.nbfIatSkew) { + throw new Error( + `Not before in the future by more than ${options.nbfIatSkew} seconds` + ) + } +} + +function validateIssuedAt(body, unixNow, options) { + if (body.iat && body.iat > unixNow + options.nbfIatSkew) { + throw new Error( + `Issued at in the future by more than ${options.nbfIatSkew} seconds` + ) + } +} + +function validateAudience(body, audiences, options) { + let auds = Array.isArray(body.aud) ? body.aud : [body.aud] + if (!auds.some(aud => audiences.includes(aud))) { + throw new Error(`Unknown audience '${auds.join(',')}'`) + } +} + +function validateExpires(body, unixNow, options) { + if (!body.exp) { + throw new Error(`No expires set on token`) + } + let notBefore = body.iat || body.nbf || unixNow + if (options.expiresMax && body.exp > notBefore + options.expiresMax) { + throw new Error( + `Expires in the future by more than ${options.expiresMax} seconds` + ) + } + if (body.exp + (options.expiresSkew || 0) <= unixNow) { + throw new Error('Token has expired') + } +} diff --git a/api/jwt/encode.mjs b/api/jwt/encode.mjs new file mode 100644 index 0000000..e45063d --- /dev/null +++ b/api/jwt/encode.mjs @@ -0,0 +1,43 @@ +import crypto from 'crypto' +import * as base64UrlSafe from './base64urlsafe.mjs' + +export default function encode(header, body, privateKeyPassword = null) { + if ( + typeof header !== 'object' || + Array.isArray(header) || + typeof body !== 'object' || + Array.isArray(body) + ) { + throw new Error('both header and body should be of type object') + } + + let hmacAlgo = null + switch (header.alg) { + case 'HS256': + hmacAlgo = 'sha256' + break + case 'HS384': + hmacAlgo = 'sha384' + break + case 'HS512': + hmacAlgo = 'sha512' + break + default: + throw new Error( + 'Only alg HS256, HS384 and HS512 are supported' + ) + } + + // Base64 encode header and body + let headerBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(header))) + let bodyBase64 = base64UrlSafe.encode(Buffer.from(JSON.stringify(body))) + let headerBodyBase64 = headerBase64 + '.' + bodyBase64 + + const hmac = crypto.createHmac(hmacAlgo, privateKeyPassword) + hmac.update(headerBodyBase64) + let signatureBuffer = hmac.digest() + + // Construct final JWT + let signatureBase64 = base64UrlSafe.encode(signatureBuffer) + return headerBodyBase64 + '.' + signatureBase64 +} \ No newline at end of file diff --git a/api/log.mjs b/api/log.mjs new file mode 100644 index 0000000..9fbfab9 --- /dev/null +++ b/api/log.mjs @@ -0,0 +1,29 @@ +import bunyan from 'bunyan-lite' +import config from './config.mjs' +import * as defaults from './defaults.mjs' + + +// Clone the settings as we will be touching +// on them slightly. +let settings = defaults.default(config.get('bunyan')) + +// Replace any instance of 'process.stdout' with the +// actual reference to the process.stdout. +for (let i = 0; i < settings.streams.length; i++) { + /* istanbul ignore else */ + if (settings.streams[i].stream === 'process.stdout') { + settings.streams[i].stream = process.stdout + } +} + +// Create our logger. +const log = bunyan.createLogger(settings) + +export default log + +log.logMiddleware = () => + (ctx, next) => { + ctx.log = log + + return next() + } diff --git a/api/media/multer.js b/api/media/multer.mjs similarity index 93% rename from api/media/multer.js rename to api/media/multer.mjs index e97c510..1281d19 100644 --- a/api/media/multer.js +++ b/api/media/multer.mjs @@ -1,7 +1,6 @@ -const fs = require('fs') import multer from 'multer' -const storage = multer.diskStorage({ +/*const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, '/tmp/my-uploads') }, @@ -9,7 +8,7 @@ const storage = multer.diskStorage({ console.log(file) cb(null, file.fieldname + '-' + Date.now()) } -}) +})*/ export function uploadFile(ctx, siteName) { return new Promise((res, rej) => { diff --git a/api/media/routes.js b/api/media/routes.mjs similarity index 63% rename from api/media/routes.js rename to api/media/routes.mjs index 0091ab1..ad7d979 100644 --- a/api/media/routes.js +++ b/api/media/routes.mjs @@ -1,6 +1,5 @@ -import config from '../../config' -import { verifyToken } from './security' -import { uploadFile, rename } from './multer' +import { verifyToken } from './security.mjs' +import { uploadFile } from './multer.mjs' export async function upload(ctx) { let site = await verifyToken(ctx) diff --git a/api/media/security.js b/api/media/security.mjs similarity index 72% rename from api/media/security.js rename to api/media/security.mjs index 1a8f4d3..8344557 100644 --- a/api/media/security.js +++ b/api/media/security.mjs @@ -1,12 +1,12 @@ -import * as jwt from '../jwt' -import config from '../../config' +// import * as jwt from '../jwt.mjs' +import config from '../config.mjs' export async function verifyToken(ctx) { if (!ctx.query.token) { throw new Error('Token is missing in query') } - let decoded = jwt.decode(ctx.query.token) + /*let decoded = jwt.decode(ctx.query.token) if (!decoded || !decoded.site) { throw new Error('Token is invalid') @@ -15,7 +15,7 @@ export async function verifyToken(ctx) { let output = await jwt.verify( ctx.query.token, config.get(`sites:${decoded.site}`) - ) + )*/ return output.site } diff --git a/api/router.js b/api/router.mjs similarity index 55% rename from api/router.js rename to api/router.mjs index b07f5f3..8e91bb6 100644 --- a/api/router.js +++ b/api/router.mjs @@ -1,11 +1,12 @@ import Router from 'koa-router' -import * as test from './test/routes' -import * as media from './media/routes' +import * as test from './test/routes.mjs' +import * as media from './media/routes.mjs' const router = new Router() router.get('/', test.testStatic) +router.get('/error', test.testError) router.post('/media', media.upload) export default router diff --git a/api/server.mjs b/api/server.mjs new file mode 100644 index 0000000..d258ad5 --- /dev/null +++ b/api/server.mjs @@ -0,0 +1,18 @@ +import Koa from 'koa-lite' + +import config from './config.mjs' +import log from './log.mjs' +import router from './router.mjs' +import { errorMiddleware } from './error.mjs' + +const app = new Koa() + +app.use(errorMiddleware) +app.use(router.routes()) +app.use(router.allowedMethods()) + +const server = app.listen(config.get('server:port'), function(a,b) { + log.info(`Server listening at ${config.get('server:port')}`) +}) + +export default server diff --git a/api/test/routes.js b/api/test/routes.mjs similarity index 59% rename from api/test/routes.js rename to api/test/routes.mjs index 4d88f9c..bdd6b86 100644 --- a/api/test/routes.js +++ b/api/test/routes.mjs @@ -1,4 +1,4 @@ -import config from '../../config' +import config from '../config.mjs' export async function testStatic(ctx) { ctx.body = { @@ -7,3 +7,7 @@ export async function testStatic(ctx) { environment: config.get('NODE_ENV'), } } + +export async function testError(ctx) { + throw new Error('This is a test') +} diff --git a/index.js b/index.js deleted file mode 100644 index 9aaf12f..0000000 --- a/index.js +++ /dev/null @@ -1,3 +0,0 @@ -require('babel-register') - -require('./server') diff --git a/package.json b/package.json index 449e454..aa4e28b 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,8 @@ "main": "index.js", "scripts": { "dev": "nodemon index.js", - "start": "node index.js", - "test": "env NODE_ENV=test mocha --require babel-register --recursive --reporter dot", - "docker": "docker run -it --rm --name my-running-script -v \"$PWD\":/usr/src/app -w /usr/src/app node:alpine", - "install:test": "npm install --no-optional && npm run test", - "install:dev": "npm install --no-optional && npm run dev", - "docker:test": "npm run docker -- npm run install:test", - "docker:dev": "npm run docker -- npm run install:dev" + "start": "node --experimental-modules api/server.mjs", + "test": "eltro test/**/*.test.mjs -r dot" }, "repository": { "type": "git", @@ -24,20 +19,13 @@ }, "homepage": "https://github.com/nfp-projects/storage-upload#readme", "dependencies": { - "babel-plugin-transform-es2015-modules-commonjs": "^6.26.0", - "babel-register": "^6.26.0", - "koa": "^2.3.0", - "multer": "^1.3.0", + "bunyan-lite": "^1.1.1", + "koa-lite": "^2.10.1", "koa-router": "^7.2.1", - "jsonwebtoken": "^8.1.0", - "nconf": "^0.8.5" + "multer": "^1.3.0", + "nconf-lite": "^2.0.0" }, "devDependencies": { - "app-root-path": "^2.0.1", - "assert-extended": "^1.0.1", - "mocha": "^4.0.1", - "nodemon": "^1.12.1", - "request-json": "^0.6.2", - "sinon": "^4.1.3" + "eltro": "^1.1.0" } } diff --git a/server.js b/server.js deleted file mode 100644 index c61ca86..0000000 --- a/server.js +++ /dev/null @@ -1,15 +0,0 @@ -import Koa from 'koa' - -import config from './config' -import router from './api/router' -import { errorMiddleware } from './api/error' - -const app = new Koa() - -app.use(errorMiddleware) -app.use(router.routes()) -app.use(router.allowedMethods()) - -const server = app.listen(config.get('server:port')) - -export default server diff --git a/test/defaults.test.js b/test/defaults.test.js deleted file mode 100644 index b4ac13c..0000000 --- a/test/defaults.test.js +++ /dev/null @@ -1,32 +0,0 @@ -import assert from 'assert-extended' -import sinon from 'sinon' - -describe('defaults', () => { - const defaults = require('../api/defaults').default - - describe('#defaults()', () => { - it('should apply defaults to flat objects', () => { - let assertOutput = { a: 1 } - let output = defaults(null, { a: 1 }) - - assert.deepEqual(output, assertOutput) - }) - - it('should allow overriding defult properties', () => { - let assertOutput = { a: 2 } - let output = defaults(assertOutput, { a: 1 }) - - assert.deepEqual(output, assertOutput) - }) - - it('should allow nesting through objects', () => { - let def = { a: { b: 2 } } - let output = defaults({ a: { c: 3} }, def) - - assert.deepEqual(output.a, { - b: 2, - c: 3, - }) - }) - }) -}) diff --git a/test/defaults.test.mjs b/test/defaults.test.mjs new file mode 100644 index 0000000..e76c07e --- /dev/null +++ b/test/defaults.test.mjs @@ -0,0 +1,31 @@ +import { Eltro as t, assert} from 'eltro' +import defaults from '../api/defaults.mjs' + +t.describe('#defaults()', () => { + t.test('should apply defaults to flat objects', () => { + let assertOutput = { a: 1 } + let output = defaults(null, { a: 1 }) + + assert.deepStrictEqual(output, assertOutput) + output = defaults({ a: 1 }) + + assert.deepStrictEqual(output, assertOutput) + }) + + t.test('should allow overriding defult properties', () => { + let assertOutput = { a: 2 } + let output = defaults(assertOutput, { a: 1 }) + + assert.deepStrictEqual(output, assertOutput) + }) + + t.test('should allow nesting through objects', () => { + let def = { a: { b: 2 } } + let output = defaults({ a: { c: 3} }, def) + + assert.deepStrictEqual(output.a, { + b: 2, + c: 3, + }) + }) +}) diff --git a/test/error.test.js b/test/error.test.js deleted file mode 100644 index 906f73d..0000000 --- a/test/error.test.js +++ /dev/null @@ -1,54 +0,0 @@ -import assert from 'assert-extended' -import sinon from 'sinon' - -import { createContext } from './helper.server' - -describe('Error (Middleware)', () => { - const error = require('../api/error') - - let ctx - - beforeEach(() => { - ctx = createContext({ }) - }) - - describe('#errorMiddleware()', () => { - let stub - - beforeEach(() => { - stub = sinon.stub() - }) - - it('should call next and not do anything if success', async () => { - await error.errorMiddleware(ctx, stub) - - assert.ok(stub.called) - assert.strictEqual(ctx.body, undefined) - assert.strictEqual(ctx.status, undefined) - }) - - it('should support stub throwing', async () => { - let assertError = new Error('testetytest') - stub.throws(assertError) - - await error.errorMiddleware(ctx, stub) - - assert.ok(ctx.body) - assert.strictEqual(ctx.status, 422) - assert.strictEqual(ctx.body.status, 422) - assert.strictEqual(ctx.body.message, assertError.message) - }) - - it('should support stub resolving false', async () => { - let assertError = new Error('herpaderpderp') - stub.rejects(assertError) - - await error.errorMiddleware(ctx, stub) - - assert.ok(ctx.body) - assert.strictEqual(ctx.status, 422) - assert.strictEqual(ctx.body.status, 422) - assert.strictEqual(ctx.body.message, assertError.message) - }) - }) -}) diff --git a/test/helper.client.js b/test/helper.client.js deleted file mode 100644 index a87a945..0000000 --- a/test/helper.client.js +++ /dev/null @@ -1,89 +0,0 @@ -import request from 'request-json' - -import defaults from '../api/defaults' -import config from '../config' - -function parseBody(body, reject) { - try { - return JSON.parse(body) - } catch (error) { - // eslint-disable-next-line no-console - console.log(body) - return reject(error) - } -} - -function callback(resolve, reject) { - return (err, res, rawBody) => { - let body = rawBody - if (err) { - return reject(err) - } - if (typeof body === 'string' && body) { - body = parseBody(body, reject) - } - if (res.statusCode >= 300 || - res.statusCode < 200) { - return reject(body) - } - resolve(body) - } -} - -export default function createClient(host = config.get('server:port'), opts) { - let options = defaults(opts, {}) - - let client = request.createClient('', options) - let prefix - - prefix = `http://localhost:${host}` - client.headers['x-request-id'] = 'asdf' - - client.auth = (user) => { - // let m = helperDB.model('user', { - // id: user.id, - // level: (user.get && user.get('level')) || 1, - // institute_id: (user.get && user.get('institute_id')) || null, - // password: (user.get && user.get('password')) || null, - // }) - // let token = jwt.createUserToken(m) - // client.headers.authorization = `Bearer ${token}` - } - - // Simple wrappers to wrap into promises - client.getAsync = (path) => - new Promise((resolve, reject) => { - if (path.slice(0, 4) === 'http') { - return client.get(path, callback(resolve, reject)) - } - client.get(prefix + path, callback(resolve, reject)) - }) - - // Simple wrappers to wrap into promises - client.saveFileAsync = (path, destination) => - new Promise((resolve, reject) => { - client.saveFile(prefix + path, destination, callback(resolve, reject, true)) - }) - - client.postAsync = (path, data) => - new Promise((resolve, reject) => { - client.post(prefix + path, data, callback(resolve, reject)) - }) - - client.putAsync = (path, data) => - new Promise((resolve, reject) => { - client.put(prefix + path, data, callback(resolve, reject)) - }) - - client.deleteAsync = (path) => - new Promise((resolve, reject) => { - client.del(prefix + path, callback(resolve, reject)) - }) - - client.sendFileAsync = (path, files, data) => - new Promise((resolve, reject) => { - client.sendFile(prefix + path, files, data || {}, callback(resolve, reject)) - }) - - return client -} diff --git a/test/helper.client.mjs b/test/helper.client.mjs new file mode 100644 index 0000000..74a7cb5 --- /dev/null +++ b/test/helper.client.mjs @@ -0,0 +1,123 @@ +import http from 'http' +import { URL } from 'url' +import defaults from '../api/defaults.mjs' +import config from '../api/config.mjs' + +export default function Client(port = config.get('server:port'), opts) { + this.options = defaults(opts, {}) + this.prefix = `http://localhost:${port}` +} + +Client.prototype.customRequest = function(method = 'GET', path, body, options) { + if (path.slice(0, 4) !== 'http') { + path = this.prefix + path + } + let urlObj = new URL(path) + + return new Promise((resolve, reject) => { + const opts = defaults(defaults(options, { + method: method, + timeout: 500, + protocol: urlObj.protocol, + username: urlObj.username, + password: urlObj.password, + host: urlObj.hostname, + port: Number(urlObj.port), + path: urlObj.pathname + urlObj.search, + })) + + const req = http.request(opts) + if (body) { + req.write(body) + } + + req.on('error', reject) + req.on('timeout', function() { reject(new Error(`Request ${method} ${path} timed out`)) }) + req.on('response', res => { + res.setEncoding('utf8') + let output = '' + + res.on('data', function (chunk) { + output += chunk.toString() + }) + + res.on('end', function () { + try { + output = JSON.parse(output) + } catch (e) { + return reject(new Error(`${e.message} while decoding: ${output}`)) + } + if (output.status) { + let err = new Error(`Request failed [${output.status}]: ${output.message}`) + err.body = output + return reject(err) + } + resolve(output) + }) + }) + req.end() + }) +} + +Client.prototype.get = function(path = '/') { + return this.customRequest('GET', path, null) +} + +/* + +export function createClient(host = config.get('server:port'), opts) { + let options = defaults(opts, {}) + + let prefix = `http://localhost:${host}` + options.headers['x-request-id'] = 'asdf' + + client.auth = (user) => { + // let m = helperDB.model('user', { + // id: user.id, + // level: (user.get && user.get('level')) || 1, + // institute_id: (user.get && user.get('institute_id')) || null, + // password: (user.get && user.get('password')) || null, + // }) + // let token = jwt.createUserToken(m) + // client.headers.authorization = `Bearer ${token}` + } + + // Simple wrappers to wrap into promises + client.getAsync = (path) => + new Promise((resolve, reject) => { + if (path.slice(0, 4) === 'http') { + return client.get(path, callback(resolve, reject)) + } + client.get(prefix + path, callback(resolve, reject)) + }) + + // Simple wrappers to wrap into promises + client.saveFileAsync = (path, destination) => + new Promise((resolve, reject) => { + client.saveFile(prefix + path, destination, callback(resolve, reject, true)) + }) + + client.postAsync = (path, data) => + new Promise((resolve, reject) => { + client.post(prefix + path, data, callback(resolve, reject)) + }) + + client.putAsync = (path, data) => + new Promise((resolve, reject) => { + client.put(prefix + path, data, callback(resolve, reject)) + }) + + client.deleteAsync = (path) => + new Promise((resolve, reject) => { + client.del(prefix + path, callback(resolve, reject)) + }) + + client.sendFileAsync = (path, files, data) => + new Promise((resolve, reject) => { + client.sendFile(prefix + path, files, data || {}, callback(resolve, reject)) + }) + + return client +} + +*/ diff --git a/test/helper.server.js b/test/helper.server.mjs similarity index 50% rename from test/helper.server.js rename to test/helper.server.mjs index 9dd3098..9147525 100644 --- a/test/helper.server.js +++ b/test/helper.server.mjs @@ -1,12 +1,12 @@ // import _ from 'lodash' // import sinon from 'sinon' -import server from '../server' -import client from './helper.client' -import defaults from '../api/defaults' +import Client from './helper.client.mjs' +import defaults from '../api/defaults.mjs' +import '../api/server.mjs' -after(() => server.close()) - -export const createClient = client +export function createClient() { + return new Client() +} export function createContext(opts) { return defaults(opts, { diff --git a/test/jwt.test.js b/test/jwt.test.js deleted file mode 100644 index eebc9db..0000000 --- a/test/jwt.test.js +++ /dev/null @@ -1,61 +0,0 @@ -import assert from 'assert-extended' -import sinon from 'sinon' - -describe('jwt', () => { - const jsonwebtoken = require('jsonwebtoken') - const jwt = require('../api/jwt') - - describe('#sign', () => { - it('should call security correctly', () => { - let token = jwt.sign({ a: 1 }, 'asdf') - assert.ok(token) - - let decoded = jsonwebtoken.decode(token) - assert.strictEqual(decoded.a, 1) - }) - - it('should support custom secret', done => { - const assertSecret = 'sdfagsda' - let token = jwt.sign({ a: 1 }, assertSecret) - - jsonwebtoken.verify(token, assertSecret, done) - }) - }) - - describe('#decode()', () => { - it('should decode correctly', () => { - let data = { a: 1, b: 2 } - let token = jwt.sign(data, 'asdf') - let decoded = jwt.decode(token) - - assert.strictEqual(decoded.a, data.a) - assert.strictEqual(decoded.b, data.b) - }) - }) - - describe('#verify', () => { - it('should verify correctly', () => { - const assertSecret = 'asdfasdf' - const assertResult = 23532 - let token = jwt.sign({ a: assertResult }, assertSecret) - - return assert.isFulfilled(jwt.verify(token, assertSecret)) - .then(data => assert.strictEqual(data.a, assertResult)) - }) - - it('should fail if secret does not match', () => { - const assertSecret = 'asdfasdf' - let token = jwt.sign({ a: 1 }, assertSecret) - - return assert.isRejected(jwt.verify(token, assertSecret + 'a')) - .then(err => assert.match(err.message, /[Ss]ignature/)) - }) - - it('should fail token has been mishandled', () => { - let token = jwt.sign({ a: 1 }, 'asdf') - - return assert.isRejected(jwt.verify(token + 'a', 'asdf')) - .then(err => assert.match(err.message, /[Ss]ignature/)) - }) - }) -}) diff --git a/test/jwt/decode.test.mjs b/test/jwt/decode.test.mjs new file mode 100644 index 0000000..d6225fe --- /dev/null +++ b/test/jwt/decode.test.mjs @@ -0,0 +1,72 @@ +import { Eltro as t, assert} from 'eltro' +import decode from '../../api/jwt/decode.mjs' + +const pubKeys = {} +const audiences = [] + +const testJwt = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjEifQ.eyJhdWQiOiJodHRwczovL2hvc3Qvb2F1dGgvdG9rZW4iLCJpc3MiOiJ0ZXN0QHRlc3QuY29tIiwiaWF0IjoxNTAzMzM1MTY5LCJleHAiOjE1MDMzMzU3NjksInNjb3BlIjpbImh0dHA6Ly9zdHVmZiIsImh0dHA6Ly9zdHVmZjIiXX0.zO278VV6NzwsvBrAIc15mOfwza-FkmLCV28NRXnrI550xw1S1145cS1UsZP5zXxcrk5f4oEgB91Jt6ble76yK5nU68fALUXtfe7xPUkhcOUIw92q_x_Iaaw4z6a71NtyishCfJlbmwkXXEq5YCVAvX3KNDtyPf_fQrAqjzsbgQc' + +const testJwtWrongAlg = + 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzEyOCJ9.eyJhdWQiOiJodHRwczovL2hvc3Qvb2F1dGgvdG9rZW4iLCJpc3MiOiJ0ZXN0QHRlc3QuY29tIiwiaWF0IjoxNTAzMzM2NzU5LCJleHAiOjE1MDMzMzczNTksInNjb3BlIjpbImh0dHA6Ly9zdHVmZiIsImh0dHA6Ly9zdHVmZjIiXX0.12co2gXwBxmZ2uLJecd26bfteCLBx7jgu_9rp2hhKAHWA4qFKm1HcQOZXqDvHkjflQDtNAQ1ZUUf3U8kntUUAmMOjhHx0BspC-xuaTFylZWqj--A2_w9e7JSk46TF_x3e_hZLB3rtyuSEAPMh_nOCsmM-4A2fnQx0Y5p-Bwbt0I' + +t.describe('jwtUtils', function() { + t.describe('decode', function() { + t.test('invalid jwt input', function() { + assert.throws( + function() { + decode({}, pubKeys, audiences) + }, + /jwt needs to a string/ + ) + }) + t.test('invalid pubKeys input', function() { + assert.throws( + function() { + decode(testJwt, [], audiences) + }, + /publicKeys needs to be a map of { issuer: { keyid: "PEM encoded key" }/ + ) + }) + t.test('invalid audiences input', function() { + assert.throws( + function() { + decode(testJwt, pubKeys, '') + }, + /audiences needs to be an array of allowed audiences/ + ) + }) + t.test('invalid options input', function() { + assert.throws( + function() { + decode(testJwt, pubKeys, audiences, '') + }, + /options needs to a map of { nbfIatSkew: 300, ... }/ + ) + }) + t.test('too few spaces', function() { + assert.throws( + function() { + decode('hello.test', pubKeys, audiences) + }, + /JWT does not contain 3 dots/ + ) + }) + t.test('invalid json', function() { + assert.throws( + function() { + decode(testJwt.substr(10), pubKeys, audiences) + }, + /Unexpected token \$ in JSON at position 0/ + ) + }) + t.test('wrong alg', function() { + assert.throws( + function() { + decode(testJwtWrongAlg, pubKeys, audiences) + }, + /Only alg HS256, HS384 and HS512 are supported/ + ) + }) + }) +}) \ No newline at end of file diff --git a/test/jwt/encode.test.mjs b/test/jwt/encode.test.mjs new file mode 100644 index 0000000..5090afa --- /dev/null +++ b/test/jwt/encode.test.mjs @@ -0,0 +1,21 @@ +import { Eltro as t, assert} from 'eltro' +import encode from '../../api/jwt/encode.mjs' + +t.describe('encode', function() { + t.test('should faile with invalid header and body', function() { + assert.throws( + function() { + encode('', '') + }, + /both header and body should be of type object/ + ) + }) + t.test('should faile with empty header and body', function() { + assert.throws( + function() { + encode({}, {}) + }, + /Only alg HS256, HS384 and HS512 are supported/ + ) + }) +}) diff --git a/test/jwt/jwt.test.mjs b/test/jwt/jwt.test.mjs new file mode 100644 index 0000000..5634951 --- /dev/null +++ b/test/jwt/jwt.test.mjs @@ -0,0 +1,343 @@ +import { Eltro as t, assert} from 'eltro' +import encode from '../../api/jwt/encode.mjs' +import decode from '../../api/jwt/decode.mjs' +import defaults from '../../api/defaults.mjs' + +const unixNow = Math.floor(Date.now() / 1000) +const jwtHeader = { + typ: 'JWT', + alg: 'HS256', + kid: '2' +} + +const jwtBody = { + aud: 'https://host/oauth/token', + iss: 'test@test.com', + iat: unixNow, + exp: unixNow + 600, + scope: ['http://stuff', 'http://stuff2'] +} + +const pubKeys = { + 'test@test.com': { + 'default@HS256': 'sharedkey', + '2@HS256': 'sharedkey', + '2@HS384': 'sharedkey', + '2@HS512': 'sharedkey', + '5@HS256': 'wrongkey' + }, + 'test@custom.com': { + '2@HS256': { + key: 'sharedkey', + expiresSkew: 600, + expiresMax: 86400 + }, + '3@HS256': { + }, + '4@HS256': [], + '5@HS256': { key: 'sharedkey' }, + }, +} + +t.describe('encode/decode', function() { + ['HS256', 'HS384', 'HS512'].forEach(function(algo) { + t.test('success with ' + algo, function() { + let customJwtHeader = defaults(jwtHeader) + customJwtHeader.kid = '2' + customJwtHeader.alg = algo + let jwt = encode(customJwtHeader, jwtBody, 'sharedkey') + let decodedJwtBody = decode(jwt, pubKeys, [ + 'https://host/oauth/token' + ]) + assert.deepStrictEqual(jwtBody, decodedJwtBody) + }) + }) + t.test('success without kid', function() { + let customJwtHeader = defaults(jwtHeader) + delete customJwtHeader.kid + let jwt = encode(customJwtHeader, jwtBody, 'sharedkey') + let decodedJwtBody = decode(jwt, pubKeys, [ + 'https://host/oauth/token' + ]) + assert.deepStrictEqual(jwtBody, decodedJwtBody) + }) + t.test('success with object key', function() { + let customJwtBody = defaults(jwtBody) + customJwtBody.kid = '5' + customJwtBody.iss = 'test@custom.com' + let jwt = encode(jwtHeader, jwtBody, 'sharedkey') + let decodedJwtBody = decode(jwt, pubKeys, [ + 'https://host/oauth/token' + ]) + assert.deepStrictEqual(jwtBody, decodedJwtBody) + }) + t.test('success with array aud', function() { + let customJwtBody = defaults(jwtBody) + customJwtBody.aud = [ + 'https://myhost/oauth/token', + 'https://host/oauth/token' + ] + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + let decodedJwtBody = decode(jwt, pubKeys, [ + 'https://host/oauth/token' + ]) + assert.deepStrictEqual(customJwtBody, decodedJwtBody) + }) + t.test('success with expired token', function() { + let customJwtBody = defaults(jwtBody) + customJwtBody.iss = 'test@custom.com' + customJwtBody.exp -= 600 + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + let decodedJwtBody = decode(jwt, pubKeys, [ + 'https://host/oauth/token' + ]) + assert.deepStrictEqual(customJwtBody, decodedJwtBody) + }) + t.test('token outside maximum expires', function() { + let customJwtBody = defaults(jwtBody) + customJwtBody.iss = 'test@custom.com' + customJwtBody.exp += 172800 + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /Expires in the future by more than 86400 seconds/ + ) + }) + t.test('token outside maximum expires using decode options', function() { + let customJwtBody = defaults(jwtBody) + customJwtBody.exp += 172800 + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token'], { + expiresMax: 600 + }) + }, + /Expires in the future by more than 600 seconds/ + ) + }) + t.test('token outside maximum expires using nbf', function() { + let customJwtBody = defaults(jwtBody) + customJwtBody.exp += 172800 + customJwtBody.nbf = customJwtBody.iat + delete customJwtBody.iat + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token'], { + expiresMax: 600 + }) + }, + /Expires in the future by more than 600 seconds/ + ) + }) + t.test('token outside maximum expires using unixNow', function() { + let customJwtBody = defaults(jwtBody) + customJwtBody.exp += 172800 + delete customJwtBody.iat + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token'], { + expiresMax: 600 + }) + }, + /Expires in the future by more than 600 seconds/ + ) + }) + t.test('unknown aud', function() { + let jwt = encode(jwtHeader, jwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://myhost/oauth/token']) + }, + /Unknown audience 'https:\/\/host\/oauth\/token'/ + ) + }) + t.test('expired', function() { + let customJwtBody = defaults(jwtBody) + customJwtBody.iat -= 1200 + customJwtBody.exp -= 800 + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /Token has expired/ + ) + }) + t.test('missing exp', function() { + let customJwtBody = defaults(jwtBody) + delete customJwtBody.exp + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /No expires set on token/ + ) + }) + t.test('missing iss', function() { + let customJwtBody = defaults(jwtBody) + delete customJwtBody.iss + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /No issuer set/ + ) + }) + t.test('iat invalid', function() { + let customJwtBody = defaults(jwtBody) + customJwtBody.iat += 1200 + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /Issued at in the future by more than 300 seconds/ + ) + }) + t.test('nbf invalid', function() { + let customJwtBody = defaults(jwtBody) + customJwtBody.nbf = customJwtBody.iat + 1200 + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /Not before in the future by more than 300 seconds/ + ) + }) + t.test('unknown issuer', function() { + let customJwtBody = defaults(jwtBody) + customJwtBody.iss = 'unknown@test.com' + let jwt = encode(jwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /Unknown issuer 'unknown@test.com'/ + ) + }) + t.test('wrong alg', function() { + let customJwtHeader = defaults(jwtHeader) + customJwtHeader.alg = 'HS128' + assert.throws( + function() { + encode(customJwtHeader, jwtBody, 'sharedkey') + }, + /Only alg HS256, HS384 and HS512 are supported/ + ) + }) + t.test('unknown kid', function() { + let customJwtHeader = defaults(jwtHeader) + customJwtHeader.kid = '3' + let jwt = encode(customJwtHeader, jwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /Unknown pubkey id '3' for this issuer/ + ) + }) + t.test('invalid signature', function() { + let customJwtHeader = defaults(jwtHeader) + customJwtHeader.kid = '2' + let jwt = encode(customJwtHeader, jwtBody, 'sharedkey') + let backup = jwt + if (jwt[jwt.length - 2] === 'A') { + jwt = jwt.slice(0, -2) + 'BB' + } else { + jwt = jwt.slice(0, -2) + 'AA' + } + try { + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /Verification failed with alg 'HS256'/ + ) + } catch (err) { + console.log('-----') + console.log(backup) + console.log(jwt) + throw err + } + jwt = encode(customJwtHeader, jwtBody, 'sharedkey') + jwt += 'a' + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /Verification failed with alg 'HS256'/ + ) + }) + t.test('invalid shared key', function() { + let customJwtHeader = defaults(jwtHeader) + customJwtHeader.kid = '5' + customJwtHeader.alg = 'HS256' + let jwt = encode(customJwtHeader, jwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /Verification failed with alg 'HS256'/ + ) + }) + t.test('invalid pubkey', function() { + let customJwtHeader = defaults(jwtHeader) + customJwtHeader.kid = '4' + let jwt = encode(customJwtHeader, jwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + } + ) + let customJwtBody = defaults(jwtBody) + customJwtBody.iss = 'test@custom.com' + customJwtHeader.kid = '3' + jwt = encode(customJwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /'3'.+misconfigured/ + ) + customJwtHeader.kid = '4' + jwt = encode(customJwtHeader, customJwtBody, 'sharedkey') + assert.throws( + function() { + decode(jwt, pubKeys, ['https://host/oauth/token']) + }, + /'4'.+misconfigured/ + ) + }) + t.test('success with broken token', function() { + let expectedJwtBody = { + id: 1, + exp: 1519802991, + iat: 1519802691, + iss: 'test@test.com', + aud: 'https://host/oauth/token' + } + let decodedJwtBody = decode( + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiaWF0IjoxNTE5ODAyNjkxfQ.p6t378Ri2JpOCm9WtC36ttyH8ILzG9-OWT_kgMrrRfo', + pubKeys, + ['https://host/oauth/token'], + { + fixup: (header, body) => { + header.kid = '2' + body.iss = 'test@test.com' + body.aud = 'https://host/oauth/token' + body.exp = body.iat + 300 + }, + expiresSkew: 307584000 + } + ) + assert.deepStrictEqual(decodedJwtBody, expectedJwtBody) + }) +}) diff --git a/test/server.test.js b/test/server.test.js deleted file mode 100644 index 0430e88..0000000 --- a/test/server.test.js +++ /dev/null @@ -1,22 +0,0 @@ -import assert from 'assert-extended' - -import * as server from './helper.server' - -describe('Server', () => { - let client - - beforeEach(() => { - client = server.createClient() - }) - - it('should run', () => - assert.isFulfilled( - client.getAsync('/') - ) - .then(data => { - assert.ok(data) - assert.ok(data.name) - assert.ok(data.version) - }) - ) -}) diff --git a/test/server.test.mjs b/test/server.test.mjs new file mode 100644 index 0000000..1daa8fb --- /dev/null +++ b/test/server.test.mjs @@ -0,0 +1,28 @@ +import { Eltro as t, assert} from 'eltro' + +import * as server from './helper.server.mjs' + +t.describe('Server', function() { + let client + + t.before(function() { + client = server.createClient() + }) + + t.test('should run', async function() { + let data = await client.get('/') + + assert.ok(data) + assert.ok(data.name) + assert.ok(data.version) + }) + + t.test('should handle errors fine', async function() { + let data = await assert.isRejected(client.get('/error')) + + assert.ok(data) + assert.ok(data.body) + assert.strictEqual(data.body.status, 422) + assert.match(data.body.message, /test/) + }) +})