Ghost/ghost/mw-error-handler/test/mw-error-handler.test.js
Daniel Lockyer ae88dc8548 Handled invalid timestamp format in filters
fix https://linear.app/tryghost/issue/SLO-85/fix-http-500-on-contentposts

- in the event we give the incorrect format in a filter, MySQL will
  throw an error and we'll throw a HTTP 500 error
- we can capture this error and return a more useful error to the user
- ideally we'd do this in a validation step before attempting the query,
  but parsing this out of NQL and detecting which columns are DATETIME
  could be quite tricky
2024-05-08 09:28:56 +02:00

413 lines
14 KiB
JavaScript

const path = require('path');
const assert = require('assert/strict');
const sinon = require('sinon');
const {InternalServerError, NotFoundError} = require('@tryghost/errors');
const {cacheControlValues} = require('@tryghost/http-cache-utils');
const {
prepareError,
jsonErrorRenderer,
handleHTMLResponse,
handleJSONResponse,
prepareErrorCacheControl,
prepareStack,
resourceNotFound,
pageNotFound
} = require('..');
describe('Prepare Error', function () {
it('Correctly prepares a non-Ghost error', function (done) {
prepareError(new Error('test!'), {}, {
set: () => {}
}, (err) => {
assert.equal(err.statusCode, 500);
assert.equal(err.name, 'InternalServerError');
assert.equal(err.message, 'An unexpected error occurred, please try again.');
assert.equal(err.context, 'test!');
assert.equal(err.code, 'UNEXPECTED_ERROR');
assert.ok(err.stack.startsWith('Error: test!'));
done();
});
});
it('Correctly prepares a Ghost error', function (done) {
prepareError(new InternalServerError({message: 'Handled Error', context: 'Details'}), {}, {
set: () => {}
}, (err) => {
assert.equal(err.statusCode, 500);
assert.equal(err.name, 'InternalServerError');
assert.equal(err.message, 'Handled Error');
assert.equal(err.context, 'Details');
assert.ok(err.stack.startsWith('InternalServerError: Handled Error'));
done();
});
});
it('Correctly prepares a 404 error', function (done) {
let error = {message: 'Oh dear', statusCode: 404};
prepareError(error, {}, {
set: () => {}
}, (err) => {
assert.equal(err.statusCode, 404);
assert.equal(err.name, 'NotFoundError');
assert.ok(err.stack.startsWith('NotFoundError: Resource could not be found'));
assert.equal(err.hideStack, true);
done();
});
});
it('Correctly prepares an error array', function (done) {
prepareError([new Error('test!')], {}, {
set: () => {}
}, (err) => {
assert.equal(err.statusCode, 500);
assert.equal(err.name, 'InternalServerError');
assert.ok(err.stack.startsWith('Error: test!'));
done();
});
});
it('Correctly prepares a handlebars error', function (done) {
let error = new Error('obscure handlebars message!');
error.stack += '\n';
error.stack += path.join('node_modules', 'handlebars', 'something');
prepareError(error, {}, {
set: () => {}
}, (err) => {
assert.equal(err.statusCode, 400);
assert.equal(err.name, 'IncorrectUsageError');
// TODO: consider if the message should be trusted here
assert.equal(err.message, 'obscure handlebars message!');
assert.ok(err.stack.startsWith('Error: obscure handlebars message!'));
done();
});
});
it('Correctly prepares an express-hbs error', function (done) {
let error = new Error('obscure express-hbs message!');
error.stack += '\n';
error.stack += path.join('node_modules', 'express-hbs', 'lib');
prepareError(error, {}, {
set: () => {}
}, (err) => {
assert.equal(err.statusCode, 400);
assert.equal(err.name, 'IncorrectUsageError');
assert.equal(err.message, 'obscure express-hbs message!');
assert.ok(err.stack.startsWith('Error: obscure express-hbs message!'));
done();
});
});
it('Correctly prepares a known ER_WRONG_VALUE mysql2 error', function (done) {
let error = new Error('select anything from anywhere where something = anything;');
error.stack += '\n';
error.stack += path.join('node_modules', 'mysql2', 'lib');
error.code = 'ER_WRONG_VALUE';
error.sql = 'select anything from anywhere where something = anything;';
error.sqlMessage = 'Incorrect DATETIME value: 3234234234';
prepareError(error, {}, {
set: () => {}
}, (err) => {
assert.equal(err.statusCode, 422);
assert.equal(err.name, 'ValidationError');
assert.equal(err.message, 'Invalid value');
assert.equal(err.code, 'ER_WRONG_VALUE');
assert.equal(err.sqlErrorCode, 'ER_WRONG_VALUE');
assert.equal(err.sql, 'select anything from anywhere where something = anything;');
assert.equal(err.sqlMessage, 'Incorrect DATETIME value: 3234234234');
done();
});
});
it('Correctly prepares an unknown mysql2 error', function (done) {
let error = new Error('select anything from anywhere where something = anything;');
error.stack += '\n';
error.stack += path.join('node_modules', 'mysql2', 'lib');
error.code = 'ER_BAD_FIELD_ERROR';
error.sql = 'select anything from anywhere where something = anything;';
error.sqlMessage = 'Incorrect value: erororoor';
prepareError(error, {}, {
set: () => {}
}, (err) => {
assert.equal(err.statusCode, 500);
assert.equal(err.name, 'InternalServerError');
assert.equal(err.message, 'An unexpected error occurred, please try again.');
assert.equal(err.code, 'UNEXPECTED_ERROR');
assert.equal(err.sqlErrorCode, 'ER_BAD_FIELD_ERROR');
assert.equal(err.sql, 'select anything from anywhere where something = anything;');
assert.equal(err.sqlMessage, 'Incorrect value: erororoor');
done();
});
});
});
describe('Prepare Stack', function () {
it('Correctly prepares the stack for an error', function (done) {
prepareStack(new Error('test!'), {}, {}, (err) => {
// Includes "Stack Trace" text prepending human readable trace
assert.ok(err.stack.startsWith('Error: test!\nStack Trace:'));
done();
});
});
});
describe('Prepare Error Cache Control', function () {
it('Sets private cache control by default', function (done) {
const res = {
set: sinon.spy()
};
prepareErrorCacheControl()(new Error('generic error'), {}, res, () => {
assert(res.set.calledOnce);
assert(res.set.calledWith({
'Cache-Control': cacheControlValues.private
}));
done();
});
});
it('Sets private cache-control header for user-specific 404 responses', function (done) {
const req = {
method: 'GET',
get: (header) => {
if (header === 'authorization') {
return 'Basic YWxhZGRpbjpvcGVuc2VzYW1l';
}
}
};
const res = {
set: sinon.spy()
};
prepareErrorCacheControl()(new NotFoundError(), req, res, () => {
assert(res.set.calledOnce);
assert(res.set.calledWith({
'Cache-Control': cacheControlValues.private
}));
done();
});
});
it('Sets noCache cache-control header for non-user-specific 404 responses', function (done) {
const req = {
method: 'GET',
get: () => {
return false;
}
};
const res = {
set: sinon.spy(),
get: () => {
return false;
}
};
prepareErrorCacheControl()(new NotFoundError(), req, res, () => {
assert(res.set.calledOnce);
assert(res.set.calledWith({
'Cache-Control': cacheControlValues.noCacheDynamic
}));
done();
});
});
});
describe('Error renderers', function () {
it('Renders JSON', function (done) {
jsonErrorRenderer(new Error('test!'), {}, {
json: (data) => {
assert.equal(data.errors.length, 1);
assert.equal(data.errors[0].message, 'test!');
done();
}
}, () => {});
});
it('Handles unknown errors when preparing user message', function (done) {
jsonErrorRenderer(new RangeError('test!'), {
frameOptions: {
docName: 'oembed',
method: 'read'
}
}, {
json: (data) => {
assert.equal(data.errors.length, 1);
assert.equal(data.errors[0].message, 'Unknown error - RangeError, cannot read oembed.');
assert.equal(data.errors[0].context, 'test!');
done();
}
}, () => {});
});
it('Uses templates when required', function (done) {
jsonErrorRenderer(new InternalServerError({
message: 'test!'
}), {
frameOptions: {
docName: 'blog',
method: 'browse'
}
}, {
json: (data) => {
assert.equal(data.errors.length, 1);
assert.equal(data.errors[0].message, 'Internal server error, cannot list blog.');
assert.equal(data.errors[0].context, 'test!');
done();
}
}, () => {});
});
it('Uses defined message + context when available', function (done) {
jsonErrorRenderer(new InternalServerError({
message: 'test!',
context: 'Image was too large.'
}), {
frameOptions: {
docName: 'images',
method: 'upload'
}
}, {
json: (data) => {
assert.equal(data.errors.length, 1);
assert.equal(data.errors[0].message, 'Internal server error, cannot upload image.');
assert.equal(data.errors[0].context, 'test! Image was too large.');
done();
}
}, () => {});
});
it('Exports the HTML renderer', function () {
const renderer = handleHTMLResponse({
errorHandler: () => {}
});
assert.equal(renderer.length, 4);
});
it('Exports the JSON renderer', function () {
const renderer = handleJSONResponse({
errorHandler: () => {}
});
assert.equal(renderer.length, 5);
});
});
describe('Resource Not Found', function () {
it('Returns 404 Not Found Error for a generic case', function (done) {
resourceNotFound({}, {}, (error) => {
assert.equal(error.statusCode, 404);
assert.equal(error.message, 'Resource not found');
done();
});
});
it('Returns 406 Request Not Acceptable Error for invalid version', function (done) {
const req = {
headers: {
'accept-version': 'foo'
}
};
const res = {
locals: {
safeVersion: '4.3'
}
};
resourceNotFound(req, res, (error) => {
assert.equal(error.statusCode, 400);
assert.equal(error.message, 'Requested version is not supported.');
done();
});
});
it('Returns 406 Request Not Acceptable Error for when requested version is behind current version', function (done) {
const req = {
headers: {
'accept-version': 'v3.9'
}
};
const res = {
locals: {
safeVersion: '4.3'
}
};
resourceNotFound(req, res, (error) => {
assert.equal(error.statusCode, 406);
assert.equal(error.message, 'Request could not be served, the endpoint was not found.');
assert.equal(error.context, 'Provided client accept-version v3.9 is behind current Ghost version v4.3.');
assert.equal(error.help, 'Try upgrading your Ghost API client.');
done();
});
});
it('Returns 406 Request Not Acceptable Error for when requested version is ahead current version', function (done) {
const req = {
headers: {
'accept-version': 'v4.8'
}
};
const res = {
locals: {
safeVersion: '4.3'
}
};
resourceNotFound(req, res, (error) => {
assert.equal(error.statusCode, 406);
assert.equal(error.message, 'Request could not be served, the endpoint was not found.');
assert.equal(error.context, 'Provided client accept-version v4.8 is ahead of current Ghost version v4.3.');
assert.equal(error.help, 'Try upgrading your Ghost install.');
done();
});
});
it('Returns 404 Not Found Error for when requested version is the same as current version', function (done) {
const req = {
headers: {
'accept-version': 'v4.3'
}
};
const res = {
locals: {
safeVersion: '4.3'
}
};
resourceNotFound(req, res, (error) => {
assert.equal(error.statusCode, 404);
assert.equal(error.message, 'Resource not found');
done();
});
});
describe('pageNotFound', function () {
it('returns 404 with special message when message not set', function (done) {
pageNotFound({}, {}, (error) => {
assert.equal(error.statusCode, 404);
assert.equal(error.message, 'Page not found');
done();
});
});
it('returns 404 with special message even if message is set', function (done) {
pageNotFound({message: 'uh oh'}, {}, (error) => {
assert.equal(error.statusCode, 404);
assert.equal(error.message, 'Page not found');
done();
});
});
});
});