UNPKG

@whook/whook

Version:

Build strong and efficient REST web services.

1,755 lines (1,687 loc) 140 kB
/* eslint max-nested-callbacks: 0 */ import { describe, test, beforeEach, jest, expect } from '@jest/globals'; import streamtest from 'streamtest'; import { YHTTPError } from 'yhttperror'; import { YError } from 'yerror'; import initHTTPRouter from './httpRouter.js'; import initSchemaValidators from './schemaValidators.js'; import initQueryParserBuilder from './queryParserBuilder.js'; import { NodeEnv } from 'application-services'; import initErrorHandler from './errorHandler.js'; import { type OpenAPIParameter, type OpenAPIExtension, } from 'ya-open-api-types'; import { type WhookHTTPTransactionService } from './httpTransaction.js'; import { type IncomingMessage, type ServerResponse } from 'node:http'; import { type Readable } from 'node:stream'; import { type LogService } from 'common-services'; import { type AppEnvVars } from 'application-services'; import { type ExpressiveJSONSchema } from 'ya-json-schema-types'; import { type WhookRouteHandler } from '../types/routes.js'; import { type WhookResponse } from '../types/http.js'; import { type WhookSchemaValidatorsService } from './schemaValidators.js'; import { type WhookOpenAPI } from '../types/openapi.js'; import { type WhookDefinitions } from './DEFINITIONS.js'; async function waitResponse(response: WhookResponse, raw = true) { if (!response.body) { return response; } const [stream, resultPromise] = streamtest.toText(); (response.body as Readable).pipe(stream); const text = await resultPromise; return Object.assign({}, response, { body: raw ? text : JSON.parse(text), }); } function prepareTransaction(result: unknown = Promise.resolve()) { const handler = jest.fn(() => null === result ? undefined : result ? Promise.resolve(result) : Promise.reject(new YError('E_NOT_SUPPOSED_TO_BE_HERE')), ) as WhookRouteHandler & ReturnType<typeof jest.fn>; const httpTransactionStart = jest.fn(async (buildResponse: () => void) => buildResponse(), ); const httpTransactionCatch = jest.fn(async (err) => { throw YHTTPError.cast(err as Error); }); const httpTransactionEnd = jest.fn( async (_res: unknown) => _res && undefined, ); const httpTransaction = jest.fn(async (req: IncomingMessage) => ({ request: { url: req.url, method: (req.method as string).toLowerCase(), headers: req.headers, body: req, }, transaction: { start: httpTransactionStart, catch: httpTransactionCatch, end: httpTransactionEnd, }, })); const ROUTES_HANDLERS = { ping: handler, headUserAvatar: handler, getUserAvatar: handler, putUserAvatar: handler, deleteUserAvatar: handler, getUser: handler, putUser: handler, }; return { ROUTES_HANDLERS, httpTransaction: httpTransaction as unknown as WhookHTTPTransactionService, httpTransactionStart, httpTransactionCatch, httpTransactionEnd, handler, }; } describe('initHTTPRouter', () => { const ENV: AppEnvVars = { NODE_ENV: NodeEnv.Test }; const DEBUG_NODE_ENVS = ['test']; const BASE_PATH = '/v1'; const API: WhookOpenAPI = { openapi: '3.1.0', info: { version: '1.0.0', title: 'Sample Swagger', description: 'A sample Swagger file for testing purpose.', }, servers: [ { url: `http://{host}:{port}{basePath}`, variables: { host: { default: 'localhost:1337', }, basePath: { default: '/v1', }, }, }, ], paths: { '/ping': { head: { operationId: 'ping', summary: "Checks API's availability.", responses: { default: { $ref: '#/components/responses/UnexpectedError', }, '200': { description: 'Pong', content: { 'application/json': { schema: { type: 'object', additionalProperties: false, properties: { pong: { type: 'string', enum: ['pong'], }, }, }, }, }, }, }, }, }, '/users/{userId}/avatar': { parameters: [ { in: 'path', name: 'userId', required: true, schema: { $ref: '#/components/schemas/UserIdSchema', }, }, { in: 'query', name: 'forFriendsUserId', required: false, schema: { type: 'array', items: { $ref: '#/components/schemas/UserIdSchema', }, }, }, { in: 'header', name: 'x-depth', required: false, schema: { type: 'number', enum: [0, 1, 2], }, }, ], head: { operationId: 'headUserAvatar', summary: "Checks user's avatar existence.", responses: { '200': { description: 'User avatar exists.', }, '404': { description: 'User avatar not found', }, }, }, get: { operationId: 'getUserAvatar', summary: "Retrieve user's avatar.", responses: { default: { $ref: '#/components/responses/UnexpectedError', }, '200': { description: 'User avatar found.', content: { 'image/jpeg': { schema: { type: 'string', format: 'binary', } as unknown as ExpressiveJSONSchema, }, }, }, '404': { description: 'User avatar not found', }, }, }, put: { operationId: 'putUserAvatar', summary: "Set user's avatar.", parameters: [ { in: 'header', name: 'content-type', required: true, schema: { $ref: '#/components/schemas/ContentType', }, }, { name: 'x-file-name', in: 'header', required: true, schema: { type: 'string' }, }, { name: 'x-file-size', in: 'header', required: true, schema: { type: 'number' }, }, { name: 'x-file-type', in: 'header', required: true, schema: { type: 'string' }, }, ], requestBody: { description: 'The input sentence', required: true, content: { 'image/jpeg': { schema: { $ref: '#/components/schemas/BinaryPayload', }, }, 'image/png': { schema: { $ref: '#/components/schemas/BinaryPayload', }, }, }, }, responses: { default: { $ref: '#/components/responses/UnexpectedError', }, '200': { description: 'User avatar set.', content: { 'image/jpeg': { schema: { type: 'string', format: 'binary', } as unknown as ExpressiveJSONSchema, }, }, }, '404': { description: 'User not found', }, }, }, delete: { operationId: 'deleteUserAvatar', summary: "Ensure user's avatar is gone.", responses: { default: { $ref: '#/components/responses/UnexpectedError', }, '410': { description: 'User avatar is gone.', }, }, }, }, '/users/{userId}': { get: { operationId: 'getUser', summary: 'Retrieve a user.', parameters: [ { in: 'path', name: 'userId', required: true, schema: { $ref: '#/components/schemas/UserIdSchema', }, }, { in: 'query', name: 'extended', required: true, schema: { type: 'boolean', }, }, { in: 'query', name: 'archived', schema: { type: 'boolean', }, }, ], responses: { default: { $ref: '#/components/responses/UnexpectedError', }, '200': { description: 'User found', content: { 'application/json': { schema: { type: 'object', properties: { id: { type: 'number', }, name: { type: 'string', }, }, }, }, 'text/plain': { schema: { type: 'string' }, }, }, }, '404': { description: 'User not found', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error', }, }, }, }, }, }, put: { operationId: 'putUser', summary: 'Upsert a user.', requestBody: { description: 'The input user', required: true, content: { 'application/json': { schema: { $ref: '#/components/schemas/User', }, }, 'application/vnd.github+json': { schema: { $ref: '#/components/schemas/User', }, }, }, }, parameters: [ { $ref: '#/components/parameters/UserId' }, { in: 'header', name: 'Authorization', required: true, schema: { type: 'string', }, }, { in: 'header', name: 'Content-Type', required: true, schema: { type: 'string', }, }, ], responses: { default: { $ref: '#/components/responses/UnexpectedError', }, '200': { description: 'User updated', content: { 'application/json': { schema: { type: 'object', properties: { id: { type: 'number', }, name: { type: 'string', }, }, }, }, }, }, '400': { $ref: '#/components/responses/BadRequest', }, }, }, }, }, components: { responses: { BadRequest: { description: 'Bad request', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error', }, }, }, }, UnexpectedError: { description: 'Unexpected error', content: { 'application/json': { schema: { $ref: '#/components/schemas/Error', }, }, }, }, }, parameters: { UserId: { in: 'path', name: 'userId', required: true, schema: { $ref: '#/components/schemas/UserIdSchema', }, }, }, schemas: { BinaryPayload: { type: 'string', format: 'binary', } as unknown as ExpressiveJSONSchema, UserIdSchema: { type: 'number', minimum: 0, multipleOf: 1, }, Error: { type: 'object', properties: { transactionId: { type: 'string', }, code: { type: 'string', pattern: '^E_[a-zA-Z0-9_]+$', }, }, }, User: { type: 'object', additionalProperties: false, required: ['name'], properties: { name: { type: 'string', }, tree: { $ref: '#/components/schemas/Recursive', }, }, }, ContentType: { type: 'string', }, Recursive: { type: 'object', required: ['id', 'children'], additionalProperties: false, properties: { id: { type: 'string' }, children: { type: 'array', items: { $ref: '#/components/schemas/Recursive' }, }, }, }, }, }, }; const DEFINITIONS = { paths: API.paths, components: API.components, security: API.security, configs: {}, } as WhookDefinitions; const log = jest.fn<LogService>(); const fakeValidator = () => true; const schemaValidators = (() => fakeValidator) as unknown as WhookSchemaValidatorsService; const res = {} as unknown as ServerResponse; beforeEach(() => { log.mockReset(); }); test('should work', async () => { const { httpTransaction, ROUTES_HANDLERS } = prepareTransaction(); const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); const httpRouter = await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API, DEFINITIONS, BASE_PATH, log, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); expect(typeof httpRouter.service).toEqual('function'); expect(httpRouter.fatalErrorPromise).toBeInstanceOf(Promise); expect(log.mock.calls).toMatchInlineSnapshot(` [ [ "warning", "⌨️ - Initializing the basic query parser.", ], [ "debug", "🚦 - HTTP Router initialized.", ], ] `); }); describe('should fail', () => { test('when the API parsing fails', async () => { try { const { httpTransaction, ROUTES_HANDLERS } = prepareTransaction(); const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API: { info: API.info, paths: { '/lol': { get: {}, }, }, } as unknown as WhookOpenAPI, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); throw new YError('E_UNEXPECTED_SUCCESS'); } catch (err) { expect(err).toMatchInlineSnapshot( `[YError: E_NO_OPERATION_ID (["/lol","get"]): E_NO_OPERATION_ID]`, ); } }); test('when operation id is lacking', async () => { try { const { httpTransaction, ROUTES_HANDLERS } = prepareTransaction(); const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API: { openapi: API.openapi, servers: API.servers, info: API.info, paths: { '/lol': { get: {} as never, }, }, }, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); throw new YError('E_UNEXPECTED_SUCCESS'); } catch (err) { expect(err).toMatchInlineSnapshot( `[YError: E_NO_OPERATION_ID (["/lol","get"]): E_NO_OPERATION_ID]`, ); } }); test('when operation path is bad', async () => { try { const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const { httpTransaction, ROUTES_HANDLERS } = prepareTransaction(); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API: { openapi: API.openapi, servers: API.servers, info: API.info, paths: { ['lol' as '/lol']: { get: {} as never, }, }, }, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); throw new YError('E_UNEXPECTED_SUCCESS'); } catch (err) { expect(err).toMatchInlineSnapshot( `[YError: E_BAD_PATH (["lol"]): E_BAD_PATH]`, ); } }); test('when a path parameter is lacking', async () => { try { const { httpTransaction, handler } = prepareTransaction(); const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS: { lol: handler, }, API: { openapi: API.openapi, servers: API.servers, info: API.info, paths: { '/{lol}': { get: { operationId: 'lol', } as never, }, }, }, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); throw new YError('E_UNEXPECTED_SUCCESS'); } catch (err) { expect(err).toMatchInlineSnapshot( `[YError: E_UNDECLARED_PATH_PARAMETER (["/v1/{lol}","{lol}"]): E_UNDECLARED_PATH_PARAMETER]`, ); } }); test('when operation handler is lacking', async () => { try { const { httpTransaction } = prepareTransaction(); const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS: {}, API, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); throw new YError('E_UNEXPECTED_SUCCESS'); } catch (err) { expect(err).toMatchInlineSnapshot( `[YError: E_NO_HANDLER (["ping"]): E_NO_HANDLER]`, ); } }); test('when an operation has no name', async () => { try { const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const { httpTransaction, ROUTES_HANDLERS } = prepareTransaction(); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API: { openapi: API.openapi, servers: API.servers, info: API.info, paths: { '/lol': { get: { operationId: 'ping', parameters: [ { in: 'query', schema: { type: 'string', }, } as OpenAPIParameter< ExpressiveJSONSchema, OpenAPIExtension >, ], } as never, }, }, }, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); throw new YError('E_UNEXPECTED_SUCCESS'); } catch (err) { expect(err).toMatchInlineSnapshot( `[YError: E_BAD_PARAMETER_NAME ([{"in":"query","schema":{"type":"string"}}]): E_BAD_PARAMETER_NAME]`, ); } }); test('when an operation has no schema', async () => { try { const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const { httpTransaction, ROUTES_HANDLERS } = prepareTransaction(); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API: { openapi: API.openapi, servers: API.servers, info: API.info, paths: { '/lol': { get: { operationId: 'ping', parameters: [ { name: 'lol', in: 'query', } as OpenAPIParameter< ExpressiveJSONSchema, OpenAPIExtension >, ], } as never, }, }, }, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); throw new YError('E_UNEXPECTED_SUCCESS'); } catch (err) { expect(err).toMatchInlineSnapshot( `[YError: E_PARAMETER_WITHOUT_SCHEMA (["lol"]): E_PARAMETER_WITHOUT_SCHEMA]`, ); } }); test('when an operation has no in value', async () => { try { const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const { httpTransaction, ROUTES_HANDLERS } = prepareTransaction(); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API: { openapi: API.openapi, servers: API.servers, info: API.info, paths: { '/lol': { get: { operationId: 'ping', parameters: [ { name: 'lol', schema: { type: 'string', }, } as OpenAPIParameter< ExpressiveJSONSchema, OpenAPIExtension >, ], } as never, }, }, }, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); throw new YError('E_UNEXPECTED_SUCCESS'); } catch (err) { expect(err).toMatchInlineSnapshot( `[YError: E_UNSUPPORTED_PARAMETER_DEFINITION (["lol","in",null]): E_UNSUPPORTED_PARAMETER_DEFINITION]`, ); } }); }); describe('httpRouter', () => { describe('HEAD', () => { test('should work with an existing route', async () => { const { httpTransaction, httpTransactionStart, httpTransactionCatch, httpTransactionEnd, handler, ROUTES_HANDLERS, } = prepareTransaction({ status: 200, headers: { 'content-type': 'image/jpeg', }, }); const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); const httpRouter = await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); const req = { method: 'HEAD', url: '/v1/users/1/avatar', headers: {}, } as IncomingMessage; log.mockReset(); await httpRouter.service(req, res); expect(handler).toHaveBeenCalled(); expect(httpTransaction).toHaveBeenCalled(); expect(httpTransactionStart).toHaveBeenCalled(); expect(httpTransactionCatch).not.toHaveBeenCalled(); expect(httpTransactionEnd).toHaveBeenCalled(); const response = await waitResponse( httpTransactionEnd.mock.calls[0][0] as WhookResponse, ); expect({ response, handlerCalls: handler.mock.calls, }).toMatchInlineSnapshot(` { "handlerCalls": [ [ { "body": undefined, "cookies": {}, "headers": { "x-depth": undefined, }, "path": { "userId": 1, }, "query": { "forFriendsUserId": undefined, }, }, { "config": {}, "method": "head", "operation": { "operationId": "headUserAvatar", "responses": { "200": { "description": "User avatar exists.", }, "404": { "description": "User avatar not found", }, }, "summary": "Checks user's avatar existence.", }, "path": "/v1/users/1/avatar", }, ], ], "response": { "headers": { "content-type": "image/jpeg", }, "status": 200, }, } `); }); test('should work with an existing GET route', async () => { const { httpTransaction, httpTransactionStart, httpTransactionCatch, httpTransactionEnd, handler, ROUTES_HANDLERS, } = prepareTransaction({ status: 200, headers: { 'content-type': 'application/json', }, body: { id: 1, name: 'John Doe', }, }); const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); const httpRouter = await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); const req = { method: 'HEAD', url: '/v1/users/1?extended=true', headers: {}, } as IncomingMessage; log.mockReset(); await httpRouter.service(req, res); expect(handler).toHaveBeenCalled(); expect(httpTransaction).toHaveBeenCalled(); expect(httpTransactionStart).toHaveBeenCalled(); expect(httpTransactionCatch).not.toHaveBeenCalled(); expect(httpTransactionEnd).toHaveBeenCalled(); const response = await waitResponse( httpTransactionEnd.mock.calls[0][0] as WhookResponse, ); expect({ response, handlerCalls: handler.mock.calls, }).toMatchInlineSnapshot(` { "handlerCalls": [ [ { "body": undefined, "cookies": {}, "headers": {}, "path": { "userId": 1, }, "query": { "archived": undefined, "extended": true, }, }, { "config": {}, "method": "head", "operation": { "operationId": "getUser", "parameters": [ { "in": "path", "name": "userId", "required": true, "schema": { "$ref": "#/components/schemas/UserIdSchema", }, }, { "in": "query", "name": "extended", "required": true, "schema": { "type": "boolean", }, }, { "in": "query", "name": "archived", "schema": { "type": "boolean", }, }, ], "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "id": { "type": "number", }, "name": { "type": "string", }, }, "type": "object", }, }, "text/plain": { "schema": { "type": "string", }, }, }, "description": "User found", }, "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error", }, }, }, "description": "User not found", }, "default": { "$ref": "#/components/responses/UnexpectedError", }, }, "summary": "Retrieve a user.", }, "path": "/v1/users/1", }, ], ], "response": { "body": undefined, "headers": { "content-type": "application/json", }, "status": 200, }, } `); }); test('should work with a */* accept header', async () => { const { httpTransaction, httpTransactionStart, httpTransactionCatch, httpTransactionEnd, handler, ROUTES_HANDLERS, } = prepareTransaction({ status: 200, headers: { 'content-type': 'application/json', }, body: { id: 1, name: 'John Doe', }, }); const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); const httpRouter = await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); const req = { method: 'HEAD', url: '/v1/users/1?extended=true', headers: { accept: 'text/html,image/webp,image/png,*/*;q=0.8', }, } as IncomingMessage; log.mockReset(); await httpRouter.service(req, res); expect(handler).toHaveBeenCalled(); expect(httpTransaction).toHaveBeenCalled(); expect(httpTransactionStart).toHaveBeenCalled(); expect(httpTransactionCatch).not.toHaveBeenCalled(); expect(httpTransactionEnd).toHaveBeenCalled(); const response = await waitResponse( httpTransactionEnd.mock.calls[0][0] as WhookResponse, ); expect({ response, handlerCalls: handler.mock.calls, }).toMatchInlineSnapshot(` { "handlerCalls": [ [ { "body": undefined, "cookies": {}, "headers": {}, "path": { "userId": 1, }, "query": { "archived": undefined, "extended": true, }, }, { "config": {}, "method": "head", "operation": { "operationId": "getUser", "parameters": [ { "in": "path", "name": "userId", "required": true, "schema": { "$ref": "#/components/schemas/UserIdSchema", }, }, { "in": "query", "name": "extended", "required": true, "schema": { "type": "boolean", }, }, { "in": "query", "name": "archived", "schema": { "type": "boolean", }, }, ], "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "id": { "type": "number", }, "name": { "type": "string", }, }, "type": "object", }, }, "text/plain": { "schema": { "type": "string", }, }, }, "description": "User found", }, "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error", }, }, }, "description": "User not found", }, "default": { "$ref": "#/components/responses/UnexpectedError", }, }, "summary": "Retrieve a user.", }, "path": "/v1/users/1", }, ], ], "response": { "body": undefined, "headers": { "content-type": "application/json", }, "status": 200, }, } `); }); }); describe('GET', () => { describe('should work', () => { test('with an existing stringified route', async () => { const { httpTransaction, httpTransactionStart, httpTransactionCatch, httpTransactionEnd, handler, ROUTES_HANDLERS, } = prepareTransaction({ status: 200, headers: { 'content-type': 'application/json', }, body: { id: 1, name: 'John Doe', }, }); const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); const httpRouter = await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); const req = { method: 'GET', url: '/v1/users/1?extended=true', headers: {}, } as IncomingMessage; log.mockReset(); await httpRouter.service(req, res); expect(handler).toHaveBeenCalled(); expect(httpTransaction).toHaveBeenCalled(); expect(httpTransactionStart).toHaveBeenCalled(); expect(httpTransactionCatch).not.toHaveBeenCalled(); expect(httpTransactionEnd).toHaveBeenCalled(); const response = await waitResponse( httpTransactionEnd.mock.calls[0][0] as WhookResponse, ); expect({ response, handlerCalls: handler.mock.calls, }).toMatchInlineSnapshot(` { "handlerCalls": [ [ { "body": undefined, "cookies": {}, "headers": {}, "path": { "userId": 1, }, "query": { "archived": undefined, "extended": true, }, }, { "config": {}, "method": "get", "operation": { "operationId": "getUser", "parameters": [ { "in": "path", "name": "userId", "required": true, "schema": { "$ref": "#/components/schemas/UserIdSchema", }, }, { "in": "query", "name": "extended", "required": true, "schema": { "type": "boolean", }, }, { "in": "query", "name": "archived", "schema": { "type": "boolean", }, }, ], "responses": { "200": { "content": { "application/json": { "schema": { "properties": { "id": { "type": "number", }, "name": { "type": "string", }, }, "type": "object", }, }, "text/plain": { "schema": { "type": "string", }, }, }, "description": "User found", }, "404": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Error", }, }, }, "description": "User not found", }, "default": { "$ref": "#/components/responses/UnexpectedError", }, }, "summary": "Retrieve a user.", }, "path": "/v1/users/1", }, ], ], "response": { "body": "{"id":1,"name":"John Doe"}", "headers": { "content-type": "application/json", }, "status": 200, }, } `); }); test('with an existing streamed route', async () => { const { httpTransaction, httpTransactionStart, httpTransactionCatch, httpTransactionEnd, handler, ROUTES_HANDLERS, } = prepareTransaction({ status: 200, headers: { 'content-type': 'image/jpeg', }, body: streamtest.fromChunks([ Buffer.from('he'), Buffer.from('llo'), ]), }); const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, ERRORS_DESCRIPTORS: {}, }); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); const httpRouter = await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, ROUTES_HANDLERS, API, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); const req = { method: 'GET', url: '/v1/users/1/avatar?forFriendsUserId=2&forFriendsUserId=3', headers: { 'x-depth': '1', }, } as unknown as IncomingMessage; log.mockReset(); await httpRouter.service(req, res); expect(handler).toHaveBeenCalled(); expect(httpTransaction).toHaveBeenCalled(); expect(httpTransactionStart).toHaveBeenCalled(); expect(httpTransactionCatch).not.toHaveBeenCalled(); expect(httpTransactionEnd).toHaveBeenCalled(); const response = await waitResponse( httpTransactionEnd.mock.calls[0][0] as WhookResponse, true, ); expect({ response, handlerCalls: handler.mock.calls, }).toMatchInlineSnapshot(` { "handlerCalls": [ [ { "body": undefined, "cookies": {}, "headers": { "x-depth": 1, }, "path": { "userId": 1, }, "query": { "forFriendsUserId": [ 2, 3, ], }, }, { "config": {}, "method": "get", "operation": { "operationId": "getUserAvatar", "responses": { "200": { "content": { "image/jpeg": { "schema": { "format": "binary", "type": "string", }, }, }, "description": "User avatar found.", }, "404": { "description": "User avatar not found", }, "default": { "$ref": "#/components/responses/UnexpectedError", }, }, "summary": "Retrieve user's avatar.", }, "path": "/v1/users/1/avatar", }, ], ], "response": { "body": "hello", "headers": { "content-type": "image/jpeg", }, "status": 200, }, } `); }); }); describe('should crash the router', () => { test('when stringifyer lack for errors too', async () => { const { httpTransaction, httpTransactionStart, httpTransactionCatch, httpTransactionEnd, handler, ROUTES_HANDLERS, } = prepareTransaction({ status: 200, headers: { 'content-type': 'application/json', }, body: { id: 1, name: 'John Doe', }, }); const errorHandler = await initErrorHandler({ ENV, DEBUG_NODE_ENVS, STRINGIFIERS: {}, ERRORS_DESCRIPTORS: {}, }); const queryParserBuilder = await initQueryParserBuilder({ API, log, }); const httpRouter = await initHTTPRouter({ ENV, DEBUG_NODE_ENVS, STRINGIFIERS: {}, ROUTES_HANDLERS, API, log, DEFINITIONS, BASE_PATH, httpTransaction, errorHandler, queryParserBuilder, schemaValidators, }); const req = { method: 'GET', url: '/v1/users/1?extended=true', headers: {}, } as IncomingMessage; log.mockReset(); await httpRouter.service(req, res); expect(handler).toHaveBeenCalled(); expect(httpTransaction).toHaveBeenCalled(); expect(httpTransactionStart).toHaveBeenCalled(); expect(httpTransactionCatch).toHaveBeenCalled(); expect(httpTransactionEnd).not.toHaveBeenCalled(); try { await httpRouter.fatalErrorPromise; throw new YError('E_UNEXPECTED_SUCCESS'); } catch (err) { expect({ errorCode: (err as YError).code, }).toEqual({ errorCode: 'E_STRINGIFYER_LACK', }); } expect(handler.mock.calls[0][0]).toEqual({ cookies: {}, path: { userId: 1, },