From 69afca437fe503d1000352894d0f9c34d2fe1418 Mon Sep 17 00:00:00 2001 From: Blair Currey <12960453+BlairCurrey@users.noreply.github.com> Date: Mon, 25 Mar 2024 10:50:53 -0400 Subject: [PATCH] feat: add cross currency flow and start refactor of tests refactor changes tests to contain entire flow, instead of seperate tests for each step of flow, which are not independent. --- test/integration/integration.test.ts | 190 ++++++++++++++++++++++----- test/integration/lib/test-actions.ts | 176 +++++++++++++++++++++++++ 2 files changed, 331 insertions(+), 35 deletions(-) create mode 100644 test/integration/lib/test-actions.ts diff --git a/test/integration/integration.test.ts b/test/integration/integration.test.ts index 4def099592..62ad838ad8 100644 --- a/test/integration/integration.test.ts +++ b/test/integration/integration.test.ts @@ -20,12 +20,14 @@ import { OutgoingPayment as OutgoingPaymentGql, OutgoingPaymentState } from './lib/generated/graphql' +import { TestActions, createTestActions } from './lib/test-actions' -jest.setTimeout(20000) +jest.setTimeout(20_000) describe('Integration tests', (): void => { let c9: MockASE let hlb: MockASE + let testActions: TestActions beforeAll(async () => { try { @@ -38,6 +40,8 @@ describe('Integration tests', (): void => { // https://github.com/jestjs/jest/issues/2713 process.exit(1) } + + testActions = createTestActions({ sendingASE: c9, receivingASE: hlb }) }) afterAll(async () => { @@ -45,38 +49,8 @@ describe('Integration tests', (): void => { hlb.shutdown() }) - describe('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') - ) - }) - + // Individual requests + describe('Requests', (): void => { test('Can Get Non-Existing Wallet Address', async (): Promise => { const notFoundWalletAddress = 'https://host.docker.internal:4100/accounts/asmith' @@ -109,6 +83,153 @@ 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('Peer to Peer', async (): Promise => { + const { + createReceiver, + createQuote, + createOutgoingPayment, + getOutgoingPayemnt + } = testActions + + const senderWalletAddress = await c9.accounts.getByWalletAddressUrl( + 'https://host.docker.internal:3100/accounts/gfranklin' + ) + assert(senderWalletAddress?.walletAddressID) + const senderWalletAddressId = senderWalletAddress.walletAddressID + const value = '500' + const createReceiverInput = { + metadata: { + description: 'For lunch!' + }, + incomingAmount: { + assetCode: 'USD', + assetScale: 2, + value: value as unknown as bigint + }, + walletAddressUrl: 'https://host.docker.internal:4100/accounts/pfry' + } + + const receiver = await createReceiver(createReceiverInput) + const quote = await createQuote(senderWalletAddressId, receiver) + const outgoingPayment = await createOutgoingPayment( + senderWalletAddressId, + quote + ) + const outgoingPayment_ = await getOutgoingPayemnt( + outgoingPayment.id, + value + ) + expect(outgoingPayment_.sentAmount.value).toBe(value) + }) + test('Peer to Peer - Cross Currency', async (): Promise => { + const { + createReceiver, + createQuote, + createOutgoingPayment, + getOutgoingPayemnt + } = testActions + + const senderWalletAddress = await c9.accounts.getByWalletAddressUrl( + 'https://host.docker.internal:3100/accounts/gfranklin' + ) + assert(senderWalletAddress?.walletAddressID) + const senderWalletAddressId = senderWalletAddress.walletAddressID + const value = '500' + const createReceiverInput = { + metadata: { + description: 'cross-currency' + }, + incomingAmount: { + assetCode: 'EUR', + assetScale: 2, + value: value as unknown as bigint + }, + walletAddressUrl: 'https://host.docker.internal:4100/accounts/lars' + } + + const receiver = await createReceiver(createReceiverInput) + const quote = await createQuote(senderWalletAddressId, receiver) + const outgoingPayment = await createOutgoingPayment( + senderWalletAddressId, + quote + ) + await getOutgoingPayemnt(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( @@ -466,8 +587,7 @@ describe('Integration tests', (): void => { expect(outgoingPayment_.sentAmount.value).toBe(amountValueToSend) }) }) - - describe('Peer to Peer Flow', (): void => { + describe.skip('Peer to Peer Flow', (): void => { const receiverWalletAddressUrl = 'https://host.docker.internal:4100/accounts/pfry' const amountValueToSend = '500' diff --git a/test/integration/lib/test-actions.ts b/test/integration/lib/test-actions.ts new file mode 100644 index 0000000000..8a04d61b7c --- /dev/null +++ b/test/integration/lib/test-actions.ts @@ -0,0 +1,176 @@ +import assert from 'assert' +import { MockASE } from './mock-ase' +import { + Receiver, + Quote, + OutgoingPayment, + OutgoingPaymentState, + CreateReceiverInput +} from './generated/graphql' +import { pollCondition } from './utils' +import { WebhookEventType } from 'mock-account-service-lib' + +interface TestActionDeps { + sendingASE: MockASE + receivingASE: MockASE +} + +export interface TestActions { + createReceiver(createReceiverInput: CreateReceiverInput): Promise + createQuote( + // TODO: refactor to senderWalletAddressId (its is sender right?). or senderWalletAddress + walletAddressId: string, + receiver: Receiver + ): Promise + createOutgoingPayment( + walletAddressId: string, + quote: Quote + ): Promise + getOutgoingPayemnt( + outgoingPaymentId: string, + amountValueToSend: string + ): Promise +} + +export function createTestActions(deps: TestActionDeps): TestActions { + return { + createReceiver: (createReceiverInput) => + createReceiver(deps, createReceiverInput), + createQuote: (walletAddressId, receiver) => + createQuote(deps, walletAddressId, receiver), + createOutgoingPayment: (walletAddressId, quote) => + createOutgoingPayment(deps, walletAddressId, quote), + getOutgoingPayemnt: (outgoingPaymentId, amountValueToSend) => + getOutgoingPayemnt(deps, outgoingPaymentId, amountValueToSend) + } +} + +async function createReceiver( + deps: TestActionDeps, + 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 getOutgoingPayemnt? + // 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) + + expect(response.code).toBe('200') + assert(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) + }) + ) + + return response.receiver +} +async function createQuote( + deps: TestActionDeps, + // TODO: refactor to senderWalletAddressId (its is sender right?). or senderWalletAddress + walletAddressId: string, + receiver: Receiver +): Promise { + const { sendingASE } = deps + const response = await sendingASE.adminClient.createQuote({ + walletAddressId, + receiver: receiver.id + }) + + expect(response.code).toBe('200') + assert(response.quote) + + return response.quote +} +async function createOutgoingPayment( + deps: TestActionDeps, + walletAddressId: string, + quote: Quote +): Promise { + const { sendingASE } = deps + const handleWebhookEventSpy = jest.spyOn( + sendingASE.integrationServer.webhookEventHandler, + 'handleWebhookEvent' + ) + + const response = await sendingASE.adminClient.createOutgoingPayment({ + walletAddressId, + quoteId: quote.id + }) + + expect(response.code).toBe('200') + assert(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) + }) + ) + + return response.payment +} +async function getOutgoingPayemnt( + deps: TestActionDeps, + outgoingPaymentId: string, + amountValueToSend: string +): Promise { + const { sendingASE } = deps + const payment = + 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 +}