UNPKG

hapi-format-error

Version:

Structure Boom errors into a more desirable format

548 lines (466 loc) 12.1 kB
'use strict'; const Hapi = require('@hapi/hapi'); const Joi = require('joi'); const Sinon = require('sinon'); const Util = require('util'); const FormatError = require('../lib'); class UnauthorizedUser extends Error { constructor (message) { super(message); this.name = 'UnauthorizedUser'; this.code = 'unauthorized'; } } describe('format error plugin', () => { let server; beforeEach(() => { Sinon.stub(Util, 'log'); server = new Hapi.Server({ port: 80 }); server.validator(Joi); server.route([{ method: 'GET', path: '/normal', config: { handler: async () => { return {}; } } }, { method: 'POST', path: '/joi', config: { handler: async () => { return {}; }, validate: { payload: { banana: Joi.number(), test: Joi.number() }, options: { abortEarly: false }, failAction: async (request, h, err) => { throw err; } } } }, { method: 'POST', path: '/custom', config: { handler: async () => { return {}; }, validate: { payload: { test: Joi.number().messages({ 'number.base': 'pass in a number' }) }, failAction: async (request, h, err) => { throw err; } } } }, { method: 'POST', path: '/error', config: { handler: async () => new Error() } }, { method: 'GET', path: '/unauth', config: { handler: async () => new UnauthorizedUser('who even are you') } }, { method: 'POST', path: '/xor', config: { handler: async () => { return {}; }, validate: { payload: Joi.object().keys({ one: Joi.string(), two: Joi.string() }) .xor('one', 'two'), failAction: async (request, h, err) => { throw err; } } } }, { method: 'POST', path: '/nested_xor', config: { handler: async () => { return {}; }, validate: { payload: { test: Joi.object().keys({ one: Joi.string(), two: Joi.string() }) .xor('one', 'two') .required() }, failAction: async (request, h, err) => { throw err; } } } }, { method: 'POST', path: '/nested_paths', config: { handler: async () => { return {}; }, validate: { payload: { test: Joi.object().keys({ one: Joi.string().valid('one', '1'), two: Joi.string().valid('two') }) }, options: { abortEarly: false }, failAction: async (request, h, err) => { throw err; } } } }, { method: 'POST', path: '/no-params', config: { handler: async () => { return {}; }, validate: { payload: Joi.object().keys({}), options: { abortEarly: false }, failAction: async (request, h, err) => { throw err; } } } }]); }); afterEach(() => { Util.log.restore(); }); it('does not do anything for non-errors', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'GET', url: '/normal' }); expect(res.statusCode).to.eql(200); expect(res.result).to.eql({}); }); it('does not alter the bad request status code if it was not provided', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/joi', payload: { test: 'not a number' } }); expect(res.statusCode).to.eql(400); }); it('converts bad request errors into the specified status code', async () => { const statusCode = 422; await server.register({ plugin: FormatError, options: { joiStatusCode: statusCode } }); const res = await server.inject({ method: 'POST', url: '/joi', payload: { test: 'not a number' } }); expect(res.statusCode).to.eql(statusCode); }); it('does not touch Joi validation errors if they do not have quotes', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/custom', payload: { test: 'not a number' } }); expect(res.result.error.message).to.eql('pass in a number'); }); it('removes quotes from Joi validation errors if they exist', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/joi', payload: { test: 'not a number' } }); expect(res.result.error.message).to.eql('test must be a number'); }); it('joins Joi errors together if it does not abort early', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/joi', payload: { banana: 'not a number', test: 'not a number' } }); expect(res.result.error.message).to.eql('banana must be a number or test must be a number'); }); it('allows for custom 500 messages', async () => { const msg = '500 - Server Error'; await server.register({ plugin: FormatError, options: { serverErrorMessage: msg } }); const res = await server.inject({ method: 'POST', url: '/error' }); expect(res.result.error.message).to.eql(msg); }); it('does not alter the 500 message if it was not provided', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/error' }); expect(res.result.error.message).to.eql('An internal server error occurred'); }); it('logs server errors by default', async () => { await server.register({ plugin: FormatError, options: {} }); await server.inject({ method: 'POST', url: '/error' }); expect(Util.log.called).to.be.true; }); it('does not log server errors if not enabled', async () => { await server.register({ plugin: FormatError, options: { logServerError: false } }); await server.inject({ method: 'POST', url: '/error' }); expect(Util.log.called).to.be.false; }); it('formats xor errors missing both parameters', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/xor', payload: {} }); expect(res.result.error.message).to.eql('one or two is required'); }); it('formats xor errors containing both parameters', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/xor', payload: { one: 'banana', two: 'strawberry' } }); expect(res.result.error.message).to.eql('either one or two is required, but not both'); }); it('formats nested xor errors missing both parameters', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/nested_xor', payload: { test: {} } }); expect(res.result.error.message).to.eql('test.one or test.two is required'); }); it('formats xor errors containing both parameters', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/nested_xor', payload: { test: { one: 'banana', two: 'strawberry' } } }); expect(res.result.error.message).to.eql('either test.one or test.two is required, but not both'); }); it('formats a single extraneous field correctly', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/no-params', payload: { bar: false } }); expect(res.result.error.message).to.eql('bar is not allowed'); }); it('formats multiple extraneous fields correctly', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/no-params', payload: { foo: true, bar: false } }); expect(res.result.error.message).to.eql('the following parameters are not allowed: foo, bar'); }); it('can override the language', async () => { await server.register({ plugin: FormatError, options: { language: { object: { unknown: { singular: 'blarf' } } } } }); const res = await server.inject({ method: 'POST', url: '/no-params', payload: { bar: false } }); expect(res.result.error.message).to.eql('blarf'); }); it('formats nested paths correctly', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/nested_paths', payload: { test: { one: 'two', two: 'two' } } }); expect(res.result.error.message).to.eql('"test.one" must be one of [one, 1]'); }); it('formats nested paths with invalid properties correctly', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/nested_paths', payload: { test: { ninety_nine: 'whoops!' } } }); expect(res.result.error.message).to.eql('test.ninety_nine is not allowed'); }); it('does not include code when permeate is false', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'GET', url: '/unauth', payload: {} }); expect(res.result.error.code).to.be.undefined; }); it('does include code when permeate is true', async () => { await server.register({ plugin: FormatError, options: { permeateErrorCode: true } }); const res = await server.inject({ method: 'GET', url: '/unauth', payload: {} }); expect(res.result.error.code).to.eql(new UnauthorizedUser().code); }); it('does not include code when permeate is true and code is not present', async () => { await server.register({ plugin: FormatError, options: { permeateErrorCode: true } }); const res = await server.inject({ method: 'GET', url: '/error', payload: {} }); expect(res.result.error.code).to.not.exist; }); it('formats multiple nested extraneous fields correctly', async () => { await server.register({ plugin: FormatError, options: {} }); const res = await server.inject({ method: 'POST', url: '/nested_paths', payload: { test: { foo: true, bar: false } } }); expect(res.result.error.message).to.eql('the following parameters are not allowed: test.foo, test.bar'); }); });