gnunet-svn
[Top][All Lists]
Advanced

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

[taler-wallet-core] branch master updated: harness: finish kyc test


From: gnunet
Subject: [taler-wallet-core] branch master updated: harness: finish kyc test
Date: Sun, 12 Feb 2023 19:31:51 +0100

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

dold pushed a commit to branch master
in repository wallet-core.

The following commit(s) were added to refs/heads/master by this push:
     new 13f044273 harness: finish kyc test
13f044273 is described below

commit 13f0442736479fb6ea8d1ecc7311cdac354a4de5
Author: Florian Dold <florian@dold.me>
AuthorDate: Sun Feb 12 19:30:59 2023 +0100

    harness: finish kyc test
    
    We mock the KYC gateway now, use the new notification-based wallet API
    and the test is not experimental anymore.
---
 packages/taler-harness/src/harness/harness.ts      |   6 +-
 packages/taler-harness/src/harness/helpers.ts      |  19 ++
 .../taler-harness/src/integrationtests/test-kyc.ts | 199 ++++++++++++++++++---
 packages/taler-util/src/notifications.ts           |  10 +-
 packages/taler-wallet-cli/src/index.ts             |   6 +-
 .../taler-wallet-core/src/operations/deposits.ts   |  40 ++++-
 .../taler-wallet-core/src/operations/withdraw.ts   |  22 ++-
 packages/taler-wallet-core/src/remote.ts           |  30 ++--
 8 files changed, 275 insertions(+), 57 deletions(-)

diff --git a/packages/taler-harness/src/harness/harness.ts 
b/packages/taler-harness/src/harness/harness.ts
index 3659ea538..275592091 100644
--- a/packages/taler-harness/src/harness/harness.ts
+++ b/packages/taler-harness/src/harness/harness.ts
@@ -2028,9 +2028,9 @@ export class WalletClient {
     return getClientFromRemoteWallet(this.remoteWallet);
   }
 
-  waitForNotificationCond(
-    cond: (n: WalletNotification) => boolean,
-  ): Promise<void> {
+  waitForNotificationCond<T>(
+    cond: (n: WalletNotification) => T | undefined | false,
+  ): Promise<T> {
     return this.waiter.waitForNotificationCond(cond);
   }
 }
diff --git a/packages/taler-harness/src/harness/helpers.ts 
b/packages/taler-harness/src/harness/helpers.ts
index 59a37e4b8..4c2ca80a7 100644
--- a/packages/taler-harness/src/harness/helpers.ts
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -53,9 +53,14 @@ import {
   MerchantServiceInterface,
   setupDb,
   WalletCli,
+  WalletClient,
+  WalletService,
   WithAuthorization,
 } from "./harness.js";
 
