merchant-backoffice

ZZZ: Inactive/Deprecated
Log | Files | Refs | Submodules | README

commit de67d9daef0b8f8fc444f8ce6f6e3525c536e9f0
parent 6924c5d75e6db30c7fad9b762f23fb6d78c95700
Author: ms <ms@taler.net>
Date:   Sat, 18 Dec 2021 20:42:50 +0100

Bank tests.

Testing appearance and disappearance of elements related
to a Taler withdrawal.

Diffstat:
Mpackages/bank/package.json | 1+
Mpackages/bank/src/pages/home/index.tsx | 185++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mpackages/bank/tests/__tests__/homepage.js | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
3 files changed, 223 insertions(+), 30 deletions(-)

diff --git a/packages/bank/package.json b/packages/bank/package.json @@ -46,6 +46,7 @@ "@storybook/preset-scss": "^1.0.3", "@testing-library/preact": "^2.0.1", "@testing-library/preact-hooks": "^1.1.0", + "@testing-library/jest-dom": "^5.16.1", "@types/enzyme": "^3.10.10", "@types/jest": "^27.0.2", "@typescript-eslint/eslint-plugin": "^5.3.0", diff --git a/packages/bank/src/pages/home/index.tsx b/packages/bank/src/pages/home/index.tsx @@ -32,8 +32,15 @@ interface CredentialsRequestType { interface PageStateType { isLoggedIn: boolean; hasError: boolean; + withdrawalInProgress: boolean; error?: string; talerWithdrawUri?: string; + withdrawalOutcome?: string; + /** + * Not strictly a presentational element, could + * be moved in a future "withdrawal state" object. + */ + withdrawalId?: string; } /** @@ -87,6 +94,7 @@ function usePageState( state: PageStateType = { isLoggedIn: false, hasError: false, + withdrawalInProgress: false, } ): [PageStateType, StateUpdater<PageStateType>] { return useState<PageStateType>(state); @@ -109,8 +117,91 @@ function usePageState( * particular API call and updates the state accordingly. * Whether a new component should be returned too, depends * on each case. + * + * FIXME: setting the Authorization headers and possing + * the body to a POST request should be factored out in + * a helper function. */ + +/** + * This function confirms a withdrawal operation AFTER + * the wallet has given the exchange's payment details + * to the bank (via the Integration API). Such details + * can be given by scanning a QR code or by passing the + * raw taler://withdraw-URI to the CLI wallet. + * + * This function will set the confirmation status in the + * 'page state' and let the related components refresh. + */ +async function confirmWithdrawalCall( + backendState: BackendStateTypeOpt, + withdrawalId: string | undefined, + pageStateSetter: StateUpdater<PageStateType> +) { + if (typeof backendState === "undefined") { + console.log("Page has a problem: no credentials found in the state."); + pageStateSetter((prevState) => ({ + ...prevState, + hasError: true, + error: "No credentials found in the state"})) + return; + } + if (typeof withdrawalId === "undefined") { + pageStateSetter((prevState) => ({ + ...prevState, + hasError: true, + error: "Withdrawal ID wasn't found in the state; cannot confirm it."})) + return; + } + try { + let headers = new Headers(); + /** + * NOTE: tests show that when a same object is being + * POSTed, caching might prevent same requests from being + * made. Hence, trying to POST twice the same amount might + * get silently ignored. + * + * headers.append("cache-control", "no-store"); + * headers.append("cache-control", "no-cache"); + * headers.append("pragma", "no-cache"); + * */ + headers.append( + "Authorization", + `Basic ${Buffer.from(backendState.username + ":" + backendState.password).toString("base64")}` + ); + var res = await fetch( + `${backendState.url}accounts/${backendState.username}/withdrawals/confirm`, { + method: 'POST', + headers: headers + }) + } catch (error) { + console.log("Could not POST withdrawal confirmation to the bank", error); + pageStateSetter((prevState) => ({ + ...prevState, + hasError: true, + error: `Could not confirm the withdrawal: ${error}`})) + return; + } + if (!res.ok) { + console.log(`Withdrawal confirmation gave response error (${res.status})`, res.statusText); + pageStateSetter((prevState) => ({ + ...prevState, + hasError: true, + error: `Withdrawal confirmation gave response error (${res.status})`})) + return; + } else { + console.log("Withdrawal operation confirmed!"); + pageStateSetter((prevState) => { + delete prevState.talerWithdrawUri; + const { talerWithdrawUri, ...rest } = prevState; + return { + ...rest, + withdrawalOutcome: "Withdrawal confirmed!" + }}) + } +} + /** * This function creates a withdrawal operation via the Access API. * @@ -121,7 +212,7 @@ function usePageState( * After the scan, the page should refresh itself and inform the user * about the operation's outcome. */ -async function createWithdrawalOperation( +async function createWithdrawalCall( amount: string, backendState: BackendStateTypeOpt, pageStateSetter: StateUpdater<PageStateType> @@ -177,7 +268,9 @@ async function createWithdrawalOperation( let resp = await res.json(); pageStateSetter((prevState) => ({ ...prevState, - talerWithdrawUri: resp.taler_withdraw_uri})) + withdrawalInProgress: true, + talerWithdrawUri: resp.taler_withdraw_uri, + withdrawalId: resp.withdrawal_id})) } } @@ -268,7 +361,7 @@ async function registrationCall( * Show only the account's balance. */ function Account(props: any) { - const { talerWithdrawUri, accountLabel } = props; + const { withdrawalOutcome, talerWithdrawUri, accountLabel } = props; const { data, error } = useSWR(`accounts/${props.accountLabel}`); console.log("account data", data); console.log("account error", error); @@ -285,7 +378,14 @@ function Account(props: any) { } } } + if (!data) return <p>Retrieving the profile page...</p>; + if (withdrawalOutcome) { + return <div> + <p>{withdrawalOutcome}</p> + {props.children} + </div> + } /** * A Taler withdrawal replaces everything in the page and * starts polling the backend until either the wallet selected @@ -296,12 +396,15 @@ function Account(props: any) { * the outcome. */ if (talerWithdrawUri) { - return <p>Give this address to your Taler wallet: {talerWithdrawUri}</p> + return (<div> + <p>Give this address to the Taler wallet: {talerWithdrawUri}</p> + {props.children} + </div>); } - return <div> + return (<div> <p>Your balance is {data.balance.amount}.</p> {props.children} - </div> + </div>); } /** @@ -327,10 +430,7 @@ function SWRWithCredentials(props: any): VNode { return r.json() } ), - }} - > - {props.children} - </SWRConfig> + }}>{props.children}</SWRConfig> ); } @@ -348,9 +448,10 @@ export function BankHome(): VNode { } /** - * Credentials were correct, now try to render the - * bank account page, with balance and transactions - * history */ + * Credentials were correct, now render the bank account page, + * with balance, transactions history, and a Taler withdrawal + * button. + */ if (pageState.isLoggedIn) { if (typeof backendState === "undefined") { pageStateSetter((prevState) => ({ ...prevState, hasError: true })); @@ -360,18 +461,52 @@ export function BankHome(): VNode { <SWRWithCredentials username={backendState.username} password={backendState.password} - backendUrl={backendState.url} - > - <Account talerWithdrawUri={pageState.talerWithdrawUri} - accountLabel={backendState.username}> - <button - onClick={() => { - createWithdrawalOperation( - "EUR:5", - backendState, - pageStateSetter - )}} - >Charge Taler wallet</button> + backendUrl={backendState.url}> + /** + * Account layer: GETs only (balance and transactions history). + */ + <Account + withdrawalOutcome={pageState.withdrawalOutcome} + talerWithdrawUri={pageState.talerWithdrawUri} + accountLabel={backendState.username}> + + /** + * Create Taler withdrawal operation via the Access API. + */ + {!pageState.withdrawalInProgress && <button onClick={() => { + createWithdrawalCall( + "EUR:5", + backendState, + pageStateSetter + )}}>Charge Taler wallet</button> + } + + /** + * This button turns visible only after a withdrawal reaches + * a persistent state (success or error), and lets the user + * navigate back to the main account / profile page. + */ + {pageState.withdrawalOutcome && <button onClick={() => { + pageStateSetter((prevState) => { + const { withdrawalOutcome, ...rest } = prevState; + return {...rest, withdrawalInProgress: false};})}}>Close</button> + } + + /** + * This button turns visible after a withdrawal operation + * gets created and let the user confirm the operation after + * the wallet has scanned/parsed the withdraw URI. + * + * Eventually, it will be replaced by a background task that + * checks whether the wallet has POSTed the exchange's payment + * details. + */ + {pageState.talerWithdrawUri && <button onClick={() => { + confirmWithdrawalCall( + backendState, + pageState.withdrawalId, + pageStateSetter);}}>Confirm withdrawal</button> + } </Account> </SWRWithCredentials> ); diff --git a/packages/bank/tests/__tests__/homepage.js b/packages/bank/tests/__tests__/homepage.js @@ -1,8 +1,9 @@ import "core-js/stable"; import "regenerator-runtime/runtime"; +import "@testing-library/jest-dom"; import { BankHome } from '../../src/pages/home'; import { h } from 'preact'; -import { cleanup, render, fireEvent, screen } from '@testing-library/preact'; +import { waitFor, cleanup, render, fireEvent, screen } from '@testing-library/preact'; import expect from 'expect'; import fetchMock from "jest-fetch-mock"; @@ -52,8 +53,11 @@ describe("withdraw", () => { paytoUri: "payto://iban/123/ABC" })) fireEvent.click(signupButton); + context.username = username; }) + let context = {username: null}; + test("network failure before withdrawal creation", async () => { let withdrawButton = screen.getByText("Charge Taler wallet"); // mock network failure. @@ -64,17 +68,70 @@ describe("withdraw", () => { test("HTTP response error upon withdrawal creation", async () => { let withdrawButton = screen.getByText("Charge Taler wallet"); - // mock network failure. fetch.once("{}", {status: 404}); fireEvent.click(withdrawButton); await screen.findByText("gave response error", {exact: false}) }) - test("Successful withdraw", async () => { + test("Successful withdrawal creation and confirmation", async () => { let withdrawButton = screen.getByText("Charge Taler wallet"); - fetch.once(JSON.stringify({taler_withdraw_uri: "taler://foo"})); + fetch.once(JSON.stringify({ + taler_withdraw_uri: "taler://withdraw/foo", + withdrawal_id: "foo" + })); + /** + * After triggering a withdrawal, check if the taler://withdraw URI + * rendered, and confirm if so. Lastly, check that a success message + * appeared on the screen. + */ fireEvent.click(withdrawButton); - await screen.findByText("give this address to your Taler wallet", {exact: false}) + expect(fetch).toHaveBeenCalledWith( + `http://localhost/accounts/${context.username}/withdrawals`, + expect.objectContaining({body: JSON.stringify({amount: "EUR:5"})}) + ) + await screen.findByText("give this address to", {exact: false}) + // assume wallet POSTed the payment details. + const confirmButton = await screen.findByText("confirm withdrawal", {exact: false}) + await waitFor(() => expect( + screen.queryByText( + "charge taler wallet", + {exact: false})).not.toBeInTheDocument() + ); + fetch.once("{}") + fireEvent.click(confirmButton); + /** + * After having confirmed above, wait that the + * pre-withdrawal elements disappears and a success + * message appears. + */ + await waitFor(() => expect( + screen.queryByText( + "confirm withdrawal", + {exact: false})).not.toBeInTheDocument() + ); + await waitFor(() => expect( + screen.queryByText( + "give this address to the taler wallet", + {exact: false})).not.toBeInTheDocument() + ); + // success message + await screen.findByText("withdrawal confirmed", {exact: false}) + + /** + * Click on a "return to homepage" button, and check that + * the withdrawal confirmation is gone, and the option to + * withdraw again reappeared. + */ + const closeButton = await screen.findByText("close", {exact: false}) + fireEvent.click(closeButton); + await waitFor(() => expect( + screen.queryByText("withdrawal confirmed", {exact: false})).not.toBeInTheDocument() + ); + await waitFor(() => expect( + screen.queryByText( + "charge taler wallet", + {exact: false})).toBeInTheDocument() + ); }) })