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:
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(