commit beb6e25bf87b4e159497a13e3feae5c69ab59871
parent e6b44344a99e9d985a9587c11d71a59bcb8187d3
Author: Florian Dold <florian@dold.me>
Date: Fri, 11 Jul 2025 00:37:46 +0200
harness,wallet: make tests pass under libeufin
In most cases, we simply didn't create accounts required for the test.
The taler-fakebank doesn't require this step and makes up accounts on
the spot.
But in withdrawal-bank-integrated, the bug was an idempotency issue in
wallet-core that only showed up when run under libeufin (likely timing
sensitive).
Diffstat:
8 files changed, 177 insertions(+), 119 deletions(-)
diff --git a/packages/taler-harness/src/harness/harness.ts b/packages/taler-harness/src/harness/harness.ts
@@ -783,6 +783,15 @@ export class FakebankService
return new FakebankService(gc, bc, cfgFilename);
}
+ changeConfig(f: (config: Configuration) => void) {
+ const config = Configuration.load(
+ this.configFile,
+ ConfigSources["taler-exchange"],
+ );
+ f(config);
+ config.writeTo(this.configFile, { excludeDefaults: true });
+ }
+
setSuggestedExchange(e: ExchangeServiceInterface, exchangePayto: string) {
if (!!this.proc) {
throw Error("Can't set suggested exchange while bank is running.");
@@ -922,12 +931,7 @@ export class LibeufinBankService
config.setString(
"libeufin-bank",
"default_debt_limit",
- bc.maxDebt ?? `${bc.currency}:100`,
- );
- config.setString(
- "libeufin-bank",
- "DEFAULT_DEBT_LIMIT",
- `${bc.currency}:100`,
+ bc.maxDebt ?? `${bc.currency}:999999`,
);
config.setString(
"libeufin-bank",
@@ -971,6 +975,15 @@ export class LibeufinBankService
return new FakebankService(gc, bc, cfgFilename);
}
+ changeConfig(f: (config: Configuration) => void) {
+ const config = Configuration.load(
+ this.configFile,
+ ConfigSources["libeufin-bank"],
+ );
+ f(config);
+ config.writeTo(this.configFile, { excludeDefaults: true });
+ }
+
setSuggestedExchange(e: ExchangeServiceInterface) {
if (!!this.proc) {
throw Error("Can't set suggested exchange while bank is running.");
@@ -1074,6 +1087,8 @@ export interface BankServiceHandle {
pingUntilAvailable(): Promise<void>;
stop(): Promise<void>;
getAdminAuth(): { username: string; password: string };
+
+ changeConfig(f: (config: Configuration) => void): void;
}
export type BankService = BankServiceHandle;
@@ -2670,7 +2685,7 @@ export async function doMerchantKycAuth(
t: GlobalTestState,
req: {
exchangeBankAccount: HarnessExchangeBankAccount;
- wireGatewayAuth: TalerWireGatewayAuth;
+ bankAdminAuth: { username: string; password: string };
merchant: MerchantServiceInterface;
bankClient: TalerCorebankApiClient;
},
@@ -2727,7 +2742,7 @@ export async function doMerchantKycAuth(
});
succeedOrThrow(
await wireGatewayApiClient.addKycAuth({
- auth: req.wireGatewayAuth,
+ auth: req.bankAdminAuth,
body: {
amount: "TESTKUDOS:0.1",
debit_account: kycRespOne.kyc_data[0].payto_uri,
diff --git a/packages/taler-harness/src/harness/tops.ts b/packages/taler-harness/src/harness/tops.ts
@@ -24,6 +24,7 @@ import {
decodeCrock,
Duration,
encodeCrock,
+ getRandomBytes,
hashNormalizedPaytoUri,
j2s,
KycRule,
@@ -688,20 +689,39 @@ export async function createTopsEnvironment(
await merchant.start();
+ const merchantAdminPayto = getTestHarnessPaytoForLabel("merchant-default");
+
await merchant.addInstanceWithWireAccount({
id: "admin",
name: "Default Instance",
- paytoUris: [getTestHarnessPaytoForLabel("merchant-default")],
+ paytoUris: [merchantAdminPayto],
defaultWireTransferDelay: TalerProtocolDuration.fromSpec({ minutes: 1 }),
});
+ await bankClient.registerAccountExtended({
+ name: "merchant-default",
+ password: encodeCrock(getRandomBytes(32)),
+ username: "merchant-default",
+ payto_uri: merchantAdminPayto,
+ });
+
+ const merchantInstId = "minst1";
+ const merchantInstPaytoUri = getTestHarnessPaytoForLabel(merchantInstId);
+
await merchant.addInstanceWithWireAccount({
- id: "minst1",
- name: "minst1",
- paytoUris: [getTestHarnessPaytoForLabel("minst1")],
+ id: merchantInstId,
+ name: merchantInstId,
+ paytoUris: [merchantInstPaytoUri],
defaultWireTransferDelay: TalerProtocolDuration.fromSpec({ minutes: 1 }),
});
+ await bankClient.registerAccountExtended({
+ name: merchantInstId,
+ password: encodeCrock(getRandomBytes(32)),
+ username: merchantInstId,
+ payto_uri: merchantInstPaytoUri,
+ });
+
const exchangeBankAccount: HarnessExchangeBankAccount = {
wireGatewayAuth: {
username: exchangeBankUsername,
diff --git a/packages/taler-harness/src/integrationtests/test-exchange-kyc-auth.ts b/packages/taler-harness/src/integrationtests/test-exchange-kyc-auth.ts
@@ -21,6 +21,7 @@ import {
Configuration,
createEddsaKeyPair,
encodeCrock,
+ getRandomBytes,
hashNormalizedPaytoUri,
HttpStatusCode,
j2s,
@@ -111,6 +112,13 @@ export async function runExchangeKycAuthTest(t: GlobalTestState) {
const merchantPayto = getTestHarnessPaytoForLabel("merchant-default");
+ await bankClient.registerAccountExtended({
+ name: "merchant-default",
+ password: encodeCrock(getRandomBytes(32)),
+ username: "merchant-default",
+ payto_uri: merchantPayto,
+ });
+
const cryptoApi = createSyncCryptoApi();
const wireGatewayApiClient = new TalerWireGatewayHttpClient(
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-decisions.ts b/packages/taler-harness/src/integrationtests/test-kyc-decisions.ts
@@ -24,6 +24,7 @@ import {
Configuration,
Duration,
encodeCrock,
+ getRandomBytes,
hashNormalizedPaytoUri,
j2s,
LimitOperationType,
@@ -84,6 +85,13 @@ export async function runKycDecisionsTest(t: GlobalTestState) {
const merchantPayto = getTestHarnessPaytoForLabel("merchant-default");
+ await bankClient.registerAccountExtended({
+ name: "merchant-default",
+ password: encodeCrock(getRandomBytes(32)),
+ username: "merchant-default",
+ payto_uri: merchantPayto,
+ });
+
const cryptoApi = createSyncCryptoApi();
const wireGatewayApiClient = new TalerWireGatewayHttpClient(
diff --git a/packages/taler-harness/src/integrationtests/test-kyc-merchant-aggregate.ts b/packages/taler-harness/src/integrationtests/test-kyc-merchant-aggregate.ts
@@ -58,7 +58,7 @@ export async function runKycMerchantAggregateTest(t: GlobalTestState) {
walletClient,
bankClient,
exchange,
- amlKeypair,
+ bank,
exchangeBankAccount,
} = await createKycTestkudosEnvironment(t, { adjustExchangeConfig });
@@ -77,9 +77,10 @@ export async function runKycMerchantAggregateTest(t: GlobalTestState) {
bankClient,
exchangeBankAccount,
merchant,
- wireGatewayAuth: exchangeBankAccount.wireGatewayAuth,
+ bankAdminAuth: bank.getAdminAuth(),
});
+
await makeTestPaymentV2(t, {
merchant,
walletClient,
diff --git a/packages/taler-harness/src/integrationtests/test-peer-push-large.ts b/packages/taler-harness/src/integrationtests/test-peer-push-large.ts
@@ -69,7 +69,11 @@ export async function runPeerPushLargeTest(t: GlobalTestState) {
{ walletClient: wallet1, bankClient, exchange },
{ walletClient: wallet2 },
] = await Promise.all([
- createSimpleTestkudosEnvironmentV3(t, coinConfigList),
+ createSimpleTestkudosEnvironmentV3(t, coinConfigList, {
+ additionalBankConfig(b) {
+
+ },
+ }),
createWalletDaemonWithClient(t, {
name: "w2"
})
diff --git a/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts b/packages/taler-harness/src/integrationtests/test-withdrawal-bank-integrated.ts
@@ -27,7 +27,10 @@ import {
j2s,
} from "@gnu-taler/taler-util";
import { WalletApiOperation } from "@gnu-taler/taler-wallet-core";
-import { createSimpleTestkudosEnvironmentV3, createWalletDaemonWithClient } from "../harness/environments.js";
+import {
+ createSimpleTestkudosEnvironmentV3,
+ createWalletDaemonWithClient,
+} from "../harness/environments.js";
import { GlobalTestState } from "../harness/harness.js";
/**
@@ -42,7 +45,7 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
createSimpleTestkudosEnvironmentV3(t),
createWalletDaemonWithClient(t, {
name: "w2",
- })
+ }),
]);
const user = await bankClient.createRandomBankUser();
bankClient.setAuth(user);
@@ -55,12 +58,9 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
t.logStep("Hand it to the wallet");
- await wallet1.call(
- WalletApiOperation.GetWithdrawalDetailsForUri,
- {
- talerWithdrawUri: withdrawal.taler_withdraw_uri,
- },
- );
+ await wallet1.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: withdrawal.taler_withdraw_uri,
+ });
t.logStep("Withdraw");
@@ -184,48 +184,34 @@ export async function runWithdrawalBankIntegratedTest(t: GlobalTestState) {
t.logStep("Check rescan idempotent");
{
- await wallet1.call(
- WalletApiOperation.GetWithdrawalDetailsForUri,
- {
- talerWithdrawUri: withdrawal.taler_withdraw_uri,
- },
- );
- // FIXME #9683 wallet should already know withdrawal already completed by this wallet
-
- const e = await t.assertThrowsTalerErrorAsyncLegacy(
- wallet1.call(
- WalletApiOperation.AcceptBankIntegratedWithdrawal,
- {
- exchangeBaseUrl: exchange.baseUrl,
- talerWithdrawUri: withdrawal.taler_withdraw_uri,
- },
- )
- );
- t.assertTrue(e.errorDetail.code === TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED)
- // FIXME #9683 We should not fail here
+ await wallet1.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: withdrawal.taler_withdraw_uri,
+ });
+
+ await wallet1.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: withdrawal.taler_withdraw_uri,
+ });
}
t.logStep("Check other wallet scan after completion");
{
- await wallet2.call(
- WalletApiOperation.GetWithdrawalDetailsForUri,
- {
- talerWithdrawUri: withdrawal.taler_withdraw_uri,
- },
- );
+ await wallet2.call(WalletApiOperation.GetWithdrawalDetailsForUri, {
+ talerWithdrawUri: withdrawal.taler_withdraw_uri,
+ });
// FIXME #9683 wallet should already know withdrawal already completed by wallet1
- const e = await t.assertThrowsTalerErrorAsyncLegacy(
- wallet2.call(
- WalletApiOperation.AcceptBankIntegratedWithdrawal,
- {
- exchangeBaseUrl: exchange.baseUrl,
- talerWithdrawUri: withdrawal.taler_withdraw_uri,
- },
- )
- )
- t.assertTrue(e.errorDetail.code === TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK)
+ const e = await t.assertThrowsTalerErrorAsync(async () => {
+ await wallet2.call(WalletApiOperation.AcceptBankIntegratedWithdrawal, {
+ exchangeBaseUrl: exchange.baseUrl,
+ talerWithdrawUri: withdrawal.taler_withdraw_uri,
+ });
+ });
+ t.assertTrue(
+ e.errorDetail.code ===
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ );
// FIXME #9683 can we have a more proper error code than this ?
}
diff --git a/packages/taler-wallet-core/src/withdraw.ts b/packages/taler-wallet-core/src/withdraw.ts
@@ -3708,6 +3708,19 @@ export async function prepareBankIntegratedWithdrawal(
};
}
+ switch (info.status) {
+ case "aborted":
+ case "selected":
+ case "confirmed":
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ `withdrawal is in status ${info.status}, unable to proceed`,
+ );
+ default:
+ break;
+ }
+
/**
* Withdrawal group without exchange and amount
* this is an special case when the user haven't yet
@@ -3932,76 +3945,79 @@ export async function confirmWithdrawal(
);
}
- let pending = false;
- await ctx.transition({}, async (rec) => {
- if (!rec) {
- return TransitionResult.stay();
- }
- switch (rec.status) {
- case WithdrawalGroupStatus.PendingWaitConfirmBank: {
- pending = true;
+ await ctx.transition(
+ {
+ extraStores: ["exchanges"],
+ },
+ async (rec, tx) => {
+ if (!rec) {
return TransitionResult.stay();
}
- case WithdrawalGroupStatus.AbortedOtherWallet: {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
- {},
- );
- }
- case WithdrawalGroupStatus.DialogProposed: {
- rec.exchangeBaseUrl = exchange.exchangeBaseUrl;
- rec.instructedAmount = req.amount;
- rec.restrictAge = req.restrictAge;
- if (initialDenoms != null) {
- rec.denomsSel = initialDenoms;
- rec.rawWithdrawalAmount = initialDenoms.totalWithdrawCost;
- rec.effectiveWithdrawalAmount = initialDenoms.totalCoinValue;
- } else {
- rec.denomsSel = undefined;
- rec.rawWithdrawalAmount = Amounts.stringify(
- Amounts.zeroOfCurrency(instructedCurrency),
+ switch (rec.status) {
+ case WithdrawalGroupStatus.Done:
+ case WithdrawalGroupStatus.PendingReady:
+ case WithdrawalGroupStatus.PendingRegisteringBank:
+ case WithdrawalGroupStatus.PendingWaitConfirmBank: {
+ // Be idempotent.
+ return TransitionResult.stay();
+ }
+ case WithdrawalGroupStatus.AbortedOtherWallet: {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_WITHDRAWAL_OPERATION_ABORTED_BY_BANK,
+ {},
+ );
+ }
+ case WithdrawalGroupStatus.DialogProposed: {
+ rec.exchangeBaseUrl = exchange.exchangeBaseUrl;
+ rec.instructedAmount = req.amount;
+ rec.restrictAge = req.restrictAge;
+ if (initialDenoms != null) {
+ rec.denomsSel = initialDenoms;
+ rec.rawWithdrawalAmount = initialDenoms.totalWithdrawCost;
+ rec.effectiveWithdrawalAmount = initialDenoms.totalCoinValue;
+ } else {
+ rec.denomsSel = undefined;
+ rec.rawWithdrawalAmount = Amounts.stringify(
+ Amounts.zeroOfCurrency(instructedCurrency),
+ );
+ rec.effectiveWithdrawalAmount = Amounts.stringify(
+ Amounts.zeroOfCurrency(instructedCurrency),
+ );
+ }
+ checkDbInvariant(
+ rec.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated,
+ "withdrawal type mismatch",
);
- rec.effectiveWithdrawalAmount = Amounts.stringify(
- Amounts.zeroOfCurrency(instructedCurrency),
+ rec.wgInfo.exchangeCreditAccounts = withdrawalAccountList;
+ rec.wgInfo.bankInfo.exchangePaytoUri = exchangePaytoUri;
+ rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
+
+ await internalPerformExchangeWasUsed(
+ wex,
+ tx,
+ exchange.exchangeBaseUrl,
+ withdrawalGroup,
+ );
+
+ return TransitionResult.transition(rec);
+ }
+
+ default: {
+ throw TalerError.fromDetail(
+ TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED,
+ {
+ message: `unable to confirm withdrawal in current state`,
+ txState: computeWithdrawalTransactionStatus(rec),
+ debugStateNum: rec.status,
+ },
);
}
- checkDbInvariant(
- rec.wgInfo.withdrawalType === WithdrawalRecordType.BankIntegrated,
- "withdrawal type mismatch",
- );
- rec.wgInfo.exchangeCreditAccounts = withdrawalAccountList;
- rec.wgInfo.bankInfo.exchangePaytoUri = exchangePaytoUri;
- rec.status = WithdrawalGroupStatus.PendingRegisteringBank;
- pending = true;
- return TransitionResult.transition(rec);
- }
- default: {
- throw TalerError.fromDetail(
- TalerErrorCode.WALLET_REQUEST_TRANSACTION_STATE_UNSUPPORTED,
- {
- message: `unable to confirm withdrawal in current state`,
- txState: computeWithdrawalTransactionStatus(rec),
- debugStateNum: rec.status,
- },
- );
}
- }
- });
+ },
+ );
await wex.taskScheduler.resetTaskRetries(ctx.taskId);
- // FIXME: Merge with transaction above!
- await wex.db.runReadWriteTx(
- { storeNames: ["exchanges"] },
- async (tx) =>
- await internalPerformExchangeWasUsed(
- wex,
- tx,
- exchange.exchangeBaseUrl,
- withdrawalGroup,
- ),
- );
-
return {
transactionId: req.transactionId as TransactionIdStr,
confirmTransferUrl: withdrawalGroup.wgInfo.bankInfo.confirmUrl,