gnunet-svn
[Top][All Lists]
Advanced

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

[taler-merchant-backoffice] branch master updated: bank: create withdraw


From: gnunet
Subject: [taler-merchant-backoffice] branch master updated: bank: create withdrawal
Date: Fri, 17 Dec 2021 11:02:25 +0100

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

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

The following commit(s) were added to refs/heads/master by this push:
     new aa05541  bank: create withdrawal
aa05541 is described below

commit aa05541c627b9fb5ee066980292805e6ee84da34
Author: ms <ms@taler.net>
AuthorDate: Fri Dec 17 11:02:03 2021 +0100

    bank: create withdrawal
---
 packages/bank/src/pages/home/index.tsx    | 140 ++++++++++++++++++++++++++----
 packages/bank/tests/__tests__/homepage.js | 101 +++++++++++++++++----
 2 files changed, 206 insertions(+), 35 deletions(-)

diff --git a/packages/bank/src/pages/home/index.tsx 
b/packages/bank/src/pages/home/index.tsx
index d056021..9ed70ea 100644
--- a/packages/bank/src/pages/home/index.tsx
+++ b/packages/bank/src/pages/home/index.tsx
@@ -1,7 +1,8 @@
-import useSWR, { SWRConfig } from "swr";
+import useSWR, { SWRConfig, useSWRConfig } from "swr";
 import { h, Fragment, ComponentChildren, VNode } from "preact";
 import { useState, useEffect, StateUpdater } from "preact/hooks";
 import { Buffer } from "buffer";
+import { useTranslator } from "../../i18n";
 
 /**********************************************
  * Type definitions for states and API calls. *
@@ -30,8 +31,9 @@ interface RegistrationRequestType {
  */
 interface PageStateType {
   isLoggedIn: boolean;
-  hasProblem: boolean;
+  hasError: boolean;
   error?: string;
+  talerWithdrawUri?: string;
 }
 
 /**
@@ -42,9 +44,9 @@ interface AccountStateType {
   /* FIXME: Need history here.  */
 }
 
