diff --git a/api/error.mjs b/api/error.mjs index 06dcf5e..01f65b9 100644 --- a/api/error.mjs +++ b/api/error.mjs @@ -1,11 +1,18 @@ +export class HttpError extends Error { + constructor(message, status = 500) { + super(message) + this.status = status + } +} + export async function errorMiddleware(ctx, next) { try { await next() } catch (e) { - ctx.status = 422 + ctx.status = e.status || 500 ctx.body = { - status: 422, + status: ctx.status, message: e.message, } } diff --git a/api/jwt/encode.mjs b/api/jwt/encode.mjs index d3c2fe1..509767f 100644 --- a/api/jwt/encode.mjs +++ b/api/jwt/encode.mjs @@ -2,17 +2,24 @@ import crypto from 'crypto' import * as base64UrlSafe from './base64urlsafe.mjs' import defaults from '../defaults.mjs' -export default function encode(header, orgBody, privateKeyPassword = null, disableAutofill = false) { - let body = defaults(orgBody) +export default function encode(orgHeader, orgBody, privateKeyPassword = null, disableAutofill = false) { if ( - typeof header !== 'object' || - Array.isArray(header) || - typeof body !== 'object' || - Array.isArray(body) + typeof orgHeader !== 'object' || + typeof orgBody !== 'object' ) { throw new Error('both header and body should be of type object') } + let header = defaults(orgHeader) + let body = defaults(orgBody) + + if (!header.alg) { + header.alg = 'HS256' + } + if (!header.typ) { + header.typ = 'JWT' + } + let hmacAlgo = null switch (header.alg) { case 'HS256': diff --git a/api/media/routes.mjs b/api/media/routes.mjs index ad7d979..70ccb24 100644 --- a/api/media/routes.mjs +++ b/api/media/routes.mjs @@ -1,13 +1,22 @@ -import { verifyToken } from './security.mjs' -import { uploadFile } from './multer.mjs' +import * as security from './security.mjs' +import * as multer from './multer.mjs' -export async function upload(ctx) { - let site = await verifyToken(ctx) +export default class MediaRoutes { + constructor(opts = {}) { + Object.assign(this, { + security: opts.security || security, + multer: opts.multer || multer, + }) + } - let result = await uploadFile(ctx, site) + async upload(ctx) { + let site = await this.security.verifyToken(ctx) - ctx.body = { - filename: result.filename, - path: `/${site}/${result.filename}` + let result = await this.multer.uploadFile(ctx, site) + + ctx.body = { + filename: result.filename, + path: `/${site}/${result.filename}` + } } } diff --git a/api/media/security.mjs b/api/media/security.mjs index d34cd0c..e9be16a 100644 --- a/api/media/security.mjs +++ b/api/media/security.mjs @@ -1,13 +1,18 @@ -// import * as jwt from '../jwt.mjs' +import { HttpError } from '../error.mjs' import decode from '../jwt/decode.mjs' import config from '../config.mjs' export function verifyToken(ctx) { - if (!ctx.query.token) { - throw new Error('Token is missing in query') + let token = ctx.query.get('token') + if (!token) { + throw new HttpError('Token is missing in query', 422) } - let decoded = decode(ctx.query.token, config.get('sites'), []) - - return decoded.iss + try { + let decoded = decode(token, config.get('sites'), []) + return decoded.iss + } catch (err) { + ctx.log.error(err, 'Error decoding token: ' + token) + throw new HttpError('Token was invalid', 422) + } } diff --git a/api/router.mjs b/api/router.mjs deleted file mode 100644 index 8e91bb6..0000000 --- a/api/router.mjs +++ /dev/null @@ -1,12 +0,0 @@ -import Router from 'koa-router' - -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 index d258ad5..26bfab4 100644 --- a/api/server.mjs +++ b/api/server.mjs @@ -1,7 +1,14 @@ +import { performance } from 'perf_hooks' import Koa from 'koa-lite' +import { Flaska, QueryHandler } from 'flaska' + +import TestRoutes from './test/routes.mjs' +import MediaRoutes from './media/routes.mjs' import config from './config.mjs' import log from './log.mjs' + +/* import router from './router.mjs' import { errorMiddleware } from './error.mjs' @@ -15,4 +22,51 @@ const server = app.listen(config.get('server:port'), function(a,b) { log.info(`Server listening at ${config.get('server:port')}`) }) -export default server +export default server */ + +const app = new Flaska({ + log: log, +}) + +app.before(function(ctx) { + ctx.__started = performance.now() +}) + +app.after(function(ctx) { + let ended = performance.now() - ctx.__started + let logger = ctx.log.info + if (ctx.status >= 400) { + logger = ctx.log.warn + } + logger({ + path: ctx.url, + status: ctx.status, + ms: Math.round(ended), + }, 'Request finished') +}) + +app.onerror(function(err, ctx) { + if (err.status && err.status >= 400 && err.status < 500) { + ctx.log.warn(err.message) + } else { + ctx.log.error(err) + } + ctx.status = err.status || 500 + ctx.body = { + status: ctx.status, + message: err.message, + } +}) + +const test = new TestRoutes() +app.get('/', test.static.bind(test)) +app.get('/error', test.error.bind(test)) + +const media = new MediaRoutes() +app.post('/media', [QueryHandler()], media.upload.bind(media)) + +app.listen(config.get('server:port'), function(a,b) { + log.info(`Server listening at ${config.get('server:port')}`) +}) + +export default app diff --git a/api/test/routes.mjs b/api/test/routes.mjs index bdd6b86..648e58f 100644 --- a/api/test/routes.mjs +++ b/api/test/routes.mjs @@ -1,13 +1,19 @@ import config from '../config.mjs' -export async function testStatic(ctx) { - ctx.body = { - name: config.get('name'), - version: config.get('version'), - environment: config.get('NODE_ENV'), +export default class TestRoutes { + constructor(opts = {}) { + Object.assign(this, { }) + } + + static(ctx) { + ctx.body = { + name: config.get('name'), + version: config.get('version'), + environment: config.get('NODE_ENV'), + } + } + + error(ctx) { + throw new Error('This is a test') } } - -export async function testError(ctx) { - throw new Error('This is a test') -} diff --git a/config/config.test.json b/config/config.test.json new file mode 100644 index 0000000..913cb6c --- /dev/null +++ b/config/config.test.json @@ -0,0 +1,15 @@ +{ + "bunyan": { + "name": "storage-upload-test", + "streams": [{ + "stream": "process.stdout", + "level": "error" + } + ] + }, + "sites": { + "development": { + "default@HS256": "asdf1234" + } + } +} diff --git a/package.json b/package.json index aa4e28b..5307c95 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,8 @@ "scripts": { "dev": "nodemon index.js", "start": "node --experimental-modules api/server.mjs", - "test": "eltro test/**/*.test.mjs -r dot" + "test": "set NODE_ENV=test&& eltro test/**/*.test.mjs -r dot", + "test:linux": "NODE_ENV=test eltro test/**/*.test.mjs -r dot" }, "repository": { "type": "git", @@ -20,12 +21,11 @@ "homepage": "https://github.com/nfp-projects/storage-upload#readme", "dependencies": { "bunyan-lite": "^1.1.1", - "koa-lite": "^2.10.1", - "koa-router": "^7.2.1", + "flaska": "^0.9.5", "multer": "^1.3.0", "nconf-lite": "^2.0.0" }, "devDependencies": { - "eltro": "^1.1.0" + "eltro": "^1.2.2" } } diff --git a/test.json b/test.json new file mode 100644 index 0000000..44e5709 --- /dev/null +++ b/test.json @@ -0,0 +1 @@ +{"name":"storage-upload","hostname":"JonatanPC","pid":26596,"level":30,"path":"/","status":200,"ms":4,"msg":"Request finished","time":"2021-10-10T23:55:12.390Z","v":0} \ No newline at end of file diff --git a/test/helper.client.mjs b/test/helper.client.mjs index 74a7cb5..d5f9bf1 100644 --- a/test/helper.client.mjs +++ b/test/helper.client.mjs @@ -1,4 +1,6 @@ import http from 'http' +import fs from 'fs/promises' +import path from 'path' import { URL } from 'url' import defaults from '../api/defaults.mjs' import config from '../api/config.mjs' @@ -50,6 +52,7 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) { if (output.status) { let err = new Error(`Request failed [${output.status}]: ${output.message}`) err.body = output + err.status = output.status return reject(err) } resolve(output) @@ -59,8 +62,37 @@ Client.prototype.customRequest = function(method = 'GET', path, body, options) { }) } -Client.prototype.get = function(path = '/') { - return this.customRequest('GET', path, null) +Client.prototype.get = function(url = '/') { + return this.customRequest('GET', url, null) +} + +Client.prototype.upload = function(url, file, method = 'POST') { + return fs.readFile(file).then(data => { + const crlf = '\r\n' + const filename = path.basename(file) + const boundary = `---------${Math.random().toString(16)}` + const headers = [ + `Content-Disposition: form-data; name="file"; filename="${filename}"` + crlf, + ] + const multipartBody = Buffer.concat([ + Buffer.from( + `${crlf}--${boundary}${crlf}` + + headers.join('') + crlf + ), + data, + Buffer.from( + `${crlf}--${boundary}--` + ), + ]) + + return this.customRequest(method, url, multipartBody, { + timeout: 5000, + headers: { + 'Content-Type': 'multipart/form-data; boundary=' + boundary, + 'Content-Length': multipartBody.length, + }, + }) + }) } /* diff --git a/test/helper.server.mjs b/test/helper.server.mjs index 9147525..b469907 100644 --- a/test/helper.server.mjs +++ b/test/helper.server.mjs @@ -1,17 +1,38 @@ -// import _ from 'lodash' -// import sinon from 'sinon' +import { stub } from 'eltro' import Client from './helper.client.mjs' import defaults from '../api/defaults.mjs' -import '../api/server.mjs' +import serv from '../api/server.mjs' + +serv.log = { + log: stub(), + warn: stub(), + info: stub(), + error: stub(), +} + +export const server = serv export function createClient() { return new Client() } +export function resetLog() { + serv.log.log.reset() + serv.log.info.reset() + serv.log.warn.reset() + serv.log.error.reset() +} + export function createContext(opts) { return defaults(opts, { - query: { }, + query: new Map(), req: { }, res: { }, + log: { + log: stub(), + warn: stub(), + info: stub(), + error: stub(), + }, }) } diff --git a/test/jwt/encode.test.mjs b/test/jwt/encode.test.mjs index 5090afa..fbed4b8 100644 --- a/test/jwt/encode.test.mjs +++ b/test/jwt/encode.test.mjs @@ -1,5 +1,6 @@ import { Eltro as t, assert} from 'eltro' import encode from '../../api/jwt/encode.mjs' +import decode from '../../api/jwt/decode.mjs' t.describe('encode', function() { t.test('should faile with invalid header and body', function() { @@ -10,12 +11,18 @@ t.describe('encode', function() { /both header and body should be of type object/ ) }) - t.test('should faile with empty header and body', function() { + t.test('should faile with invalid alg type', function() { assert.throws( function() { - encode({}, {}) + encode({ alg: 'asdf' }, {}) }, /Only alg HS256, HS384 and HS512 are supported/ ) }) + t.test('should have default header options', function() { + const assertTest = '1234' + let token = encode(null, { iss: 'test', test: assertTest }, 'bla') + let decoded = decode(token, { 'test': { 'default@HS256': 'bla' } }, []) + assert.strictEqual(decoded.test, assertTest) + }) }) diff --git a/test/media/api.test.js b/test/media/api.test.js deleted file mode 100644 index 5f72833..0000000 --- a/test/media/api.test.js +++ /dev/null @@ -1,61 +0,0 @@ -import { Eltro as t, assert} from 'eltro' -import fs from 'fs' -import { fileURLToPath } from 'url' -import path from 'path' - -import createClient from '../helper.client.mjs' -import config from '../../api/config.mjs' -import jwt from '../../api/jwt.mjs' - -let __dirname = path.dirname(fileURLToPath(import.meta.url)) - -console.log(__dirname) - -t.describe('Media (API)', () => { - let testFile - let client = createClient() - - t.before(() => { - config.set('sites', { - development: 'hello-world' - }) - }) - - t.after(done => { - if (testFile) { - // path.resolve(path.join(__dirname, 'fixtures', file)) - return fs.unlink( appRoot.resolve(`/public${testFile}`), done) - } - done() - }) - - t.describe('POST /media', function temp() { - this.timeout(10000) - - t.test('should require authentication', async () => { - let err = await assert.isRejected( - client.sendFileAsync('/media', - appRoot.resolve('/test/media/test.png'))) - - assert.strictEqual(err.status, 422) - assert.match(err.message, /[Tt]oken/) - }) - - t.test('should upload file and create file', async () => { - let token = jwt.sign({ site: 'development' }, 'hello-world') - - let data = await assert.isFulfilled( - client.sendFileAsync( - `/media?token=${token}`, - appRoot.resolve('/test/media/test.png') - ) - ) - - assert.ok(data) - assert.ok(data.filename) - assert.ok(data.path) - - testFile = data.path - }) - }) -}) diff --git a/test/media/api.test.mjs b/test/media/api.test.mjs new file mode 100644 index 0000000..00d6a2a --- /dev/null +++ b/test/media/api.test.mjs @@ -0,0 +1,94 @@ +import { Eltro as t, assert} from 'eltro' +import fs from 'fs' +import { fileURLToPath } from 'url' +import path from 'path' + +import { server, resetLog } from '../helper.server.mjs' +import Client from '../helper.client.mjs' +import config from '../../api/config.mjs' +import encode from '../../api/jwt/encode.mjs' + +let __dirname = path.dirname(fileURLToPath(import.meta.url)) + +function resolve(file) { + return path.resolve(path.join(__dirname, file)) +} + +t.describe('Media (API)', () => { + const client = new Client() + const secret = 'asdf1234' + let testFile + + t.after(function(done) { + if (testFile) { + return fs.unlink(resolve(`../../public/${testFile}`), done) + } + done() + }) + + t.timeout(10000).describe('POST /media', function temp() { + t.test('should require authentication', async () => { + resetLog() + assert.strictEqual(server.log.error.callCount, 0) + assert.strictEqual(server.log.warn.callCount, 0) + let err = await assert.isRejected( + client.upload('/media', + resolve('test.png') + ) + ) + + assert.strictEqual(err.status, 422) + assert.match(err.message, /[Tt]oken/) + assert.match(err.message, /[Mm]issing/) + + assert.strictEqual(server.log.error.callCount, 0) + assert.strictEqual(server.log.warn.callCount, 2) + assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') + assert.match(server.log.warn.firstCall[0], /[Tt]oken/) + assert.match(server.log.warn.firstCall[0], /[Mm]issing/) + }) + + t.test('should verify token correctly', async () => { + const assertToken = 'asdf.asdf.asdf' + resetLog() + assert.strictEqual(server.log.error.callCount, 0) + assert.strictEqual(server.log.warn.callCount, 0) + assert.strictEqual(server.log.info.callCount, 0) + + let err = await assert.isRejected( + client.upload('/media?token=' + assertToken, + resolve('test.png') + ) + ) + + assert.strictEqual(err.status, 422) + assert.match(err.message, /[Tt]oken/) + assert.match(err.message, /[Ii]nvalid/) + + assert.strictEqual(server.log.error.callCount, 1) + assert.strictEqual(server.log.warn.callCount, 2) + assert.strictEqual(typeof(server.log.warn.firstCall[0]), 'string') + assert.match(server.log.warn.firstCall[0], /[Tt]oken/) + assert.match(server.log.warn.firstCall[0], /[Ii]nvalid/) + assert.ok(server.log.error.lastCall[0] instanceof Error) + assert.match(server.log.error.lastCall[1], new RegExp(assertToken)) + }) + + t.test('should upload file and create file', async () => { + let token = encode(null, { iss: 'development' }, secret) + + let data = await assert.isFulfilled( + client.upload( + `/media?token=${token}`, + resolve('test.png') + ) + ) + + assert.ok(data) + assert.ok(data.filename) + assert.ok(data.path) + + testFile = data.path + }) + }) +}) diff --git a/test/media/routes.test.js b/test/media/routes.test.js deleted file mode 100644 index f697495..0000000 --- a/test/media/routes.test.js +++ /dev/null @@ -1,73 +0,0 @@ -import assert from 'assert-extended' -import sinon from 'sinon' - -import { createContext } from '../helper.server' - -describe('Media (Routes)', () => { - const multer = require('../../api/media/multer') - const routes = require('../../api/media/routes') - const security = require('../../api/media/security') - const config = require('../../config') - - let sandbox - let ctx - - beforeEach(() => { - sandbox = sinon.sandbox.create() - ctx = createContext({ - req: { - file: { }, - }, - }) - }) - - afterEach(() => { - sandbox.restore() - }) - - describe('#upload', () => { - let stubVerifyToken - let stubUpload - - beforeEach(() => { - stubVerifyToken = sandbox.stub(security, 'verifyToken') - stubUpload = sandbox.stub(multer, 'uploadFile').resolves({}) - }) - - it('should call security correctly', async () => { - const assertError = new Error('temp') - stubVerifyToken.rejects(assertError) - - let err = await assert.isRejected(routes.upload(ctx)) - - assert.ok(stubVerifyToken.called) - assert.strictEqual(err, assertError) - assert.strictEqual(stubVerifyToken.firstCall.args[0], ctx) - }) - - it('should call upload correctly', async () => { - const assertSiteName = 'benshapiro' - const assertError = new Error('hello') - stubVerifyToken.resolves(assertSiteName) - stubUpload.rejects(assertError) - - let err = await assert.isRejected(routes.upload(ctx)) - - assert.ok(stubUpload.called) - assert.strictEqual(err, assertError) - assert.strictEqual(stubUpload.firstCall.args[0], ctx) - assert.strictEqual(stubUpload.firstCall.args[1], assertSiteName) - }) - - it('should otherwise set context status to 204 and file in result', async () => { - const assertFilename = 'asdfsafd' - const assertSite = 'mario' - stubVerifyToken.resolves(assertSite) - stubUpload.resolves({ filename: assertFilename }) - await routes.upload(ctx) - - assert.strictEqual(ctx.body.filename, assertFilename) - assert.strictEqual(ctx.body.path, `/${assertSite}/${assertFilename}`) - }) - }) -}) diff --git a/test/media/routes.test.mjs b/test/media/routes.test.mjs new file mode 100644 index 0000000..f0c5349 --- /dev/null +++ b/test/media/routes.test.mjs @@ -0,0 +1,64 @@ +import { Eltro as t, assert, stub } from 'eltro' +import { createContext } from '../helper.server.mjs' + +import MediaRoutes from '../../api/media/routes.mjs' + +t.describe('#upload', () => { + const stubVerify = stub() + const stubUpload = stub() + + const routes = new MediaRoutes({ + security: { verifyToken: stubVerify }, + multer: { uploadFile: stubUpload }, + }) + + function reset() { + stubVerify.reset() + stubUpload.reset() + } + + t.test('should call security correctly', async () => { + reset() + + let ctx = createContext() + const assertError = new Error('temp') + stubVerify.rejects(assertError) + + let err = await assert.isRejected(routes.upload(ctx)) + + assert.ok(stubVerify.called) + assert.strictEqual(err, assertError) + assert.strictEqual(stubVerify.firstCall[0], ctx) + }) + + t.test('should call upload correctly', async () => { + reset() + + let ctx = createContext() + const assertSiteName = 'benshapiro' + const assertError = new Error('hello') + stubVerify.resolves(assertSiteName) + stubUpload.rejects(assertError) + + let err = await assert.isRejected(routes.upload(ctx)) + + assert.ok(stubUpload.called) + assert.strictEqual(err, assertError) + assert.strictEqual(stubUpload.firstCall[0], ctx) + assert.strictEqual(stubUpload.firstCall[1], assertSiteName) + }) + + t.test('should otherwise set context status to 204 and file in result', async () => { + reset() + + let ctx = createContext() + const assertFilename = 'asdfsafd' + const assertSite = 'mario' + stubVerify.resolves(assertSite) + stubUpload.resolves({ filename: assertFilename }) + await routes.upload(ctx) + + assert.strictEqual(ctx.body.filename, assertFilename) + assert.strictEqual(ctx.body.path, `/${assertSite}/${assertFilename}`) + }) +}) diff --git a/test/media/security.test.mjs b/test/media/security.test.mjs index c2d782f..533db1f 100644 --- a/test/media/security.test.mjs +++ b/test/media/security.test.mjs @@ -2,6 +2,7 @@ import { Eltro as t, assert} from 'eltro' import { createContext } from '../helper.server.mjs' import { verifyToken } from '../../api/media/security.mjs' +import { HttpError } from '../../api/error.mjs' import encode from '../../api/jwt/encode.mjs' import config from '../../api/config.mjs' @@ -16,51 +17,73 @@ t.describe('#verifyToken()', function() { t.test('should fail if query token is missing', function() { let ctx = createContext({ }) - delete ctx.query.token + ctx.query.delete('token') - assert.throws(function() { verifyToken(ctx) }, /[Mm]issing/) + assert.throws(function() { verifyToken(ctx) }, function(err) { + assert.ok(err instanceof HttpError) + assert.ok(err instanceof Error) + assert.strictEqual(err.status, 422) + assert.match(err.message, /[Qq]uery/) + assert.match(err.message, /[Tt]oken/) + return true + }) }) + function assertInvalidToken(err) { + assert.ok(err instanceof HttpError) + assert.ok(err instanceof Error) + assert.strictEqual(err.status, 422) + assert.match(err.message, /[Ii]nvalid/) + assert.match(err.message, /[Tt]oken/) + return true + } + t.test('should fail if token is invalid', function() { let ctx = createContext({ }) - ctx.query.token = 'asdfasdgassdga' + ctx.query.set('token', 'asdfasdgassdga') - assert.throws(function() { verifyToken(ctx) }) + assert.throws(function() { verifyToken(ctx) }, assertInvalidToken) + assert.ok(ctx.log.error.lastCall) + assert.match(ctx.log.error.lastCall[0].message, /3 dots/) - ctx.query.token = 'asdfasdgassdga.asdfasdg.sadfsadfas' + ctx.query.set('token', 'asdfasdgassdga.asdfasdg.sadfsadfas') - assert.throws(function() { verifyToken(ctx) }, /invalid/) + assert.throws(function() { verifyToken(ctx) }, assertInvalidToken) + assert.match(ctx.log.error.lastCall[0].message, /[Ii]nvalid/) - ctx.query.token = encode( + ctx.query.set('token', encode( { typ: 'JWT', alg: 'HS256' }, { iss: 'justatest' }, 'mysharedkey' - ) - assert.throws(function() { verifyToken(ctx) }, /pubkey/) + )) + assert.throws(function() { verifyToken(ctx) }, assertInvalidToken) + assert.match(ctx.log.error.lastCall[0].message, /pubkey/) - ctx.query.token = encode( + ctx.query.set('token', encode( { typ: 'JWT', alg: 'HS512' }, { iss: 'notexist' }, 'mysharedkey' - ) - assert.throws(function() { verifyToken(ctx) }, /notexist/) + )) + assert.throws(function() { verifyToken(ctx) }, assertInvalidToken) + assert.match(ctx.log.error.lastCall[0].message, /notexist/) - ctx.query.token = encode( + ctx.query.set('token', encode( { typ: 'JWT', alg: 'HS512' }, { iss: 'justatest' }, 'mysharedkey2' - ) - assert.throws(function() { verifyToken(ctx) }, /HS512/) - assert.throws(function() { verifyToken(ctx) }, /[vV]erification/) + )) + assert.throws(function() { verifyToken(ctx) }, assertInvalidToken) + assert.match(ctx.log.error.lastCall[0].message, /HS512/) + assert.match(ctx.log.error.lastCall[0].message, /[vV]erification/) }) t.test('should otherwise return the issuer', function() { let ctx = createContext({ }) - ctx.query.token = encode( + ctx.query.set('token', encode( { typ: 'JWT', alg: 'HS512' }, { iss: 'justatest' }, 'mysharedkey' - ) + )) let site = verifyToken(ctx) assert.strictEqual(site, 'justatest') }) diff --git a/test/server.test.mjs b/test/server.test.mjs index 1daa8fb..10135a2 100644 --- a/test/server.test.mjs +++ b/test/server.test.mjs @@ -1,28 +1,40 @@ import { Eltro as t, assert} from 'eltro' -import * as server from './helper.server.mjs' +import { createClient, server, resetLog } from './helper.server.mjs' t.describe('Server', function() { let client t.before(function() { - client = server.createClient() + client = createClient() }) t.test('should run', async function() { + resetLog() + assert.strictEqual(server.log.info.callCount, 0) let data = await client.get('/') assert.ok(data) assert.ok(data.name) assert.ok(data.version) + assert.strictEqual(server.log.info.callCount, 1) + assert.strictEqual(server.log.info.lastCall[0].status, 200) + assert.strictEqual(server.log.info.lastCall[0].path, '/') + assert.strictEqual(typeof(server.log.info.lastCall[0].ms), 'number') }) t.test('should handle errors fine', async function() { + resetLog() + assert.strictEqual(server.log.warn.callCount, 0) let data = await assert.isRejected(client.get('/error')) assert.ok(data) assert.ok(data.body) - assert.strictEqual(data.body.status, 422) + assert.strictEqual(data.body.status, 500) assert.match(data.body.message, /test/) + assert.strictEqual(server.log.warn.callCount, 1) + assert.strictEqual(server.log.warn.lastCall[0].status, 500) + assert.strictEqual(server.log.warn.lastCall[0].path, '/error') + assert.strictEqual(typeof(server.log.warn.lastCall[0].ms), 'number') }) })