taler-typescript-core

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

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:
Mpackages/libeufin-bank-webui/src/hooks/session.ts | 6+++++-
Apackages/libeufin-bank-webui/src/utils.test.ts | 107+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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); + }); +});