-/*******************
+/************
  * Helpers. *
- ******************/
+ ***********/
 
 const getRootPath = () => {
   return typeof window !== undefined
@@ -84,7 +86,7 @@ function useAccountState(
 function usePageState(
   state: PageStateType = {
     isLoggedIn: false,
-    hasProblem: false,
+    hasError: false,
   }
 ): [PageStateType, StateUpdater<PageStateType>] {
   return useState<PageStateType>(state);
@@ -109,6 +111,76 @@ function usePageState(
  * on each case.
  */
 
+/**
+ * This function creates a withdrawal operation via the Access API.
+ *
+ * After having successfully created the withdrawal operation, the
+ * user should receive a QR code of the "taler://withdraw/" type and
+ * supposed to scan it with their phone.
+ *
+ * After the scan, the page should refresh itself and inform the user
+ * about the operation's outcome.
+ */
+async function createWithdrawalOperation(
+  amount: string,
+  backendState: BackendStateTypeOpt,
+  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;
+  }
+  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`, {
+        method: 'POST',
+       headers: headers,
+       body: JSON.stringify({amount: amount}),
+      }
+    );
+  } catch (error) {
+    console.log("Could not POST withdrawal request to the bank", error);
+    pageStateSetter((prevState) => ({
+      ...prevState,
+      hasError: true,
+      error: `Could not create withdrawal operation: ${error}`}))
+    return;
+  }
+  if (!res.ok) {
+    console.log(`Withdrawal creation gave response error (${res.status})`, 
res.statusText);
+    pageStateSetter((prevState) => ({
+      ...prevState,
+      hasError: true,
+      error: `Withdrawal creation gave response error (${res.status})`}))
+      return;
+  } else {
+    console.log("Withdrawal operation created!");
+    let resp = await res.json();
+    pageStateSetter((prevState) => ({
+      ...prevState,
+      talerWithdrawUri: resp.taler_withdraw_uri}))
+  }
+}
+
 /**
  * This function requests /register.
  *
@@ -118,24 +190,31 @@ function usePageState(
  */
 async function registrationCall(
   req: RegistrationRequestType,
+  /**
+   * FIXME: figure out if the two following
+   * functions can be retrieved somewhat from
+   * the state.
+   */
   backendStateSetter: StateUpdater<BackendStateTypeOpt>,
   pageStateSetter: StateUpdater<PageStateType>
 ) {
 
   let baseUrl = getRootPath();
+  let headersNoCache = new Headers();
   try {
     var res = await fetch(
-      `${baseUrl}testing/register`,
-      {method: 'POST', body: JSON.stringify(req)}
-    );
+      `${baseUrl}testing/register`, {
+      method: 'POST',
+      body: JSON.stringify(req),
+    });
   } catch (error) {
     console.log("Could not POST new registration to the bank", error);
-    pageStateSetter((prevState) => ({ ...prevState, hasProblem: true }));
+    pageStateSetter((prevState) => ({ ...prevState, hasError: true }));
     return;
   }
   if (!res.ok) {
     console.log(`New registration gave response error (${res.status})`, 
res.statusText);
-    pageStateSetter((prevState) => ({ ...prevState, hasProblem: true }));
+    pageStateSetter((prevState) => ({ ...prevState, hasError: true }));
   } else {
     console.log("Credentials are valid");
     pageStateSetter((prevState) => ({ ...prevState, isLoggedIn: true }));
@@ -156,12 +235,28 @@ async function registrationCall(
  * Show only the account's balance.
  */
 function Account(props: any) {
+  const { talerWithdrawUri, accountLabel } = props;
   const { data, error } = useSWR(`accounts/${props.accountLabel}`);
   if (typeof error != "undefined") {
     return <p>Account information could not be retrieved</p>
   }
-  if (!data) return <p>Retrieving the balance...</p>;
-  return <p>Your balance is {data.balance.amount}.</p>;
+  if (!data) return <p>Retrieving the profile page...</p>;
+  /**
+   * A Taler withdrawal replaces everything in the page and
+   * starts polling the backend until either the wallet selected
+   * a exchange and reserve public key, or a error / abort happened.
+   *
+   * After reaching one of the above states, the user should be
+   * brought to this ("Account") page where they get informed about
+   * the outcome.
+   */
+  if (talerWithdrawUri) {
+    return <p>Give this address to your Taler wallet: {talerWithdrawUri}</p>
+  }
+  return <div>
+    <p>Your balance is {data.balance.amount}.</p>
+    {props.children}
+  </div>
 }
 
 /**
@@ -196,8 +291,8 @@ export function BankHome(): VNode {
   var [pageState, pageStateSetter] = usePageState();
   var [accountState, accountStateSetter] = useAccountState();
 
-  if (pageState.hasProblem) {
-    return <p>Page has a problem.</p>;
+  if (pageState.hasError) {
+    return <p>Page has a problem: {pageState.error}</p>;
   }
 
   /**
@@ -206,9 +301,8 @@ export function BankHome(): VNode {
    * history */
   if (pageState.isLoggedIn) {
     if (typeof backendState === "undefined") {
-      console.log("Credentials not found in state, even after login.", 
backendState);
-      pageStateSetter((state) => ({ ...state, hasProblem: true }));
-      return <p>Page has a problem</p>;
+      pageStateSetter((prevState) => ({ ...prevState, hasError: true }));
+      return <p>Page has a problem: logged in but backend state is lost.</p>;
     }
     return (
       <SWRWithCredentials
@@ -216,7 +310,17 @@ export function BankHome(): VNode {
         password={backendState.password}
         backendUrl={backendState.url}
       >
-        <Account accountLabel={backendState.username} />
+        <Account talerWithdrawUri={pageState.talerWithdrawUri}
+                accountLabel={backendState.username}>
+          <button
+            onClick={() => {
+              createWithdrawalOperation(
+               "EUR:5",
+               backendState,
+               pageStateSetter
+             )}}
+          >Charge Taler wallet</button>
+       </Account>
       </SWRWithCredentials>
     );
 
diff --git a/packages/bank/tests/__tests__/homepage.js 
b/packages/bank/tests/__tests__/homepage.js
index d78bd67..03a909d 100644
--- a/packages/bank/tests/__tests__/homepage.js
+++ b/packages/bank/tests/__tests__/homepage.js
@@ -2,58 +2,117 @@ import "core-js/stable";
 import "regenerator-runtime/runtime";
 import { BankHome } from '../../src/pages/home';
 import { h } from 'preact';
-import { render, fireEvent, screen } from '@testing-library/preact';
+import { cleanup, render, fireEvent, screen } from '@testing-library/preact';
 import expect from 'expect';
 import fetchMock from "jest-fetch-mock";
 
-fetchMock.enableMocks();
-
-beforeEach(() => {
-  fetch.resetMocks();
-});
-
-// Insert username and password into the registration
-// form and returns the submit button.
+/**
+ * Insert username and password into the registration
+ * form and returns the submit button.  NOTE: the username
+ * must be given always fresh, as it acts as a SWR key and
+ * therefore might prevent calls from being made, because of
+ * caching reasons.  That is not a problem per-se but can
+ * disrupt ".toHaveLastBeenCalledWith()"-like asserts.
+ *
+ * Return the username and the submit button.
+ */
 function fillRegistrationForm() {
+  const username = Math.random().toString().substring(2);
   const u = screen.getByPlaceholderText("username");
   const p = screen.getByPlaceholderText("password");
-  fireEvent.input(u, {target: {value: "foo"}})
+  fireEvent.input(u, {target: {value: username}})
   fireEvent.input(p, {target: {value: "bar"}})
-  return screen.getByText("Submit");
+  const submitButton = screen.getByText("Submit");
+  return {username: username, submitButton: submitButton};
 }
+fetchMock.enableMocks();
+
+describe("withdraw", () => {
+  afterEach(() => {
+    fetch.resetMocks();
+    cleanup();
+  })
+
+  // Register and land on the profile page.
+  beforeEach(() => {
+    render(<BankHome />);
+    const { username, submitButton } = fillRegistrationForm();
+    fetch.once("{}", {
+      status: 200
+    }).once(JSON.stringify({
+      balance: {
+        amount: "EUR:10",
+       credit_debit_indicator: "credit"
+      },
+      paytoUri: "payto://iban/123/ABC"
+    }))
+    fireEvent.click(submitButton);
+  })
+
+  test("network failure before withdrawal creation", async () => {
+    let withdrawButton = screen.getByText("Charge Taler wallet");
+    // mock network failure.
+    fetch.mockReject("API is down");
+    fireEvent.click(withdrawButton);
+    await screen.findByText("could not create withdrawal operation", {exact: 
false})
+  })
+
+  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 () => {
+    let withdrawButton = screen.getByText("Charge Taler wallet");
+    fetch.once(JSON.stringify({taler_withdraw_uri: "taler://foo"}));
+    fireEvent.click(withdrawButton);
+    await screen.findByText("give this address to your Taler wallet", {exact: 
false})
+  })
+})
 
 describe("home page", () => {
+  afterEach(() => {
+    fetch.resetMocks();
+    cleanup();
+  })
+
   // check page informs about the current balance
   // after a successful registration.
 
   test("new registration response error 404", async () => {
     render(<BankHome />);
-    let submitButton = fillRegistrationForm();
+    let { username, submitButton } = fillRegistrationForm();
     fetch.mockResponseOnce("Not found", {status: 404})
     fireEvent.click(submitButton);
     await screen.findByText("has a problem", {exact: false});
     expect(fetch).toHaveBeenCalledWith(
       "http://localhost/testing/register";,
-      {body: JSON.stringify({username: "foo", password: "bar"}), method: 
"POST"},
+      {body: JSON.stringify({username: username, password: "bar"}), method: 
"POST"},
     )
   })
 
   test("registration network failure", async () => {
     render(<BankHome />);
-    let submitButton = fillRegistrationForm();
+    const { username, submitButton } = fillRegistrationForm();
     // Mocking network failure.
     fetch.mockReject("API is down");
     fireEvent.click(submitButton);
     await screen.findByText("has a problem", {exact: false});
     expect(fetch).toHaveBeenCalledWith(
       "http://localhost/testing/register";,
-      {body: JSON.stringify({username: "foo", password: "bar"}), method: 
"POST"},
+      {body: JSON.stringify({username: username, password: "bar"}), method: 
"POST"},
     )
   })
   
   test("registration success", async () => {
     render(<BankHome />);
-    let submitButton = fillRegistrationForm();
+    const { username, submitButton } = fillRegistrationForm();
+    /**
+     * Mock successful registration and balance request.
+     */
     fetch.once("{}", {
       status: 200
     }).once(JSON.stringify({
@@ -64,9 +123,17 @@ describe("home page", () => {
       paytoUri: "payto://iban/123/ABC"
     }))
     fireEvent.click(submitButton);
+    /**
+     * Tests that a balance is shown after the successful
+     * registration.
+     */
     await screen.findByText("balance is EUR:10", {exact: false})
+    /**
+     * The expectation below tests whether the account
+     * balance was requested after the successful registration.
+     */
     expect(fetch).toHaveBeenLastCalledWith(
-      "http://localhost/accounts/foo";,
+      `http://localhost/accounts/${username}`,
       expect.anything() // no need to match auth headers.
     )
   })

-- 
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]