/* 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 !== "completed") 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(); }); });