taler-typescript-core

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

commit e5039a31a44caeff82cc51d62002b34aec48339c
parent 575e42a985efd857cac0ca824481b504b6cfbe34
Author: Florian Dold <dold@taler.net>
Date:   Tue,  3 Nov 2026 23:44:16 +0100

taler-harness: cover ToS acceptance across exchange refreshes

Exercise repeated withdrawals and different document formats and
languages without accepting unchanged terms again. Check that a new
legal version requires acceptance and returns consistent version tags.

Issue: https://bugs.taler.net/n/11816

Diffstat:
Apackages/taler-harness/src/integrationtests/test-wallet-tos-acceptance.ts | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpackages/taler-harness/src/integrationtests/testrunner.ts | 2++
2 files changed, 169 insertions(+), 0 deletions(-)

diff --git a/packages/taler-harness/src/integrationtests/test-wallet-tos-acceptance.ts b/packages/taler-harness/src/integrationtests/test-wallet-tos-acceptance.ts @@ -0,0 +1,167 @@ +/* + This file is part of GNU Taler + (C) 2026 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/> + */ + +import { ExchangeTosStatus } from "@gnu-taler/taler-util"; +import { WalletApiOperation } from "@gnu-taler/taler-wallet-core"; +import * as fs from "node:fs"; +import { + createSimpleTestkudosEnvironmentV3, + withdrawViaBankV4, +} from "../harness/environments.js"; +import { GlobalTestState, harnessHttpLib } from "../harness/harness.js"; + +/** + * Regression for https://bugs.taler.net/n/11816: reading the terms used to + * store the document ETag, while exchange updates stored the legal version. + * Refreshing therefore revoked acceptance of unchanged terms. Reopening them + * could return proposed status with equal accepted/current tags, leaving iOS + * asking for acceptance while hiding its Accept button. + */ +export async function runWalletTosAcceptanceTest(t: GlobalTestState) { + const tosDir = `${t.testDir}/tos`; + const variants = [ + { language: "en", format: "text/plain", extension: "txt" }, + { language: "de", format: "text/plain", extension: "txt" }, + { language: "de", format: "text/markdown", extension: "md" }, + ]; + for (const version of ["v1", "v2"]) { + for (const variant of variants) { + const dir = `${tosDir}/${variant.language}`; + fs.mkdirSync(dir, { recursive: true }); + fs.writeFileSync( + `${dir}/${version}.${variant.extension}`, + `Terms ${version}: ${variant.language}, ${variant.format}`, + ); + } + } + + const { walletClient, exchange, bank } = + await createSimpleTestkudosEnvironmentV3(t, undefined, { + additionalExchangeConfig: (ex) => { + ex.changeConfig((config) => { + config.setString("exchange", "terms_etag", "v1"); + config.setString("exchange", "terms_dir", tosDir); + }); + }, + }); + const exchangeArgs = { exchangeBaseUrl: exchange.baseUrl }; + const getTerms = (variant = variants[0]) => + walletClient.call(WalletApiOperation.GetExchangeTos, { + ...exchangeArgs, + acceptedFormat: [variant.format], + acceptLanguage: variant.language, + }); + const refresh = async () => { + await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { + ...exchangeArgs, + force: true, + }); + await walletClient.call(WalletApiOperation.TestingWaitExchangeReady, { + ...exchangeArgs, + forceUpdate: true, + }); + }; + const assertStatus = async (expected: ExchangeTosStatus) => { + const entry = await walletClient.call( + WalletApiOperation.GetExchangeEntryByUrl, + exchangeArgs, + ); + t.assertDeepEqual(entry.tosStatus, expected); + }; + + const initial = await getTerms(); + t.assertDeepEqual(initial.tosStatus, ExchangeTosStatus.Proposed); + await walletClient.call( + WalletApiOperation.SetExchangeTosAccepted, + exchangeArgs, + ); + await assertStatus(ExchangeTosStatus.Accepted); + const firstWithdrawal = await withdrawViaBankV4(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:10", + }); + await firstWithdrawal.withdrawalFinishedCond; + + // Check the exchange entry BEFORE opening the document again: downloading + // it used to overwrite the metadata version and temporarily mask the bug. + await refresh(); + await assertStatus(ExchangeTosStatus.Accepted); + const documentEtags = new Set<string>(); + for (const variant of variants) { + const response = await harnessHttpLib.fetch( + new URL("terms", exchange.baseUrl).href, + { + headers: { + Accept: variant.format, + "Accept-Language": variant.language, + }, + }, + ); + t.assertDeepEqual(response.status, 200); + t.assertDeepEqual(response.headers.get("taler-terms-version"), "v1"); + const etag = response.headers.get("etag"); + t.assertTrue(etag != null); + documentEtags.add(etag); + + const terms = await getTerms(variant); + t.assertDeepEqual( + terms.content, + `Terms v1: ${variant.language}, ${variant.format}`, + ); + t.assertDeepEqual(terms.tosStatus, ExchangeTosStatus.Accepted); + t.assertDeepEqual(terms.currentEtag, "v1"); + t.assertDeepEqual(terms.acceptedEtag, terms.currentEtag); + await refresh(); + await assertStatus(ExchangeTosStatus.Accepted); + } + t.assertDeepEqual(documentEtags.size, variants.length); + + // A second withdrawal must work without another acceptance call. + const secondWithdrawal = await withdrawViaBankV4(t, { + walletClient, + bank, + exchange, + amount: "TESTKUDOS:10", + }); + await secondWithdrawal.withdrawalFinishedCond; + + // A new legal version must still require explicit acceptance. + await exchange.stop(); + exchange.changeConfig((config) => { + config.setString("exchange", "terms_etag", "v2"); + }); + await exchange.start(); + await refresh(); + await assertStatus(ExchangeTosStatus.Proposed); + const changed = await getTerms(); + t.assertDeepEqual(changed.tosStatus, ExchangeTosStatus.Proposed); + t.assertDeepEqual(changed.acceptedEtag, "v1"); + t.assertDeepEqual(changed.currentEtag, "v2"); + await walletClient.call( + WalletApiOperation.SetExchangeTosAccepted, + exchangeArgs, + ); + await refresh(); + await assertStatus(ExchangeTosStatus.Accepted); + const accepted = await getTerms(); + t.assertDeepEqual(accepted.tosStatus, ExchangeTosStatus.Accepted); + t.assertDeepEqual(accepted.acceptedEtag, "v2"); + t.assertDeepEqual(accepted.currentEtag, "v2"); +} + +runWalletTosAcceptanceTest.suites = ["wallet"]; diff --git a/packages/taler-harness/src/integrationtests/testrunner.ts b/packages/taler-harness/src/integrationtests/testrunner.ts @@ -230,6 +230,7 @@ import { runWalletGenDbTest } from "./test-wallet-gendb.js"; import { runWalletInsufficientBalanceTest } from "./test-wallet-insufficient-balance.js"; import { runWalletMailboxBasicTest } from "./test-wallet-mailbox-basic.js"; import { runWalletMissingTosTest } from "./test-wallet-missing-tos.js"; +import { runWalletTosAcceptanceTest } from "./test-wallet-tos-acceptance.js"; import { runWalletNetworkAvailabilityTest } from "./test-wallet-network-availability.js"; import { runWalletNotificationsTest } from "./test-wallet-notifications.js"; import { runWalletRefreshErrorsTest } from "./test-wallet-refresh-errors.js"; @@ -425,6 +426,7 @@ const allTests: TestMainFunction[] = [ runWithdrawalConversionTest, runWithdrawalHugeTest, runTermOfServiceFormatTest, + runWalletTosAcceptanceTest, runPaymentExpiredTest, runWalletGenDbTest, runLibeufinBankTest,