From 614a3e3c8702bb7436398acb911880caae0fdee7 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sat, 30 Jul 2022 20:55:41 -0300 Subject: standarizing components --- .../src/cta/Payment/test.ts | 447 +++++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 packages/taler-wallet-webextension/src/cta/Payment/test.ts (limited to 'packages/taler-wallet-webextension/src/cta/Payment/test.ts') diff --git a/packages/taler-wallet-webextension/src/cta/Payment/test.ts b/packages/taler-wallet-webextension/src/cta/Payment/test.ts new file mode 100644 index 000000000..aea70b7ca --- /dev/null +++ b/packages/taler-wallet-webextension/src/cta/Payment/test.ts @@ -0,0 +1,447 @@ +/* + This file is part of GNU Taler + (C) 2022 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 { useComponentState } from "./state.js"; +import * as wxApi from "../../wxApi.js"; + +const nullFunction: any = () => null; +type VoidFunction = () => void; + +type Subs = { + [key in NotificationType]?: VoidFunction; +}; + +export 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("Payment CTA states", () => { + it("should tell the user that the URI is missing", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + mountHook(() => + useComponentState({ talerPayUri: undefined, goBack: nullFunction, goToWalletManualWithdraw: nullFunction }, { + onUpdateNotification: nullFunction, + } as Partial as any), + ); + + { + const { status, error } = getLastResultOrThrow(); + expect(status).equals("loading"); + expect(error).undefined; + } + + await waitNextUpdate(); + + { + const { status, error } = getLastResultOrThrow(); + + expect(status).equals("loading-uri"); + if (error === undefined) expect.fail(); + expect(error.hasError).true; + expect(error.operational).false; + } + + await assertNoPendingUpdate(); + }); + + it("should response with no balance", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + mountHook(() => + useComponentState({ talerPayUri: "taller://pay", goBack: nullFunction, goToWalletManualWithdraw: nullFunction }, { + onUpdateNotification: nullFunction, + preparePay: async () => + ({ + amountRaw: "USD:10", + status: PreparePayResultType.InsufficientBalance, + } as Partial), + getBalance: async () => + ({ + balances: [], + } as Partial), + } as Partial as any), + ); + + { + const { status, error } = getLastResultOrThrow(); + expect(status).equals("loading"); + expect(error).undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "no-balance-for-currency") expect.fail(); + expect(r.balance).undefined; + expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10")); + } + + await assertNoPendingUpdate(); + }); + + it("should not be able to pay if there is no enough balance", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + mountHook(() => + useComponentState({ talerPayUri: "taller://pay", goBack: nullFunction, goToWalletManualWithdraw: nullFunction }, { + 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, error } = getLastResultOrThrow(); + expect(status).equals("loading"); + expect(error).undefined; + } + + await waitNextUpdate(); + + { + const r = getLastResultOrThrow(); + if (r.status !== "no-enough-balance") expect.fail(); + expect(r.balance).deep.equal(Amounts.parseOrThrow("USD:5")); + expect(r.amount).deep.equal(Amounts.parseOrThrow("USD:10")); + } + + await assertNoPendingUpdate(); + }); + + it("should be able to pay (without fee)", async () => { + const { getLastResultOrThrow, waitNextUpdate, assertNoPendingUpdate } = + mountHook(() => + useComponentState({ talerPayUri: "taller://pay", goBack: nullFunction, goToWalletManualWithdraw: nullFunction }, { + 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, error } = getLastResultOrThrow(); + expect(status).equals("loading"); + expect(error).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({ talerPayUri: "taller://pay", goBack: nullFunction, goToWalletManualWithdraw: nullFunction }, { + 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, error } = getLastResultOrThrow(); + expect(status).equals("loading"); + expect(error).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({ talerPayUri: "taller://pay", goBack: nullFunction, goToWalletManualWithdraw: nullFunction }, { + 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, error } = getLastResultOrThrow(); + expect(status).equals("loading"); + expect(error).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({ talerPayUri: "taller://pay", goBack: nullFunction, goToWalletManualWithdraw: nullFunction }, { + 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, error } = getLastResultOrThrow(); + expect(status).equals("loading"); + expect(error).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({ talerPayUri: "taller://pay", goBack: nullFunction, goToWalletManualWithdraw: nullFunction }, { + 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, error } = getLastResultOrThrow(); + expect(status).equals("loading"); + expect(error).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(); + }); +}); -- cgit v1.2.3