Skip to content

Commit

Permalink
fix(backend): support SPSP queries (#1032)
Browse files Browse the repository at this point in the history
* fix(backend): allow SPSP query to payment pointer

* feat(backend): allow SPSP query to Open Payments connection

Generalize SPSP routes.

* chore(backend): add connection middleware

Add incoming payment to ctx for both SPSP and connection query.

* chore(backend): add SPSP middleware
  • Loading branch information
wilsonianb authored Feb 2, 2023
1 parent d7d5a31 commit 4df802b
Show file tree
Hide file tree
Showing 10 changed files with 362 additions and 171 deletions.
22 changes: 12 additions & 10 deletions packages/backend/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,10 @@ import { loadSchemaSync } from '@graphql-tools/load'

import { resolvers } from './graphql/resolvers'
import { HttpTokenService } from './httpToken/service'
import { AssetService } from './asset/service'
import { AssetService, AssetOptions } from './asset/service'
import { AccountingService } from './accounting/service'
import { PeerService } from './peer/service'
import { connectionMiddleware } from './open_payments/connection/middleware'
import { createPaymentPointerMiddleware } from './open_payments/payment_pointer/middleware'
import { PaymentPointer } from './open_payments/payment_pointer/model'
import { PaymentPointerService } from './open_payments/payment_pointer/service'
Expand All @@ -33,6 +34,7 @@ import {
RequestAction
} from './open_payments/auth/middleware'
import { RatesService } from './rates/service'
import { spspMiddleware } from './spsp/middleware'
import { SPSPRoutes } from './spsp/routes'
import { IncomingPaymentRoutes } from './open_payments/payment/incoming/routes'
import { PaymentPointerKeyRoutes } from './open_payments/payment_pointer/key/routes'
Expand Down Expand Up @@ -138,6 +140,11 @@ export type ReadContext = SubresourceContext
export type CompleteContext = SubresourceContext
export type ListContext = CollectionContext<never, PageQueryParams>

export interface SPSPContext extends AppContext {
paymentTag: string
asset: AssetOptions
}

type ContextType<T> = T extends (
ctx: infer Context
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -284,7 +291,6 @@ export class App {
ctx.status = 200
})

const spspRoutes = await this.container.use('spspRoutes')
const paymentPointerKeyRoutes = await this.container.use(
'paymentPointerKeyRoutes'
)
Expand Down Expand Up @@ -355,6 +361,8 @@ export class App {
route = connectionRoutes.get
router[method](
toRouterPath(path),
connectionMiddleware,
spspMiddleware,
createValidatorMiddleware<ContextType<typeof route>>(
resourceServerSpec,
{
Expand Down Expand Up @@ -411,18 +419,12 @@ export class App {
router.get(
PAYMENT_POINTER_PATH,
createPaymentPointerMiddleware(),
spspMiddleware,
createValidatorMiddleware<PaymentPointerContext>(resourceServerSpec, {
path: '/',
method: HttpMethod.GET
}),
async (ctx: PaymentPointerContext): Promise<void> => {
// Fall back to legacy protocols if client doesn't support Open Payments.
if (ctx.accepts('application/json')) await paymentPointerRoutes.get(ctx)
//else if (ctx.accepts('application/ilp-stream+json')) // TODO https://docs.openpayments.dev/accounts#payment-details
else if (ctx.accepts('application/spsp4+json'))
await spspRoutes.get(ctx)
else ctx.throw(406, 'no accepted Content-Type available')
}
paymentPointerRoutes.get
)

koa.use(router.routes())
Expand Down
70 changes: 70 additions & 0 deletions packages/backend/src/open_payments/connection/middleware.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import assert from 'assert'
import { v4 as uuid } from 'uuid'
import { connectionMiddleware, ConnectionContext } from './middleware'
import { Config } from '../../config/app'
import { IocContract } from '@adonisjs/fold'
import { initIocContainer } from '../../'
import { AppServices } from '../../app'
import { createTestApp, TestContainer } from '../../tests/app'
import { createAsset } from '../../tests/asset'
import { createContext } from '../../tests/context'
import { createIncomingPayment } from '../../tests/incomingPayment'
import { createPaymentPointer } from '../../tests/paymentPointer'
import { truncateTables } from '../../tests/tableManager'

describe('Connection Middleware', (): void => {
let deps: IocContract<AppServices>
let appContainer: TestContainer
let ctx: ConnectionContext
let next: jest.MockedFunction<() => Promise<void>>

beforeAll(async (): Promise<void> => {
deps = await initIocContainer(Config)
appContainer = await createTestApp(deps)
})

beforeEach((): void => {
ctx = createContext<ConnectionContext>(
{
headers: {
Accept: 'application/json'
}
},
{}
)
ctx.container = deps
next = jest.fn()
})

afterEach(async (): Promise<void> => {
await truncateTables(appContainer.knex)
})

afterAll(async (): Promise<void> => {
await appContainer.shutdown()
})

test('returns 404 for unknown connection id', async (): Promise<void> => {
ctx.params.id = uuid()
await expect(connectionMiddleware(ctx, next)).rejects.toMatchObject({
status: 404,
message: 'Not Found'
})
expect(next).not.toHaveBeenCalled()
})

test('sets the context incomingPayment and calls next', async (): Promise<void> => {
const asset = await createAsset(deps)
const { id: paymentPointerId } = await createPaymentPointer(deps, {
assetId: asset.id
})
const incomingPayment = await createIncomingPayment(deps, {
paymentPointerId
})
assert.ok(incomingPayment.connectionId)
ctx.params.id = incomingPayment.connectionId
await expect(connectionMiddleware(ctx, next)).resolves.toBeUndefined()
expect(next).toHaveBeenCalled()
expect(ctx.incomingPayment).toEqual(incomingPayment)
})
})
23 changes: 23 additions & 0 deletions packages/backend/src/open_payments/connection/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { AppContext } from '../../app'
import { IncomingPayment } from '../payment/incoming/model'

export interface ConnectionContext extends AppContext {
incomingPayment: IncomingPayment
}

export const connectionMiddleware = async (
ctx: Omit<ConnectionContext, 'incomingPayment'> & {
incomingPayment: Partial<ConnectionContext['incomingPayment']>
},
next: () => Promise<unknown>
): Promise<void> => {
const incomingPaymentService = await ctx.container.use(
'incomingPaymentService'
)
const incomingPayment = await incomingPaymentService.getByConnection(
ctx.params.id
)
if (!incomingPayment) return ctx.throw(404)
ctx.incomingPayment = incomingPayment
await next()
}
50 changes: 13 additions & 37 deletions packages/backend/src/open_payments/connection/routes.test.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { IocContract } from '@adonisjs/fold'
import { Knex } from 'knex'
import jestOpenAPI from 'jest-openapi'
import { v4 as uuid } from 'uuid'

import { AppServices, ReadContext } from '../../app'
import { AppServices } from '../../app'
import { Config, IAppConfig } from '../../config/app'
import { createTestApp, TestContainer } from '../../tests/app'
import { truncateTables } from '../../tests/tableManager'
import { initIocContainer } from '../../'
import { ConnectionContext } from './middleware'
import { ConnectionRoutes } from './routes'
import { createAsset } from '../../tests/asset'
import { createContext } from '../../tests/context'
Expand Down Expand Up @@ -67,22 +67,6 @@ describe('Connection Routes', (): void => {
})

describe('get', (): void => {
test('returns 404 for nonexistent connection id on incoming payment', async (): Promise<void> => {
const ctx = createContext<ReadContext>(
{
headers: { Accept: 'application/json' },
url: `/connections/${incomingPayment.connectionId}`
},
{
id: uuid()
}
)
await expect(connectionRoutes.get(ctx)).rejects.toHaveProperty(
'status',
404
)
})

test.each`
state
${IncomingPaymentState.Completed}
Expand All @@ -95,32 +79,24 @@ describe('Connection Routes', (): void => {
expiresAt:
state === IncomingPaymentState.Expired ? new Date() : undefined
})
const ctx = createContext<ReadContext>(
{
headers: { Accept: 'application/json' },
url: `/connections/${incomingPayment.connectionId}`
},
{
id: incomingPayment.connectionId as string
}
)
const ctx = createContext<ConnectionContext>({
headers: { Accept: 'application/json' },
url: `/connections/${incomingPayment.connectionId}`
})
ctx.incomingPayment = incomingPayment
await expect(connectionRoutes.get(ctx)).rejects.toHaveProperty(
'status',
404
)
}
)

test('returns 200 for correct connection id', async (): Promise<void> => {
const ctx = createContext<ReadContext>(
{
headers: { Accept: 'application/json' },
url: `/connections/${incomingPayment.connectionId}`
},
{
id: incomingPayment.connectionId as string
}
)
test('returns 200 with connection', async (): Promise<void> => {
const ctx = createContext<ConnectionContext>({
headers: { Accept: 'application/json' },
url: `/connections/${incomingPayment.connectionId}`
})
ctx.incomingPayment = incomingPayment
await expect(connectionRoutes.get(ctx)).resolves.toBeUndefined()
expect(ctx.response).toSatisfyApiSpec()

Expand Down
15 changes: 5 additions & 10 deletions packages/backend/src/open_payments/connection/routes.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Logger } from 'pino'
import { ReadContext } from '../../app'
import { IncomingPaymentService } from '../payment/incoming/service'
import { ConnectionContext } from './middleware'
import { ConnectionService } from './service'

interface ServiceDependencies {
Expand All @@ -10,7 +10,7 @@ interface ServiceDependencies {
}

export interface ConnectionRoutes {
get(ctx: ReadContext): Promise<void>
get(ctx: ConnectionContext): Promise<void>
}

export function createConnectionRoutes(
Expand All @@ -21,20 +21,15 @@ export function createConnectionRoutes(
})
const deps = { ...deps_, logger }
return {
get: (ctx: ReadContext) => getConnection(deps, ctx)
get: (ctx: ConnectionContext) => getConnection(deps, ctx)
}
}

async function getConnection(
deps: ServiceDependencies,
ctx: ReadContext
ctx: ConnectionContext
): Promise<void> {
const incomingPayment = await deps.incomingPaymentService.getByConnection(
ctx.params.id
)
if (!incomingPayment) return ctx.throw(404)

const connection = deps.connectionService.get(incomingPayment)
const connection = deps.connectionService.get(ctx.incomingPayment)
if (!connection) return ctx.throw(404)
ctx.body = connection.toJSON()
}
Loading

0 comments on commit 4df802b

Please sign in to comment.