Skip to content

Commit

Permalink
feat(open-payments): make schema validation in the open-payments clie…
Browse files Browse the repository at this point in the history
…nt 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 b5f986a.

# 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 9285d60.

* 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
  • Loading branch information
mkurapov authored May 2, 2024
1 parent 79535bf commit 1bf703c
Show file tree
Hide file tree
Showing 16 changed files with 847 additions and 590 deletions.
6 changes: 6 additions & 0 deletions .changeset/shiny-carrots-return.md
Original file line number Diff line number Diff line change
@@ -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).
11 changes: 6 additions & 5 deletions packages/open-payments/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
140 changes: 97 additions & 43 deletions packages/open-payments/src/client/grant.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,63 +30,105 @@ describe('grant', (): void => {
const accessToken = 'someAccessToken'

describe('request', () => {
test('calls post method with correct validator', async (): Promise<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
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<void> => {
jest
.spyOn(openApi, 'createResponseValidator')
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
})
Expand Down
36 changes: 19 additions & 17 deletions packages/open-payments/src/client/grant.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { HttpMethod } from '@interledger/openapi'
import { HttpMethod, ResponseValidator } from '@interledger/openapi'
import {
GrantOrTokenRequestArgs,
RouteDeps,
Expand Down Expand Up @@ -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<PendingGrant | Grant>
let continueGrantValidator: ResponseValidator<GrantContinuation | Grant>
let cancelGrantValidator: ResponseValidator<void>

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: (
Expand Down
Loading

0 comments on commit 1bf703c

Please sign in to comment.