From 148dfabd445404caea2a33b407f440cdcf39b92c Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Tue, 26 Mar 2024 23:35:46 -0400 Subject: [PATCH] refactor: test steps into fns and make each flow 1 test --- pnpm-lock.yaml | 6 - test/integration/integration.test.ts | 717 ++++-------------- test/integration/lib/integration-server.ts | 4 +- .../admin.ts} | 62 +- test/integration/lib/test-actions/index.ts | 136 ++++ .../lib/test-actions/open-payments.ts | 365 +++++++++ test/integration/package.json | 2 - 7 files changed, 668 insertions(+), 624 deletions(-) rename test/integration/lib/{test-actions.ts => test-actions/admin.ts} (69%) create mode 100644 test/integration/lib/test-actions/index.ts create mode 100644 test/integration/lib/test-actions/open-payments.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3b08d2b124..ab3db27741 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -650,9 +650,6 @@ importers: '@types/node': specifier: ^18.19.19 version: 18.19.19 - '@types/uuid': - specifier: ^9.0.8 - version: 9.0.8 dotenv: specifier: ^16.4.1 version: 16.4.5 @@ -662,9 +659,6 @@ importers: mock-account-service-lib: specifier: workspace:* version: link:../../packages/mock-account-service-lib - uuid: - specifier: ^9.0.1 - version: 9.0.1 yaml: specifier: ^2.3.4 version: 2.4.1 diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index 7092ff173f..26ad082636 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -1,25 +1,8 @@ import assert from 'assert' -import { validate as isUuid } from 'uuid' -import { - isPendingGrant, - isFinalizedGrant, - WalletAddress, - IncomingPayment, - Quote, - PendingGrant, - Grant, - OutgoingPayment -} from '@interledger/open-payments' import { C9_CONFIG, HLB_CONFIG } from './lib/config' import { MockASE } from './lib/mock-ase' import { WebhookEventType } from 'mock-account-service-lib' -import { parseCookies, poll, pollCondition, wait } from './lib/utils' -import { - Receiver as ReceiverGql, - Quote as QuoteGql, - OutgoingPayment as OutgoingPaymentGql, - OutgoingPaymentState -} from './lib/generated/graphql' +import { poll } from './lib/utils' import { TestActions, createTestActions } from './lib/test-actions' jest.setTimeout(20_000) @@ -35,8 +18,7 @@ describe('Integration tests', (): void => { hlb = await MockASE.create(HLB_CONFIG) } catch (e) { console.error(e) - // Prevents jest from running all tests, which obfuscates error, - // when beforeAll errors. + // Prevents jest from running all tests, which obfuscates errors in beforeAll // https://github.com/jestjs/jest/issues/2713 process.exit(1) } @@ -87,15 +69,154 @@ describe('Integration tests', (): void => { // Series of requests depending on eachother describe('Flows', () => { - test('Open Payments with Continuation via Polling', async (): Promise => {}) - test('Open Payments with Continuation via "interact_ref"', async (): Promise => {}) + test('Open Payments with Continuation via Polling', async (): Promise => { + const { + grantRequestIncomingPayment, + createIncomingPayment, + grantRequestQuote, + createQuote, + grantRequestOutgoingPayment, + pollGrantContinue, + createOutgoingPayment, + getOutgoingPayment + } = testActions.openPayments + const { consentInteraction } = testActions + + const receiverWalletAddressUrl = + 'http://host.docker.internal:4100/accounts/pfry' + const senderWalletAddressUrl = + 'http://host.docker.internal:3100/accounts/gfranklin' + const amountValueToSend = '100' + + const receiverWalletAddress = await c9.opClient.walletAddress.get({ + url: receiverWalletAddressUrl + }) + expect(receiverWalletAddress.id).toBe( + receiverWalletAddressUrl.replace('http', 'https') + ) + + const senderWalletAddress = await c9.opClient.walletAddress.get({ + url: senderWalletAddressUrl + }) + expect(senderWalletAddress.id).toBe( + senderWalletAddressUrl.replace('http', 'https') + ) + + const incomingPaymentGrant = await grantRequestIncomingPayment( + receiverWalletAddress + ) + const incomingPayment = await createIncomingPayment( + receiverWalletAddress, + amountValueToSend, + incomingPaymentGrant.access_token.value + ) + const quoteGrant = await grantRequestQuote(senderWalletAddress) + const quote = await createQuote( + senderWalletAddress, + quoteGrant.access_token.value, + incomingPayment + ) + const outgoingPaymentGrant = await grantRequestOutgoingPayment( + senderWalletAddress, + quote + ) + await consentInteraction(outgoingPaymentGrant, senderWalletAddress) + const grantContinue = await pollGrantContinue(outgoingPaymentGrant) + const outgoingPayment = await createOutgoingPayment( + senderWalletAddress, + grantContinue, + quote + ) + const outgoingPayment_ = await getOutgoingPayment( + outgoingPayment.id, + grantContinue + ) + expect(outgoingPayment_.receiveAmount.value).toBe(amountValueToSend) + expect(outgoingPayment_.sentAmount.value).toBe(amountValueToSend) + }) + test('Open Payments with Continuation via finish method', async (): Promise => { + const { + grantRequestIncomingPayment, + createIncomingPayment, + grantRequestQuote, + createQuote, + grantRequestOutgoingPayment, + grantContinue, + createOutgoingPayment, + getOutgoingPayment + } = testActions.openPayments + const { consentInteractionWithInteractRef } = testActions + + const receiverWalletAddressUrl = + 'http://host.docker.internal:4100/accounts/pfry' + const senderWalletAddressUrl = + 'http://host.docker.internal:3100/accounts/gfranklin' + const amountValueToSend = '100' + + const receiverWalletAddress = await c9.opClient.walletAddress.get({ + url: receiverWalletAddressUrl + }) + expect(receiverWalletAddress.id).toBe( + receiverWalletAddressUrl.replace('http', 'https') + ) + + const senderWalletAddress = await c9.opClient.walletAddress.get({ + url: senderWalletAddressUrl + }) + expect(senderWalletAddress.id).toBe( + senderWalletAddressUrl.replace('http', 'https') + ) + + const incomingPaymentGrant = await grantRequestIncomingPayment( + receiverWalletAddress + ) + const incomingPayment = await createIncomingPayment( + receiverWalletAddress, + amountValueToSend, + incomingPaymentGrant.access_token.value + ) + const quoteGrant = await grantRequestQuote(senderWalletAddress) + const quote = await createQuote( + senderWalletAddress, + quoteGrant.access_token.value, + incomingPayment + ) + const outgoingPaymentGrant = await grantRequestOutgoingPayment( + senderWalletAddress, + quote, + { + method: 'redirect', + uri: 'https://example.com', + nonce: '456' + } + ) + const interactRef = await consentInteractionWithInteractRef( + outgoingPaymentGrant, + senderWalletAddress + ) + const finalizedGrant = await grantContinue( + outgoingPaymentGrant, + interactRef + ) + const outgoingPayment = await createOutgoingPayment( + senderWalletAddress, + finalizedGrant, + quote + ) + const outgoingPayment_ = await getOutgoingPayment( + outgoingPayment.id, + finalizedGrant + ) + expect(outgoingPayment_.receiveAmount.value).toBe(amountValueToSend) + expect(outgoingPayment_.sentAmount.value).toBe(amountValueToSend) + }) test('Peer to Peer', async (): Promise => { const { createReceiver, createQuote, createOutgoingPayment, getOutgoingPayment - } = testActions + } = testActions.admin const senderWalletAddress = await c9.accounts.getByWalletAddressUrl( 'https://host.docker.internal:3100/accounts/gfranklin' @@ -133,7 +254,7 @@ describe('Integration tests', (): void => { createQuote, createOutgoingPayment, getOutgoingPayment - } = testActions + } = testActions.admin const senderWalletAddress = await c9.accounts.getByWalletAddressUrl( 'https://host.docker.internal:3100/accounts/gfranklin' @@ -160,554 +281,6 @@ describe('Integration tests', (): void => { quote ) await getOutgoingPayment(outgoingPayment.id, value) - // TODO: more assertions about cross currecny? not sure what assumptions - // we want to bake-in here. conversion rate/fees/asset code/scale etc. - }) - }) - - // Previous implementation of tests - describe.skip('Open Payments Flow', (): void => { - const receiverWalletAddressUrl = - 'http://host.docker.internal:4100/accounts/pfry' - const senderWalletAddressUrl = - 'http://host.docker.internal:3100/accounts/gfranklin' - const amountValueToSend = '100' - - let receiverWalletAddress: WalletAddress - let senderWalletAddress: WalletAddress - let accessToken: string - let incomingPayment: IncomingPayment - let quote: Quote - let outgoingPaymentGrant: PendingGrant - let grantContinue: Grant - let outgoingPayment: OutgoingPayment - - test('Can Get Existing Wallet Address', async (): Promise => { - receiverWalletAddress = await c9.opClient.walletAddress.get({ - url: receiverWalletAddressUrl - }) - senderWalletAddress = await c9.opClient.walletAddress.get({ - url: senderWalletAddressUrl - }) - - expect(receiverWalletAddress.id).toBe( - receiverWalletAddressUrl.replace('http', 'https') - ) - expect(senderWalletAddress.id).toBe( - senderWalletAddressUrl.replace('http', 'https') - ) - }) - - // test('Can Get Non-Existing Wallet Address', async (): Promise => { - // const notFoundWalletAddress = - // 'https://host.docker.internal:4100/accounts/asmith' - - // const handleWebhookEventSpy = jest.spyOn( - // hlb.integrationServer.webhookEventHandler, - // 'handleWebhookEvent' - // ) - - // // Poll in case the webhook response to create wallet address is slow, - // // but initial request may very well resolve immediately. - // const walletAddress = await poll( - // async () => - // c9.opClient.walletAddress.get({ - // url: notFoundWalletAddress - // }), - // (responseData) => responseData.id === notFoundWalletAddress, - // 5, - // 0.5 - // ) - - // assert(walletAddress) - // expect(walletAddress.id).toBe(notFoundWalletAddress) - // expect(handleWebhookEventSpy).toHaveBeenCalledWith( - // expect.objectContaining({ - // type: WebhookEventType.WalletAddressNotFound, - // data: expect.objectContaining({ - // walletAddressUrl: notFoundWalletAddress - // }) - // }) - // ) - // }) - - test('Grant Request Incoming Payment', async (): Promise => { - const grant = await c9.opClient.grant.request( - { - url: receiverWalletAddress.authServer - }, - { - access_token: { - access: [ - { - type: 'incoming-payment', - actions: ['create', 'read', 'list', 'complete'] - } - ] - } - } - ) - - assert(!isPendingGrant(grant)) - accessToken = grant.access_token.value - }) - - test('Create Incoming Payment', async (): Promise => { - const now = new Date() - const tomorrow = new Date(now) - tomorrow.setDate(now.getDate() + 1) - - const handleWebhookEventSpy = jest.spyOn( - hlb.integrationServer.webhookEventHandler, - 'handleWebhookEvent' - ) - - incomingPayment = await c9.opClient.incomingPayment.create( - { - url: receiverWalletAddress.resourceServer, - accessToken - }, - { - walletAddress: receiverWalletAddressUrl.replace('http', 'https'), - incomingAmount: { - value: amountValueToSend, - assetCode: receiverWalletAddress.assetCode, - assetScale: receiverWalletAddress.assetScale - }, - metadata: { description: 'Free Money!' }, - expiresAt: tomorrow.toISOString() - } - ) - - await pollCondition( - () => { - return handleWebhookEventSpy.mock.calls.some( - (call) => call[0]?.type === WebhookEventType.IncomingPaymentCreated - ) - }, - 5, - 0.5 - ) - - expect(handleWebhookEventSpy).toHaveBeenCalledWith( - expect.objectContaining({ - type: WebhookEventType.IncomingPaymentCreated, - data: expect.any(Object) - }) - ) - }) - - test('Grant Request Quote', async (): Promise => { - const grant = await c9.opClient.grant.request( - { - url: senderWalletAddress.authServer - }, - { - access_token: { - access: [ - { - type: 'quote', - actions: ['read', 'create'] - } - ] - } - } - ) - - assert(!isPendingGrant(grant)) - accessToken = grant.access_token.value - }) - - test('Create Quote', async (): Promise => { - quote = await c9.opClient.quote.create( - { - url: senderWalletAddress.resourceServer, - accessToken - }, - { - walletAddress: senderWalletAddressUrl.replace('http', 'https'), - receiver: incomingPayment.id.replace('https', 'http'), - method: 'ilp' - } - ) - }) - - // --- GRANT CONTINUATION WITH FINISH METHOD --- - // TODO: Grant Continuation w/ finish in another Open Payments Flow test - test.skip('Grant Request Outgoing Payment', async (): Promise => { - const grant = await hlb.opClient.grant.request( - { - url: senderWalletAddress.authServer - }, - { - access_token: { - access: [ - { - type: 'outgoing-payment', - actions: ['create', 'read', 'list'], - identifier: senderWalletAddressUrl.replace('http', 'https'), - limits: { - debitAmount: quote.debitAmount, - receiveAmount: quote.receiveAmount - } - } - ] - }, - interact: { - start: ['redirect'], - finish: { - method: 'redirect', - uri: 'https://example.com', - nonce: '456' - } - } - } - ) - - assert(isPendingGrant(grant)) - outgoingPaymentGrant = grant - - // Delay following request according to the continue wait time - await wait((outgoingPaymentGrant.continue.wait ?? 5) * 1000) - }) - - test.skip('Continuation Request', async (): Promise => { - const { redirect: startInteractionUrl } = outgoingPaymentGrant.interact - const tokens = startInteractionUrl.split('/interact/') - const interactId = tokens[1] ? tokens[1].split('/')[0] : null - const nonce = outgoingPaymentGrant.interact.finish - assert(interactId) - - // Start interaction - const interactResponse = await fetch(startInteractionUrl, { - redirect: 'manual' // dont follow redirects - }) - expect(interactResponse.status).toBe(302) - - const cookie = parseCookies(interactResponse) - - // Accept - const acceptResponse = await fetch( - `${senderWalletAddress.authServer}/grant/${interactId}/${nonce}/accept`, - { - method: 'POST', - headers: { - 'x-idp-secret': 'replace-me', - cookie - } - } - ) - expect(acceptResponse.status).toBe(202) - - // Finish interaction - const finishResponse = await fetch( - `${senderWalletAddress.authServer}/interact/${interactId}/${nonce}/finish`, - { - method: 'GET', - headers: { - 'x-idp-secret': 'replace-me', - cookie - }, - redirect: 'manual' // dont follow redirects - } - ) - expect(finishResponse.status).toBe(302) - - const redirectURI = finishResponse.headers.get('location') - assert(redirectURI) - - const url = new URL(redirectURI) - const interact_ref = url.searchParams.get('interact_ref') - assert(interact_ref) - - const { access_token, uri } = outgoingPaymentGrant.continue - const grantContinue_ = await c9.opClient.grant.continue( - { - accessToken: access_token.value, - url: uri - }, - { interact_ref } - ) - assert(isFinalizedGrant(grantContinue_)) - grantContinue = grantContinue_ - }) - // --- GRANT CONTINUATION WITH FINISH METHOD --- - - test('Grant Request Outgoing Payment', async (): Promise => { - const grant = await hlb.opClient.grant.request( - { - url: senderWalletAddress.authServer - }, - { - access_token: { - access: [ - { - type: 'outgoing-payment', - actions: ['create', 'read', 'list'], - identifier: senderWalletAddressUrl.replace('http', 'https'), - limits: { - debitAmount: quote.debitAmount, - receiveAmount: quote.receiveAmount - } - } - ] - }, - interact: { - start: ['redirect'] - } - } - ) - - assert(isPendingGrant(grant)) - outgoingPaymentGrant = grant - - // Delay following request according to the continue wait time - await wait((outgoingPaymentGrant.continue.wait ?? 5) * 1000) - }) - - test('Continuation Request', async (): Promise => { - const { redirect: startInteractionUrl } = outgoingPaymentGrant.interact - const tokens = startInteractionUrl.split('/interact/') - const interactId = tokens[1] ? tokens[1].split('/')[0] : null - const nonce = outgoingPaymentGrant.interact.finish - assert(interactId) - - // Start interaction - const interactResponse = await fetch(startInteractionUrl, { - redirect: 'manual' // dont follow redirects - }) - expect(interactResponse.status).toBe(302) - - const cookie = parseCookies(interactResponse) - - // Accept - const acceptResponse = await fetch( - `${senderWalletAddress.authServer}/grant/${interactId}/${nonce}/accept`, - { - method: 'POST', - headers: { - 'x-idp-secret': 'replace-me', - cookie - } - } - ) - - expect(acceptResponse.status).toBe(202) - - // Finish interaction - const finishResponse = await fetch( - `${senderWalletAddress.authServer}/interact/${interactId}/${nonce}/finish`, - { - method: 'GET', - headers: { - 'x-idp-secret': 'replace-me', - cookie - } - } - ) - expect(finishResponse.status).toBe(202) - - const { access_token, uri } = outgoingPaymentGrant.continue - const grantContinue_ = await poll( - async () => - c9.opClient.grant.continue({ - accessToken: access_token.value, - url: uri - }), - (responseData) => 'access_token' in responseData, - 20, - 5 - ) - - assert(isFinalizedGrant(grantContinue_)) - grantContinue = grantContinue_ - }) - - test('Create Outgoing Payment', async (): Promise => { - const handleWebhookEventSpy = jest.spyOn( - c9.integrationServer.webhookEventHandler, - 'handleWebhookEvent' - ) - - outgoingPayment = await c9.opClient.outgoingPayment.create( - { - url: senderWalletAddress.resourceServer, - accessToken: grantContinue.access_token.value - }, - { - walletAddress: senderWalletAddressUrl.replace('http', 'https'), - metadata: {}, - quoteId: quote.id - } - ) - - await pollCondition( - () => { - return ( - handleWebhookEventSpy.mock.calls.some( - (call) => - call[0]?.type === WebhookEventType.OutgoingPaymentCreated - ) && - handleWebhookEventSpy.mock.calls.some( - (call) => - call[0]?.type === WebhookEventType.OutgoingPaymentCompleted - ) - ) - }, - 5, - 0.5 - ) - - expect(handleWebhookEventSpy).toHaveBeenCalledWith( - expect.objectContaining({ - type: WebhookEventType.OutgoingPaymentCreated, - data: expect.any(Object) - }) - ) - expect(handleWebhookEventSpy).toHaveBeenCalledWith( - expect.objectContaining({ - type: WebhookEventType.OutgoingPaymentCompleted, - data: expect.any(Object) - }) - ) - }) - - test('Get Outgoing Payment', async (): Promise => { - const id = outgoingPayment.id.split('/').pop() - assert(id) - expect(isUuid(id)).toBe(true) - - const outgoingPayment_ = await c9.opClient.outgoingPayment.get({ - url: `${senderWalletAddress.resourceServer}/outgoing-payments/${id}`, - accessToken: grantContinue.access_token.value - }) - - expect(outgoingPayment_.id).toBe(outgoingPayment.id) - expect(outgoingPayment_.receiveAmount.value).toBe(amountValueToSend) - expect(outgoingPayment_.sentAmount.value).toBe(amountValueToSend) - }) - }) - describe.skip('Peer to Peer Flow', (): void => { - const receiverWalletAddressUrl = - 'https://host.docker.internal:4100/accounts/pfry' - const amountValueToSend = '500' - - let gfranklinWalletAddressId: string - let receiver: ReceiverGql - let quote: QuoteGql - let outgoingPayment: OutgoingPaymentGql - - beforeAll(async () => { - const gfranklinWalletAddress = await c9.accounts.getByWalletAddressUrl( - 'https://host.docker.internal:3100/accounts/gfranklin' - ) - assert(gfranklinWalletAddress?.walletAddressID) - gfranklinWalletAddressId = gfranklinWalletAddress.walletAddressID - }) - - test('Create Receiver (remote Incoming Payment)', async (): Promise => { - const handleWebhookEventSpy = jest.spyOn( - hlb.integrationServer.webhookEventHandler, - 'handleWebhookEvent' - ) - const response = await c9.adminClient.createReceiver({ - metadata: { - description: 'For lunch!' - }, - incomingAmount: { - assetCode: 'USD', - assetScale: 2, - value: amountValueToSend as unknown as bigint - }, - walletAddressUrl: receiverWalletAddressUrl - }) - - expect(response.code).toBe('200') - assert(response.receiver) - - receiver = response.receiver - - await pollCondition( - () => { - return handleWebhookEventSpy.mock.calls.some( - (call) => call[0]?.type === WebhookEventType.IncomingPaymentCreated - ) - }, - 5, - 0.5 - ) - - expect(handleWebhookEventSpy).toHaveBeenCalledWith( - expect.objectContaining({ - type: WebhookEventType.IncomingPaymentCreated, - data: expect.any(Object) - }) - ) - }) - test('Create Quote', async (): Promise => { - const response = await c9.adminClient.createQuote({ - walletAddressId: gfranklinWalletAddressId, - receiver: receiver.id - }) - - expect(response.code).toBe('200') - assert(response.quote) - - quote = response.quote - }) - test('Create Outgoing Payment', async (): Promise => { - const handleWebhookEventSpy = jest.spyOn( - c9.integrationServer.webhookEventHandler, - 'handleWebhookEvent' - ) - - const response = await c9.adminClient.createOutgoingPayment({ - walletAddressId: gfranklinWalletAddressId, - quoteId: quote.id - }) - - expect(response.code).toBe('200') - assert(response.payment) - - outgoingPayment = response.payment - - await pollCondition( - () => { - return ( - handleWebhookEventSpy.mock.calls.some( - (call) => - call[0]?.type === WebhookEventType.OutgoingPaymentCreated - ) && - handleWebhookEventSpy.mock.calls.some( - (call) => - call[0]?.type === WebhookEventType.OutgoingPaymentCompleted - ) - ) - }, - 5, - 0.5 - ) - - expect(handleWebhookEventSpy).toHaveBeenCalledWith( - expect.objectContaining({ - type: WebhookEventType.OutgoingPaymentCreated, - data: expect.any(Object) - }) - ) - expect(handleWebhookEventSpy).toHaveBeenCalledWith( - expect.objectContaining({ - type: WebhookEventType.OutgoingPaymentCompleted, - data: expect.any(Object) - }) - ) - }) - test('Get Outgoing Payment', async (): Promise => { - const payment = await c9.adminClient.getOutgoingPayment( - outgoingPayment.id - ) - expect(payment.state).toBe(OutgoingPaymentState.Completed) - expect(payment.receiveAmount.value).toBe(amountValueToSend) - expect(payment.sentAmount.value).toBe(amountValueToSend) }) }) }) diff --git a/test/integration/lib/integration-server.ts b/test/integration/lib/integration-server.ts index a9eb50b96c..703ab57984 100644 --- a/test/integration/lib/integration-server.ts +++ b/test/integration/lib/integration-server.ts @@ -1,7 +1,7 @@ +import crypto from 'crypto' import Koa from 'koa' import bodyParser from '@koa/bodyparser' import http from 'http' -import { v4 as uuid } from 'uuid' import { AccountProvider, WebhookEventType, @@ -171,7 +171,7 @@ export class WebhookEventHandler { const response = await this.adminClient.depositOutgoingPaymentLiquidity({ outgoingPaymentId: payment.id, - idempotencyKey: uuid() + idempotencyKey: crypto.randomUUID() }) if (response.code !== '200') { diff --git a/test/integration/lib/test-actions.ts b/test/integration/lib/test-actions/admin.ts similarity index 69% rename from test/integration/lib/test-actions.ts rename to test/integration/lib/test-actions/admin.ts index 504cf04f71..4c73a0e371 100644 --- a/test/integration/lib/test-actions.ts +++ b/test/integration/lib/test-actions/admin.ts @@ -1,29 +1,25 @@ import assert from 'assert' -import { MockASE } from './mock-ase' import { Receiver, Quote, OutgoingPayment, OutgoingPaymentState, CreateReceiverInput -} from './generated/graphql' -import { pollCondition } from './utils' +} from '../generated/graphql' +import { MockASE } from '../mock-ase' +import { pollCondition } from '../utils' import { WebhookEventType } from 'mock-account-service-lib' -interface TestActionDeps { +interface AdminActionsDeps { sendingASE: MockASE receivingASE: MockASE } -export interface TestActions { +export interface AdminActions { createReceiver(createReceiverInput: CreateReceiverInput): Promise - createQuote( - // TODO: refactor to senderWalletAddressId (its is sender right?). or senderWalletAddress - walletAddressId: string, - receiver: Receiver - ): Promise + createQuote(senderWalletAddressId: string, receiver: Receiver): Promise createOutgoingPayment( - walletAddressId: string, + senderWalletAddressId: string, quote: Quote ): Promise getOutgoingPayment( @@ -32,42 +28,28 @@ export interface TestActions { ): Promise } -export function createTestActions(deps: TestActionDeps): TestActions { +export function createAdminActions(deps: AdminActionsDeps): AdminActions { return { createReceiver: (createReceiverInput) => createReceiver(deps, createReceiverInput), - createQuote: (walletAddressId, receiver) => - createQuote(deps, walletAddressId, receiver), - createOutgoingPayment: (walletAddressId, quote) => - createOutgoingPayment(deps, walletAddressId, quote), + createQuote: (senderWalletAddressId, receiver) => + createQuote(deps, senderWalletAddressId, receiver), + createOutgoingPayment: (senderWalletAddressId, quote) => + createOutgoingPayment(deps, senderWalletAddressId, quote), getOutgoingPayment: (outgoingPaymentId, amountValueToSend) => getOutgoingPayment(deps, outgoingPaymentId, amountValueToSend) } } async function createReceiver( - deps: TestActionDeps, + deps: AdminActionsDeps, createReceiverInput: CreateReceiverInput - // receiverWalletAddressUrl: string, - // amountValueToSend: string ): Promise { const { receivingASE, sendingASE } = deps const handleWebhookEventSpy = jest.spyOn( receivingASE.integrationServer.webhookEventHandler, 'handleWebhookEvent' ) - // TODO: paramaterize metadata and expect in getOutgoingPayment? - // const response = await sendingASE.adminClient.createReceiver({ - // metadata: { - // description: 'For lunch!' - // }, - // incomingAmount: { - // assetCode: 'USD', - // assetScale: 2, - // value: amountValueToSend as unknown as bigint - // }, - // walletAddressUrl: receiverWalletAddressUrl - // }) const response = await sendingASE.adminClient.createReceiver(createReceiverInput) @@ -94,14 +76,13 @@ async function createReceiver( return response.receiver } async function createQuote( - deps: TestActionDeps, - // TODO: refactor to senderWalletAddressId (its is sender right?). or senderWalletAddress - walletAddressId: string, + deps: AdminActionsDeps, + senderWalletAddressId: string, receiver: Receiver ): Promise { const { sendingASE } = deps const response = await sendingASE.adminClient.createQuote({ - walletAddressId, + walletAddressId: senderWalletAddressId, receiver: receiver.id }) @@ -111,8 +92,8 @@ async function createQuote( return response.quote } async function createOutgoingPayment( - deps: TestActionDeps, - walletAddressId: string, + deps: AdminActionsDeps, + senderWalletAddressId: string, quote: Quote ): Promise { const { sendingASE } = deps @@ -122,7 +103,7 @@ async function createOutgoingPayment( ) const response = await sendingASE.adminClient.createOutgoingPayment({ - walletAddressId, + walletAddressId: senderWalletAddressId, quoteId: quote.id }) @@ -160,7 +141,7 @@ async function createOutgoingPayment( return response.payment } async function getOutgoingPayment( - deps: TestActionDeps, + deps: AdminActionsDeps, outgoingPaymentId: string, amountValueToSend: string ): Promise { @@ -169,8 +150,5 @@ async function getOutgoingPayment( await sendingASE.adminClient.getOutgoingPayment(outgoingPaymentId) expect(payment.state).toBe(OutgoingPaymentState.Completed) expect(payment.receiveAmount.value).toBe(amountValueToSend) - // - // expect(payment.sentAmount.value).toBe(amountValueToSend) - return payment } diff --git a/test/integration/lib/test-actions/index.ts b/test/integration/lib/test-actions/index.ts new file mode 100644 index 0000000000..b67aef23ce --- /dev/null +++ b/test/integration/lib/test-actions/index.ts @@ -0,0 +1,136 @@ +import assert from 'assert' +import { MockASE } from '../mock-ase' +import { parseCookies } from '../utils' +import { WalletAddress, PendingGrant } from '@interledger/open-payments' +import { AdminActions, createAdminActions } from './admin' +import { OpenPaymentsActions, createOpenPaymentsActions } from './open-payments' + +export interface TestActionsDeps { + sendingASE: MockASE + receivingASE: MockASE +} + +export interface TestActions { + consentInteraction( + outgoingPaymentGrant: PendingGrant, + senderWalletAddress: WalletAddress + ): Promise + consentInteractionWithInteractRef( + outgoingPaymentGrant: PendingGrant, + senderWalletAddress: WalletAddress + ): Promise + admin: AdminActions + openPayments: OpenPaymentsActions +} + +export function createTestActions(deps: TestActionsDeps): TestActions { + return { + consentInteraction: (outgoingPaymentGrant, senderWalletAddress) => + consentInteraction(deps, outgoingPaymentGrant, senderWalletAddress), + consentInteractionWithInteractRef: ( + outgoingPaymentGrant, + senderWalletAddress + ) => + consentInteractionWithInteractRef( + deps, + outgoingPaymentGrant, + senderWalletAddress + ), + admin: createAdminActions(deps), + openPayments: createOpenPaymentsActions(deps) + } +} + +async function consentInteraction( + deps: TestActionsDeps, + outgoingPaymentGrant: PendingGrant, + senderWalletAddress: WalletAddress +) { + const { interactId, nonce, cookie } = await _startAndAcceptInteraction( + outgoingPaymentGrant, + senderWalletAddress + ) + + // Finish interacton + const finishResponse = await fetch( + `${senderWalletAddress.authServer}/interact/${interactId}/${nonce}/finish`, + { + method: 'GET', + headers: { + 'x-idp-secret': 'replace-me', + cookie + } + } + ) + expect(finishResponse.status).toBe(202) +} + +async function consentInteractionWithInteractRef( + deps: TestActionsDeps, + outgoingPaymentGrant: PendingGrant, + senderWalletAddress: WalletAddress +): Promise { + const { interactId, nonce, cookie } = await _startAndAcceptInteraction( + outgoingPaymentGrant, + senderWalletAddress + ) + + // Finish interacton + const finishResponse = await fetch( + `${senderWalletAddress.authServer}/interact/${interactId}/${nonce}/finish`, + { + method: 'GET', + headers: { + 'x-idp-secret': 'replace-me', + cookie + }, + redirect: 'manual' // dont follow redirects + } + ) + expect(finishResponse.status).toBe(302) + + const redirectURI = finishResponse.headers.get('location') + assert(redirectURI) + + const url = new URL(redirectURI) + const interact_ref = url.searchParams.get('interact_ref') + assert(interact_ref) + + return interact_ref +} + +async function _startAndAcceptInteraction( + outgoingPaymentGrant: PendingGrant, + senderWalletAddress: WalletAddress +): Promise<{ nonce: string; interactId: string; cookie: string }> { + const { redirect: startInteractionUrl } = outgoingPaymentGrant.interact + + // Start interaction + const interactResponse = await fetch(startInteractionUrl, { + redirect: 'manual' // dont follow redirects + }) + expect(interactResponse.status).toBe(302) + + const cookie = parseCookies(interactResponse) + + const nonce = outgoingPaymentGrant.interact.finish + const tokens = startInteractionUrl.split('/interact/') + const interactId = tokens[1] ? tokens[1].split('/')[0] : null + assert(interactId) + + // Accept + const acceptResponse = await fetch( + `${senderWalletAddress.authServer}/grant/${interactId}/${nonce}/accept`, + { + method: 'POST', + headers: { + 'x-idp-secret': 'replace-me', + cookie + } + } + ) + + expect(acceptResponse.status).toBe(202) + + return { nonce, interactId, cookie } +} diff --git a/test/integration/lib/test-actions/open-payments.ts b/test/integration/lib/test-actions/open-payments.ts new file mode 100644 index 0000000000..20796118bf --- /dev/null +++ b/test/integration/lib/test-actions/open-payments.ts @@ -0,0 +1,365 @@ +import assert from 'assert' +import { + Grant, + GrantRequest, + IncomingPayment, + OutgoingPayment, + PendingGrant, + Quote, + WalletAddress, + isFinalizedGrant, + isPendingGrant +} from '@interledger/open-payments' +import { MockASE } from '../mock-ase' +import { poll, pollCondition, wait } from '../utils' +import { WebhookEventType } from 'mock-account-service-lib' + +export interface OpenPaymentsActionsDeps { + sendingASE: MockASE + receivingASE: MockASE +} + +export interface OpenPaymentsActions { + grantRequestIncomingPayment( + receiverWalletAddress: WalletAddress + ): Promise + createIncomingPayment( + receiverWalletAddress: WalletAddress, + amountValueToSend: string, + accessToken: string + ): Promise + grantRequestQuote(senderWalletAddress: WalletAddress): Promise + createQuote( + senderWalletAddress: WalletAddress, + accessToken: string, + incomingPayment: IncomingPayment + ): Promise + grantRequestOutgoingPayment( + senderWalletAddress: WalletAddress, + quote: Quote, + finish?: InteractFinish + ): Promise + pollGrantContinue(outgoingPaymentGrant: PendingGrant): Promise + grantContinue( + outgoingPaymentGrant: PendingGrant, + interact_ref: string + ): Promise + createOutgoingPayment( + senderWalletAddress: WalletAddress, + grant: Grant, + quote: Quote + ): Promise + getOutgoingPayment( + url: string, + grantContinue: Grant + ): Promise +} + +export function createOpenPaymentsActions( + deps: OpenPaymentsActionsDeps +): OpenPaymentsActions { + return { + grantRequestIncomingPayment: (receiverWalletAddress) => + grantRequestIncomingPayment(deps, receiverWalletAddress), + createIncomingPayment: ( + receiverWalletAddress, + amountValueToSend, + accessToken + ) => + createIncomingPayment( + deps, + receiverWalletAddress, + amountValueToSend, + accessToken + ), + grantRequestQuote: (senderWalletAddress) => + grantRequestQuote(deps, senderWalletAddress), + createQuote: (senderWalletAddress, accessToken, incomingPayment) => + createQuote(deps, senderWalletAddress, accessToken, incomingPayment), + grantRequestOutgoingPayment: (senderWalletAddress, quote, finish) => + grantRequestOutgoingPayment(deps, senderWalletAddress, quote, finish), + pollGrantContinue: (outgoingPaymentGrant) => + pollGrantContinue(deps, outgoingPaymentGrant), + grantContinue: (outgoingPaymentGrant, interact_ref) => + grantContinue(deps, outgoingPaymentGrant, interact_ref), + createOutgoingPayment: (senderWalletAddress, grant, quote) => + createOutgoingPayment(deps, senderWalletAddress, grant, quote), + getOutgoingPayment: (url, grantContinue) => + getOutgoingPayment(deps, url, grantContinue) + } +} +async function grantRequestIncomingPayment( + deps: OpenPaymentsActionsDeps, + receiverWalletAddress: WalletAddress +): Promise { + const { sendingASE } = deps + const grant = await sendingASE.opClient.grant.request( + { + url: receiverWalletAddress.authServer + }, + { + access_token: { + access: [ + { + type: 'incoming-payment', + actions: ['create', 'read', 'list', 'complete'] + } + ] + } + } + ) + assert(!isPendingGrant(grant)) + return grant +} + +async function createIncomingPayment( + deps: OpenPaymentsActionsDeps, + receiverWalletAddress: WalletAddress, + amountValueToSend: string, + accessToken: string +) { + const { sendingASE, receivingASE } = deps + const now = new Date() + const tomorrow = new Date(now) + tomorrow.setDate(now.getDate() + 1) + + const handleWebhookEventSpy = jest.spyOn( + receivingASE.integrationServer.webhookEventHandler, + 'handleWebhookEvent' + ) + + const incomingPayment = await sendingASE.opClient.incomingPayment.create( + { + url: receiverWalletAddress.resourceServer, + accessToken + }, + { + walletAddress: receiverWalletAddress.id, + incomingAmount: { + value: amountValueToSend, + assetCode: receiverWalletAddress.assetCode, + assetScale: receiverWalletAddress.assetScale + }, + metadata: { description: 'Free Money!' }, + expiresAt: tomorrow.toISOString() + } + ) + + await pollCondition( + () => { + return handleWebhookEventSpy.mock.calls.some( + (call) => call[0]?.type === WebhookEventType.IncomingPaymentCreated + ) + }, + 5, + 0.5 + ) + + expect(handleWebhookEventSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: WebhookEventType.IncomingPaymentCreated, + data: expect.any(Object) + }) + ) + + return incomingPayment +} + +async function grantRequestQuote( + deps: OpenPaymentsActionsDeps, + senderWalletAddress: WalletAddress +): Promise { + const { sendingASE } = deps + const grant = await sendingASE.opClient.grant.request( + { + url: senderWalletAddress.authServer + }, + { + access_token: { + access: [ + { + type: 'quote', + actions: ['read', 'create'] + } + ] + } + } + ) + assert(!isPendingGrant(grant)) + return grant +} + +async function createQuote( + deps: OpenPaymentsActionsDeps, + senderWalletAddress: WalletAddress, + accessToken: string, + incomingPayment: IncomingPayment +): Promise { + const { sendingASE } = deps + return await sendingASE.opClient.quote.create( + { + url: senderWalletAddress.resourceServer, + accessToken + }, + { + walletAddress: senderWalletAddress.id, + receiver: incomingPayment.id.replace('https', 'http'), + method: 'ilp' + } + ) +} + +type InteractFinish = NonNullable['finish'] + +async function grantRequestOutgoingPayment( + deps: OpenPaymentsActionsDeps, + senderWalletAddress: WalletAddress, + quote: Quote, + finish?: InteractFinish +): Promise { + const { receivingASE } = deps + const grant = await receivingASE.opClient.grant.request( + { + url: senderWalletAddress.authServer + }, + { + access_token: { + access: [ + { + type: 'outgoing-payment', + actions: ['create', 'read', 'list'], + identifier: senderWalletAddress.id, + limits: { + debitAmount: quote.debitAmount, + receiveAmount: quote.receiveAmount + } + } + ] + }, + interact: { + start: ['redirect'], + finish + } + } + ) + + assert(isPendingGrant(grant)) + + if (grant.continue.wait) { + // Delay following request according to the continue wait time (if any) + await wait(grant.continue.wait * 1000) + } + + return grant +} + +async function pollGrantContinue( + deps: OpenPaymentsActionsDeps, + outgoingPaymentGrant: PendingGrant +): Promise { + const { sendingASE } = deps + const { access_token, uri } = outgoingPaymentGrant.continue + const grantContinue = await poll( + async () => + sendingASE.opClient.grant.continue({ + accessToken: access_token.value, + url: uri + }), + (responseData) => 'access_token' in responseData, + 20, + 5 + ) + + assert(isFinalizedGrant(grantContinue)) + return grantContinue +} + +async function grantContinue( + deps: OpenPaymentsActionsDeps, + outgoingPaymentGrant: PendingGrant, + interact_ref: string +): Promise { + const { sendingASE } = deps + const { access_token, uri } = outgoingPaymentGrant.continue + const grantContinue = await sendingASE.opClient.grant.continue( + { + accessToken: access_token.value, + url: uri + }, + { interact_ref } + ) + + assert(isFinalizedGrant(grantContinue)) + return grantContinue +} + +async function createOutgoingPayment( + deps: OpenPaymentsActionsDeps, + senderWalletAddress: WalletAddress, + grantContinue: Grant, + quote: Quote +): Promise { + const { sendingASE } = deps + const handleWebhookEventSpy = jest.spyOn( + sendingASE.integrationServer.webhookEventHandler, + 'handleWebhookEvent' + ) + + const outgoingPayment = await sendingASE.opClient.outgoingPayment.create( + { + url: senderWalletAddress.resourceServer, + accessToken: grantContinue.access_token.value + }, + { + walletAddress: senderWalletAddress.id, + metadata: {}, + quoteId: quote.id + } + ) + + await pollCondition( + () => { + return ( + handleWebhookEventSpy.mock.calls.some( + (call) => call[0]?.type === WebhookEventType.OutgoingPaymentCreated + ) && + handleWebhookEventSpy.mock.calls.some( + (call) => call[0]?.type === WebhookEventType.OutgoingPaymentCompleted + ) + ) + }, + 5, + 0.5 + ) + + expect(handleWebhookEventSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: WebhookEventType.OutgoingPaymentCreated, + data: expect.any(Object) + }) + ) + expect(handleWebhookEventSpy).toHaveBeenCalledWith( + expect.objectContaining({ + type: WebhookEventType.OutgoingPaymentCompleted, + data: expect.any(Object) + }) + ) + + return outgoingPayment +} + +async function getOutgoingPayment( + deps: OpenPaymentsActionsDeps, + url: string, + grantContinue: Grant +) { + const { sendingASE } = deps + const outgoingPayment = await sendingASE.opClient.outgoingPayment.get({ + url, + accessToken: grantContinue.access_token.value + }) + + expect(outgoingPayment.id).toBe(outgoingPayment.id) + + return outgoingPayment +} diff --git a/test/integration/package.json b/test/integration/package.json index c9b8671819..e54c60b6be 100644 --- a/test/integration/package.json +++ b/test/integration/package.json @@ -20,11 +20,9 @@ "@types/koa": "2.14.0", "@types/koa-bodyparser": "^4.3.12", "@types/node": "^18.19.19", - "@types/uuid": "^9.0.8", "dotenv": "^16.4.1", "koa": "^2.15.0", "mock-account-service-lib": "workspace:*", - "uuid": "^9.0.1", "yaml": "^2.3.4" } }