+/**
+ * @deprecated
+ */
 export interface SimpleTestEnvironment {
   commonDb: DbInfo;
   bank: BankService;
@@ -65,6 +70,20 @@ export interface SimpleTestEnvironment {
   wallet: WalletCli;
 }
 
+/**
+ * Improved version of the simple test environment,
+ * with the daemonized wallet.
+ */
+export interface SimpleTestEnvironmentNg {
+  commonDb: DbInfo;
+  bank: BankService;
+  exchange: ExchangeService;
+  exchangeBankAccount: HarnessExchangeBankAccount;
+  merchant: MerchantService;
+  walletClient: WalletClient;
+  walletService: WalletService;
+}
+
 export interface EnvOptions {
   /**
    * If provided, enable age restrictions with the specified age mask string.
diff --git a/packages/taler-harness/src/integrationtests/test-kyc.ts 
b/packages/taler-harness/src/integrationtests/test-kyc.ts
index c652c86fa..b08db66f7 100644
--- a/packages/taler-harness/src/integrationtests/test-kyc.ts
+++ b/packages/taler-harness/src/integrationtests/test-kyc.ts
@@ -17,7 +17,13 @@
 /**
  * Imports.
  */
-import { Duration } from "@gnu-taler/taler-util";
+import { Duration, j2s, NotificationType } from "@gnu-taler/taler-util";
+import {
+  BankAccessApi,
+  BankApi,
+  NodeHttpLib,
+  WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
 import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
 import {
   BankService,
@@ -26,20 +32,17 @@ import {
   GlobalTestState,
   MerchantService,
   setupDb,
-  WalletCli,
+  WalletClient,
+  WalletService,
 } from "../harness/harness.js";
-import {
-  withdrawViaBank,
-  makeTestPayment,
-  EnvOptions,
-  SimpleTestEnvironment,
-} from "../harness/helpers.js";
+import { EnvOptions, SimpleTestEnvironmentNg } from "../harness/helpers.js";
+import * as http from "node:http";
 
 export async function createKycTestkudosEnvironment(
   t: GlobalTestState,
   coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
   opts: EnvOptions = {},
-): Promise<SimpleTestEnvironment> {
+): Promise<SimpleTestEnvironmentNg> {
   const db = await setupDb(t);
 
   const bank = await BankService.create(t, {
@@ -117,11 +120,11 @@ export async function createKycTestkudosEnvironment(
     config.setString(
       myprov,
       "kyc_oauth2_info_url",
-      "http://localhost:6666/oauth/v2/login";,
+      "http://localhost:6666/oauth/v2/info";,
     );
     config.setString(myprov, "kyc_oauth2_client_id", "taler-exchange");
     config.setString(myprov, "kyc_oauth2_client_secret", "exchange-secret");
-    config.setString(myprov, "kyc_oauth2_post_url", "https://taler.com";);
+    config.setString(myprov, "kyc_oauth2_post_url", "https://taler.net";);
 
     config.setString(
       "kyc-legitimization-withdraw1",
@@ -167,40 +170,186 @@ export async function createKycTestkudosEnvironment(
     ),
   });
 
-  console.log("setup done!");
+  const walletService = new WalletService(t, {
+    name: "wallet",
+    useInMemoryDb: true,
+  });
+  await walletService.start();
+  await walletService.pingUntilAvailable();
 
-  const wallet = new WalletCli(t);
+  const walletClient = new WalletClient({
+    unixPath: walletService.socketPath,
+    onNotification(n) {
+      console.log("got notification", n);
+    },
+  });
+  await walletClient.connect();
+  await walletClient.client.call(WalletApiOperation.InitWallet, {
+    skipDefaults: true,
+  });
+
+  console.log("setup done!");
 
   return {
     commonDb: db,
     exchange,
     merchant,
-    wallet,
+    walletClient,
+    walletService,
     bank,
     exchangeBankAccount,
   };
 }
 
+interface TestfakeKycService {
+  stop: () => void;
+}
+
+function splitInTwoAt(s: string, separator: string): [string, string] {
+  const idx = s.indexOf(separator);
+  if (idx === -1) {
+    return [s, ""];
+  }
+  return [s.slice(0, idx), s.slice(idx + 1)];
+}
+
+/**
+ * Testfake for the kyc service that the exchange talks to.
+ */
+async function runTestfakeKycService(): Promise<TestfakeKycService> {
+  const server = http.createServer((req, res) => {
+    const requestUrl = req.url!;
+    console.log(`kyc: got ${req.method} request`, requestUrl);
+
+    const [path, query] = splitInTwoAt(requestUrl, "?");
+
+    const qp = new URLSearchParams(query);
+
+    if (path === "/oauth/v2/login") {
+      // Usually this would render some HTML page for the user to log in,
+      // but we return JSON here.
+      const redirUri = new URL(qp.get("redirect_uri")!);
+      redirUri.searchParams.set("code", "code_is_ok");
+      res.writeHead(200, { "Content-Type": "application/json" });
+      res.end(
+        JSON.stringify({
+          redirect_uri: redirUri.href,
+        }),
+      );
+    } else if (path === "/oauth/v2/token") {
+      let reqBody = "";
+      req.on("data", (x) => {
+        reqBody += x;
+      });
+
+      req.on("end", () => {
+        console.log("login request body:", reqBody);
+
+        res.writeHead(200, { "Content-Type": "application/json" });
+        // Normally, the access_token would also include which user we're 
trying
+        // to get info about, but we (for now) skip it in this test.
+        res.end(
+          JSON.stringify({
+            access_token: "exchange_access_token",
+            token_type: "Bearer",
+          }),
+        );
+      });
+    } else if (path === "/oauth/v2/info") {
+      console.log("authorization header:", req.headers.authorization);
+      res.writeHead(200, { "Content-Type": "application/json" });
+      res.end(
+        JSON.stringify({
+          status: "success",
+          data: {
+            id: "foobar",
+          },
+        }),
+      );
+    } else {
+      res.writeHead(400, { "Content-Type": "application/json" });
+      res.end(JSON.stringify({ code: 1, message: "bad request" }));
+    }
+  });
+  await new Promise<void>((resolve, reject) => {
+    server.listen(6666, () => resolve());
+  });
+  return {
+    stop() {
+      server.close();
+    },
+  };
+}
+
 export async function runKycTest(t: GlobalTestState) {
   // Set up test environment
 
-  const { wallet, bank, exchange, merchant } =
+  const { walletClient, bank, exchange, merchant } =
     await createKycTestkudosEnvironment(t);
 
+  const kycServer = await runTestfakeKycService();
+
   // Withdraw digital cash into the wallet.
 
-  await withdrawViaBank(t, { wallet, bank, exchange, amount: "TESTKUDOS:20" });
+  const amount = "TESTKUDOS:20";
+  const user = await BankApi.createRandomBankUser(bank);
+  const wop = await BankAccessApi.createWithdrawalOperation(bank, user, 
amount);
 
-  const order = {
-    summary: "Buy me!",
-    amount: "TESTKUDOS:5",
-    fulfillment_url: "taler://fulfillment-success/thx",
-  };
+  // Hand it to the wallet
+
+  await walletClient.client.call(
+    WalletApiOperation.GetWithdrawalDetailsForUri,
+    {
+      talerWithdrawUri: wop.taler_withdraw_uri,
+    },
+  );
+
+  // Withdraw
+
+  const kycNotificationCond = walletClient.waitForNotificationCond((x) => {
+    if (x.type === NotificationType.WithdrawalKycRequested) {
+      return x;
+    }
+    return false;
+  });
+
+  const withdrawalDoneCond = walletClient.waitForNotificationCond(
+    (x) => x.type === NotificationType.WithdrawGroupFinished,
+  );
+
+  await walletClient.client.call(
+    WalletApiOperation.AcceptBankIntegratedWithdrawal,
+    {
+      exchangeBaseUrl: exchange.baseUrl,
+      talerWithdrawUri: wop.taler_withdraw_uri,
+    },
+  );
+
+  // Confirm it
+
+  await BankApi.confirmWithdrawalOperation(bank, user, wop);
+
+  const kycNotif = await kycNotificationCond;
+
+  console.log("got kyc notification:", j2s(kycNotif));
+
+  // We now simulate the user interacting with the KYC service,
+  // which would usually done in the browser.
+
+  const httpClient = new NodeHttpLib();
+  const kycServerResp = await httpClient.get(kycNotif.kycUrl);
+  const kycLoginResp = await kycServerResp.json();
+  console.log("kyc server resp:", j2s(kycLoginResp));
+  const kycProofUrl = kycLoginResp.redirect_uri;
+  const proofHttpResp = await httpClient.get(kycProofUrl);
+  console.log("proof resp status", proofHttpResp.status);
+  console.log("resp headers", proofHttpResp.headers.toJSON());
+
+  // Now that KYC is done, withdrawal should finally succeed.
+
+  await withdrawalDoneCond;
 
-  await makeTestPayment(t, { wallet, merchant, order });
-  await wallet.runUntilDone();
+  kycServer.stop();
 }
 
 runKycTest.suites = ["wallet"];
-// See bugs.taler.net/n/7599
-runKycTest.experimental = true;
diff --git a/packages/taler-util/src/notifications.ts 
b/packages/taler-util/src/notifications.ts
index bc1c4b71f..9d3ca32b0 100644
--- a/packages/taler-util/src/notifications.ts
+++ b/packages/taler-util/src/notifications.ts
@@ -62,6 +62,7 @@ export enum NotificationType {
   PendingOperationProcessed = "pending-operation-processed",
   ProposalRefused = "proposal-refused",
   ReserveRegisteredWithBank = "reserve-registered-with-bank",
+  WithdrawalKycRequested = "withdrawal-kyc-requested",
   DepositOperationError = "deposit-operation-error",
 }
 
@@ -117,6 +118,12 @@ export interface RefreshMeltedNotification {
   type: NotificationType.RefreshMelted;
 }
 
+export interface WithdrawalKycRequested {
+  type: NotificationType.WithdrawalKycRequested;
+  transactionId: string;
+  kycUrl: string;
+}
+
 export interface RefreshRevealedNotification {
   type: NotificationType.RefreshRevealed;
 }
@@ -285,4 +292,5 @@ export type WalletNotification =
   | ProposalRefusedNotification
   | ReserveRegisteredWithBankNotification
   | ReserveNotYetFoundNotification
-  | PayOperationSuccessNotification;
+  | PayOperationSuccessNotification
+  | WithdrawalKycRequested;
diff --git a/packages/taler-wallet-cli/src/index.ts 
b/packages/taler-wallet-cli/src/index.ts
index 6641dab09..7e942ede7 100644
--- a/packages/taler-wallet-cli/src/index.ts
+++ b/packages/taler-wallet-cli/src/index.ts
@@ -228,9 +228,9 @@ export interface WalletContext {
    * Return a promise that resolves after the wallet has emitted a notification
    * that meets the criteria of the "cond" predicate.
    */
-  waitForNotificationCond(
-    cond: (n: WalletNotification) => boolean,
-  ): Promise<void>;
+  waitForNotificationCond<T>(
+    cond: (n: WalletNotification) => T | false | undefined,
+  ): Promise<T>;
 }
 
 async function createLocalWallet(
diff --git a/packages/taler-wallet-core/src/operations/deposits.ts 
b/packages/taler-wallet-core/src/operations/deposits.ts
index e97738b55..4ff6a65cd 100644
--- a/packages/taler-wallet-core/src/operations/deposits.ts
+++ b/packages/taler-wallet-core/src/operations/deposits.ts
@@ -38,8 +38,10 @@ import {
   hashTruncate32,
   hashWire,
   HttpStatusCode,
+  j2s,
   Logger,
   MerchantContractTerms,
+  NotificationType,
   parsePaytoUri,
   PayCoinSelection,
   PrepareDepositRequest,
@@ -61,7 +63,7 @@ import {
   TransactionStatus,
 } from "../db.js";
 import { TalerError } from "../errors.js";
-import { checkKycStatus } from "../index.js";
+import { checkWithdrawalKycStatus, KycPendingInfo, KycUserType } from 
"../index.js";
 import { InternalWalletState } from "../internal-wallet-state.js";
 import { readSuccessResponseJsonOrThrow } from "../util/http.js";
 import { OperationAttemptResult } from "../util/retries.js";
@@ -80,6 +82,40 @@ import { getTotalRefreshCost } from "./refresh.js";
  */
 const logger = new Logger("deposits.ts");
 
+
+export async function checkDepositKycStatus(
+  ws: InternalWalletState,
+  exchangeUrl: string,
+  kycInfo: KycPendingInfo,
+  userType: KycUserType,
+): Promise<void> {
+  const url = new URL(
+    `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
+    exchangeUrl,
+  );
+  logger.info(`kyc url ${url.href}`);
+  const kycStatusReq = await ws.http.fetch(url.href, {
+    method: "GET",
+  });
+  if (kycStatusReq.status === HttpStatusCode.Ok) {
+    logger.warn("kyc requested, but already fulfilled");
+    return;
+  } else if (kycStatusReq.status === HttpStatusCode.Accepted) {
+    const kycStatus = await kycStatusReq.json();
+    logger.info(`kyc status: ${j2s(kycStatus)}`);
+    // FIXME: This error code is totally wrong
+    throw TalerError.fromDetail(
+      TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
+      {
+        kycUrl: kycStatus.kyc_url,
+      },
+      `KYC check required for deposit`,
+    );
+  } else {
+    throw Error(`unexpected response from kyc-check (${kycStatusReq.status})`);
+  }
+}
+
 /**
  * @see {processDepositGroup}
  */
@@ -162,7 +198,7 @@ export async function processDepositGroup(
           const paytoHash = encodeCrock(
             hashTruncate32(stringToBytes(depositGroup.wire.payto_uri + "\0")),
           );
-          await checkKycStatus(
+          await checkDepositKycStatus(
             ws,
             perm.exchange_url,
             { paytoHash, requirementRow },
diff --git a/packages/taler-wallet-core/src/operations/withdraw.ts 
b/packages/taler-wallet-core/src/operations/withdraw.ts
index 5c9854c0f..28754c77e 100644
--- a/packages/taler-wallet-core/src/operations/withdraw.ts
+++ b/packages/taler-wallet-core/src/operations/withdraw.ts
@@ -1250,12 +1250,7 @@ export async function processWithdrawalGroup(
 
   if (numKycRequired > 0) {
     if (kycInfo) {
-      await checkKycStatus(
-        ws,
-        withdrawalGroup.exchangeBaseUrl,
-        kycInfo,
-        "individual",
-      );
+      await checkWithdrawalKycStatus(ws, withdrawalGroup, kycInfo, 
"individual");
       return {
         type: OperationAttemptResultType.Pending,
         result: undefined,
@@ -1293,12 +1288,13 @@ export async function processWithdrawalGroup(
   };
 }
 
-export async function checkKycStatus(
+export async function checkWithdrawalKycStatus(
   ws: InternalWalletState,
-  exchangeUrl: string,
+  wg: WithdrawalGroupRecord,
   kycInfo: KycPendingInfo,
   userType: KycUserType,
 ): Promise<void> {
+  const exchangeUrl = wg.exchangeBaseUrl;
   const url = new URL(
     `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
     exchangeUrl,
@@ -1307,12 +1303,20 @@ export async function checkKycStatus(
   const kycStatusReq = await ws.http.fetch(url.href, {
     method: "GET",
   });
-  logger.warn("kyc requested, but already fulfilled");
   if (kycStatusReq.status === HttpStatusCode.Ok) {
+    logger.warn("kyc requested, but already fulfilled");
     return;
   } else if (kycStatusReq.status === HttpStatusCode.Accepted) {
     const kycStatus = await kycStatusReq.json();
     logger.info(`kyc status: ${j2s(kycStatus)}`);
+    ws.notify({
+      type: NotificationType.WithdrawalKycRequested,
+      kycUrl: kycStatus.kyc_url,
+      transactionId: makeTransactionId(
+        TransactionType.Withdrawal,
+        wg.withdrawalGroupId,
+      ),
+    });
     throw TalerError.fromDetail(
       TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
       {
diff --git a/packages/taler-wallet-core/src/remote.ts 
b/packages/taler-wallet-core/src/remote.ts
index 2628fea07..bc0be9d30 100644
--- a/packages/taler-wallet-core/src/remote.ts
+++ b/packages/taler-wallet-core/src/remote.ts
@@ -145,9 +145,14 @@ export function getClientFromRemoteWallet(
 
 export interface WalletNotificationWaiter {
   notify(wn: WalletNotification): void;
-  waitForNotificationCond(
-    cond: (n: WalletNotification) => boolean,
-  ): Promise<void>;
+  waitForNotificationCond<T>(
+    cond: (n: WalletNotification) => T | false | undefined,
+  ): Promise<T>;
+}
+
+interface NotificationCondEntry<T> {
+  condition: (n: WalletNotification) => T | false | undefined;
+  promiseCapability: OpenedPromise<T>;
 }
 
 /**
@@ -157,22 +162,19 @@ export interface WalletNotificationWaiter {
 export function makeNotificationWaiter(): WalletNotificationWaiter {
   // Bookkeeping for waiting on notification conditions
   let nextCondIndex = 1;
-  const condMap: Map<
-    number,
-    {
-      condition: (n: WalletNotification) => boolean;
-      promiseCapability: OpenedPromise<void>;
-    }
-  > = new Map();
+  const condMap: Map<number, NotificationCondEntry<any>> = new Map();
   function onNotification(n: WalletNotification) {
     condMap.forEach((cond, condKey) => {
-      if (cond.condition(n)) {
-        cond.promiseCapability.resolve();
+      const res = cond.condition(n);
+      if (res) {
+        cond.promiseCapability.resolve(res);
       }
     });
   }
-  function waitForNotificationCond(cond: (n: WalletNotification) => boolean) {
-    const promCap = openPromise<void>();
+  function waitForNotificationCond<T>(
+    cond: (n: WalletNotification) => T | false | undefined,
+  ) {
+    const promCap = openPromise<T>();
     condMap.set(nextCondIndex++, {
       condition: cond,
       promiseCapability: promCap,

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