From 280d28dbe1ec343f753e19fa3c8286c681b8bd61 Mon Sep 17 00:00:00 2001 From: AF Date: Thu, 22 Jun 2023 16:18:45 +0200 Subject: [PATCH 1/4] Replace MockRequest and MockResponse with Http* --- .../express/getRequestResponseRouter.ts | 4 ++-- src/adapters/express/mockHandler.ts | 12 +++++----- src/adapters/prism/makeMock.ts | 9 +++++++- src/domain/Mock.ts | 12 ++-------- src/domain/RequestResponse.ts | 10 +++++--- src/domain/__tests__/data.ts | 23 +++++++++---------- src/useCases/__tests__/processRequest.test.ts | 4 ++-- src/useCases/processRequest.ts | 8 +++---- 8 files changed, 42 insertions(+), 40 deletions(-) diff --git a/src/adapters/express/getRequestResponseRouter.ts b/src/adapters/express/getRequestResponseRouter.ts index c95c3a0..4b1feb4 100644 --- a/src/adapters/express/getRequestResponseRouter.ts +++ b/src/adapters/express/getRequestResponseRouter.ts @@ -20,9 +20,9 @@ const makeAPIRequestResponse = ( body: input.request.body || {}, }, response: { - status: input.response.status, + status: input.response.statusCode, headers: input.response.headers || {}, - body: input.response.data || {}, + body: input.response.body || {}, }, }); diff --git a/src/adapters/express/mockHandler.ts b/src/adapters/express/mockHandler.ts index 23d1ef4..29c554f 100644 --- a/src/adapters/express/mockHandler.ts +++ b/src/adapters/express/mockHandler.ts @@ -6,14 +6,14 @@ import { pipe } from 'fp-ts/lib/function'; import * as O from 'fp-ts/lib/Option'; import * as RTE from 'fp-ts/lib/ReaderTaskEither'; import * as RR from 'fp-ts/lib/ReadonlyRecord'; -import { MockRequest } from '../../domain/Mock'; +import { HttpRequest } from '../../domain/RequestResponse'; import { processRequest } from '../../useCases/processRequest'; import { AppEnv } from './AppEnv'; import { problemDetail500 } from './errors'; export const makeMethod = ( method: express.Request['method'] -): MockRequest['method'] => { +): HttpRequest['method'] => { switch (method) { case 'DELETE': return 'delete'; @@ -36,7 +36,7 @@ export const makeMethod = ( const makeHeaders = ( headers: express.Request['headers'] -): MockRequest['headers'] => +): HttpRequest['headers'] => pipe( headers, RR.filterMap(O.fromNullable), @@ -49,7 +49,7 @@ const makeHeaders = ( */ export const makeMockRequestFromExpressRequest = ( request: express.Request -): MockRequest => ({ +): HttpRequest => ({ url: { path: request.path, }, @@ -68,8 +68,8 @@ export const makeMockHandler = processRequest(makeMockRequestFromExpressRequest(req)), RTE.fold( (_) => RTE.of(res.status(500).json(problemDetail500)), - ({ status, headers, data }) => - RTE.of(res.status(status).header(headers).send(data)) + ({ statusCode, headers, body }) => + RTE.of(res.status(statusCode).header(headers).send(body)) ), RTE.toUnion )(env)(); diff --git a/src/adapters/prism/makeMock.ts b/src/adapters/prism/makeMock.ts index aa9d680..b75cbf7 100644 --- a/src/adapters/prism/makeMock.ts +++ b/src/adapters/prism/makeMock.ts @@ -36,7 +36,14 @@ const makePrismHttp = (openapi: string): TE.TaskEither => */ export const makeMockFromPrismHttp = (prismHttp: PrismHttp): Mock => ({ generateResponse: (req) => - TE.tryCatch(() => prismHttp.request(req.url.path, req), E.toError), + pipe( + TE.tryCatch(() => prismHttp.request(req.url.path, req), E.toError), + TE.map(({ status, headers, data }) => ({ + statusCode: status, + headers, + body: data, + })) + ), }); /** diff --git a/src/domain/Mock.ts b/src/domain/Mock.ts index 0969934..2e2aada 100644 --- a/src/domain/Mock.ts +++ b/src/domain/Mock.ts @@ -1,13 +1,5 @@ import * as TE from 'fp-ts/lib/TaskEither'; -import { IHttpRequest } from '@stoplight/prism-http'; -import { PrismHttp } from '@stoplight/prism-http/dist/client'; - -// For now use the types of prism -export type MockRequest = IHttpRequest; -export type MockOutput = Omit< - Awaited>, - 'config' ->; +import { HttpRequest, HttpResponse } from './RequestResponse'; /** * This type exposes the capability to create fake responses @@ -16,5 +8,5 @@ export type Mock = { /** * Given a request, produce a fake response. */ - generateResponse: (req: MockRequest) => TE.TaskEither; + generateResponse: (req: HttpRequest) => TE.TaskEither; }; diff --git a/src/domain/RequestResponse.ts b/src/domain/RequestResponse.ts index 342c03f..0c75222 100644 --- a/src/domain/RequestResponse.ts +++ b/src/domain/RequestResponse.ts @@ -1,5 +1,9 @@ import * as TE from 'fp-ts/TaskEither'; -import { MockRequest, MockOutput } from './Mock'; +import { IHttpRequest, IHttpResponse } from '@stoplight/prism-http'; + +// For now use the types of prism +export type HttpRequest = IHttpRequest; +export type HttpResponse = IHttpResponse; /** * Represents the request-response pair provided by the Mock. @@ -7,8 +11,8 @@ import { MockRequest, MockOutput } from './Mock'; export type RequestResponse = { // I didn't found a better term from the RFC 2616. // https://www.rfc-editor.org/rfc/rfc2616.html#section-4 - request: MockRequest; - response: MockOutput; + request: HttpRequest; + response: HttpResponse; }; /** diff --git a/src/domain/__tests__/data.ts b/src/domain/__tests__/data.ts index b23fe39..9735f14 100644 --- a/src/domain/__tests__/data.ts +++ b/src/domain/__tests__/data.ts @@ -1,30 +1,29 @@ import * as TE from 'fp-ts/TaskEither'; import { mock } from 'jest-mock-extended'; import { Capabilities } from '../Capabilities'; -import { MockOutput, MockRequest } from '../Mock'; +import { HttpRequest, HttpResponse } from '../RequestResponse'; -const aMockRequest: MockRequest = { +const anHttpRequest: HttpRequest = { method: 'get', url: { path: 'http://localhost:8080/hello?name=Rupert' }, }; -const aMockOutput: MockOutput = { - status: 200, +const anHttpResponse: HttpResponse = { + statusCode: 200, headers: {}, - data: {}, - request: aMockRequest, - violations: { input: [], output: [] }, + body: {}, +}; }; export const data = { mock: { - aMockRequest, - aMockOutput, + anHttpRequest, + anHttpResponse, }, requestResponse: { aRequestResponse: { - request: aMockRequest, - response: aMockOutput, + request: anHttpRequest, + response: anHttpResponse, }, }, }; @@ -37,7 +36,7 @@ export const makeFakeCapabilities = (defaultData: typeof data = data) => { }; // default behavior mocked.mock.generateResponse.mockReturnValue( - TE.right(defaultData.mock.aMockOutput) + TE.right(defaultData.mock.anHttpResponse) ); mocked.requestResponseWriter.record.mockImplementation(TE.of); mocked.requestResponseReader.list.mockReturnValue( diff --git a/src/useCases/__tests__/processRequest.test.ts b/src/useCases/__tests__/processRequest.test.ts index 4c9fdde..4963469 100644 --- a/src/useCases/__tests__/processRequest.test.ts +++ b/src/useCases/__tests__/processRequest.test.ts @@ -6,14 +6,14 @@ describe('processRequest', () => { it('should return mock response', async () => { const { env, envData } = makeFakeCapabilities(); - const actual = await processRequest(envData.mock.aMockRequest)(env)(); + const actual = await processRequest(envData.mock.anHttpRequest)(env)(); const expected = envData.requestResponse.aRequestResponse.response; expect(actual).toStrictEqual(E.right(expected)); }); it('should record the request-response pair', async () => { const { env, envData } = makeFakeCapabilities(); - await processRequest(envData.mock.aMockRequest)(env)(); + await processRequest(envData.mock.anHttpRequest)(env)(); const expected = envData.requestResponse.aRequestResponse; expect(env.requestResponseWriter.record).nthCalledWith(1, expected); }); diff --git a/src/useCases/processRequest.ts b/src/useCases/processRequest.ts index 2fbe450..ae6165c 100644 --- a/src/useCases/processRequest.ts +++ b/src/useCases/processRequest.ts @@ -5,18 +5,18 @@ import { pipe } from 'fp-ts/lib/function'; import * as TE from 'fp-ts/TaskEither'; import * as R from 'fp-ts/Reader'; import { Capabilities } from '../domain/Capabilities'; -import { MockRequest } from '../domain/Mock'; +import { HttpRequest } from '../domain/RequestResponse'; /** * Process the given request */ -export const processRequest = (mockRequest: MockRequest) => +export const processRequest = (request: HttpRequest) => pipe( R.ask>(), R.map(({ mock, requestResponseWriter }) => pipe( - mock.generateResponse(mockRequest), - TE.map((response) => ({ request: mockRequest, response })), + mock.generateResponse(request), + TE.map((response) => ({ request, response })), TE.chain(requestResponseWriter.record), TE.map(({ response }) => response) ) From 5ed9590115ae922e010f21fef0836e62cdf91155 Mon Sep 17 00:00:00 2001 From: AF Date: Thu, 22 Jun 2023 17:14:32 +0200 Subject: [PATCH 2/4] Add custom response definition --- src/adapters/express/AppEnv.ts | 1 + src/domain/Capabilities.ts | 2 ++ src/domain/CustomResponseDefinition.ts | 9 ++++++++ src/domain/__tests__/data.ts | 23 +++++++++++++++++++ src/useCases/__tests__/processRequest.test.ts | 10 ++++++++ src/useCases/processRequest.ts | 18 ++++++++++++--- 6 files changed, 60 insertions(+), 3 deletions(-) create mode 100644 src/domain/CustomResponseDefinition.ts diff --git a/src/adapters/express/AppEnv.ts b/src/adapters/express/AppEnv.ts index f0cd525..d904e9e 100644 --- a/src/adapters/express/AppEnv.ts +++ b/src/adapters/express/AppEnv.ts @@ -20,5 +20,6 @@ export const makeAppEnv = (config: Config): TE.TaskEither => mock, requestResponseReader: requestResponseStore, requestResponseWriter: requestResponseStore, + listCustomResponseDefinition: () => [], })) ); diff --git a/src/domain/Capabilities.ts b/src/domain/Capabilities.ts index 006d18d..e8fe016 100644 --- a/src/domain/Capabilities.ts +++ b/src/domain/Capabilities.ts @@ -2,6 +2,7 @@ * This file is a capabilities mapper, maps a capability to a given key. */ +import { ListCustomResponseDefinition } from './CustomResponseDefinition'; import { Mock } from './Mock'; import { RequestResponseReader, @@ -18,4 +19,5 @@ export type Capabilities = { mock: Mock; requestResponseReader: RequestResponseReader; requestResponseWriter: RequestResponseWriter; + listCustomResponseDefinition: ListCustomResponseDefinition; }; diff --git a/src/domain/CustomResponseDefinition.ts b/src/domain/CustomResponseDefinition.ts new file mode 100644 index 0000000..f32d947 --- /dev/null +++ b/src/domain/CustomResponseDefinition.ts @@ -0,0 +1,9 @@ +import { HttpRequest, HttpResponse } from './RequestResponse'; + +export type CustomResponseDefinition = { + match: HttpRequest; + response: HttpResponse; +}; + +export type ListCustomResponseDefinition = + () => ReadonlyArray; diff --git a/src/domain/__tests__/data.ts b/src/domain/__tests__/data.ts index 9735f14..780545d 100644 --- a/src/domain/__tests__/data.ts +++ b/src/domain/__tests__/data.ts @@ -1,6 +1,7 @@ import * as TE from 'fp-ts/TaskEither'; import { mock } from 'jest-mock-extended'; import { Capabilities } from '../Capabilities'; +import { CustomResponseDefinition } from '../CustomResponseDefinition'; import { HttpRequest, HttpResponse } from '../RequestResponse'; const anHttpRequest: HttpRequest = { @@ -13,6 +14,19 @@ const anHttpResponse: HttpResponse = { headers: {}, body: {}, }; + +const customResponseDefinition: CustomResponseDefinition = { + match: { + method: 'post', + url: { path: 'http://localhost:8080/hello?name=Rupert' }, + }, + response: { + statusCode: 200, + headers: { + 'x-custom': 'custom-response-definition', + }, + body: {}, + }, }; export const data = { @@ -26,6 +40,10 @@ export const data = { response: anHttpResponse, }, }, + customResponseDefinition: { + anHttpRequest: customResponseDefinition.match, + anHttpResponse: customResponseDefinition.response, + }, }; export const makeFakeCapabilities = (defaultData: typeof data = data) => { @@ -33,6 +51,8 @@ export const makeFakeCapabilities = (defaultData: typeof data = data) => { mock: mock(), requestResponseReader: mock(), requestResponseWriter: mock(), + listCustomResponseDefinition: + mock().listCustomResponseDefinition, }; // default behavior mocked.mock.generateResponse.mockReturnValue( @@ -42,6 +62,9 @@ export const makeFakeCapabilities = (defaultData: typeof data = data) => { mocked.requestResponseReader.list.mockReturnValue( TE.of([defaultData.requestResponse.aRequestResponse]) ); + mocked.listCustomResponseDefinition.mockReturnValue([ + customResponseDefinition, + ]); // return within data return { env: mocked, envData: defaultData }; diff --git a/src/useCases/__tests__/processRequest.test.ts b/src/useCases/__tests__/processRequest.test.ts index 4963469..440d1da 100644 --- a/src/useCases/__tests__/processRequest.test.ts +++ b/src/useCases/__tests__/processRequest.test.ts @@ -17,4 +17,14 @@ describe('processRequest', () => { const expected = envData.requestResponse.aRequestResponse; expect(env.requestResponseWriter.record).nthCalledWith(1, expected); }); + it('should return the response defined by user if any', async () => { + const { env, envData } = makeFakeCapabilities(); + + const actual = await processRequest( + envData.customResponseDefinition.anHttpRequest + )(env)(); + const expected = envData.customResponseDefinition.anHttpResponse; + expect(env.mock.generateResponse).toBeCalledTimes(0); + expect(actual).toStrictEqual(E.right(expected)); + }); }); diff --git a/src/useCases/processRequest.ts b/src/useCases/processRequest.ts index ae6165c..ab3014c 100644 --- a/src/useCases/processRequest.ts +++ b/src/useCases/processRequest.ts @@ -3,6 +3,8 @@ */ import { pipe } from 'fp-ts/lib/function'; import * as TE from 'fp-ts/TaskEither'; +import * as RA from 'fp-ts/ReadonlyArray'; +import * as O from 'fp-ts/Option'; import * as R from 'fp-ts/Reader'; import { Capabilities } from '../domain/Capabilities'; import { HttpRequest } from '../domain/RequestResponse'; @@ -12,10 +14,20 @@ import { HttpRequest } from '../domain/RequestResponse'; */ export const processRequest = (request: HttpRequest) => pipe( - R.ask>(), - R.map(({ mock, requestResponseWriter }) => + R.ask< + Pick< + Capabilities, + 'mock' | 'requestResponseWriter' | 'listCustomResponseDefinition' + > + >(), + R.map(({ mock, requestResponseWriter, listCustomResponseDefinition }) => pipe( - mock.generateResponse(request), + listCustomResponseDefinition(), + RA.findFirst(({ match }) => match === request), + O.fold( + () => mock.generateResponse(request), + ({ response }) => TE.of(response) + ), TE.map((response) => ({ request, response })), TE.chain(requestResponseWriter.record), TE.map(({ response }) => response) From 142922265721335deb1ad5ad2ff61caf3628422c Mon Sep 17 00:00:00 2001 From: AF Date: Thu, 22 Jun 2023 17:52:51 +0200 Subject: [PATCH 3/4] Improve comment following code-review suggestion --- src/useCases/processRequest.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/useCases/processRequest.ts b/src/useCases/processRequest.ts index ab3014c..770205d 100644 --- a/src/useCases/processRequest.ts +++ b/src/useCases/processRequest.ts @@ -10,7 +10,8 @@ import { Capabilities } from '../domain/Capabilities'; import { HttpRequest } from '../domain/RequestResponse'; /** - * Process the given request + * This function is the entry point for processing an HTTP request. Its goal is + * to deliver a dummy answer to the specified HTTP request. */ export const processRequest = (request: HttpRequest) => pipe( From 7a4b77ef3815174b080ab809e2f747097ba5d569 Mon Sep 17 00:00:00 2001 From: AF Date: Tue, 11 Jul 2023 09:20:38 +0200 Subject: [PATCH 4/4] Refactor request match --- src/domain/CustomResponseDefinition.ts | 4 ++++ .../CustomResponseDefinition.test.ts | 24 +++++++++++++++++++ src/useCases/__tests__/processRequest.test.ts | 6 ++--- src/useCases/processRequest.ts | 3 ++- 4 files changed, 33 insertions(+), 4 deletions(-) create mode 100644 src/domain/__tests__/CustomResponseDefinition.test.ts diff --git a/src/domain/CustomResponseDefinition.ts b/src/domain/CustomResponseDefinition.ts index f32d947..8027368 100644 --- a/src/domain/CustomResponseDefinition.ts +++ b/src/domain/CustomResponseDefinition.ts @@ -5,5 +5,9 @@ export type CustomResponseDefinition = { response: HttpResponse; }; +// Return true if match matches request +export const matches = (match: HttpRequest, request: HttpRequest): boolean => + match.method === request.method && match.url.path === request.url.path; + export type ListCustomResponseDefinition = () => ReadonlyArray; diff --git a/src/domain/__tests__/CustomResponseDefinition.test.ts b/src/domain/__tests__/CustomResponseDefinition.test.ts new file mode 100644 index 0000000..e760145 --- /dev/null +++ b/src/domain/__tests__/CustomResponseDefinition.test.ts @@ -0,0 +1,24 @@ +import { matches } from '../CustomResponseDefinition'; +import { data } from './data'; + +const request = data.requestResponse.aRequestResponse.request; + +describe('matches', () => { + it('should match requests without error', () => { + expect(matches(request, request)).toBeTruthy(); + expect(matches({ ...request, method: 'post' }, request)).toBeFalsy(); + // ignore body during match + expect( + matches( + { + ...request, + body: { a: 'a' }, + }, + { + ...request, + body: { b: 'b' }, + } + ) + ).toBeTruthy(); + }); +}); diff --git a/src/useCases/__tests__/processRequest.test.ts b/src/useCases/__tests__/processRequest.test.ts index 440d1da..62d1970 100644 --- a/src/useCases/__tests__/processRequest.test.ts +++ b/src/useCases/__tests__/processRequest.test.ts @@ -20,9 +20,9 @@ describe('processRequest', () => { it('should return the response defined by user if any', async () => { const { env, envData } = makeFakeCapabilities(); - const actual = await processRequest( - envData.customResponseDefinition.anHttpRequest - )(env)(); + const actual = await processRequest({ + ...envData.customResponseDefinition.anHttpRequest, + })(env)(); const expected = envData.customResponseDefinition.anHttpResponse; expect(env.mock.generateResponse).toBeCalledTimes(0); expect(actual).toStrictEqual(E.right(expected)); diff --git a/src/useCases/processRequest.ts b/src/useCases/processRequest.ts index 770205d..406fb30 100644 --- a/src/useCases/processRequest.ts +++ b/src/useCases/processRequest.ts @@ -8,6 +8,7 @@ import * as O from 'fp-ts/Option'; import * as R from 'fp-ts/Reader'; import { Capabilities } from '../domain/Capabilities'; import { HttpRequest } from '../domain/RequestResponse'; +import { matches } from '../domain/CustomResponseDefinition'; /** * This function is the entry point for processing an HTTP request. Its goal is @@ -24,7 +25,7 @@ export const processRequest = (request: HttpRequest) => R.map(({ mock, requestResponseWriter, listCustomResponseDefinition }) => pipe( listCustomResponseDefinition(), - RA.findFirst(({ match }) => match === request), + RA.findFirst(({ match }) => matches(match, request)), O.fold( () => mock.generateResponse(request), ({ response }) => TE.of(response)