From 64acf8e2b1083de6f78b7d21dd2701af2fee1911 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Thu, 21 Apr 2022 14:23:53 -0300 Subject: payments test case --- .../taler-wallet-webextension/src/cta/Pay.test.ts | 408 +++++++++++++++++++++ 1 file changed, 408 insertions(+) create mode 100644 packages/taler-wallet-webextension/src/cta/Pay.test.ts (limited to 'packages/taler-wallet-webextension/src/cta/Pay.test.ts') diff --git a/packages/taler-wallet-webextension/src/cta/Pay.test.ts b/packages/taler-wallet-webextension/src/cta/Pay.test.ts new file mode 100644 index 000000000..4c0fe45ca --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/Pay.test.ts @@ -0,0 +1,408 @@ +/* + This file is part of GNU Taler + (C) 2021 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see + */ + +/** + * + * @author Sebastian Javier Marchano (sebasjm) + */ + +import { AmountJson, Amounts, BalancesResponse, ConfirmPayResult, ConfirmPayResultType, NotificationType, PreparePayResult, PreparePayResultType } from "@gnu-taler/taler-util"; +import { expect } from "chai"; +import { mountHook } from "../test-utils.js"; +import * as wxApi from "../wxApi.js"; +import { useComponentState } from "./Pay.jsx"; + +const nullFunction: any = () => null; +type VoidFunction = () => void; + +type Subs = { + [key in NotificationType]?: VoidFunction +} + +class SubsHandler { + private subs: Subs = {}; + + constructor() { + this.saveSubscription = this.saveSubscription.bind(this); + } + + saveSubscription(messageTypes: NotificationType[], callback: VoidFunction): VoidFunction { + messageTypes.forEach(m => { + this.subs[m] = callback; + }) + return nullFunction; + } + + notifyEvent(event: NotificationType): void { + const cb = this.subs[event]; + if (cb === undefined) expect.fail(`Expected to have a subscription for ${event}`); + cb() + } +} + + +describe("Pay CTA states", () => { + it("should tell the user that the URI is missing", async () => { + + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState(undefined, { + onUpdateNotification: nullFunction, + } as Partial as any) + ); + + { + const { status, hook } = getLastResultOrThrow() + expect(status).equals('loading') + expect(hook).undefined; + } + + await waitNextUpdate() + + { + const { status, hook } = getLastResultOrThrow() + + expect(status).equals('loading') + if (hook === undefined) expect.fail() + expect(hook.hasError).true; + expect(hook.operational).false; + } + + await assertNoPendingUpdate() + }); + + it("should response with no balance", async () => { + + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState('taller://pay', { + onUpdateNotification: nullFunction, + preparePay: async () => ({ + amountRaw: 'USD:10', + status: PreparePayResultType.InsufficientBalance, + } as Partial), + getBalance: async () => ({ + balances: [] + } as Partial), + } as Partial as any) + ); + + { + const { status, hook } = getLastResultOrThrow() + expect(status).equals('loading') + expect(hook).undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow() + if (r.status !== 'ready') expect.fail() + expect(r.balance).undefined; + expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:10')) + expect(r.payHandler.onClick).undefined; + } + + await assertNoPendingUpdate() + }); + + it("should not be able to pay if there is no enough balance", async () => { + + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState('taller://pay', { + onUpdateNotification: nullFunction, + preparePay: async () => ({ + amountRaw: 'USD:10', + status: PreparePayResultType.InsufficientBalance, + } as Partial), + getBalance: async () => ({ + balances: [{ + available: 'USD:5' + }] + } as Partial), + } as Partial as any) + ); + + { + const { status, hook } = getLastResultOrThrow() + expect(status).equals('loading') + expect(hook).undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow() + if (r.status !== 'ready') expect.fail() + expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:5')); + expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:10')) + expect(r.payHandler.onClick).undefined; + } + + await assertNoPendingUpdate() + }); + + it("should be able to pay (without fee)", async () => { + + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState('taller://pay', { + onUpdateNotification: nullFunction, + preparePay: async () => ({ + amountRaw: 'USD:10', + amountEffective: 'USD:10', + status: PreparePayResultType.PaymentPossible, + } as Partial), + getBalance: async () => ({ + balances: [{ + available: 'USD:15' + }] + } as Partial), + } as Partial as any) + ); + + { + const { status, hook } = getLastResultOrThrow() + expect(status).equals('loading') + expect(hook).undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow() + if (r.status !== 'ready') expect.fail() + expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); + expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:10')) + expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:0')) + expect(r.payHandler.onClick).not.undefined; + } + + await assertNoPendingUpdate() + }); + + it("should be able to pay (with fee)", async () => { + + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState('taller://pay', { + onUpdateNotification: nullFunction, + preparePay: async () => ({ + amountRaw: 'USD:9', + amountEffective: 'USD:10', + status: PreparePayResultType.PaymentPossible, + } as Partial), + getBalance: async () => ({ + balances: [{ + available: 'USD:15' + }] + } as Partial), + } as Partial as any) + ); + + { + const { status, hook } = getLastResultOrThrow() + expect(status).equals('loading') + expect(hook).undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow() + if (r.status !== 'ready') expect.fail() + expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); + expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) + expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) + expect(r.payHandler.onClick).not.undefined; + } + + await assertNoPendingUpdate() + }); + + it("should get confirmation done after pay successfully", async () => { + + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState('taller://pay', { + onUpdateNotification: nullFunction, + preparePay: async () => ({ + amountRaw: 'USD:9', + amountEffective: 'USD:10', + status: PreparePayResultType.PaymentPossible, + } as Partial), + getBalance: async () => ({ + balances: [{ + available: 'USD:15' + }] + } as Partial), + confirmPay: async () => ({ + type: ConfirmPayResultType.Done, + contractTerms: {} + } as Partial), + } as Partial as any) + ); + + { + const { status, hook } = getLastResultOrThrow() + expect(status).equals('loading') + expect(hook).undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow() + if (r.status !== 'ready') expect.fail() + expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); + expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) + expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) + if (r.payHandler.onClick === undefined) expect.fail(); + r.payHandler.onClick() + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow() + if (r.status !== 'confirmed') expect.fail() + expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); + expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) + expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) + if (r.payResult.type !== ConfirmPayResultType.Done) expect.fail(); + expect(r.payResult.contractTerms).not.undefined; + expect(r.payHandler.onClick).undefined; + } + + await assertNoPendingUpdate() + }); + + it("should not stay in ready state after pay with error", async () => { + + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState('taller://pay', { + onUpdateNotification: nullFunction, + preparePay: async () => ({ + amountRaw: 'USD:9', + amountEffective: 'USD:10', + status: PreparePayResultType.PaymentPossible, + } as Partial), + getBalance: async () => ({ + balances: [{ + available: 'USD:15' + }] + } as Partial), + confirmPay: async () => ({ + type: ConfirmPayResultType.Pending, + lastError: { code: 1 }, + } as Partial), + } as Partial as any) + ); + + { + const { status, hook } = getLastResultOrThrow() + expect(status).equals('loading') + expect(hook).undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow() + if (r.status !== 'ready') expect.fail() + expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); + expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) + expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) + if (r.payHandler.onClick === undefined) expect.fail(); + r.payHandler.onClick() + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow() + if (r.status !== 'ready') expect.fail() + expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); + expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) + expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) + expect(r.payHandler.onClick).undefined; + if (r.payHandler.error === undefined) expect.fail(); + //FIXME: error message here is bad + expect(r.payHandler.error.errorDetail.hint).eq("could not confirm payment") + expect(r.payHandler.error.errorDetail.payResult).deep.equal({ + type: ConfirmPayResultType.Pending, + lastError: { code: 1 } + }) + } + + await assertNoPendingUpdate() + }); + + it("should update balance if a coins is withdraw", async () => { + const subscriptions = new SubsHandler(); + let availableBalance = Amounts.parseOrThrow("USD:10"); + + function notifyCoinWithdrawn(newAmount: AmountJson): void { + availableBalance = Amounts.add(availableBalance, newAmount).amount + subscriptions.notifyEvent(NotificationType.CoinWithdrawn) + } + + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = mountHook(() => + useComponentState('taller://pay', { + onUpdateNotification: subscriptions.saveSubscription, + preparePay: async () => ({ + amountRaw: 'USD:9', + amountEffective: 'USD:10', + status: PreparePayResultType.PaymentPossible, + } as Partial), + getBalance: async () => ({ + balances: [{ + available: Amounts.stringify(availableBalance) + }] + } as Partial), + } as Partial as any) + ); + + { + const { status, hook } = getLastResultOrThrow() + expect(status).equals('loading') + expect(hook).undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow() + if (r.status !== 'ready') expect.fail() + expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:10')); + expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) + expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) + expect(r.payHandler.onClick).not.undefined; + + notifyCoinWithdrawn(Amounts.parseOrThrow("USD:5")); + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow() + if (r.status !== 'ready') expect.fail() + expect(r.balance).deep.equal(Amounts.parseOrThrow('USD:15')); + expect(r.amount).deep.equal(Amounts.parseOrThrow('USD:9')) + expect(r.totalFees).deep.equal(Amounts.parseOrThrow('USD:1')) + expect(r.payHandler.onClick).not.undefined; + } + + await assertNoPendingUpdate() + }); + + +}); \ No newline at end of file -- cgit v1.2.3