aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Dold <florian@dold.me>2024-08-20 16:29:45 +0200
committerFlorian Dold <florian@dold.me>2024-08-20 16:29:45 +0200
commitdcde71193724e34f0f9c99491ffc4f6fade0a704 (patch)
tree2e4f5b0d9b142ef71be24742191bf91bffe833f2
parent02fadc83b997e4f5175d0dcd91ab925cebb08f47 (diff)
downloadwallet-core-dcde71193724e34f0f9c99491ffc4f6fade0a704.tar.xz
harness: test for FORM-based KYC
-rw-r--r--packages/taler-harness/src/harness/helpers.ts15
-rw-r--r--packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts311
-rw-r--r--packages/taler-harness/src/integrationtests/testrunner.ts2
-rw-r--r--packages/taler-util/src/taler-signatures.ts6
-rw-r--r--packages/taler-util/src/types-taler-wallet-transactions.ts8
-rw-r--r--packages/taler-wallet-core/src/db.ts2
-rw-r--r--packages/taler-wallet-core/src/withdraw.ts14
7 files changed, 354 insertions, 4 deletions
diff --git a/packages/taler-harness/src/harness/helpers.ts b/packages/taler-harness/src/harness/helpers.ts
index fb8d85fc4..183664638 100644
--- a/packages/taler-harness/src/harness/helpers.ts
+++ b/packages/taler-harness/src/harness/helpers.ts
@@ -759,8 +759,21 @@ export async function createFaultInjectedMerchantTestkudosEnvironment(
}
export interface WithdrawViaBankResult {
+ /**
+ * Payto URI of the account that the withdrawal
+ * originated from. Typically a new account used for testing.
+ */
accountPaytoUri: string;
+
+ /**
+ * Helper promise that resolves when withdrawal has finished successfully.
+ */
withdrawalFinishedCond: Promise<true>;
+
+ /**
+ * The wallet-core withdrawal transaction ID.
+ */
+ transactionId: string;
}
/**
@@ -819,6 +832,7 @@ export async function withdrawViaBankV2(
return {
accountPaytoUri: user.accountPaytoUri,
withdrawalFinishedCond,
+ transactionId: acceptRes.transactionId,
};
}
@@ -884,6 +898,7 @@ export async function withdrawViaBankV3(
return {
accountPaytoUri: user.accountPaytoUri,
withdrawalFinishedCond,
+ transactionId: acceptRes.transactionId,
};
}
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts b/packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts
new file mode 100644
index 000000000..1ad4b5bb4
--- /dev/null
+++ b/packages/taler-harness/src/integrationtests/test-kyc-form-withdrawal.ts
@@ -0,0 +1,311 @@
+/*
+ This file is part of GNU Taler
+ (C) 2020 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 <http://www.gnu.org/licenses/>
+ */
+
+/**
+ * Imports.
+ */
+import {
+ codecForAny,
+ codecForKycProcessClientInformation,
+ decodeCrock,
+ encodeCrock,
+ j2s,
+ signAmlQuery,
+ TalerCorebankApiClient,
+ TransactionIdStr,
+ TransactionMajorState,
+ TransactionMinorState,
+} from "@gnu-taler/taler-util";
+import { readResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
+import {
+ createSyncCryptoApi,
+ EddsaKeypair,
+ WalletApiOperation,
+} from "@gnu-taler/taler-wallet-core";
+import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js";
+import {
+ BankService,
+ DbInfo,
+ ExchangeService,
+ generateRandomPayto,
+ GlobalTestState,
+ HarnessExchangeBankAccount,
+ harnessHttpLib,
+ setupDb,
+ WalletClient,
+ WalletService,
+} from "../harness/harness.js";
+import { EnvOptions, withdrawViaBankV3 } from "../harness/helpers.js";
+
+interface KycTestEnv {
+ commonDb: DbInfo;
+ bankClient: TalerCorebankApiClient;
+ exchange: ExchangeService;
+ exchangeBankAccount: HarnessExchangeBankAccount;
+ walletClient: WalletClient;
+ walletService: WalletService;
+ amlKeypair: EddsaKeypair;
+}
+
+async function createKycTestkudosEnvironment(
+ t: GlobalTestState,
+ coinConfig: CoinConfig[] = defaultCoinConfig.map((x) => x("TESTKUDOS")),
+ opts: EnvOptions = {},
+): Promise<KycTestEnv> {
+ const db = await setupDb(t);
+
+ const bank = await BankService.create(t, {
+ allowRegistrations: true,
+ currency: "TESTKUDOS",
+ database: db.connStr,
+ httpPort: 8082,
+ });
+
+ const exchange = ExchangeService.create(t, {
+ name: "testexchange-1",
+ currency: "TESTKUDOS",
+ httpPort: 8081,
+ database: db.connStr,
+ });
+
+ let receiverName = "Exchange";
+ let exchangeBankUsername = "exchange";
+ let exchangeBankPassword = "mypw";
+ let exchangePaytoUri = generateRandomPayto(exchangeBankUsername);
+
+ await exchange.addBankAccount("1", {
+ accountName: exchangeBankUsername,
+ accountPassword: exchangeBankPassword,
+ wireGatewayApiBaseUrl: new URL(
+ "accounts/exchange/taler-wire-gateway/",
+ bank.baseUrl,
+ ).href,
+ accountPaytoUri: exchangePaytoUri,
+ });
+
+ bank.setSuggestedExchange(exchange, exchangePaytoUri);
+
+ await bank.start();
+
+ await bank.pingUntilAvailable();
+
+ const bankClient = new TalerCorebankApiClient(bank.corebankApiBaseUrl, {
+ auth: {
+ username: "admin",
+ password: "adminpw",
+ },
+ });
+
+ await bankClient.registerAccountExtended({
+ name: receiverName,
+ password: exchangeBankPassword,
+ username: exchangeBankUsername,
+ is_taler_exchange: true,
+ payto_uri: exchangePaytoUri,
+ });
+
+ exchange.addCoinConfigList(coinConfig);
+
+ await exchange.modifyConfig(async (config) => {
+ config.setString("exchange", "enable_kyc", "yes");
+
+ config.setString("KYC-RULE-R1", "operation_type", "withdraw");
+ config.setString("KYC-RULE-R1", "enabled", "yes");
+ config.setString("KYC-RULE-R1", "exposed", "yes");
+ config.setString("KYC-RULE-R1", "is_and_combinator", "yes");
+ config.setString("KYC-RULE-R1", "threshold", "TESTKUDOS:5");
+ config.setString("KYC-RULE-R1", "timeframe", "1d");
+ config.setString("KYC-RULE-R1", "next_measures", "M1 M2");
+
+ config.setString("KYC-MEASURE-M1", "check_name", "C1");
+ config.setString("KYC-MEASURE-M1", "context", "{}");
+ config.setString("KYC-MEASURE-M1", "program", "P1");
+
+ config.setString("KYC-MEASURE-M2", "check_name", "C2");
+ config.setString("KYC-MEASURE-M2", "context", "{}");
+ config.setString("KYC-MEASURE-M2", "program", "P2");
+
+ config.setString(
+ "AML-PROGRAM-P1",
+ "command",
+ "taler-exchange-helper-measure-test-form",
+ );
+ config.setString("AML-PROGRAM-P1", "enabled", "true");
+ config.setString(
+ "AML-PROGRAM-P1",
+ "description",
+ "test for full_name and birthdate",
+ );
+ config.setString("AML-PROGRAM-P1", "description_i18n", "{}");
+ config.setString("AML-PROGRAM-P1", "fallback", "M1");
+
+ config.setString("AML-PROGRAM-P2", "command", "/bin/true");
+ config.setString("AML-PROGRAM-P2", "enabled", "true");
+ config.setString("AML-PROGRAM-P2", "description", "does nothing");
+ config.setString("AML-PROGRAM-P2", "description_i18n", "{}");
+ config.setString("AML-PROGRAM-P2", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C1", "type", "FORM");
+ config.setString("KYC-CHECK-C1", "form_name", "myform");
+ config.setString("KYC-CHECK-C1", "description", "my check!");
+ config.setString("KYC-CHECK-C1", "description_i18n", "{}");
+ config.setString("KYC-CHECK-C1", "outputs", "full_name birthdate");
+ config.setString("KYC-CHECK-C1", "fallback", "M1");
+
+ config.setString("KYC-CHECK-C2", "type", "INFO");
+ config.setString("KYC-CHECK-C2", "description", "my check info!");
+ config.setString("KYC-CHECK-C2", "description_i18n", "{}");
+ config.setString("KYC-CHECK-C2", "fallback", "M2");
+ });
+
+ await exchange.start();
+
+ const cryptoApi = createSyncCryptoApi();
+ const amlKeypair = await cryptoApi.createEddsaKeypair({});
+
+ await exchange.enableAmlAccount(amlKeypair.pub, "Alice");
+
+ const walletService = new WalletService(t, {
+ name: "wallet",
+ useInMemoryDb: true,
+ });
+ await walletService.start();
+ await walletService.pingUntilAvailable();
+
+ const walletClient = new WalletClient({
+ name: "wallet",
+ unixPath: walletService.socketPath,
+ onNotification(n) {
+ console.log("got notification", n);
+ },
+ });
+ await walletClient.connect();
+ await walletClient.client.call(WalletApiOperation.InitWallet, {
+ config: {
+ testing: {
+ skipDefaults: true,
+ },
+ },
+ });
+
+ console.log("setup done!");
+
+ return {
+ commonDb: db,
+ exchange,
+ amlKeypair,
+ walletClient,
+ walletService,
+ bankClient,
+ exchangeBankAccount: {
+ accountName: "",
+ accountPassword: "",
+ accountPaytoUri: "",
+ wireGatewayApiBaseUrl: "",
+ },
+ };
+}
+
+export async function runKycFormWithdrawalTest(t: GlobalTestState) {
+ // Set up test environment
+
+ const { walletClient, bankClient, exchange, amlKeypair } =
+ await createKycTestkudosEnvironment(t);
+
+ // Withdraw digital cash into the wallet.
+
+ const wres = await withdrawViaBankV3(t, {
+ amount: "TESTKUDOS:20",
+ bankClient,
+ exchange,
+ walletClient,
+ });
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: wres.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Pending,
+ minor: TransactionMinorState.KycRequired,
+ },
+ });
+
+ const txDetails = await walletClient.call(
+ WalletApiOperation.GetTransactionById,
+ {
+ transactionId: wres.transactionId,
+ },
+ );
+
+ console.log(j2s(txDetails));
+ const accessToken = txDetails.kycAccessToken;
+ t.assertTrue(!!accessToken);
+
+ const infoResp = await harnessHttpLib.fetch(
+ new URL(`kyc-info/${txDetails.kycAccessToken}`, exchange.baseUrl).href,
+ );
+
+ const clientInfo = await readResponseJsonOrThrow(
+ infoResp,
+ codecForKycProcessClientInformation(),
+ );
+
+ console.log(j2s(clientInfo));
+
+ const kycId = clientInfo.requirements.find((x) => x.id != null)?.id;
+ t.assertTrue(!!kycId);
+
+ const uploadResp = await harnessHttpLib.fetch(
+ new URL(`kyc-upload/${kycId}`, exchange.baseUrl).href,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/x-www-form-urlencoded",
+ },
+ body: "full_name=Alice+Abc&birthdate=2000-01-01",
+ },
+ );
+
+ console.log("resp status", uploadResp.status);
+
+ t.assertDeepEqual(uploadResp.status, 204);
+
+ const sig = signAmlQuery(decodeCrock(amlKeypair.priv));
+
+ const decisionsResp = await harnessHttpLib.fetch(
+ new URL(`aml/${amlKeypair.pub}/decisions`, exchange.baseUrl).href,
+ {
+ headers: {
+ "Taler-AML-Officer-Signature": encodeCrock(sig),
+ },
+ },
+ );
+
+ const decisions = await readResponseJsonOrThrow(decisionsResp, codecForAny());
+ console.log(j2s(decisions));
+
+ t.assertDeepEqual(decisionsResp.status, 200);
+
+ // KYC should pass now
+
+ await walletClient.call(WalletApiOperation.TestingWaitTransactionState, {
+ transactionId: wres.transactionId as TransactionIdStr,
+ txState: {
+ major: TransactionMajorState.Done,
+ },
+ });
+}
+
+runKycFormWithdrawalTest.suites = ["wallet"];
diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts
index 9aea886f7..5c4ba41d9 100644
--- a/packages/taler-harness/src/integrationtests/testrunner.ts
+++ b/packages/taler-harness/src/integrationtests/testrunner.ts
@@ -49,6 +49,7 @@ import { runFeeRegressionTest } from "./test-fee-regression.js";
import { runForcedSelectionTest } from "./test-forced-selection.js";
import { runKycDepositAggregateTest } from "./test-kyc-deposit-aggregate.js";
import { runKycExchangeWalletTest } from "./test-kyc-exchange-wallet.js";
+import { runKycFormWithdrawalTest } from "./test-kyc-form-withdrawal.js";
import { runKycPeerPullTest } from "./test-kyc-peer-pull.js";
import { runKycPeerPushTest } from "./test-kyc-peer-push.js";
import { runKycThresholdWithdrawalTest } from "./test-kyc-threshold-withdrawal.js";
@@ -254,6 +255,7 @@ const allTests: TestMainFunction[] = [
runKycPeerPushTest,
runKycPeerPullTest,
runKycDepositAggregateTest,
+ runKycFormWithdrawalTest,
];
export interface TestRunSpec {
diff --git a/packages/taler-util/src/taler-signatures.ts b/packages/taler-util/src/taler-signatures.ts
index 81b7c242e..5c9690528 100644
--- a/packages/taler-util/src/taler-signatures.ts
+++ b/packages/taler-util/src/taler-signatures.ts
@@ -61,3 +61,9 @@ export function signAmlDecision(
return eddsaSign(sigBlob, priv);
}
+
+export function signAmlQuery(key: Uint8Array): Uint8Array {
+ const sigBlob = buildSigPS(TalerSignaturePurpose.AML_QUERY).build();
+
+ return eddsaSign(sigBlob, key);
+}
diff --git a/packages/taler-util/src/types-taler-wallet-transactions.ts b/packages/taler-util/src/types-taler-wallet-transactions.ts
index 4064052a5..000afb86a 100644
--- a/packages/taler-util/src/types-taler-wallet-transactions.ts
+++ b/packages/taler-util/src/types-taler-wallet-transactions.ts
@@ -222,7 +222,15 @@ export interface TransactionCommon {
*/
kycUrl?: string;
+ /**
+ * KYC payto hash. Useful for testing, not so useful for UIs.
+ */
kycPaytoHash?: string;
+
+ /**
+ * KYC access token. Useful for testing, not so useful for UIs.
+ */
+ kycAccessToken?: string;
}
export type Transaction =
diff --git a/packages/taler-wallet-core/src/db.ts b/packages/taler-wallet-core/src/db.ts
index 0dd67d2fa..531e19b63 100644
--- a/packages/taler-wallet-core/src/db.ts
+++ b/packages/taler-wallet-core/src/db.ts
@@ -1472,6 +1472,8 @@ export interface WithdrawalGroupRecord {
kycUrl?: string;
+ kycAccessToken?: string;
+
/**
* Secret seed used to derive planchets.
* Stored since planchets are created lazily.
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
index ef04f8737..fa1066f0f 100644
--- a/packages/taler-wallet-core/src/withdraw.ts
+++ b/packages/taler-wallet-core/src/withdraw.ts
@@ -26,6 +26,7 @@ import {
AbsoluteTime,
AcceptManualWithdrawalResult,
AcceptWithdrawalResponse,
+ AccountKycStatus,
AgeRestriction,
Amount,
AmountJson,
@@ -229,6 +230,7 @@ function buildTransactionForBankIntegratedWithdraw(
wg.status === WithdrawalGroupStatus.PendingReady,
},
kycUrl: wg.kycUrl,
+ kycAccessToken: wg.kycAccessToken,
kycPaytoHash: wg.kycPending?.paytoHash,
timestamp: timestampPreciseFromDb(wg.timestampStart),
transactionId: constructTransactionIdentifier({
@@ -1210,7 +1212,7 @@ async function handleKycRequired(
["Account-Owner-Signature"]: sigResp.sig,
},
});
- let kycUrl: string;
+ let kycStatus: AccountKycStatus;
if (
kycStatusRes.status === HttpStatusCode.Ok ||
// FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
@@ -1220,12 +1222,11 @@ async function handleKycRequired(
logger.warn("kyc requested, but already fulfilled");
return;
} else if (kycStatusRes.status === HttpStatusCode.Accepted) {
- const kycStatus = await readSuccessResponseJsonOrThrow(
+ kycStatus = await readSuccessResponseJsonOrThrow(
kycStatusRes,
codecForAccountKycStatus(),
);
logger.info(`kyc status: ${j2s(kycStatus)}`);
- kycUrl = new URL(`kyc-spa/${kycStatus.access_token}`, exchangeUrl).href;
} else {
throwUnexpectedRequestError(
kycStatusRes,
@@ -1255,11 +1256,16 @@ async function handleKycRequired(
if (wg2.status !== WithdrawalGroupStatus.PendingReady) {
return TransitionResult.stay();
}
+ // FIXME: Why not just store the whole kycState?!
wg2.kycPending = {
paytoHash: uuidResp.h_payto,
requirementRow: uuidResp.requirement_row,
};
- wg2.kycUrl = kycUrl;
+ wg2.kycUrl = new URL(
+ `kyc-spa/${kycStatus.access_token}`,
+ exchangeUrl,
+ ).href;
+ wg2.kycAccessToken = kycStatus.access_token;
wg2.status = WithdrawalGroupStatus.PendingKyc;
return TransitionResult.transition(wg2);
},