gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[taler-merchant-backoffice] 02/11: Bank tests.


From: gnunet
Subject: [taler-merchant-backoffice] 02/11: Bank tests.
Date: Mon, 20 Dec 2021 17:45:00 +0100

This is an automated email from the git hooks/post-receive script.

ms pushed a commit to branch master
in repository merchant-backoffice.

commit de67d9daef0b8f8fc444f8ce6f6e3525c536e9f0
Author: ms <ms@taler.net>
AuthorDate: Sat Dec 18 20:42:50 2021 +0100

    Bank tests.
    
    Testing appearance and disappearance of elements related
    to a Taler withdrawal.
---
 packages/bank/package.json                |   1 +
 packages/bank/src/pages/home/index.tsx    | 185 ++++++++++++++++++++++++++----
 packages/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
index 2b3b446..3db98b1 100644
--- 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
index 04f01cb..f7b0f51 100644
--- 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
index 46c6ae8..645c733 100644
--- 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()
+    );
   })
 })
 

-- 
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.



reply via email to

[Prev in Thread] Current Thread [Next in Thread]