Skip to content

Commit

Permalink
feat(backend): SPSPRouteError
Browse files Browse the repository at this point in the history
  • Loading branch information
mkurapov committed May 7, 2024
1 parent e6b3de2 commit b0b6e13
Show file tree
Hide file tree
Showing 7 changed files with 248 additions and 65 deletions.
20 changes: 20 additions & 0 deletions packages/backend/src/open_payments/route-errors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { IocContract } from '@adonisjs/fold'
import { initIocContainer } from '..'
import { Config } from '../config/app'
import { OpenAPIValidatorMiddlewareError } from '@interledger/openapi'
import { SPSPRouteError } from '../payment-method/ilp/spsp/middleware'

describe('openPaymentServerErrorMiddleware', (): void => {
let deps: IocContract<AppServices>
Expand Down Expand Up @@ -68,6 +69,25 @@ describe('openPaymentServerErrorMiddleware', (): void => {
expect(next).toHaveBeenCalledTimes(1)
})

test('handles SPSPRouteError error', async (): Promise<void> => {
const error = new SPSPRouteError(400, 'SPSP error')
const next = jest.fn().mockImplementationOnce(() => {
throw error
})

const ctxThrowSpy = jest.spyOn(ctx, 'throw')

await expect(
openPaymentsServerErrorMiddleware(ctx, next)
).rejects.toMatchObject({
status: error.status,
message: error.message
})

expect(ctxThrowSpy).toHaveBeenCalledWith(error.status, error.message)
expect(next).toHaveBeenCalledTimes(1)
})

test('handles unspecified error', async (): Promise<void> => {
const error = new Error('Some unspecified error')
const next = jest.fn().mockImplementationOnce(() => {
Expand Down
17 changes: 16 additions & 1 deletion packages/backend/src/open_payments/route-errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { AppContext } from '../app'
import { OpenAPIValidatorMiddlewareError } from '@interledger/openapi'
import { SPSPRouteError } from '../payment-method/ilp/spsp/middleware'

export class OpenPaymentsServerRouteError extends Error {
public status: number
Expand Down Expand Up @@ -43,7 +44,9 @@ export async function openPaymentsServerErrorMiddleware(
'Received error when handling Open Payments request'
)
ctx.throw(err.status, err.message)
} else if (err instanceof OpenAPIValidatorMiddlewareError) {
}

if (err instanceof OpenAPIValidatorMiddlewareError) {
const finalStatus = err.status || 400

logger.info(
Expand All @@ -57,6 +60,18 @@ export async function openPaymentsServerErrorMiddleware(
ctx.throw(finalStatus, err.message)
}

if (err instanceof SPSPRouteError) {
logger.info(
{
...baseLog,
message: err.message,
details: err.details
},
'Received error when handling SPSP request'
)
ctx.throw(err.status, err.message)
}

logger.error(
{ ...baseLog, err },
'Received unhandled error in Open Payments request'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const setup = <
options.params
)
ctx.walletAddress = options.walletAddress
ctx.walletAddressUrl = options.walletAddress.url
ctx.grant = options.grant
ctx.client = options.client
ctx.accessAction = options.accessAction
Expand Down
147 changes: 100 additions & 47 deletions packages/backend/src/payment-method/ilp/spsp/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { createSpspMiddleware, SPSPWalletAddressContext } from './middleware'
import {
createSpspMiddleware,
SPSPRouteError,
SPSPWalletAddressContext
} from './middleware'
import { setup } from '../../../open_payments/wallet_address/model.test'
import { Config } from '../../../config/app'
import { IocContract } from '@adonisjs/fold'
Expand All @@ -8,18 +12,38 @@ import { createTestApp, TestContainer } from '../../../tests/app'
import { createAsset } from '../../../tests/asset'
import { createWalletAddress } from '../../../tests/walletAddress'
import { truncateTables } from '../../../tests/tableManager'
import { WalletAddress } from '../../../open_payments/wallet_address/model'
import assert from 'assert'
import { SPSPRoutes } from './routes'
import { WalletAddressService } from '../../../open_payments/wallet_address/service'

describe('SPSP Middleware', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let spspRoutes: SPSPRoutes
let walletAddressService: WalletAddressService
let next: jest.MockedFunction<() => Promise<void>>

let ctx: SPSPWalletAddressContext
let walletAddress: WalletAddress

beforeAll(async (): Promise<void> => {
deps = await initIocContainer(Config)
deps = initIocContainer(Config)
appContainer = await createTestApp(deps)
spspRoutes = await deps.use('spspRoutes')
walletAddressService = await deps.use('walletAddressService')
})

beforeEach((): void => {
beforeEach(async (): Promise<void> => {
const asset = await createAsset(deps)
walletAddress = await createWalletAddress(deps, {
assetId: asset.id
})
ctx = setup<SPSPWalletAddressContext>({
reqOpts: {},
walletAddress
})
ctx.container = deps
next = jest.fn()
})

Expand All @@ -31,50 +55,79 @@ describe('SPSP Middleware', (): void => {
await appContainer.shutdown()
})

describe('Wallet Address', (): void => {
let ctx: SPSPWalletAddressContext

beforeEach(async (): Promise<void> => {
const asset = await createAsset(deps)
const walletAddress = await createWalletAddress(deps, {
assetId: asset.id
})
ctx = setup<SPSPWalletAddressContext>({
reqOpts: {},
walletAddress
})
ctx.container = deps
})

test.each`
header | spspEnabled | description
${'application/json'} | ${true} | ${'calls next'}
${'application/json'} | ${false} | ${'calls next'}
${'application/spsp4+json'} | ${true} | ${'calls SPSP route'}
${'application/spsp4+json'} | ${false} | ${'calls next'}
`(
'$description for accept header: $header and spspEnabled: $spspEnabled',
async ({ header, spspEnabled }): Promise<void> => {
const spspRoutes = await ctx.container.use('spspRoutes')
const spspSpy = jest
.spyOn(spspRoutes, 'get')
.mockResolvedValueOnce(undefined)
ctx.headers['accept'] = header
const spspMiddleware = createSpspMiddleware(spspEnabled)
await expect(spspMiddleware(ctx, next)).resolves.toBeUndefined()
if (!spspEnabled || header == 'application/json') {
expect(spspSpy).not.toHaveBeenCalled()
expect(next).toHaveBeenCalled()
} else {
expect(spspSpy).toHaveBeenCalledTimes(1)
expect(next).not.toHaveBeenCalled()
expect(ctx.paymentTag).toEqual(ctx.walletAddress.id)
expect(ctx.asset).toEqual({
code: ctx.walletAddress.asset.code,
scale: ctx.walletAddress.asset.scale
})
}
test.each`
header | spspEnabled | description
${'application/json'} | ${true} | ${'calls next'}
${'application/json'} | ${false} | ${'calls next'}
${'application/spsp4+json'} | ${true} | ${'calls SPSP route'}
${'application/spsp4+json'} | ${false} | ${'calls next'}
`(
'$description for accept header: $header and spspEnabled: $spspEnabled',
async ({ header, spspEnabled }): Promise<void> => {
const spspSpy = jest
.spyOn(spspRoutes, 'get')
.mockResolvedValueOnce(undefined)
ctx.headers['accept'] = header
const spspMiddleware = createSpspMiddleware(spspEnabled)
await expect(spspMiddleware(ctx, next)).resolves.toBeUndefined()
if (!spspEnabled || header == 'application/json') {
expect(spspSpy).not.toHaveBeenCalled()
expect(next).toHaveBeenCalled()
} else {
expect(spspSpy).toHaveBeenCalledTimes(1)
expect(next).not.toHaveBeenCalled()
expect(ctx.paymentTag).toEqual(walletAddress.id)
expect(ctx.asset).toEqual({
code: walletAddress.asset.code,
scale: walletAddress.asset.scale
})
}
)
}
)

test('throws error if could not find wallet address', async () => {
const spspGetRouteSpy = jest.spyOn(spspRoutes, 'get')

jest
.spyOn(walletAddressService, 'getByUrl')
.mockResolvedValueOnce(undefined)

ctx.header['accept'] = 'application/spsp4+json'

const spspMiddleware = createSpspMiddleware(true)

expect.assertions(4)
try {
await spspMiddleware(ctx, next)
} catch (err) {
assert.ok(err instanceof SPSPRouteError)
expect(err.status).toBe(404)
expect(err.message).toBe('Could not get wallet address')
expect(next).not.toHaveBeenCalled()
expect(spspGetRouteSpy).not.toHaveBeenCalled()
}
})

test('throws error if inactive wallet address', async () => {
const spspGetRouteSpy = jest.spyOn(spspRoutes, 'get')

ctx.header['accept'] = 'application/spsp4+json'

await walletAddress
.$query(appContainer.knex)
.patch({ deactivatedAt: new Date() })

const spspMiddleware = createSpspMiddleware(true)

expect.assertions(4)
try {
await spspMiddleware(ctx, next)
} catch (err) {
assert.ok(err instanceof SPSPRouteError)
expect(err.status).toBe(404)
expect(err.message).toBe('Could not get wallet address')
expect(next).not.toHaveBeenCalled()
expect(spspGetRouteSpy).not.toHaveBeenCalled()
}
})
})
36 changes: 30 additions & 6 deletions packages/backend/src/payment-method/ilp/spsp/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,25 @@ import { WalletAddressContext, SPSPContext } from '../../../app'

export type SPSPWalletAddressContext = WalletAddressContext &
SPSPContext & {
incomingPayment?: never
walletAddressUrl: string
}

export class SPSPRouteError extends Error {
public status: number
public details?: Record<string, unknown>

constructor(
status: number,
message: string,
details?: Record<string, unknown>
) {
super(message)
this.name = 'SPSPRouteError'
this.status = status
this.details = details
}
}

export function createSpspMiddleware(spspEnabled: boolean) {
if (spspEnabled) {
return spspMiddleware
Expand All @@ -22,13 +38,21 @@ const spspMiddleware = async (
ctx: SPSPWalletAddressContext,
next: () => Promise<unknown>
): Promise<void> => {
// Fall back to legacy protocols if client doesn't support Open Payments.
if (ctx.accepts('application/spsp4+json')) {
const receiver = ctx.walletAddress ?? ctx.incomingPayment
ctx.paymentTag = receiver.id
const walletAddressService = await ctx.container.use('walletAddressService')

const walletAddress = await walletAddressService.getByUrl(
ctx.walletAddressUrl
)

if (!walletAddress?.isActive) {
throw new SPSPRouteError(404, 'Could not get wallet address')
}

ctx.paymentTag = walletAddress.id
ctx.asset = {
code: receiver.asset.code,
scale: receiver.asset.scale
code: walletAddress.asset.code,
scale: walletAddress.asset.scale
}
const spspRoutes = await ctx.container.use('spspRoutes')
await spspRoutes.get(ctx)
Expand Down
Loading

0 comments on commit b0b6e13

Please sign in to comment.