taler-typescript-core

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

commit 335c996d4dc00d1f7f98bd8f39e883db0c8fb9d5
parent d8ed968c6704cacf49f850d4c1d92d7562589435
Author: Florian Dold <dold@taler.net>
Date:   Tue, 18 Aug 2026 00:26:35 +0200

wallet: fix integration test failures

Diffstat:
Mpackages/taler-harness/src/harness/tops.ts | 53+++++++++++++++++++++++++++--------------------------
Mpackages/taler-harness/src/integrationtests/test-exchange-management-fault.ts | 24++++++++++++------------
Mpackages/taler-harness/src/integrationtests/test-kyc-wallet-deposit-abort.ts | 5++++-
Mpackages/taler-harness/src/integrationtests/test-payment-order-gone.ts | 2+-
Mpackages/taler-harness/src/integrationtests/test-peer-repair.ts | 19+++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/test-revocation.ts | 6+++++-
Mpackages/taler-harness/src/integrationtests/test-tops-aml-null-current-rules.ts | 20+++++++++++++++++---
Mpackages/taler-harness/src/integrationtests/test-wallet-wirefees.ts | 4+++-
Mpackages/taler-harness/src/integrationtests/test-withdrawal-huge.ts | 10+++++++++-
Mpackages/taler-wallet-core/src/withdraw.ts | 4++++
10 files changed, 101 insertions(+), 46 deletions(-)

