import ApiError, { ExtendableError, DetailedError, ResourceNotFoundError } from 'commonjs-errors';
import request from 'supertest';
import { Request, Response, NextFunction, Router, RequestHandler } from 'express';
import { forwardError } from 'express-forward-error';
import * as OpenApiValidator from 'express-openapi-validator';
import { API } from '../constants';
import App from '../index';
import { join } from 'path';
const appConfig = {
router: Router(),
logger: { logLevel: 'error', logStyle: 'cli', appName: 'nodejs-postgres-base', moduleName: 'App' },
ignoredAccessLogPaths: '',
openapiBaseSchema: join(__dirname, 'api.schema.yml'),
env: 'development',
};
// This acts as sort of a Application factory for tests
export const app = new App(appConfig).init();
describe('Service', () => {
it(`should expose health check endpoint on ${API.HEALTH_CHECK}`, async () => {
// when
const response = await request(app).get(API.HEALTH_CHECK);
// then
expect(response.statusCode).toBe(200);
expect(response.text).toBe('OK');
});
it(`should expose ready check endpoint on ${API.READY_CHECK}`, async () => {
// when
const response = await request(app).get(API.READY_CHECK);
// then
expect(response.statusCode).toBe(200);
expect(response.text).toBe('OK');
});
it(`should return 503 from ${API.READY_CHECK} when not ready`, async () => {
// when
const appInstance = new App(appConfig);
const appNotReady = appInstance.init();
// simulate signal SIGTERM received
appInstance.shutdown();
const response = await request(appNotReady).get(API.READY_CHECK);
// then
expect(response.statusCode).toBe(503);
expect(response.text).toBe('Service Unavailable');
});
it(`should expose OpenAPI specification on ${API.SWAGGER_UI}`, async () => {
// when
const response = await request(app).get(`${API.SWAGGER_UI}/`);
// then
expect(response.statusCode).toBe(200);
expect(response.headers['content-type']).toBe('text/html; charset=utf-8');
expect(response.text).toContain('
Swagger UI');
});
it('should return 404 on /invalid/resource', async () => {
// when
const response = await request(app).get('/invalid/resource');
// then
expect(response.statusCode).toBe(404);
expect(response.body.message).toEqual('not found');
});
it("should transform openapi validators' BadRequest error to API error", async () => {
// given
const extendedApp = new App(appConfig);
const expectedMessage = 'Bad Request';
const openapiValidationError = new OpenApiValidator.error.BadRequest({
path: '/test',
message: expectedMessage,
});
extendedApp.app.get('/test', async (_req: Request, _res: Response, next: NextFunction) => {
next(openapiValidationError);
});
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(400);
expect(response.body.message).toBe(expectedMessage);
});
it("should transform openapi validators' Unauthorized error to API error", async () => {
// given
const extendedApp = new App(appConfig);
const expectedMessage = 'Test endpoint method unauthorized message';
const openapiValidationError = new OpenApiValidator.error.Unauthorized({
path: '/test',
message: expectedMessage,
});
extendedApp.app.get('/test', async (_req: Request, _res: Response, next: NextFunction) => {
next(openapiValidationError);
});
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(401);
expect(response.body.message).toBe(expectedMessage);
});
it("should transform openapi validators' MethodNotAllowed error to API error", async () => {
// given
const extendedApp = new App(appConfig);
const expectedMessage = 'Test endpoint method not allowed message';
const openapiValidationError = new OpenApiValidator.error.MethodNotAllowed({
path: '/test',
message: expectedMessage,
});
extendedApp.app.get('/test', async (_req: Request, _res: Response, next: NextFunction) => {
next(openapiValidationError);
});
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(405);
expect(response.body.message).toBe(expectedMessage);
});
it('should handle APIError', async () => {
// given
const extendedApp = new App(appConfig);
const stackError = new Error('Original cause of the test APIError');
const apiError = new ApiError(
'This error was produced by service.test.ts',
{ code: 'error.some-error', isPublic: false },
stackError,
);
extendedApp.app.get('/test', async (_req: Request, _res: Response, next: NextFunction) => {
next(apiError);
});
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(500);
expect(response.body.message).toBe('An unexpected error has occurred');
expect(response.body.code).toBe('error.some-error');
});
it('should handle APIError marked as public', async () => {
// given
const extendedApp = new App(appConfig);
const stackError = new Error('Original cause of the test APIError');
const apiError = new ApiError(
'This error was produced by service.test.ts',
{ code: 'error.some-error', isPublic: true, status: 405 },
stackError,
);
extendedApp.app.get('/test', async (_req: Request, _res: Response, next: NextFunction) => {
next(apiError);
});
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(405);
expect(response.body.message).toBe('This error was produced by service.test.ts');
expect(response.body.code).toBe('error.some-error');
});
it('should handle APIError marked with default parameters', async () => {
// given
const extendedApp = new App(appConfig);
const apiError = new ApiError('This error was produced by service.test.ts', {});
extendedApp.app.get('/test', async (_req: Request, _res: Response, next: NextFunction) => {
next(apiError);
});
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(500);
expect(response.body.message).toBe('An unexpected error has occurred');
expect(response.body.code).toBe('error.unexpected');
});
it('should handle DetailedError', async () => {
// given
const extendedApp = new App(appConfig);
extendedApp.app.get('/test', async (_req: Request, _res: Response, next: NextFunction) => {
const stackError = new Error('Original cause of the test StatusCodeError');
const statusCodeError = new DetailedError(
'This error was produced by service.test.ts',
[],
{
status: 500,
code: '10',
isPublic: true,
},
stackError,
);
next(statusCodeError);
});
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(500);
expect(response.body.message).toBe('This error was produced by service.test.ts');
expect(response.body.code).toBe('10');
});
it('should handle DetailedError marked as private', async () => {
// given
const extendedApp = new App(appConfig);
extendedApp.app.get('/test', async (_req: Request, _res: Response, next: NextFunction) => {
next(
new DetailedError('This error was produced by service.test.ts', [], {
status: 500,
code: '10',
isPublic: false,
}),
);
});
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(500);
expect(response.body.message).toBe('An unexpected error has occurred');
expect(response.body.code).toBe('10');
});
it('should handle DetailedError with default parameters', async () => {
// given
const extendedApp = new App(appConfig);
extendedApp.app.get('/test', async (_req: Request, _res: Response, next: NextFunction) => {
next(new DetailedError('This error was produced by service.test.ts', [], { code: '10' }));
});
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(500);
expect(response.body.message).toBe('An unexpected error has occurred');
expect(response.body.code).toBe('10');
});
it('should handle ExtendableError', async () => {
// given
const extendedApp = new App(appConfig);
const extendableError = new ExtendableError('This error was produced by service.test.ts', 'extendable-error');
extendedApp.app.get('/test', async (_req: Request, _res: Response, next: NextFunction) => {
next(extendableError);
});
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(500);
expect(response.body.message).toBe('An unexpected error has occurred');
expect(response.body.code).toBe('extendable-error');
});
it('should handle children of ApiError', async () => {
// given
const extendedApp = new App(appConfig);
const extendableError = new ResourceNotFoundError('TestId');
extendedApp.app.get(
'/test',
forwardError(async () => {
throw extendableError;
}) as RequestHandler,
);
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(404);
expect(response.body.message).toBe('Requested resource with ID: TestId was not found');
expect(response.body.code).toBe('error.resource.not-found');
});
it('should handle Error', async () => {
// given
const extendedApp = new App(appConfig);
const extendableError = new Error('This error was produced by service.test.ts');
extendedApp.app.get(
'/test',
forwardError(async () => {
throw extendableError;
}) as RequestHandler,
);
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(500);
expect(response.body.message).toBe('An unexpected error has occurred');
expect(response.body.code).toBe('error.unexpected');
});
it('should skip error handling when headers were already sent', async () => {
// given
const extendedApp = new App(appConfig);
const extendableError = new Error('This error was produced by service.test.ts');
extendedApp.app.get(
'/test',
forwardError(async (_req: Request, res: Response) => {
res.send({ message: 'ok' }).status(200);
throw extendableError;
}) as RequestHandler,
);
// when
const response = await request(extendedApp.init()).get('/test');
expect(response.status).toBe(200);
expect(response.body.message).toBe('ok');
});
it('should handle SyntaxError', async () => {
// given
const invalidJsonPayload = `{"message":}`;
const extendedApp = new App(appConfig);
extendedApp.app.post('/test', async (_req: Request, res: Response) => {
res.send('ok');
});
// when
const response = await request(extendedApp.init())
.post('/test')
.set('Content-Type', 'application/json')
.send(invalidJsonPayload);
expect(response.status).toBe(400);
expect(response.body.message).toBe('Unexpected token } in JSON at position 11');
expect(response.body.code).toBe('error.request.invalid');
});
it('should allow you to override default body parsers', async () => {
let isFunctionCalled = false;
const customBodyParser = () => {
isFunctionCalled = true;
};
const appWithoutCustomParser = new App({ ...appConfig }) as any;
expect(isFunctionCalled).toBe(false);
console.log(appWithoutCustomParser);
expect(appWithoutCustomParser.app._router.stack.length).toBeTruthy();
const newApp = new App({ ...appConfig, customBodyParser }) as any;
expect(isFunctionCalled).toBe(true);
expect(newApp.app._router).toBeUndefined();
});
});