From 1bf703cf4ab8633d43434128e63f99f69d8efd6b Mon Sep 17 00:00:00 2001 From: Max Kurapov Date: Thu, 2 May 2024 17:06:04 +0200 Subject: [PATCH] feat(open-payments): make schema validation in the open-payments client behind a flag (#467) * chore: use node 20 * chore: update @types/node * chore(ci): update actions * feat(open-payments): use ky as httpclient * chore(open-payments): fix request signing * feat(open-payments): add validateResponses flag to optionally disable openapi schema validation * chore(open-payments): dont parse DELETE response body * chore(open-payments): handle DELETE body parsing * chore(open-payments): handle DELETE body parsing * chore(open-payments): handle DELETE body parsing * chore(open-payments): only try parsing DELETE body if its not a 204 response * chore(open-payments): only try parsing DELETE body if its not a 204 response * feat(open-payments): finalize and test requests * chore(open-payments): update nock and ky usage in jest * chore(open-payments): fix tests after update to ky * Revert "feat(open-payments): add validateResponses flag to optionally disable openapi schema validation" This reverts commit b5f986af4ad2497bc69a69e78276e1c4afd11808. # Conflicts: # packages/open-payments/src/client/requests.test.ts # packages/open-payments/src/client/requests.ts * chore(open-payments): cleanup files after commit revert * chore(ci): update actions * chore: use node 20 * chore(ci): update actions * chore(open-payments): use dynamic import to resolve ky package * chore(open-payments): allow nock to patch global.fetch * chore(open-payments): allow nock to patch global.fetch in global setup * chore(open-payments): fix test & lint * chore(open-payments): continue nock cleanup * chore: add changeset * chore: update jest in workspace * Revert "chore(open-payments): allow nock to patch global.fetch in global setup" This reverts commit 9285d60dbc9bf2c2fb045fce5068897fa80e7c42. * chore: update @swc/jest * chore(open-payments): nock cleanup * feat(open-payments): add validateResponses flag to optionally disable openapi schema validation # Conflicts: # packages/open-payments/src/client/requests.test.ts # packages/open-payments/src/client/requests.ts * chore(open-payments): add tests for optional schema validation * chore(open-payments): update readme * chore(open-payments):add changeset * feat(open-payments): make openApi schema optional in client * chore: edit changeset * chore: simplify init --- .changeset/shiny-carrots-return.md | 6 + packages/open-payments/README.md | 11 +- .../open-payments/src/client/grant.test.ts | 140 +++++-- packages/open-payments/src/client/grant.ts | 36 +- .../src/client/incoming-payment.test.ts | 380 ++++++++++-------- .../src/client/incoming-payment.ts | 54 +-- packages/open-payments/src/client/index.ts | 33 +- .../src/client/outgoing-payment.test.ts | 211 +++++----- .../src/client/outgoing-payment.ts | 15 +- .../open-payments/src/client/quote.test.ts | 136 ++++--- packages/open-payments/src/client/quote.ts | 21 +- packages/open-payments/src/client/requests.ts | 36 +- .../open-payments/src/client/token.test.ts | 120 ++++-- packages/open-payments/src/client/token.ts | 46 ++- .../src/client/wallet-address.test.ts | 165 ++++---- .../src/client/wallet-address.ts | 27 +- 16 files changed, 847 insertions(+), 590 deletions(-) create mode 100644 .changeset/shiny-carrots-return.md diff --git a/.changeset/shiny-carrots-return.md b/.changeset/shiny-carrots-return.md new file mode 100644 index 00000000..f1cf7f28 --- /dev/null +++ b/.changeset/shiny-carrots-return.md @@ -0,0 +1,6 @@ +--- +'@interledger/open-payments': minor +--- + +Adding `validateResponses` flag to Open Payments client initialization functions. +This flag enables or disables response validation against the Open Payments OpenAPI specs (via the @interledger/openapi package). diff --git a/packages/open-payments/README.md b/packages/open-payments/README.md index c7e7d941..ff1fa2ef 100644 --- a/packages/open-payments/README.md +++ b/packages/open-payments/README.md @@ -52,11 +52,12 @@ const incomingPayment = await client.walletAddress.get({ #### Optional client initiation parameters -| Variable | Description | -| ------------------ | -------------------------------------------------------------------------------- | -| `requestTimeoutMs` | (optional) The timeout in ms for each request by the client. Defaults to 5000. | -| `logger` | (optional) The custom logger to provide for the client. Defaults to pino logger. | -| `logLevel` | (optional) The log level for the client. Defaults to `info` | +| Variable | Description | +| ------------------- | --------------------------------------------------------------------------------------------------------------- | +| `requestTimeoutMs` | (optional) The timeout in ms for each request by the client. Defaults to 5000. | +| `logger` | (optional) The custom logger to provide for the client. Defaults to pino logger. | +| `logLevel` | (optional) The log level for the client. Defaults to `info`. | +| `validateResponses` | (optional) Enables or disables response validation against the Open Payments OpenAPI specs. Defaults to `true`. | ### `AuthenticatedClient` diff --git a/packages/open-payments/src/client/grant.test.ts b/packages/open-payments/src/client/grant.test.ts index 340a8514..2c0dfa39 100644 --- a/packages/open-payments/src/client/grant.test.ts +++ b/packages/open-payments/src/client/grant.test.ts @@ -30,63 +30,105 @@ describe('grant', (): void => { const accessToken = 'someAccessToken' describe('request', () => { - test('calls post method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/' && method === HttpMethod.POST + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls post method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/' && method === HttpMethod.POST - jest - .spyOn(openApi, 'createResponseValidator') - .mockImplementation(mockResponseValidator as any) + jest + .spyOn(openApi, 'createResponseValidator') + .mockImplementation(mockResponseValidator as any) - const postSpy = jest.spyOn(requestors, 'post') - const grantRequest = mockGrantRequest() + const postSpy = jest.spyOn(requestors, 'post') + const grantRequest = mockGrantRequest() - await createGrantRoutes({ openApi, client, ...deps }).request( - { url }, - grantRequest - ) + await createGrantRoutes({ + openApi: validateResponses ? openApi : undefined, + client, + ...deps + }).request({ url }, grantRequest) - expect(postSpy).toHaveBeenCalledWith( - deps, - { - url, - body: { - ...grantRequest, - client - } - }, - true - ) - }) + expect(postSpy).toHaveBeenCalledWith( + deps, + { + url, + body: { + ...grantRequest, + client + } + }, + validateResponses ? true : undefined + ) + } + ) }) describe('cancel', () => { - test('calls delete method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/continue/{id}' && method === HttpMethod.DELETE + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls delete method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/continue/{id}' && method === HttpMethod.DELETE - jest - .spyOn(openApi, 'createResponseValidator') - .mockImplementation(mockResponseValidator as any) + jest + .spyOn(openApi, 'createResponseValidator') + .mockImplementation(mockResponseValidator as any) - const deleteSpy = jest - .spyOn(requestors, 'deleteRequest') - .mockResolvedValueOnce() + const deleteSpy = jest + .spyOn(requestors, 'deleteRequest') + .mockResolvedValueOnce() - await createGrantRoutes({ openApi, client, ...deps }).cancel({ - url, - accessToken - }) + await createGrantRoutes({ + openApi: validateResponses ? openApi : undefined, + client, + ...deps + }).cancel({ + url, + accessToken + }) - expect(deleteSpy).toHaveBeenCalledWith(deps, { url, accessToken }, true) - }) + expect(deleteSpy).toHaveBeenCalledWith( + deps, + { url, accessToken }, + validateResponses ? true : undefined + ) + } + ) }) describe('continue', () => { - describe('calls post method with correct validator', (): void => { + describe('calls post method', (): void => { const mockResponseValidator = ({ path, method }) => path === '/continue/{id}' && method === HttpMethod.POST + test('without response validation', async (): Promise => { + const postSpy = jest.spyOn(requestors, 'post') + + await createGrantRoutes({ + openApi: undefined, + client, + ...deps + }).continue({ + url, + accessToken + }) + + expect(postSpy).toHaveBeenCalledWith( + deps, + { url, accessToken }, + undefined + ) + }) + test('with interact_ref', async (): Promise => { jest .spyOn(openApi, 'createResponseValidator') @@ -95,7 +137,11 @@ describe('grant', (): void => { const postSpy = jest.spyOn(requestors, 'post') const interact_ref = uuid() - await createGrantRoutes({ openApi, client, ...deps }).continue( + await createGrantRoutes({ + openApi, + client, + ...deps + }).continue( { url, accessToken @@ -117,7 +163,11 @@ describe('grant', (): void => { const postSpy = jest.spyOn(requestors, 'post') const body = {} - await createGrantRoutes({ openApi, client, ...deps }).continue( + await createGrantRoutes({ + openApi, + client, + ...deps + }).continue( { url, accessToken @@ -138,7 +188,11 @@ describe('grant', (): void => { const postSpy = jest.spyOn(requestors, 'post') - await createGrantRoutes({ openApi, client, ...deps }).continue({ + await createGrantRoutes({ + openApi, + client, + ...deps + }).continue({ url, accessToken }) diff --git a/packages/open-payments/src/client/grant.ts b/packages/open-payments/src/client/grant.ts index 3918806a..83492d5b 100644 --- a/packages/open-payments/src/client/grant.ts +++ b/packages/open-payments/src/client/grant.ts @@ -1,4 +1,4 @@ -import { HttpMethod } from '@interledger/openapi' +import { HttpMethod, ResponseValidator } from '@interledger/openapi' import { GrantOrTokenRequestArgs, RouteDeps, @@ -33,22 +33,24 @@ export interface GrantRoutes { export const createGrantRoutes = (deps: GrantRouteDeps): GrantRoutes => { const { openApi, client, ...baseDeps } = deps - const requestGrantValidator = openApi.createResponseValidator< - PendingGrant | Grant - >({ - path: getASPath('/'), - method: HttpMethod.POST - }) - const continueGrantValidator = openApi.createResponseValidator< - GrantContinuation | Grant - >({ - path: getASPath('/continue/{id}'), - method: HttpMethod.POST - }) - const cancelGrantValidator = openApi.createResponseValidator({ - path: getASPath('/continue/{id}'), - method: HttpMethod.DELETE - }) + let requestGrantValidator: ResponseValidator + let continueGrantValidator: ResponseValidator + let cancelGrantValidator: ResponseValidator + + if (openApi) { + requestGrantValidator = openApi.createResponseValidator({ + path: getASPath('/'), + method: HttpMethod.POST + }) + continueGrantValidator = openApi.createResponseValidator({ + path: getASPath('/continue/{id}'), + method: HttpMethod.POST + }) + cancelGrantValidator = openApi.createResponseValidator({ + path: getASPath('/continue/{id}'), + method: HttpMethod.DELETE + }) + } return { request: ( diff --git a/packages/open-payments/src/client/incoming-payment.test.ts b/packages/open-payments/src/client/incoming-payment.test.ts index 2d76f655..6e750808 100644 --- a/packages/open-payments/src/client/incoming-payment.test.ts +++ b/packages/open-payments/src/client/incoming-payment.test.ts @@ -96,7 +96,7 @@ describe('incoming-payment', (): void => { }, openApiValidators.successfulValidator ) - ).rejects.toThrowError() + ).rejects.toThrow() }) test('throws if incoming payment does not pass open api validation', async (): Promise => { @@ -116,7 +116,7 @@ describe('incoming-payment', (): void => { }, openApiValidators.failedValidator ) - ).rejects.toThrowError() + ).rejects.toThrow() }) }) @@ -153,7 +153,7 @@ describe('incoming-payment', (): void => { }, openApiValidators.failedValidator ) - ).rejects.toThrowError() + ).rejects.toThrow() }) }) @@ -178,13 +178,13 @@ describe('incoming-payment', (): void => { const result = await createIncomingPayment( deps, { url: serverAddress, accessToken }, - openApiValidators.successfulValidator, { walletAddress, incomingAmount, expiresAt, metadata - } + }, + openApiValidators.successfulValidator ) scope.done() @@ -213,8 +213,8 @@ describe('incoming-payment', (): void => { await createIncomingPayment( deps, { url: serverAddress, accessToken }, - openApiValidators.successfulValidator, - { walletAddress } + { walletAddress }, + openApiValidators.successfulValidator ) } catch (error) { assert.ok(error instanceof OpenPaymentsClientError) @@ -239,10 +239,10 @@ describe('incoming-payment', (): void => { createIncomingPayment( deps, { url: serverAddress, accessToken }, - openApiValidators.failedValidator, - { walletAddress } + { walletAddress }, + openApiValidators.failedValidator ) - ).rejects.toThrowError() + ).rejects.toThrow() scope.done() }) }) @@ -321,7 +321,7 @@ describe('incoming-payment', (): void => { }, openApiValidators.failedValidator ) - ).rejects.toThrowError(OpenPaymentsClientError) + ).rejects.toThrow(OpenPaymentsClientError) scope.done() }) @@ -478,7 +478,7 @@ describe('incoming-payment', (): void => { { url: serverAddress, walletAddress, accessToken }, openApiValidators.failedValidator ) - ).rejects.toThrowError(OpenPaymentsClientError) + ).rejects.toThrow(OpenPaymentsClientError) scope.done() }) @@ -611,200 +611,252 @@ describe('incoming-payment', (): void => { describe('routes', (): void => { describe('get', (): void => { - test('calls get method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/incoming-payments/{id}' && method === HttpMethod.GET + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls get method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/incoming-payments/{id}' && method === HttpMethod.GET - const url = `${serverAddress}/incoming-payments/1` + const url = `${serverAddress}/incoming-payments/1` - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) - const getSpy = jest - .spyOn(requestors, 'get') - .mockResolvedValueOnce(mockIncomingPayment()) + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce(mockIncomingPayment()) - await createIncomingPaymentRoutes({ - openApi, - ...deps - }).get({ url, accessToken }) + await createIncomingPaymentRoutes({ + ...deps, + openApi: validateResponses ? openApi : undefined + }).get({ url, accessToken }) - expect(getSpy).toHaveBeenCalledWith( - deps, - { - url, - accessToken - }, - true - ) - }) + expect(getSpy).toHaveBeenCalledWith( + deps, + { + url, + accessToken + }, + validateResponses ? true : undefined + ) + } + ) }) describe('getPublic', (): void => { - test('calls getPublic method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/incoming-payments/{id}' && method === HttpMethod.GET + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls get method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/incoming-payments/{id}' && method === HttpMethod.GET - const url = `${serverAddress}/incoming-payments/1` + const url = `${serverAddress}/incoming-payments/1` - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) - const publicIncomingPayment = mockPublicIncomingPayment() + const publicIncomingPayment = mockPublicIncomingPayment() - const getSpy = jest - .spyOn(requestors, 'get') - .mockResolvedValueOnce(publicIncomingPayment) + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce(publicIncomingPayment) - await createIncomingPaymentRoutes({ - openApi, - ...deps - }).getPublic({ url }) + await createIncomingPaymentRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).getPublic({ url }) - expect(getSpy).toHaveBeenCalledWith(deps, { url }, true) - }) + expect(getSpy).toHaveBeenCalledWith( + deps, + { url }, + validateResponses ? true : undefined + ) + } + ) }) describe('list', (): void => { - test('calls get method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/incoming-payments' && method === HttpMethod.GET - - const incomingPaymentPaginationResult = - mockIncomingPaymentPaginationResult({ - result: [mockIncomingPayment()] - }) - const url = `${serverAddress}${getRSPath('/incoming-payments')}` - - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) - - const getSpy = jest - .spyOn(requestors, 'get') - .mockResolvedValueOnce(incomingPaymentPaginationResult) - - await createIncomingPaymentRoutes({ - openApi, - ...deps - }).list({ url: serverAddress, walletAddress, accessToken }) - - expect(getSpy).toHaveBeenCalledWith( - deps, - { - url, - accessToken, - queryParams: { - 'wallet-address': walletAddress - } - }, - true - ) - }) - }) + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls get method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/incoming-payments' && method === HttpMethod.GET - describe('create', (): void => { - test('calls post method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/incoming-payments' && method === HttpMethod.POST - - const url = `${serverAddress}/incoming-payments` - const incomingPaymentCreateArgs = { - walletAddress, - description: 'Invoice', - incomingAmount: { assetCode: 'USD', assetScale: 2, value: '10' } + const incomingPaymentPaginationResult = + mockIncomingPaymentPaginationResult({ + result: [mockIncomingPayment()] + }) + const url = `${serverAddress}${getRSPath('/incoming-payments')}` + + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) + + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce(incomingPaymentPaginationResult) + + await createIncomingPaymentRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).list({ url: serverAddress, walletAddress, accessToken }) + + expect(getSpy).toHaveBeenCalledWith( + deps, + { + url, + accessToken, + queryParams: { + 'wallet-address': walletAddress + } + }, + validateResponses ? true : undefined + ) } + ) + }) - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) + describe('create', (): void => { + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls post method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/incoming-payments' && method === HttpMethod.POST - const postSpy = jest - .spyOn(requestors, 'post') - .mockResolvedValueOnce(mockIncomingPayment(incomingPaymentCreateArgs)) + const url = `${serverAddress}/incoming-payments` + const incomingPaymentCreateArgs = { + walletAddress, + description: 'Invoice', + incomingAmount: { assetCode: 'USD', assetScale: 2, value: '10' } + } - await createIncomingPaymentRoutes({ - openApi, - ...deps - }).create( - { url: serverAddress, accessToken }, - incomingPaymentCreateArgs - ) + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) + + const postSpy = jest + .spyOn(requestors, 'post') + .mockResolvedValueOnce( + mockIncomingPayment(incomingPaymentCreateArgs) + ) + + await createIncomingPaymentRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).create( + { url: serverAddress, accessToken }, + incomingPaymentCreateArgs + ) - expect(postSpy).toHaveBeenCalledWith( - deps, - { url, accessToken, body: incomingPaymentCreateArgs }, - true - ) - }) + expect(postSpy).toHaveBeenCalledWith( + deps, + { url, accessToken, body: incomingPaymentCreateArgs }, + validateResponses ? true : undefined + ) + } + ) }) describe('complete', (): void => { - test('calls post method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/incoming-payments/{id}/complete' && - method === HttpMethod.POST + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls post method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/incoming-payments/{id}/complete' && + method === HttpMethod.POST - const incomingPaymentUrl = `${serverAddress}/incoming-payments/1` + const incomingPaymentUrl = `${serverAddress}/incoming-payments/1` - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) - const postSpy = jest - .spyOn(requestors, 'post') - .mockResolvedValueOnce(mockIncomingPayment({ completed: true })) + const postSpy = jest + .spyOn(requestors, 'post') + .mockResolvedValueOnce(mockIncomingPayment({ completed: true })) - await createIncomingPaymentRoutes({ - openApi, - ...deps - }).complete({ url: incomingPaymentUrl, accessToken }) + await createIncomingPaymentRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).complete({ url: incomingPaymentUrl, accessToken }) - expect(postSpy).toHaveBeenCalledWith( - deps, - { - url: `${incomingPaymentUrl}/complete`, - accessToken - }, - true - ) - }) + expect(postSpy).toHaveBeenCalledWith( + deps, + { + url: `${incomingPaymentUrl}/complete`, + accessToken + }, + validateResponses ? true : undefined + ) + } + ) }) }) describe('unauthenticated routes', (): void => { describe('get', (): void => { - test('calls get method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/incoming-payments/{id}' && method === HttpMethod.GET + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls get method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/incoming-payments/{id}' && method === HttpMethod.GET - const url = `${serverAddress}/incoming-payments/1` + const url = `${serverAddress}/incoming-payments/1` - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) - const publicIncomingPayment = mockPublicIncomingPayment() + const publicIncomingPayment = mockPublicIncomingPayment() - const getSpy = jest - .spyOn(requestors, 'get') - .mockResolvedValueOnce(publicIncomingPayment) + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce(publicIncomingPayment) - await createUnauthenticatedIncomingPaymentRoutes({ - openApi, - ...deps - }).get({ url }) + await createUnauthenticatedIncomingPaymentRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).get({ url }) - expect(getSpy).toHaveBeenCalledWith(deps, { url }, true) - }) + expect(getSpy).toHaveBeenCalledWith( + deps, + { url }, + validateResponses ? true : undefined + ) + } + ) }) }) }) diff --git a/packages/open-payments/src/client/incoming-payment.ts b/packages/open-payments/src/client/incoming-payment.ts index edab95eb..0ae0f701 100644 --- a/packages/open-payments/src/client/incoming-payment.ts +++ b/packages/open-payments/src/client/incoming-payment.ts @@ -41,47 +41,48 @@ export const createIncomingPaymentRoutes = ( ): IncomingPaymentRoutes => { const { openApi, ...baseDeps } = deps - const getIncomingPaymentOpenApiValidator = - openApi.createResponseValidator({ + let getIncomingPaymentOpenApiValidator: ResponseValidator + let getPublicIncomingPaymentOpenApiValidator: ResponseValidator + let createIncomingPaymentOpenApiValidator: ResponseValidator + let completeIncomingPaymentOpenApiValidator: ResponseValidator + let listIncomingPaymentOpenApiValidator: ResponseValidator + + if (openApi) { + getIncomingPaymentOpenApiValidator = openApi.createResponseValidator({ path: getRSPath('/incoming-payments/{id}'), method: HttpMethod.GET }) - const getPublicIncomingPaymentOpenApiValidator = - openApi.createResponseValidator({ + getPublicIncomingPaymentOpenApiValidator = openApi.createResponseValidator({ path: getRSPath('/incoming-payments/{id}'), method: HttpMethod.GET }) - const createIncomingPaymentOpenApiValidator = - openApi.createResponseValidator({ + createIncomingPaymentOpenApiValidator = openApi.createResponseValidator({ path: getRSPath('/incoming-payments'), method: HttpMethod.POST }) - const completeIncomingPaymentOpenApiValidator = - openApi.createResponseValidator({ + completeIncomingPaymentOpenApiValidator = openApi.createResponseValidator({ path: getRSPath('/incoming-payments/{id}/complete'), method: HttpMethod.POST }) - const listIncomingPaymentOpenApiValidator = - openApi.createResponseValidator({ + listIncomingPaymentOpenApiValidator = openApi.createResponseValidator({ path: getRSPath('/incoming-payments'), method: HttpMethod.GET }) + } return { get: (args: ResourceRequestArgs) => getIncomingPayment(baseDeps, args, getIncomingPaymentOpenApiValidator), - getPublic: (args: UnauthenticatedResourceRequestArgs) => { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - return getPublicIncomingPayment( + getPublic: (args: UnauthenticatedResourceRequestArgs) => + getPublicIncomingPayment( baseDeps, args, getPublicIncomingPaymentOpenApiValidator - ) - }, + ), create: ( requestArgs: ResourceRequestArgs, createArgs: CreateIncomingPaymentArgs @@ -89,8 +90,8 @@ export const createIncomingPaymentRoutes = ( createIncomingPayment( baseDeps, requestArgs, - createIncomingPaymentOpenApiValidator, - createArgs + createArgs, + createIncomingPaymentOpenApiValidator ), complete: (args: ResourceRequestArgs) => completeIncomingPayment( @@ -117,11 +118,14 @@ export const createUnauthenticatedIncomingPaymentRoutes = ( ): UnauthenticatedIncomingPaymentRoutes => { const { openApi, ...baseDeps } = deps - const getPublicIncomingPaymentOpenApiValidator = - openApi.createResponseValidator({ + let getPublicIncomingPaymentOpenApiValidator: ResponseValidator + + if (openApi) { + getPublicIncomingPaymentOpenApiValidator = openApi.createResponseValidator({ path: getRSPath('/incoming-payments/{id}'), method: HttpMethod.GET }) + } return { get: (args: UnauthenticatedResourceRequestArgs) => @@ -136,7 +140,7 @@ export const createUnauthenticatedIncomingPaymentRoutes = ( export const getIncomingPayment = async ( deps: BaseDeps, args: ResourceRequestArgs, - validateOpenApiResponse: ResponseValidator + validateOpenApiResponse?: ResponseValidator ) => { const { url } = args @@ -163,7 +167,7 @@ export const getIncomingPayment = async ( export const getPublicIncomingPayment = async ( deps: BaseDeps, args: UnauthenticatedResourceRequestArgs, - validateOpenApiResponse: ResponseValidator + validateOpenApiResponse?: ResponseValidator ) => { return await get(deps, args, validateOpenApiResponse) } @@ -171,8 +175,8 @@ export const getPublicIncomingPayment = async ( export const createIncomingPayment = async ( deps: BaseDeps, requestArgs: ResourceRequestArgs, - validateOpenApiResponse: ResponseValidator, - createArgs: CreateIncomingPaymentArgs + createArgs: CreateIncomingPaymentArgs, + validateOpenApiResponse?: ResponseValidator ) => { const { url: baseUrl, accessToken } = requestArgs const url = `${baseUrl}${getRSPath('/incoming-payments')}` @@ -198,7 +202,7 @@ export const createIncomingPayment = async ( export const completeIncomingPayment = async ( deps: BaseDeps, args: ResourceRequestArgs, - validateOpenApiResponse: ResponseValidator + validateOpenApiResponse?: ResponseValidator ) => { const { url: incomingPaymentUrl, accessToken } = args const url = `${incomingPaymentUrl}/complete` @@ -224,7 +228,7 @@ export const completeIncomingPayment = async ( export const listIncomingPayment = async ( deps: BaseDeps, args: CollectionRequestArgs, - validateOpenApiResponse: ResponseValidator, + validateOpenApiResponse?: ResponseValidator, pagination?: PaginationArgs ) => { const { url: baseUrl, accessToken, walletAddress } = args diff --git a/packages/open-payments/src/client/index.ts b/packages/open-payments/src/client/index.ts index bc8d13dc..88ea2ff7 100644 --- a/packages/open-payments/src/client/index.ts +++ b/packages/open-payments/src/client/index.ts @@ -39,17 +39,17 @@ export interface BaseDeps { } interface UnauthenticatedClientDeps extends BaseDeps { - walletAddressServerOpenApi: OpenAPI - resourceServerOpenApi: OpenAPI + walletAddressServerOpenApi?: OpenAPI + resourceServerOpenApi?: OpenAPI } interface AuthenticatedClientDeps extends UnauthenticatedClientDeps { - authServerOpenApi: OpenAPI + authServerOpenApi?: OpenAPI } export interface RouteDeps extends BaseDeps { httpClient: HttpClient - openApi: OpenAPI + openApi?: OpenAPI logger: Logger } @@ -129,6 +129,7 @@ const parseKey = ( const createUnauthenticatedDeps = async ({ useHttp = false, + validateResponses = true, ...args }: Partial = {}): Promise => { const logger = args?.logger ?? createLogger({ name: 'Open Payments Client' }) @@ -141,8 +142,13 @@ const createUnauthenticatedDeps = async ({ args?.requestTimeoutMs ?? config.DEFAULT_REQUEST_TIMEOUT_MS }) - const walletAddressServerOpenApi = await getWalletAddressServerOpenAPI() - const resourceServerOpenApi = await getResourceServerOpenAPI() + let walletAddressServerOpenApi: OpenAPI | undefined + let resourceServerOpenApi: OpenAPI | undefined + + if (validateResponses) { + walletAddressServerOpenApi = await getWalletAddressServerOpenAPI() + resourceServerOpenApi = await getResourceServerOpenAPI() + } return { httpClient, @@ -155,6 +161,7 @@ const createUnauthenticatedDeps = async ({ const createAuthenticatedClientDeps = async ({ useHttp = false, + validateResponses = true, ...args }: | CreateAuthenticatedClientArgs @@ -196,9 +203,15 @@ const createAuthenticatedClientDeps = async ({ }) } - const walletAddressServerOpenApi = await getWalletAddressServerOpenAPI() - const resourceServerOpenApi = await getResourceServerOpenAPI() - const authServerOpenApi = await getAuthServerOpenAPI() + let walletAddressServerOpenApi: OpenAPI | undefined + let resourceServerOpenApi: OpenAPI | undefined + let authServerOpenApi: OpenAPI | undefined + + if (validateResponses) { + walletAddressServerOpenApi = await getWalletAddressServerOpenAPI() + resourceServerOpenApi = await getResourceServerOpenAPI() + authServerOpenApi = await getAuthServerOpenAPI() + } return { httpClient, @@ -219,6 +232,8 @@ export interface CreateUnauthenticatedClientArgs { logLevel?: LevelWithSilent /** If enabled, all requests will use http as protocol. Use in development mode only. */ useHttp?: boolean + /** Enables or disables response validation against the Open Payments OpenAPI specs. Defaults to true. */ + validateResponses?: boolean } export interface UnauthenticatedClient { diff --git a/packages/open-payments/src/client/outgoing-payment.test.ts b/packages/open-payments/src/client/outgoing-payment.test.ts index d766ca8f..cb970732 100644 --- a/packages/open-payments/src/client/outgoing-payment.test.ts +++ b/packages/open-payments/src/client/outgoing-payment.test.ts @@ -493,114 +493,141 @@ describe('outgoing-payment', (): void => { describe('routes', (): void => { describe('get', (): void => { - test('calls get method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/outgoing-payments/{id}' && method === HttpMethod.GET + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls get method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/outgoing-payments/{id}' && method === HttpMethod.GET - const url = `${serverAddress}/outgoing-payments/1` + const url = `${serverAddress}/outgoing-payments/1` - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) - const getSpy = jest - .spyOn(requestors, 'get') - .mockResolvedValueOnce(mockOutgoingPayment()) + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce(mockOutgoingPayment()) - await createOutgoingPaymentRoutes({ - openApi, - ...deps - }).get({ url, accessToken: 'accessToken' }) + await createOutgoingPaymentRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).get({ url, accessToken: 'accessToken' }) - expect(getSpy).toHaveBeenCalledWith( - deps, - { - url, - accessToken: 'accessToken' - }, - true - ) - }) + expect(getSpy).toHaveBeenCalledWith( + deps, + { + url, + accessToken: 'accessToken' + }, + validateResponses ? true : undefined + ) + } + ) }) describe('list', (): void => { - test('calls get method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/outgoing-payments' && method === HttpMethod.GET + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls get method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/outgoing-payments' && method === HttpMethod.GET + + const outgoingPaymentPaginationResult = + mockOutgoingPaymentPaginationResult({ + result: [mockOutgoingPayment()] + }) + const url = `${serverAddress}/outgoing-payments` + + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) - const outgoingPaymentPaginationResult = - mockOutgoingPaymentPaginationResult({ - result: [mockOutgoingPayment()] + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce(outgoingPaymentPaginationResult) + + await createOutgoingPaymentRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).list({ + url: serverAddress, + walletAddress, + accessToken: 'accessToken' }) - const url = `${serverAddress}/outgoing-payments` - - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) - - const getSpy = jest - .spyOn(requestors, 'get') - .mockResolvedValueOnce(outgoingPaymentPaginationResult) - - await createOutgoingPaymentRoutes({ - openApi, - ...deps - }).list({ - url: serverAddress, - walletAddress, - accessToken: 'accessToken' - }) - expect(getSpy).toHaveBeenCalledWith( - deps, - { - url, - accessToken: 'accessToken', - queryParams: { - 'wallet-address': walletAddress - } - }, - true - ) - }) + expect(getSpy).toHaveBeenCalledWith( + deps, + { + url, + accessToken: 'accessToken', + queryParams: { + 'wallet-address': walletAddress + } + }, + validateResponses ? true : undefined + ) + } + ) }) describe('create', (): void => { - test('calls post method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/outgoing-payments' && method === HttpMethod.POST - - const url = `${serverAddress}/outgoing-payments` - const outgoingPaymentCreateArgs = { - quoteId: uuid(), - walletAddress - } + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls post method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/outgoing-payments' && method === HttpMethod.POST - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) - - const postSpy = jest - .spyOn(requestors, 'post') - .mockResolvedValueOnce(mockOutgoingPayment(outgoingPaymentCreateArgs)) - - await createOutgoingPaymentRoutes({ - openApi, - ...deps - }).create( - { url: serverAddress, accessToken: 'accessToken' }, - outgoingPaymentCreateArgs - ) + const url = `${serverAddress}/outgoing-payments` + const outgoingPaymentCreateArgs = { + quoteId: uuid(), + walletAddress + } - expect(postSpy).toHaveBeenCalledWith( - deps, - { url, accessToken: 'accessToken', body: outgoingPaymentCreateArgs }, - true - ) - }) + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) + + const postSpy = jest + .spyOn(requestors, 'post') + .mockResolvedValueOnce( + mockOutgoingPayment(outgoingPaymentCreateArgs) + ) + + await createOutgoingPaymentRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).create( + { url: serverAddress, accessToken: 'accessToken' }, + outgoingPaymentCreateArgs + ) + + expect(postSpy).toHaveBeenCalledWith( + deps, + { + url, + accessToken: 'accessToken', + body: outgoingPaymentCreateArgs + }, + validateResponses ? true : undefined + ) + } + ) }) }) }) diff --git a/packages/open-payments/src/client/outgoing-payment.ts b/packages/open-payments/src/client/outgoing-payment.ts index 3c448a56..3a5a33a8 100644 --- a/packages/open-payments/src/client/outgoing-payment.ts +++ b/packages/open-payments/src/client/outgoing-payment.ts @@ -32,23 +32,26 @@ export const createOutgoingPaymentRoutes = ( ): OutgoingPaymentRoutes => { const { openApi, ...baseDeps } = deps - const getOutgoingPaymentOpenApiValidator = - openApi.createResponseValidator({ + let getOutgoingPaymentOpenApiValidator: ResponseValidator + let listOutgoingPaymentOpenApiValidator: ResponseValidator + let createOutgoingPaymentOpenApiValidator: ResponseValidator + + if (openApi) { + getOutgoingPaymentOpenApiValidator = openApi.createResponseValidator({ path: getRSPath('/outgoing-payments/{id}'), method: HttpMethod.GET }) - const listOutgoingPaymentOpenApiValidator = - openApi.createResponseValidator({ + listOutgoingPaymentOpenApiValidator = openApi.createResponseValidator({ path: getRSPath('/outgoing-payments'), method: HttpMethod.GET }) - const createOutgoingPaymentOpenApiValidator = - openApi.createResponseValidator({ + createOutgoingPaymentOpenApiValidator = openApi.createResponseValidator({ path: getRSPath('/outgoing-payments'), method: HttpMethod.POST }) + } return { get: (requestArgs: ResourceRequestArgs) => diff --git a/packages/open-payments/src/client/quote.test.ts b/packages/open-payments/src/client/quote.test.ts index 3cbce3b7..12391e4f 100644 --- a/packages/open-payments/src/client/quote.test.ts +++ b/packages/open-payments/src/client/quote.test.ts @@ -101,68 +101,86 @@ describe('quote', (): void => { describe('routes', (): void => { describe('get', (): void => { - test('calls get method with the correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === `/quotes/{id}` && method === HttpMethod.GET - - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) - - const getSpy = jest - .spyOn(requestors, 'get') - .mockResolvedValueOnce(quote) - const url = `${baseUrl}${getRSPath('/quotes/{id}')}` - - await createQuoteRoutes({ - openApi, - ...deps - }).get({ - url, - accessToken - }) - - expect(getSpy).toHaveBeenCalledWith(deps, { url, accessToken }, true) - }) + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls get method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === `/quotes/{id}` && method === HttpMethod.GET + + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) + + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce(quote) + const url = `${baseUrl}${getRSPath('/quotes/{id}')}` + + await createQuoteRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).get({ + url, + accessToken + }) + + expect(getSpy).toHaveBeenCalledWith( + deps, + { url, accessToken }, + validateResponses ? true : undefined + ) + } + ) }) describe('create', (): void => { - test('calls post method with the correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === `/quotes` && method === HttpMethod.POST - - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) - - const postSpy = jest - .spyOn(requestors, 'post') - .mockResolvedValueOnce(quote) - const url = `${baseUrl}${getRSPath('/quotes')}` - - await createQuoteRoutes({ - openApi, - ...deps - }).create( - { - url: baseUrl, - accessToken - }, - { receiver: quote.receiver, method: 'ilp', walletAddress } - ) - - expect(postSpy).toHaveBeenCalledWith( - deps, - { - url, - accessToken, - body: { receiver: quote.receiver, method: 'ilp', walletAddress } - }, - true - ) - }) + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls post method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === `/quotes` && method === HttpMethod.POST + + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) + + const postSpy = jest + .spyOn(requestors, 'post') + .mockResolvedValueOnce(quote) + const url = `${baseUrl}${getRSPath('/quotes')}` + + await createQuoteRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).create( + { + url: baseUrl, + accessToken + }, + { receiver: quote.receiver, method: 'ilp', walletAddress } + ) + + expect(postSpy).toHaveBeenCalledWith( + deps, + { + url, + accessToken, + body: { receiver: quote.receiver, method: 'ilp', walletAddress } + }, + validateResponses ? true : undefined + ) + } + ) }) }) }) diff --git a/packages/open-payments/src/client/quote.ts b/packages/open-payments/src/client/quote.ts index 6b99cf75..46f03ce1 100644 --- a/packages/open-payments/src/client/quote.ts +++ b/packages/open-payments/src/client/quote.ts @@ -14,15 +14,20 @@ export interface QuoteRoutes { export const createQuoteRoutes = (deps: RouteDeps): QuoteRoutes => { const { openApi, ...baseDeps } = deps - const getQuoteOpenApiValidator = openApi.createResponseValidator({ - path: getRSPath('/quotes/{id}'), - method: HttpMethod.GET - }) + let getQuoteOpenApiValidator: ResponseValidator + let createQuoteOpenApiValidator: ResponseValidator - const createQuoteOpenApiValidator = openApi.createResponseValidator({ - path: getRSPath('/quotes'), - method: HttpMethod.POST - }) + if (openApi) { + getQuoteOpenApiValidator = openApi.createResponseValidator({ + path: getRSPath('/quotes/{id}'), + method: HttpMethod.GET + }) + + createQuoteOpenApiValidator = openApi.createResponseValidator({ + path: getRSPath('/quotes'), + method: HttpMethod.POST + }) + } return { get: (args: ResourceRequestArgs) => diff --git a/packages/open-payments/src/client/requests.ts b/packages/open-payments/src/client/requests.ts index 3161aea5..79437408 100644 --- a/packages/open-payments/src/client/requests.ts +++ b/packages/open-payments/src/client/requests.ts @@ -27,7 +27,7 @@ interface DeleteArgs { export const get = async ( deps: BaseDeps, args: GetArgs, - openApiResponseValidator: ResponseValidator + openApiResponseValidator?: ResponseValidator ): Promise => { const { httpClient } = deps const { accessToken } = args @@ -46,10 +46,12 @@ export const get = async ( const responseBody = await response.json() - openApiResponseValidator({ - status: response.status, - body: responseBody - }) + if (openApiResponseValidator) { + openApiResponseValidator({ + status: response.status, + body: responseBody + }) + } return responseBody } catch (error) { @@ -79,7 +81,7 @@ const getUrlWithQueryParams = ( export const post = async ( deps: BaseDeps, args: PostArgs, - openApiResponseValidator: ResponseValidator + openApiResponseValidator?: ResponseValidator ): Promise => { const { httpClient } = deps const { body, accessToken } = args @@ -98,10 +100,12 @@ export const post = async ( const responseBody = await response.json() - openApiResponseValidator({ - status: response.status, - body: responseBody - }) + if (openApiResponseValidator) { + openApiResponseValidator({ + status: response.status, + body: responseBody + }) + } return responseBody } catch (error) { @@ -112,7 +116,7 @@ export const post = async ( export const deleteRequest = async ( deps: BaseDeps, args: DeleteArgs, - openApiResponseValidator: ResponseValidator + openApiResponseValidator?: ResponseValidator ): Promise => { const { httpClient } = deps const { accessToken } = args @@ -128,10 +132,12 @@ export const deleteRequest = async ( : {} }) - openApiResponseValidator({ - status: response.status, - body: undefined - }) + if (openApiResponseValidator) { + openApiResponseValidator({ + status: response.status, + body: undefined + }) + } } catch (error) { return handleError(deps, { url, error, requestType: 'DELETE' }) } diff --git a/packages/open-payments/src/client/token.test.ts b/packages/open-payments/src/client/token.test.ts index 402541d1..f9693e05 100644 --- a/packages/open-payments/src/client/token.test.ts +++ b/packages/open-payments/src/client/token.test.ts @@ -30,50 +30,6 @@ describe('token', (): void => { const openApiValidators = mockOpenApiResponseValidators() - describe('createTokenRoutes', (): void => { - const url = 'http://localhost:1000' - const accessToken = 'someAccessToken' - - test('creates rotateTokenValidator properly', async (): Promise => { - const mockedAccessToken = mockAccessToken() - const mockResponseValidator = ({ path, method }) => - path === '/token/{id}' && method === HttpMethod.POST - - jest - .spyOn(openApi, 'createResponseValidator') - .mockImplementation(mockResponseValidator as any) - - const postSpy = jest - .spyOn(requestors, 'post') - .mockResolvedValueOnce(mockedAccessToken) - - createTokenRoutes({ openApi, ...deps }).rotate({ - url, - accessToken - }) - expect(postSpy).toHaveBeenCalledWith(deps, { url, accessToken }, true) - }) - - test('creates revokeTokenValidator properly', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/token/{id}' && method === HttpMethod.DELETE - - jest - .spyOn(openApi, 'createResponseValidator') - .mockImplementation(mockResponseValidator as any) - - const deleteSpy = jest - .spyOn(requestors, 'deleteRequest') - .mockResolvedValueOnce() - - createTokenRoutes({ openApi, ...deps }).revoke({ - url, - accessToken - }) - expect(deleteSpy).toHaveBeenCalledWith(deps, { url, accessToken }, true) - }) - }) - describe('rotateToken', (): void => { test('returns accessToken if passes validation', async (): Promise => { const accessToken = mockAccessToken() @@ -157,4 +113,80 @@ describe('token', (): void => { scope.done() }) }) + + describe('routes', (): void => { + const url = 'http://localhost:1000' + const accessToken = 'someAccessToken' + + describe('rotate', (): void => { + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls post method $description', + async ({ validateResponses }): Promise => { + const mockedAccessToken = mockAccessToken() + const mockResponseValidator = ({ path, method }) => + path === '/token/{id}' && method === HttpMethod.POST + + jest + .spyOn(openApi, 'createResponseValidator') + .mockImplementation(mockResponseValidator as any) + + const postSpy = jest + .spyOn(requestors, 'post') + .mockResolvedValueOnce(mockedAccessToken) + + createTokenRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).rotate({ + url, + accessToken + }) + expect(postSpy).toHaveBeenCalledWith( + deps, + { url, accessToken }, + validateResponses ? true : undefined + ) + } + ) + }) + + describe('revoke', (): void => { + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls delete method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/token/{id}' && method === HttpMethod.DELETE + + jest + .spyOn(openApi, 'createResponseValidator') + .mockImplementation(mockResponseValidator as any) + + const deleteSpy = jest + .spyOn(requestors, 'deleteRequest') + .mockResolvedValueOnce() + + createTokenRoutes({ + openApi: validateResponses ? openApi : undefined, + ...deps + }).revoke({ + url, + accessToken + }) + expect(deleteSpy).toHaveBeenCalledWith( + deps, + { url, accessToken }, + validateResponses ? true : undefined + ) + } + ) + }) + }) }) diff --git a/packages/open-payments/src/client/token.ts b/packages/open-payments/src/client/token.ts index 3e73044b..992a331c 100644 --- a/packages/open-payments/src/client/token.ts +++ b/packages/open-payments/src/client/token.ts @@ -8,6 +8,32 @@ export interface TokenRoutes { revoke(args: GrantOrTokenRequestArgs): Promise } +export const createTokenRoutes = (deps: RouteDeps): TokenRoutes => { + const { openApi, ...baseDeps } = deps + + let rotateTokenValidator: ResponseValidator + let revokeTokenValidator: ResponseValidator + + if (openApi) { + rotateTokenValidator = openApi.createResponseValidator({ + path: getASPath('/token/{id}'), + method: HttpMethod.POST + }) + + revokeTokenValidator = openApi.createResponseValidator({ + path: getASPath('/token/{id}'), + method: HttpMethod.DELETE + }) + } + + return { + rotate: (args: GrantOrTokenRequestArgs) => + rotateToken(baseDeps, args, rotateTokenValidator), + revoke: (args: GrantOrTokenRequestArgs) => + revokeToken(baseDeps, args, revokeTokenValidator) + } +} + export const rotateToken = async ( deps: BaseDeps, args: GrantOrTokenRequestArgs, @@ -41,23 +67,3 @@ export const revokeToken = async ( validateOpenApiResponse ) } - -export const createTokenRoutes = (deps: RouteDeps): TokenRoutes => { - const { openApi, ...baseDeps } = deps - const rotateTokenValidator = openApi.createResponseValidator({ - path: getASPath('/token/{id}'), - method: HttpMethod.POST - }) - - const revokeTokenValidator = openApi.createResponseValidator({ - path: getASPath('/token/{id}'), - method: HttpMethod.DELETE - }) - - return { - rotate: (args: GrantOrTokenRequestArgs) => - rotateToken(baseDeps, args, rotateTokenValidator), - revoke: (args: GrantOrTokenRequestArgs) => - revokeToken(baseDeps, args, revokeTokenValidator) - } -} diff --git a/packages/open-payments/src/client/wallet-address.test.ts b/packages/open-payments/src/client/wallet-address.test.ts index cb418172..93cc042c 100644 --- a/packages/open-payments/src/client/wallet-address.test.ts +++ b/packages/open-payments/src/client/wallet-address.test.ts @@ -31,84 +31,105 @@ describe('wallet-address', (): void => { const walletAddress = mockWalletAddress() describe('get', (): void => { - test('calls get method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/' && method === HttpMethod.GET - - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) - - const getSpy = jest - .spyOn(requestors, 'get') - .mockResolvedValueOnce(walletAddress) - - await createWalletAddressRoutes({ - openApi, - ...deps - }).get({ url: walletAddress.id }) - - expect(getSpy).toHaveBeenCalledWith( - deps, - { url: walletAddress.id }, - true - ) - }) + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls get method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/' && method === HttpMethod.GET + + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) + + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce(walletAddress) + + await createWalletAddressRoutes({ + ...deps, + openApi: validateResponses ? openApi : undefined + }).get({ url: walletAddress.id }) + + expect(getSpy).toHaveBeenCalledWith( + deps, + { url: walletAddress.id }, + validateResponses ? true : undefined + ) + } + ) }) describe('getKeys', (): void => { - test('calls get method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/jwks.json' && method === HttpMethod.GET - - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) - - const getSpy = jest - .spyOn(requestors, 'get') - .mockResolvedValueOnce([mockJwk()]) - - await createWalletAddressRoutes({ - openApi, - ...deps - }).getKeys({ url: walletAddress.id }) - - expect(getSpy).toHaveBeenCalledWith( - deps, - { url: `${walletAddress.id}/jwks.json` }, - true - ) - }) + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls get method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/jwks.json' && method === HttpMethod.GET + + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) + + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce([mockJwk()]) + + await createWalletAddressRoutes({ + ...deps, + openApi: validateResponses ? openApi : undefined + }).getKeys({ url: walletAddress.id }) + + expect(getSpy).toHaveBeenCalledWith( + deps, + { url: `${walletAddress.id}/jwks.json` }, + validateResponses ? true : undefined + ) + } + ) }) describe('getDIDDocument', (): void => { - test('calls get method with correct validator', async (): Promise => { - const mockResponseValidator = ({ path, method }) => - path === '/did.json' && method === HttpMethod.GET - - jest - .spyOn(openApi, 'createResponseValidator') - // eslint-disable-next-line @typescript-eslint/no-explicit-any - .mockImplementation(mockResponseValidator as any) - - const getSpy = jest - .spyOn(requestors, 'get') - .mockResolvedValueOnce([mockDIDDocument()]) - - await createWalletAddressRoutes({ - openApi, - ...deps - }).getDIDDocument({ url: walletAddress.id }) - - expect(getSpy).toHaveBeenCalledWith( - deps, - { url: `${walletAddress.id}/did.json` }, - true - ) - }) + test.each` + validateResponses | description + ${true} | ${'with response validation'} + ${false} | ${'without response validation'} + `( + 'calls get method $description', + async ({ validateResponses }): Promise => { + const mockResponseValidator = ({ path, method }) => + path === '/did.json' && method === HttpMethod.GET + + jest + .spyOn(openApi, 'createResponseValidator') + // eslint-disable-next-line @typescript-eslint/no-explicit-any + .mockImplementation(mockResponseValidator as any) + + const getSpy = jest + .spyOn(requestors, 'get') + .mockResolvedValueOnce([mockDIDDocument()]) + + await createWalletAddressRoutes({ + ...deps, + openApi: validateResponses ? openApi : undefined + }).getDIDDocument({ url: walletAddress.id }) + + expect(getSpy).toHaveBeenCalledWith( + deps, + { url: `${walletAddress.id}/did.json` }, + validateResponses ? true : undefined + ) + } + ) }) }) }) diff --git a/packages/open-payments/src/client/wallet-address.ts b/packages/open-payments/src/client/wallet-address.ts index a3f003ee..42057c0e 100644 --- a/packages/open-payments/src/client/wallet-address.ts +++ b/packages/open-payments/src/client/wallet-address.ts @@ -1,4 +1,4 @@ -import { HttpMethod } from '@interledger/openapi' +import { HttpMethod, ResponseValidator } from '@interledger/openapi' import { RouteDeps, UnauthenticatedResourceRequestArgs } from '.' import { JWKS, WalletAddress, DIDDocument, getWAPath } from '../types' import { get } from './requests' @@ -15,21 +15,26 @@ export const createWalletAddressRoutes = ( ): WalletAddressRoutes => { const { openApi, ...baseDeps } = deps - const getWalletAddressValidator = - openApi.createResponseValidator({ + let getWalletAddressValidator: ResponseValidator + let getWalletAddressKeysValidator: ResponseValidator + let getDidDocumentValidator: ResponseValidator + + if (openApi) { + getWalletAddressValidator = openApi.createResponseValidator({ path: getWAPath('/'), method: HttpMethod.GET }) - const getWalletAddressKeysValidator = openApi.createResponseValidator({ - path: getWAPath('/jwks.json'), - method: HttpMethod.GET - }) + getWalletAddressKeysValidator = openApi.createResponseValidator({ + path: getWAPath('/jwks.json'), + method: HttpMethod.GET + }) - const getDidDocumentValidator = openApi.createResponseValidator({ - path: getWAPath('/did.json'), - method: HttpMethod.GET - }) + getDidDocumentValidator = openApi.createResponseValidator({ + path: getWAPath('/did.json'), + method: HttpMethod.GET + }) + } return { get: (args: UnauthenticatedResourceRequestArgs) =>