diff --git a/packages/taler-harness/src/harness/tops.ts b/packages/taler-harness/src/harness/tops.ts @@ -1115,16 +1115,7 @@ export async function setupMeasuresTestEnvironment( console.log(`existing decision:`, j2s(decisionsResp)); const currDec = decisionsResp.records[0]; - let decisionTime = TalerProtocolTimestamp.now(); - if ( - currDec.decision_time.t_s !== "never" && - decisionTime.t_s !== "never" && - decisionTime.t_s <= currDec.decision_time.t_s - ) { - decisionTime = TalerProtocolTimestamp.fromSeconds( - currDec.decision_time.t_s + 1, - ); - } + const decisionTime = nextAmlDecisionTimestamp(currDec.decision_time); succeedOrThrow( await exchangeClient.makeAmlDesicion(officerAcc, { @@ -1270,6 +1261,21 @@ export function isFrozen(decision: AmlDecision): boolean { return true; } +function nextAmlDecisionTimestamp( + previousDecisionTime?: TalerProtocolTimestamp, +): TalerProtocolTimestamp { + const now = TalerProtocolTimestamp.now(); + if ( + previousDecisionTime?.t_s !== undefined && + previousDecisionTime.t_s !== "never" && + now.t_s !== "never" && + now.t_s <= previousDecisionTime.t_s + ) { + return TalerProtocolTimestamp.fromSeconds(previousDecisionTime.t_s + 1); + } + return now; +} + async function doTriggerReset( t: GlobalTestState, args: { @@ -1316,9 +1322,18 @@ async function doTriggerReset( rule_name: secName.substring(rulePrefix.length).toLowerCase(), }); } + const decisionsResp = succeedOrThrow( + await exchangeClient.getAmlDecisions(officerAcc, { + active: true, + }), + ); + const previousDecision = decisionsResp.records[0]; + const decisionTime = nextAmlDecisionTimestamp( + previousDecision?.decision_time, + ); succeedOrThrow( await exchangeClient.makeAmlDesicion(officerAcc, { - decision_time: TalerProtocolTimestamp.now(), + decision_time: decisionTime, h_payto: merchantPaytoHash, justification: "reset", properties: {}, @@ -1342,10 +1357,6 @@ async function doTriggerMeasure( }, ): Promise<{ currentDecision: AmlDecision }> { const { officerAcc, exchangeClient, merchantPaytoHash } = args; - // AML decision timestamps have second precision. Form programs can create - // a successor decision immediately before the officer triggers the next - // measure, so allow the clock to move to the next representable timestamp. - await waitMs(1100); const decisionsResp = succeedOrThrow( await exchangeClient.getAmlDecisions(officerAcc, { active: true, @@ -1372,17 +1383,7 @@ async function doTriggerMeasure( previousDecisionTime = rec.decision_time; } - let decisionTime = TalerProtocolTimestamp.now(); - if ( - previousDecisionTime?.t_s !== undefined && - previousDecisionTime.t_s !== "never" && - decisionTime.t_s !== "never" && - decisionTime.t_s <= previousDecisionTime.t_s - ) { - decisionTime = TalerProtocolTimestamp.fromSeconds( - previousDecisionTime.t_s + 1, - ); - } + const decisionTime = nextAmlDecisionTimestamp(previousDecisionTime); succeedOrThrow( await exchangeClient.makeAmlDesicion(officerAcc, { diff --git a/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts b/packages/taler-harness/src/integrationtests/test-exchange-management-fault.ts @@ -41,7 +41,7 @@ import { } from "../harness/harness.js"; /** - * Test if the wallet handles outdated exchange versions correctly. + * Test if the wallet rejects malformed and outdated exchange versions. */ export async function runExchangeManagementFaultTest( t: GlobalTestState, @@ -206,8 +206,10 @@ export async function runExchangeManagementFaultTest( // Response is malformed, since it didn't even contain a version code // in a format the wallet can understand. + t.assertTrue(err1.hasErrorCode(TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE)); t.assertTrue( - err1.errorDetail.code === TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE, + err1.errorDetail.innerError?.code === + TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, ); exchangesList = await wallet.client.call( @@ -215,11 +217,9 @@ export async function runExchangeManagementFaultTest( {}, ); console.log("exchanges list", j2s(exchangesList)); - t.assertTrue(exchangesList.exchanges.length === 1); - t.assertTrue( - exchangesList.exchanges[0].lastUpdateErrorInfo?.error.code === - TalerErrorCode.WALLET_RECEIVED_MALFORMED_RESPONSE, - ); + // A failed explicit addition stays ephemeral and is purged when this + // command-line wallet invocation closes. + t.assertTrue(exchangesList.exchanges.length === 0); /* * ========================================================================= @@ -251,16 +251,16 @@ export async function runExchangeManagementFaultTest( ); t.assertTrue(err2.hasErrorCode(TalerErrorCode.WALLET_EXCHANGE_UNAVAILABLE)); + t.assertTrue( + err2.errorDetail.innerError?.code === + TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE, + ); exchangesList = await wallet.client.call( WalletApiOperation.ListExchanges, {}, ); - t.assertTrue(exchangesList.exchanges.length === 1); - t.assertTrue( - exchangesList.exchanges[0].lastUpdateErrorInfo?.error.code === - TalerErrorCode.WALLET_EXCHANGE_PROTOCOL_VERSION_INCOMPATIBLE, - ); + t.assertTrue(exchangesList.exchanges.length === 0); /* * ========================================================================= diff --git a/packages/taler-harness/src/integrationtests/test-kyc-wallet-deposit-abort.ts b/packages/taler-harness/src/integrationtests/test-kyc-wallet-deposit-abort.ts @@ -125,6 +125,7 @@ export async function runKycWalletDepositAbortTest(t: GlobalTestState) { { const bal = await walletClient.call(WalletApiOperation.GetBalances, {}); console.log(`balance in kyc: ${j2s(bal)}`); + t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:14.69"); t.assertAmountEquals(bal.balances[0].pendingOutgoing, "TESTKUDOS:5"); t.assertAmountEquals(bal.balances[0].pendingIncoming, "TESTKUDOS:0"); } @@ -147,7 +148,9 @@ export async function runKycWalletDepositAbortTest(t: GlobalTestState) { const bal = await walletClient.call(WalletApiOperation.GetBalances, {}); console.log(j2s(bal)); - t.assertDeepEqual(bal.balances[0].available, "TESTKUDOS:19.05"); + // The deposit selection and its abort each refresh change for 0.15. The + // initial withdrawal made 19.84 available, so aborting restores 19.54. + t.assertAmountEquals(bal.balances[0].available, "TESTKUDOS:19.54"); } runKycWalletDepositAbortTest.suites = ["wallet", "kyc"]; diff --git a/packages/taler-harness/src/integrationtests/test-payment-order-gone.ts b/packages/taler-harness/src/integrationtests/test-payment-order-gone.ts @@ -171,7 +171,7 @@ export async function runPaymentOrderGoneTest(t: GlobalTestState) { await merchantClient.deleteOrder( merchantAdminAccessToken, order.orderId, - false, + true, ), ); diff --git a/packages/taler-harness/src/integrationtests/test-peer-repair.ts b/packages/taler-harness/src/integrationtests/test-peer-repair.ts @@ -79,8 +79,23 @@ export async function runPeerRepairTest(t: GlobalTestState) { await withdrawalDoneCond; const w1DbPath = w1.walletService.dbPath; const w1DbCopyPath = w1.walletService.dbPath + ".copy"; + + // Stop the wallet before taking the rollback snapshot. Copying only the + // main SQLite file while the wallet is writing in WAL mode can produce a + // torn snapshot that SQLite later reports as a malformed database. + w1.walletClient.remoteWallet?.close(); + await w1.walletService.stop(); fs.copyFileSync(w1DbPath, w1DbCopyPath); + w1 = await createWalletDaemonWithClient(t, { + name: "w1", + persistent: true, + handleNotification(wn) { + allW1Notifications.push(wn); + }, + }); + wallet1 = w1.walletClient; + const purse_expiration = AbsoluteTime.toProtocolTimestamp( AbsoluteTime.addDuration( AbsoluteTime.now(), @@ -147,6 +162,10 @@ export async function runPeerRepairTest(t: GlobalTestState) { w1.walletClient.remoteWallet?.close(); await w1.walletService.stop(); + // Discard WAL state belonging to the post-transfer database before putting + // the older main database back in place. + fs.rmSync(`${w1DbPath}-wal`, { force: true }); + fs.rmSync(`${w1DbPath}-shm`, { force: true }); fs.copyFileSync(w1DbCopyPath, w1DbPath); console.log(`copied back to ${w1DbPath}`); diff --git a/packages/taler-harness/src/integrationtests/test-revocation.ts b/packages/taler-harness/src/integrationtests/test-revocation.ts @@ -114,6 +114,8 @@ async function createTestEnvironment( accountPaytoUri: exchangePaytoUri, }; + await exchange.addBankAccount("1", exchangeBankAccount); + bank.setSuggestedExchange(exchange, exchangePaytoUri); await bank.start(); @@ -202,7 +204,7 @@ async function createTestEnvironment( } /** - * Basic time travel test. + * Test wallet recoup after revoking freshly withdrawn and refreshed coins. */ export async function runRevocationTest(t: GlobalTestState) { // Set up test environment @@ -232,6 +234,7 @@ export async function runRevocationTest(t: GlobalTestState) { // is implemented. await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { exchangeBaseUrl: exchange.baseUrl, + force: true, }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); const bal = await walletClient.call(WalletApiOperation.GetBalances, {}); @@ -274,6 +277,7 @@ export async function runRevocationTest(t: GlobalTestState) { // is implemented. await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { exchangeBaseUrl: exchange.baseUrl, + force: true, }); await walletClient.call(WalletApiOperation.TestingWaitTransactionsFinal, {}); { diff --git a/packages/taler-harness/src/integrationtests/test-tops-aml-null-current-rules.ts b/packages/taler-harness/src/integrationtests/test-tops-aml-null-current-rules.ts @@ -120,9 +120,25 @@ export async function runTopsAmlNullCurrentRulesTest(t: GlobalTestState) { await decideReset(); + const decisionsResp = succeedOrThrow( + await exchangeClient.getAmlDecisions(officerAcc, { active: true }), + ); + t.assertDeepEqual(decisionsResp.records.length, 1); + const previousDecisionTime = decisionsResp.records[0].decision_time; + let decisionTime = TalerProtocolTimestamp.now(); + if ( + previousDecisionTime.t_s !== "never" && + decisionTime.t_s !== "never" && + decisionTime.t_s <= previousDecisionTime.t_s + ) { + decisionTime = TalerProtocolTimestamp.fromSeconds( + previousDecisionTime.t_s + 1, + ); + } + succeedOrThrow( await exchangeClient.makeAmlDesicion(officerAcc, { - decision_time: TalerProtocolTimestamp.now(), + decision_time: decisionTime, h_payto: accountPaytoHash, justification: "expire into an interactive successor measure", keep_investigating: false, @@ -181,5 +197,3 @@ export async function runTopsAmlNullCurrentRulesTest(t: GlobalTestState) { runTopsAmlNullCurrentRulesTest.suites = ["wallet"]; runTopsAmlNullCurrentRulesTest.timeoutMs = 120000; -// The bug this test reproduces is in the exchange, not in the wallet. -runTopsAmlNullCurrentRulesTest.todo = true; diff --git a/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts b/packages/taler-harness/src/integrationtests/test-wallet-wirefees.ts @@ -221,7 +221,9 @@ export async function runWalletWirefeesTest(t: GlobalTestState) { console.log(`amountEffective: ${choice.amountEffective}`); - t.assertAmountEquals(choice.amountEffective, "TESTKUDOS:6.12"); + // 1 for the order, 4.90 of the wire fee above max_fee, 0.01 deposit fee, + // and 0.06 to refresh the change from the selected 8-unit coin. + t.assertAmountEquals(choice.amountEffective, "TESTKUDOS:5.97"); await walletClient.call(WalletApiOperation.ConfirmPay, { transactionId: preparePayResult.transactionId, diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-huge.ts @@ -30,6 +30,7 @@ import { CoinConfig, defaultCoinConfig } from "../harness/denomStructures.js"; import { BankService, ExchangeService, + getTestHarnessPaytoForLabel, GlobalTestState, setupDb, WalletClient, @@ -62,7 +63,7 @@ export async function runWithdrawalHugeTest(t: GlobalTestState) { database: db.connStr, }); - let paytoUri = "payto://x-taler-bank/localhost/exchange" as PaytoString; + const paytoUri = getTestHarnessPaytoForLabel("exchange") as PaytoString; await exchange.addBankAccount("1", { wireGatewayAuth: { @@ -115,6 +116,13 @@ export async function runWithdrawalHugeTest(t: GlobalTestState) { unixPath: walletService.socketPath, }); await wallet.connect(); + await wallet.client.call(WalletApiOperation.InitWallet, { + config: { + testing: { + skipDefaults: true, + }, + }, + }); const withdrawalFinishedCond = wallet.waitForNotificationCond( (wn) => diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts @@ -956,6 +956,10 @@ async function processWithdrawalGroupRedenominate( throw Error("invalid state (no exchange base URL)"); } await fetchFreshExchange(wex, exchangeBaseUrl, { + // Redenomination is entered after the exchange rejected denomination + // keys. A previous update can have completed while the exchange still + // had no replacement keys, so its otherwise-fresh cache is not enough. + forceUpdate: true, noBail: true, }); await redenominateWithdrawal(