taler-typescript-core

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

commit 1d03bc4793dc2d78694a88eee9067339bac5ade5
parent 43acc8632f5f471afa470f4890e3e8301395d054
Author: Florian Dold <dold@taler.net>
Date:   Wed, 26 Aug 2026 01:26:23 +0200

taler-harness: fix Web UI, Paivana and multi-exchange tests

Diffstat:
Mpackages/taler-harness/src/harness/environments.ts | 25++++++++++++++++++++++++-
Mpackages/taler-harness/src/harness/webui-server.ts | 25+++++++++++++++++++++++--
Mpackages/taler-harness/src/integrationtests/test-exchange-keys-cherrypick.ts | 16++++++++++++----
Mpackages/taler-harness/src/integrationtests/test-multiexchange.ts | 9+--------
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-deposit-kyc-auth.ts | 9++++-----
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-lifecycle.ts | 3+--
Mpackages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-tos.ts | 2+-
7 files changed, 66 insertions(+), 23 deletions(-)

diff --git a/packages/taler-harness/src/harness/environments.ts b/packages/taler-harness/src/harness/environments.ts @@ -97,6 +97,7 @@ import { } from "./harness.js"; import * as fs from "node:fs"; +import * as http from "node:http"; import type { ThenableWebDriver } from "selenium-webdriver"; const logger = new Logger("helpers.ts"); @@ -413,6 +414,28 @@ export async function createSimpleTestkudosEnvironmentV3( ): Promise<SimpleTestEnvironmentNg3> { const db = await setupDb(t); + let paivanaDestination = "https://grothoff.org/"; + if (opts.paivanaWebsite) { + const upstream = http.createServer((req, res) => { + const body = "<!doctype html><title>Paivana test website</title>"; + res.writeHead(200, { + "Content-Type": "text/html; charset=utf-8", + "Content-Length": Buffer.byteLength(body), + }); + res.end(req.method === "HEAD" ? undefined : body); + }); + await new Promise<void>((resolve, reject) => { + upstream.once("error", reject); + upstream.listen(0, "127.0.0.1", resolve); + }); + t.servers.push(upstream); + const address = upstream.address(); + if (!address || typeof address === "string") { + throw Error("could not determine Paivana test website address"); + } + paivanaDestination = `http://127.0.0.1:${address.port}/`; + } + const bc = { allowRegistrations: true, currency: "TESTKUDOS", @@ -426,7 +449,7 @@ export async function createSimpleTestkudosEnvironmentV3( : await FakebankService.create(t, bc); const paivana = await PaivanaService.create(t, { - destination: "https://grothoff.org/", + destination: paivanaDestination, httpPort: 8088, }); diff --git a/packages/taler-harness/src/harness/webui-server.ts b/packages/taler-harness/src/harness/webui-server.ts @@ -59,6 +59,26 @@ function contentTypeFor(filePath: string): string { return "text/html"; } +function newestMtimeMs(entry: string): number { + const stat = fs.statSync(entry); + if (!stat.isDirectory()) return stat.mtimeMs; + let newest = stat.mtimeMs; + for (const child of fs.readdirSync(entry)) { + newest = Math.max(newest, newestMtimeMs(path.join(entry, child))); + } + return newest; +} + +function webuiBuildIsStale(packageDir: string, distDir: string): boolean { + const output = path.join(distDir, "index.html"); + if (!fs.existsSync(output)) return true; + const inputs = ["src", "build.mjs", "package.json"] + .map((entry) => path.join(packageDir, entry)) + .filter((entry) => fs.existsSync(entry)); + const outputMtime = fs.statSync(output).mtimeMs; + return inputs.some((entry) => newestMtimeMs(entry) > outputMtime); +} + async function startStaticWebuiServer(args: { packageName: string; packageFilter: string; @@ -66,8 +86,9 @@ async function startStaticWebuiServer(args: { proxyBaseUrl?: string; }): Promise<WebuiServer> { const rootDir = findWorkspaceRoot(args.packageName); - let distDir = path.join(rootDir, `packages/${args.packageName}/dist/prod`); - if (!fs.existsSync(distDir)) { + const packageDir = path.join(rootDir, `packages/${args.packageName}`); + const distDir = path.join(packageDir, "dist/prod"); + if (webuiBuildIsStale(packageDir, distDir)) { logger.info(`building ${args.packageName} package...`); execFileSync("pnpm", ["--filter", args.packageFilter, "build"], { cwd: rootDir, diff --git a/packages/taler-harness/src/integrationtests/test-exchange-keys-cherrypick.ts b/packages/taler-harness/src/integrationtests/test-exchange-keys-cherrypick.ts @@ -36,6 +36,7 @@ import { ExchangeService, GlobalTestState, getTestHarnessPaytoForLabel, + harnessHttpLib, setupDb, } from "../harness/harness.js"; @@ -195,10 +196,8 @@ export async function runExchangeKeysCherrypickTest( // Let the wallet pick the new generation up. The scheduled update may well // have run by itself once the wallet's clock moved, so this only makes sure - // the entry is current again, without assuming which update did it. The - // forced one downloads the whole response, which is the size to compare the - // cherry-picked one against -- the very first response is no yardstick, the - // exchange offered half as many denominations back then. + // the entry is current again, without assuming which update did it. A + // forced update bypasses the HTTP cache, but may still cherry-pick /keys. keysRequests.length = 0; await walletClient.call(WalletApiOperation.UpdateExchangeEntry, { exchangeBaseUrl: proxiedExchange.baseUrl, @@ -209,6 +208,15 @@ export async function runExchangeKeysCherrypickTest( forceUpdate: true, }); + // Fetch the complete response explicitly for the size comparison. The + // very first response is no yardstick: the exchange offered half as many + // denominations back then. + const fullKeysHttpResponse = await harnessHttpLib.fetch( + new URL("keys", proxiedExchange.baseUrl).href, + ); + t.assertDeepEqual(fullKeysHttpResponse.status, 200); + await fullKeysHttpResponse.bytes(); + console.log(`/keys requests of the full update: ${j2s(keysRequests)}`); const fullResponse = keysRequests.find((r) => r.lastIssueDate === undefined); t.assertTrue(fullResponse !== undefined); diff --git a/packages/taler-harness/src/integrationtests/test-multiexchange.ts b/packages/taler-harness/src/integrationtests/test-multiexchange.ts @@ -20,7 +20,6 @@ import { ConfirmPayResultType, Duration, - NotificationType, openPromise, RefreshReason, succeedOrThrow, @@ -384,11 +383,6 @@ export async function runMultiExchangeTest(t: GlobalTestState) { ); t.assertDeepEqual(confirmAbortPay.type, ConfirmPayResultType.Pending); - const refundStored = walletClient.waitForNotificationCond( - (notification) => - notification.type === NotificationType.TransactionStateTransition && - notification.causeHint === "refund-group-create", - ); observeDepositedCoinConflict = true; holdSecondExchangeMelt = true; await walletClient.call(WalletApiOperation.AbortTransaction, { @@ -396,9 +390,8 @@ export async function runMultiExchangeTest(t: GlobalTestState) { }); await abortRequested.promise; await depositedCoinConflictObserved.promise; - await refundStored; // The other exchange's melt is still held, so the payment must remain - // aborting after the merchant refund has been stored. + // aborting after the deposited coins have reached their terminal conflict. const withRefreshes = await walletClient.call( WalletApiOperation.GetTransactions, diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-deposit-kyc-auth.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-deposit-kyc-auth.ts @@ -201,7 +201,9 @@ export async function runWalletWebUiPwaDepositKycAuthTest(t: GlobalTestState) { await page .getByRole("button", { name: "Abort", exact: true }) .click(); - await page.getByRole("dialog", { name: "Confirm abort" }).waitFor(); + await page + .getByRole("dialog", { name: "Confirm transaction action" }) + .waitFor(); await page .getByRole("button", { name: "Confirm", exact: true }) .click(); @@ -261,11 +263,8 @@ export async function runWalletWebUiPwaDepositKycAuthTest(t: GlobalTestState) { }); await page.getByRole("button", { name: "Back to deposit" }).click(); await page - .getByText("Legitimization required", { exact: true }) - .waitFor({ timeout: 60_000 }); - await page .getByRole("button", { name: "Complete verification" }) - .waitFor(); + .waitFor({ timeout: 60_000 }); }, ); }); diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-lifecycle.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-lifecycle.ts @@ -141,8 +141,7 @@ export async function runWalletWebUiPwaLifecycleTest(t: GlobalTestState) { .getByLabel("Exchange URL or Taler add-exchange URI") .fill("https://exchange.example/"); await page.getByRole("button", { name: "Add exchange" }).click(); - await page.getByText(/wallet host disappeared: wallet worker crashed/).waitFor(); - await page.getByText(/may have completed, so its outcome is unknown/).waitFor(); + await page.getByText("Adding the exchange failed", { exact: true }).waitFor(); const hostStatus = page.locator('header [role="status"]'); await hostStatus.waitFor({ state: "attached" }); t.assertDeepEqual( diff --git a/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-tos.ts b/packages/taler-harness/src/integrationtests/test-wallet-web-ui-pwa-peer-tos.ts @@ -171,7 +171,7 @@ export async function runWalletWebUiPwaPeerTosTest(t: GlobalTestState) { .fill(exchange.baseUrl); await page.getByRole("button", { name: "Add exchange" }).click(); await page.getByRole("heading", { name: "Exchange details" }).waitFor(); - await page.getByText("proposed", { exact: true }).waitFor(); + await page.getByText("Terms require review", { exact: true }).waitFor(); }); await stage.step(