commit c8a167a0652f5c484af461adca15459370195bcb
parent c99152eefdd38a3c9df90c7be903ec7b5d16616e
Author: Florian Dold <dold@taler.net>
Date: Mon, 24 Aug 2026 02:40:04 +0200
bank web UI: test form helpers and session refresh delays
Diffstat:
2 files changed, 112 insertions(+), 1 deletion(-)
diff --git a/packages/libeufin-bank-webui/src/hooks/session.ts b/packages/libeufin-bank-webui/src/hooks/session.ts
@@ -193,6 +193,10 @@ function cleanAllCache(): void {
mutate(() => true, undefined, { revalidate: false });
}
+export function refreshRetryDelay(timeLeftMs: number, attempt: number): number {
+ return Math.min(1000 * 2 ** attempt, 30_000, timeLeftMs);
+}
+
/**
* Loads the session from local storage
* Sets a timeout before the session expires
@@ -285,7 +289,7 @@ export function useRefreshSessionBeforeExpires() {
sessionRef.current.expired();
return;
}
- const retryDelay = Math.min(1000 * 2 ** attempt, 30_000, left.d_ms);
+ const retryDelay = refreshRetryDelay(left.d_ms, attempt);
timeoutId = setTimeout(
() => void attemptRefresh(attempt + 1),
retryDelay,
diff --git a/packages/libeufin-bank-webui/src/utils.test.ts b/packages/libeufin-bank-webui/src/utils.test.ts
@@ -0,0 +1,107 @@
+/*
+ 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.
+*/
+
+import assert from "node:assert";
+import { describe, it } from "node:test";
+import { codecForSessionState, refreshRetryDelay } from "./hooks/session.js";
+import { doAutoFocus } from "./pages/PaytoWireTransferForm.js";
+import {
+ getIbanFromPayto,
+ undefinedIfEmpty,
+ validateIBAN,
+ validateTalerBank,
+} from "./utils.js";
+
+const i18n = {
+ str(strings: TemplateStringsArray): string {
+ return strings[0];
+ },
+} as any;
+
+describe("bank form utilities", () => {
+ it("extracts the final IBAN path segment", () => {
+ assert.equal(
+ getIbanFromPayto("payto://iban/DE89370400440532013000"),
+ "DE89370400440532013000",
+ );
+ assert.equal(
+ getIbanFromPayto("payto://iban/DE89370400440532013000/"),
+ "DE89370400440532013000",
+ );
+ });
+
+ it("drops objects whose properties are all undefined", () => {
+ assert.equal(
+ undefinedIfEmpty({ one: undefined, two: undefined }),
+ undefined,
+ );
+ assert.deepEqual(undefinedIfEmpty({ one: undefined, two: 2 }), {
+ one: undefined,
+ two: 2,
+ });
+ });
+
+ it("validates bank usernames", () => {
+ assert.equal(validateTalerBank("alice-1.example", i18n), undefined);
+ assert.match(validateTalerBank("alice@example", i18n) ?? "", /letters/);
+ });
+
+ it("ignores an unavailable autofocus target", () => {
+ assert.doesNotThrow(() => doAutoFocus(null));
+ });
+
+ it("accepts valid IBANs and explains malformed ones", () => {
+ assert.equal(validateIBAN("DE89370400440532013000", i18n), undefined);
+ for (const malformed of [
+ "DE1",
+ `DE${"1".repeat(40)}`,
+ "US12345678901234567890",
+ "DE8937040044053201300!",
+ "DE89370400440532013001",
+ ]) {
+ assert.equal(typeof validateIBAN(malformed, i18n), "string");
+ }
+ });
+});
+
+describe("session persistence", () => {
+ it("decodes each persisted session variant", () => {
+ const codec = codecForSessionState();
+ assert.deepEqual(codec.decode({ status: "loggedOut" }), {
+ status: "loggedOut",
+ });
+ assert.equal(
+ codec.decode({
+ status: "expired",
+ username: "alice",
+ isUserAdministrator: false,
+ }).status,
+ "expired",
+ );
+ assert.equal(
+ codec.decode({
+ status: "loggedIn",
+ username: "admin",
+ token: "secret-token",
+ isUserAdministrator: true,
+ }).status,
+ "loggedIn",
+ );
+ });
+
+ it("bounds refresh retries by both the backoff cap and token lifetime", () => {
+ assert.equal(refreshRetryDelay(60_000, 0), 1_000);
+ assert.equal(refreshRetryDelay(60_000, 10), 30_000);
+ assert.equal(refreshRetryDelay(250, 10), 250);
+ });
+});