-
Notifications
You must be signed in to change notification settings - Fork 89
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(backend): add Open Payments client service #595
Changes from all commits
5051462
e7f71d1
e78c60f
5d7c193
d34228c
8928c14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
import { IocContract } from '@adonisjs/fold' | ||
import { faker } from '@faker-js/faker' | ||
import { Knex } from 'knex' | ||
import nock from 'nock' | ||
import { URL } from 'url' | ||
import { v4 as uuid } from 'uuid' | ||
|
||
import { OpenPaymentsClientService } from './service' | ||
import { createTestApp, TestContainer } from '../../tests/app' | ||
import { Config } from '../../config/app' | ||
import { initIocContainer } from '../..' | ||
import { AppServices } from '../../app' | ||
import { createIncomingPayment } from '../../tests/incomingPayment' | ||
import { createPaymentPointer } from '../../tests/paymentPointer' | ||
import { truncateTables } from '../../tests/tableManager' | ||
|
||
describe('Open Payments Client Service', (): void => { | ||
let deps: IocContract<AppServices> | ||
let appContainer: TestContainer | ||
let clientService: OpenPaymentsClientService | ||
let knex: Knex | ||
|
||
beforeAll(async (): Promise<void> => { | ||
deps = await initIocContainer(Config) | ||
appContainer = await createTestApp(deps) | ||
clientService = await deps.use('openPaymentsClientService') | ||
knex = await deps.use('knex') | ||
}) | ||
|
||
afterEach(async (): Promise<void> => { | ||
jest.useRealTimers() | ||
await truncateTables(knex) | ||
}) | ||
|
||
afterAll(async (): Promise<void> => { | ||
await appContainer.shutdown() | ||
}) | ||
describe('incomingPayment.get', (): void => { | ||
test.each` | ||
incomingAmount | description | externalRef | ||
${undefined} | ${undefined} | ${undefined} | ||
${BigInt(123)} | ${'Test'} | ${'#123'} | ||
`( | ||
'resolves incoming payment from Open Payments server', | ||
async ({ incomingAmount, description, externalRef }): Promise<void> => { | ||
const paymentPointer = await createPaymentPointer(deps, { | ||
mockServerPort: appContainer.openPaymentsPort | ||
}) | ||
const incomingPayment = await createIncomingPayment(deps, { | ||
paymentPointerId: paymentPointer.id, | ||
description, | ||
incomingAmount: incomingAmount && { | ||
value: incomingAmount, | ||
assetCode: paymentPointer.asset.code, | ||
assetScale: paymentPointer.asset.scale | ||
}, | ||
externalRef | ||
}) | ||
const resolvedPayment = await clientService.incomingPayment.get( | ||
incomingPayment.url | ||
) | ||
paymentPointer.scope.isDone() | ||
expect(resolvedPayment).toEqual({ | ||
...incomingPayment.toJSON(), | ||
id: incomingPayment.url, | ||
paymentPointer: incomingPayment.paymentPointer.url, | ||
ilpStreamConnection: { | ||
id: `${Config.openPaymentsUrl}/connections/${incomingPayment.connectionId}`, | ||
ilpAddress: expect.any(String), | ||
sharedSecret: expect.any(String) | ||
} | ||
}) | ||
} | ||
) | ||
test.each` | ||
statusCode | ||
${404} | ||
${500} | ||
`( | ||
'returns undefined for unsuccessful request ($statusCode)', | ||
async ({ statusCode }): Promise<void> => { | ||
const incomingPaymentUrl = new URL( | ||
`${faker.internet.url()}/incoming-payments/${uuid()}` | ||
) | ||
const scope = nock(incomingPaymentUrl.host) | ||
.get(incomingPaymentUrl.pathname) | ||
.reply(statusCode) | ||
scope.isDone() | ||
await expect( | ||
clientService.incomingPayment.get(incomingPaymentUrl.href) | ||
).resolves.toBeUndefined() | ||
} | ||
) | ||
test('returns undefined for invalid incoming payment response', async (): Promise<void> => { | ||
const incomingPaymentUrl = new URL( | ||
`${faker.internet.url()}/incoming-payments/${uuid()}` | ||
) | ||
const scope = nock(incomingPaymentUrl.host) | ||
.get(incomingPaymentUrl.pathname) | ||
.reply(200, () => ({ | ||
validPayment: 0 | ||
})) | ||
scope.isDone() | ||
await expect( | ||
clientService.incomingPayment.get(incomingPaymentUrl.href) | ||
).resolves.toBeUndefined() | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
import axios from 'axios' | ||
import { OpenAPI, HttpMethod, ValidateFunction } from 'openapi' | ||
|
||
import { BaseService } from '../../shared/baseService' | ||
import { IncomingPaymentJSON } from '../payment/incoming/model' | ||
|
||
const REQUEST_TIMEOUT = 5_000 // millseconds | ||
|
||
export interface OpenPaymentsClientService { | ||
incomingPayment: { | ||
get(url: string): Promise<IncomingPaymentJSON | undefined> | ||
} | ||
} | ||
|
||
export interface ServiceDependencies extends BaseService { | ||
accessToken: string | ||
openApi: OpenAPI | ||
validateResponse: ValidateFunction<IncomingPaymentJSON> | ||
} | ||
|
||
export async function createOpenPaymentsClientService( | ||
deps_: Omit<ServiceDependencies, 'validateResponse'> | ||
): Promise<OpenPaymentsClientService> { | ||
const log = deps_.logger.child({ | ||
service: 'OpenPaymentsClientService' | ||
}) | ||
const validateResponse = | ||
deps_.openApi.createResponseValidator<IncomingPaymentJSON>({ | ||
path: '/incoming-payments/{incomingPaymentId}', | ||
method: HttpMethod.GET | ||
}) | ||
Comment on lines
+27
to
+31
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does that have to be overwritten? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, the latter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🤔 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Never mind, I misread. |
||
const deps: ServiceDependencies = { | ||
...deps_, | ||
logger: log, | ||
validateResponse | ||
} | ||
return { | ||
incomingPayment: { | ||
get: (url) => getIncomingPayment(deps, url) | ||
} | ||
} | ||
} | ||
|
||
export async function getIncomingPayment( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could we check whether the incoming payment is a resource on the same Rafiki instance and if so, we just use the service to get it instead of the axios call + all the validation? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I initially tried that but didn't like having two code paths, one dealing with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Alright, we keep it simple then. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks up local incoming payments / connections and keeps it simpler by calling the route methods: |
||
deps: ServiceDependencies, | ||
url: string | ||
): Promise<IncomingPaymentJSON | undefined> { | ||
const requestHeaders = { | ||
Authorization: `GNAP ${deps.accessToken}`, | ||
'Content-Type': 'application/json' | ||
} | ||
try { | ||
const { status, data } = await axios.get(url, { | ||
headers: requestHeaders, | ||
timeout: REQUEST_TIMEOUT, | ||
validateStatus: (status) => status === 200 | ||
}) | ||
if ( | ||
!deps.validateResponse({ | ||
status, | ||
body: data | ||
}) | ||
) { | ||
throw new Error('unreachable') | ||
} | ||
return data | ||
} catch (_) { | ||
return undefined | ||
} | ||
} |
Check failure
Code scanning / CodeQL
Hard-coded credentials