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