hapi-format-error
Version:
Structure Boom errors into a more desirable format
548 lines (466 loc) • 12.1 kB
JavaScript
'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');
});
});