taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 8676f8716f6f9e5e944f062f72feb487d90e6430
parent 1ce3866c9b6d1055d682b4a92d8cef902c0e95e9
Author: Florian Dold <florian@dold.me>
Date:   Wed, 11 Mar 2026 14:12:25 +0100

harness: test for wire metadata

Diffstat:
Mpackages/taler-harness/src/harness/environments.ts | 107++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mpackages/taler-harness/src/harness/harness.ts | 6++++++
Apackages/taler-harness/src/integrationtests/test-wire-metadata.ts | 314+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 4+++-
Mpackages/taler-util/src/http-client/bank-core.ts | 39+++++++++++++++++++++++++++++++--------
5 files changed, 458 insertions(+), 12 deletions(-)

diff --git a/packages/taler-harness/src/harness/environments.ts b/packages/taler-harness/src/harness/environments.ts @@ -28,6 +28,7 @@ import { AccountProperties, AmlDecisionRequest, AmlDecisionRequestWithoutSignature, + Amounts, AmountString, assertUnreachable, Configuration, @@ -914,6 +915,8 @@ export interface WithdrawViaBankResult { /** * Withdraw via a bank with the testing API enabled. * Uses the new Corebank API. + * + * @deprecated use withdrawViaBankV4 */ export async function withdrawViaBankV3( t: GlobalTestState, @@ -985,6 +988,105 @@ export async function withdrawViaBankV3( }; } +/** + * Withdraw via a bank that supports the corebank testing API. + */ +export async function withdrawViaBankV4( + t: GlobalTestState, + p: { + walletClient: WalletClient; + exchange: ExchangeServiceInterface; + bank: BankService; + amount: AmountString | string; + restrictAge?: number; + bankAdminTok: AccessToken; + }, +): Promise<WithdrawViaBankResult> { + const { walletClient: wallet, exchange, amount, bank, bankAdminTok } = p; + + const bankClient = new TalerCoreBankHttpClient(bank.corebankApiBaseUrl); + + const username = "user-" + encodeCrock(getRandomBytes(10)).toLowerCase(); + const password = "pw-" + encodeCrock(getRandomBytes(10)).toLowerCase(); + + const user = succeedOrThrow( + await bankClient.createAccount(bankAdminTok, { + name: username, + password: password, + username: username, + }), + ); + + const token = succeedOrThrow( + await bankClient.createAccessToken( + username, + { + type: "basic", + password, + }, + { + scope: "readwrite", + }, + ), + ).access_token; + + const userAuth = { + username, + token, + }; + + const wop = succeedOrThrow( + await bankClient.createWithdrawal(userAuth, { + amount: Amounts.stringify(amount), + }), + ); + + // Hand it to the wallet + + await wallet.client.call(WalletApiOperation.GetWithdrawalDetailsForUri, { + talerWithdrawUri: wop.taler_withdraw_uri, + restrictAge: p.restrictAge, + }); + + // Withdraw (AKA select) + + const acceptRes = await wallet.client.call( + WalletApiOperation.AcceptBankIntegratedWithdrawal, + { + exchangeBaseUrl: exchange.baseUrl, + talerWithdrawUri: wop.taler_withdraw_uri, + restrictAge: p.restrictAge, + }, + ); + + const withdrawalFinishedCond = wallet.waitForNotificationCond( + (x) => + x.type === NotificationType.TransactionStateTransition && + x.newTxState.major === TransactionMajorState.Done && + x.transactionId === acceptRes.transactionId, + ); + + await wallet.call(WalletApiOperation.TestingWaitTransactionState, { + transactionId: acceptRes.transactionId, + txState: { + major: TransactionMajorState.Pending, + minor: TransactionMinorState.BankConfirmTransfer, + }, + }); + + // Confirm it + + succeedOrThrow( + await bankClient.confirmWithdrawalById(userAuth, {}, wop.withdrawal_id), + ); + + return { + accountPaytoUri: user.internal_payto_uri, + withdrawalFinishedCond, + transactionId: acceptRes.transactionId, + }; +} + export async function applyTimeTravelV2( timetravelOffsetMs: number, s: { @@ -1024,17 +1126,16 @@ export async function makeTestPaymentV2( merchantAdminAccessToken: AccessToken; walletClient: WalletClient; order: TalerMerchantApi.Order; - instance?: string; refundDelay?: Duration; }, auth: WithAuthorization = {}, ): Promise<{ transactionId: TransactionIdStr; orderId: string }> { // Set up order. - const { walletClient, merchant, instance, merchantAdminAccessToken } = args; + const { walletClient, merchant, merchantAdminAccessToken } = args; const merchantClient = new TalerMerchantInstanceHttpClient( - merchant.makeInstanceBaseUrl(instance), + merchant.makeInstanceBaseUrl(), ); const orderResp = succeedOrThrow( diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts @@ -31,6 +31,7 @@ import { ConfigSources, Configuration, CoreApiResponse, + Credentials, Duration, EddsaKeyPair, InstanceAuthConfigurationMessage, @@ -934,6 +935,11 @@ export class LibeufinNexusService { } } +export const harnessBankAdminCreds: Credentials = { + type: "basic", + password: "admin-password", +}; + /** * Implementation of the bank service using the libeufin-bank implementation. */ diff --git a/packages/taler-harness/src/integrationtests/test-wire-metadata.ts b/packages/taler-harness/src/integrationtests/test-wire-metadata.ts @@ -0,0 +1,314 @@ +/* + This file is part of GNU Taler + (C) 2021 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 { + ConfirmPayResultType, + Duration, + j2s, + LoginTokenScope, + MerchantAuthMethod, + PreparePayResultType, + succeedOrThrow, + TalerCoreBankHttpClient, + TalerMerchantInstanceHttpClient, + TalerMerchantManagementHttpClient, + TalerProtocolTimestamp, + UserAndToken, +} from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import { defaultCoinConfig } from "../harness/denomStructures.js"; +import { + applyTimeTravelV2, + createWalletDaemonWithClient, + withdrawViaBankV4, +} from "../harness/environments.js"; +import { + BankService, + ExchangeService, + getTestHarnessPaytoForLabel, + GlobalTestState, + harnessBankAdminCreds, + HarnessExchangeBankAccount, + LibeufinBankService, + MerchantService, + setupDb, + waitMs, +} from "../harness/harness.js"; + +export async function runWireMetadataTest(t: GlobalTestState) { + // Set up test environment + + const db = await setupDb(t); + + const exchange = ExchangeService.create(t, { + name: "testexchange-1", + currency: "TESTKUDOS", + httpPort: 8081, + database: db.connStr, + }); + + const bc = { + allowRegistrations: true, + currency: "TESTKUDOS", + database: db.connStr, + httpPort: 8082, + }; + + const bank: BankService = await LibeufinBankService.create(t, bc); + + const receiverName = "Exchange"; + const exchangeBankUsername = "exchange"; + const exchangeBankPassword = "mypw-password"; + const exchangePaytoUri = getTestHarnessPaytoForLabel(exchangeBankUsername); + const wireGatewayApiBaseUrl = new URL( + `accounts/${exchangeBankUsername}/taler-wire-gateway/`, + bank.corebankApiBaseUrl, + ).href; + + const exchangeBankAccount: HarnessExchangeBankAccount = { + wireGatewayApiBaseUrl, + wireGatewayAuth: { + username: exchangeBankUsername, + password: exchangeBankPassword, + }, + accountPaytoUri: exchangePaytoUri, + }; + + await exchange.addBankAccount("1", exchangeBankAccount); + + exchange.addOfferedCoins(defaultCoinConfig); + + bank.setSuggestedExchange(exchange, exchangeBankAccount.accountPaytoUri); + + await bank.start(); + + const bankClient = new TalerCoreBankHttpClient(bank.corebankApiBaseUrl); + const bankAdminTok = succeedOrThrow( + await bankClient.createAccessToken("admin", harnessBankAdminCreds, { + scope: "readwrite", + }), + ).access_token; + + await bankClient.createAccount(bankAdminTok, { + name: receiverName, + password: exchangeBankPassword, + username: exchangeBankUsername, + is_taler_exchange: true, + payto_uri: exchangePaytoUri, + }); + + await exchange.start(); + + const merchant = await MerchantService.create(t, { + name: "testmerchant-1", + httpPort: 8083, + database: db.connStr, + }); + const merchantMgmtClient = new TalerMerchantManagementHttpClient( + merchant.makeInstanceBaseUrl(), + ); + + merchant.addExchange(exchange); + + await merchant.start(); + + succeedOrThrow( + await merchantMgmtClient.createInstance(undefined, { + id: "admin", + address: {}, + auth: { + method: MerchantAuthMethod.TOKEN, + password: "123", + }, + jurisdiction: {}, + name: "Admin", + use_stefan: true, + }), + ); + + const merchantAdminClient = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl(), + ); + + const merchantAdminTok = succeedOrThrow( + await merchantAdminClient.createAccessToken("admin", "123", { + scope: LoginTokenScope.All, + }), + ).access_token; + + console.log(`merchantAdminTok: ${merchantAdminTok}`); + + succeedOrThrow( + await merchantMgmtClient.createInstance(merchantAdminTok, { + id: "minst1", + address: {}, + auth: { + method: MerchantAuthMethod.TOKEN, + password: "123", + }, + jurisdiction: {}, + name: "My Instance One", + use_stefan: true, + }), + ); + + const merchantMinst1Client = new TalerMerchantInstanceHttpClient( + merchant.makeInstanceBaseUrl("minst1"), + ); + + const merchantMinst1Tok = succeedOrThrow( + await merchantMinst1Client.createAccessToken("minst1", "123", { + scope: LoginTokenScope.All, + }), + ).access_token; + + succeedOrThrow( + await bankClient.createAccount(bankAdminTok, { + name: "minst1", + password: "minst1-password", + username: "minst1", + payto_uri: getTestHarnessPaytoForLabel("minst1"), + }), + ); + + succeedOrThrow( + await merchantMinst1Client.addBankAccount(merchantMinst1Tok, { + payto_uri: getTestHarnessPaytoForLabel("minst1"), + extra_wire_subject_metadata: "foo", + }), + ); + + const { walletClient, walletService } = await createWalletDaemonWithClient( + t, + { + name: "wallet", + persistent: true, + }, + ); + + const wres = await withdrawViaBankV4(t, { + walletClient, + bankAdminTok, + amount: "TESTKUDOS:20", + bank, + exchange, + }); + await wres.withdrawalFinishedCond; + + { + const orderResp = succeedOrThrow( + await merchantMinst1Client.createOrder(merchantMinst1Tok, { + order: { + amount: "TESTKUDOS:5", + summary: "Hello", + wire_transfer_deadline: TalerProtocolTimestamp.now(), + }, + }), + ); + + let orderStatus = succeedOrThrow( + await merchantMinst1Client.getOrderDetails( + merchantMinst1Tok, + orderResp.order_id, + ), + ); + + t.assertTrue(orderStatus.order_status === "unpaid"); + + const preparePayResult = await walletClient.call( + WalletApiOperation.PreparePayForUri, + { + talerPayUri: orderStatus.taler_pay_uri, + }, + ); + + t.assertTrue( + preparePayResult.status === PreparePayResultType.PaymentPossible, + ); + + const r2 = await walletClient.call(WalletApiOperation.ConfirmPay, { + transactionId: preparePayResult.transactionId, + }); + + t.assertDeepEqual(r2.type, ConfirmPayResultType.Done); + + // Check if payment was successful. + + orderStatus = succeedOrThrow( + await merchantMinst1Client.getOrderDetails( + merchantMinst1Tok, + orderResp.order_id, + ), + ); + + t.assertDeepEqual(orderStatus.order_status, "paid"); + } + + await applyTimeTravelV2( + Duration.toMilliseconds( + Duration.fromSpec({ + days: 14, + }), + ), + { + merchant, + exchange, + walletClient, + }, + ); + + { + const client = new TalerCoreBankHttpClient(bank.corebankApiBaseUrl); + const btok = succeedOrThrow( + await client.createAccessToken( + "exchange", + { + type: "basic", + password: "mypw-password", + }, + { + scope: "readwrite", + }, + ), + ).access_token; + const bankAuth: UserAndToken = { + token: btok, + username: "exchange", + }; + + while (1) { + const txns = succeedOrThrow(await client.getTransactions(bankAuth)); + console.log(`bank txns for exchange: ${j2s(txns)}`); + if (txns.transactions.length >= 2) { + const myTx = txns.transactions.find((x) => x.direction === "debit"); + if (!myTx) { + throw Error("unexpected transaction"); + } + t.assertTrue(myTx.subject.includes("foo")); + break; + } + console.log("waiting for transaction..."); + await waitMs(1000); + await exchange.runAggregatorOnce(); + await exchange.runTransferOnce(); + } + } +} + +runWireMetadataTest.suites = ["merchant"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -117,6 +117,7 @@ import { runMerchantLongpollingTest } from "./test-merchant-longpolling.js"; import { runMerchantRefundApiTest } from "./test-merchant-refund-api.js"; import { runMerchantReportsTest } from "./test-merchant-reports.js"; import { runMerchantSelfProvisionActivationAndLoginTest } from "./test-merchant-self-provision-activation-and-login.js"; +import { runMerchantSelfProvisionActivationTwoBankAccountsTest } from "./test-merchant-self-provision-activation-two-bank-account.js"; import { runMerchantSelfProvisionActivationTest } from "./test-merchant-self-provision-activation.js"; import { runMerchantSelfProvisionForgotPasswordTest } from "./test-merchant-self-provision-forgot-password.js"; import { runMerchantSelfProvisionInactiveAccountPermissionsTest } from "./test-merchant-self-provision-inactive-account-permissions.js"; @@ -208,6 +209,7 @@ import { runWalletWirefeesTest } from "./test-wallet-wirefees.js"; import { runWalletWithdrawalRedenominateTest } from "./test-wallet-withdrawal-redenominate.js"; import { runWallettestingTest } from "./test-wallettesting.js"; import { runWebMerchantLoginTest } from "./test-web-merchant-login.js"; +import { runWireMetadataTest } from "./test-wire-metadata.js"; import { runWithdrawalAbortBankTest } from "./test-withdrawal-abort-bank.js"; import { runWithdrawalAmountTest } from "./test-withdrawal-amount.js"; import { runWithdrawalBankIntegratedTest } from "./test-withdrawal-bank-integrated.js"; @@ -223,7 +225,6 @@ import { runWithdrawalHugeTest } from "./test-withdrawal-huge.js"; import { runWithdrawalIdempotentTest } from "./test-withdrawal-idempotent.js"; import { runWithdrawalManualTest } from "./test-withdrawal-manual.js"; import { runWithdrawalPrepareTest } from "./test-withdrawal-prepare.js"; -import { runMerchantSelfProvisionActivationTwoBankAccountsTest } from "./test-merchant-self-provision-activation-two-bank-account.js"; /** * Test runner. @@ -433,6 +434,7 @@ const allTests: TestMainFunction[] = [ runTopsMerchantTosTest, runWalletWithdrawalRedenominateTest, runMerchantDepositLargeTest, + runWireMetadataTest, ]; export interface TestRunSpec { diff --git a/packages/taler-util/src/http-client/bank-core.ts b/packages/taler-util/src/http-client/bank-core.ts @@ -17,23 +17,21 @@ import { AbsoluteTime, AccessToken, + ChallengeResponse, HttpStatusCode, LibtoolVersion, LongPollParams, + OperationAlternative, OperationFail, OperationOk, PaginationParams, - TalerError, TalerErrorCode, TokenRequest, UserAndToken, assertUnreachable, carefullyParseConfig, - codecForTalerCommonConfigResponse, codecForTokenInfoList, codecForTokenSuccessResponse, - codecOptional, - codecOptionalDefault, opKnownAlternativeHttpFailure, opKnownHttpFailure, opKnownTalerFailure, @@ -41,7 +39,6 @@ import { import { HttpRequestLibrary, createPlatformHttpLib, - readSuccessResponseJsonOrThrow, readTalerErrorResponse, } from "@gnu-taler/taler-util/http"; import { @@ -63,7 +60,7 @@ import { CreateTransactionRequest, MonitorTimeframeParam, RegisterAccountRequest, - TalerCorebankConfigResponse, + RegisterAccountResponse, codecForAccountData, codecForBankAccountCreateWithdrawalResponse, codecForBankAccountTransactionInfo, @@ -314,7 +311,23 @@ export class TalerCoreBankHttpClient { async createAccount( auth: AccessToken | undefined, body: RegisterAccountRequest, - ) { + ): Promise< + | OperationOk<RegisterAccountResponse> + | OperationFail<HttpStatusCode.Unauthorized> + | OperationFail<HttpStatusCode.BadRequest> + | OperationFail<TalerErrorCode.BANK_REGISTER_USERNAME_REUSE> + | OperationFail<TalerErrorCode.BANK_REGISTER_PAYTO_URI_REUSE> + | OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT> + | OperationFail<TalerErrorCode.BANK_RESERVED_USERNAME_CONFLICT> + | OperationFail<TalerErrorCode.BANK_NON_ADMIN_PATCH_DEBT_LIMIT> + | OperationFail<TalerErrorCode.BANK_NON_ADMIN_SET_CONVERSION_RATE_CLASS> + | OperationFail<TalerErrorCode.BANK_NON_ADMIN_SET_TAN_CHANNEL> + | OperationFail<TalerErrorCode.BANK_TAN_CHANNEL_NOT_SUPPORTED> + | OperationFail<TalerErrorCode.BANK_MISSING_TAN_INFO> + | OperationFail<TalerErrorCode.BANK_PASSWORD_TOO_SHORT> + | OperationFail<TalerErrorCode.BANK_PASSWORD_TOO_LONG> + | OperationFail<TalerErrorCode.BANK_CONVERSION_RATE_CLASS_UNKNOWN> + > { const url = new URL(`accounts`, this.baseUrl); const headers: Record<string, string> = {}; if (auth) { @@ -810,7 +823,17 @@ export class TalerCoreBankHttpClient { body: BankAccountConfirmWithdrawalRequest, wid: string, params: { challengeIds?: string[] } = {}, - ) { + ): Promise< + | OperationFail<HttpStatusCode.NotFound> + | OperationAlternative<HttpStatusCode.Accepted, ChallengeResponse> + | OperationOk<void> + | OperationFail<HttpStatusCode.BadRequest> + | OperationFail<TalerErrorCode.BANK_UNALLOWED_DEBIT> + | OperationFail<TalerErrorCode.BANK_CONFIRM_ABORT_CONFLICT> + | OperationFail<TalerErrorCode.BANK_CONFIRM_INCOMPLETE> + | OperationFail<TalerErrorCode.BANK_AMOUNT_DIFFERS> + | OperationFail<TalerErrorCode.BANK_AMOUNT_REQUIRED> + > { const url = new URL( `accounts/${auth.username}/withdrawals/${wid}/confirm`, this.baseUrl,