ae88dc8548
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
413 lines
14 KiB
JavaScript
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();
|
|
});
|
|
});
|
|
});
|
|
});
|