taler-typescript-core

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

commit 6af0cb6842298379e7db0c93f000935424390bbf
parent 16765d1477999b3d513e265c1e465a825d1face2
Author: Florian Dold <dold@taler.net>
Date:   Tue, 21 Jul 2026 02:03:29 +0200

util: test the native-request timeout and cancellation helper

Diffstat:
Apackages/taler-util/src/http-native-request.test.ts | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 98 insertions(+), 0 deletions(-)

diff --git a/packages/taler-util/src/http-native-request.test.ts b/packages/taler-util/src/http-native-request.test.ts @@ -0,0 +1,98 @@ +/* + 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/> + */ + +// The qtart HTTP backend cannot be imported under node (it pulls in the +// QuickJS "std" module), so its timeout/cancellation/cleanup logic lives in a +// pure helper here that is exercised with a stubbed native request. + +import { test } from "node:test"; +import assert from "node:assert"; +import * as httpCommon from "./http-common.js"; +import { CancellationToken } from "./CancellationToken.js"; +import { Duration } from "./time.js"; + +// Resolved dynamically so this file compiles before the helper exists. +const awaitNativeRequest = (httpCommon as any).awaitNativeRequest as <T>( + native: { promise: Promise<T>; cancelFn: () => void }, + opts: { timeout?: Duration; cancellationToken?: CancellationToken }, +) => Promise<T>; + +function stubNative<T>(kind: "never" | "resolve" | "reject", value?: any) { + let cancels = 0; + const promise: Promise<T> = + kind === "never" + ? new Promise<T>(() => {}) + : kind === "resolve" + ? Promise.resolve(value) + : Promise.reject(value ?? new Error("native failure")); + return { + native: { promise, cancelFn: () => void cancels++ }, + get cancels() { + return cancels; + }, + }; +} + +test("timeout cancels the native request and throws", async (t) => { + const s = stubNative("never"); + await assert.rejects( + awaitNativeRequest(s.native, { timeout: Duration.fromMilliseconds(20) }), + /timed out/i, + ); + assert.strictEqual(s.cancels, 1); +}); + +test("cancellation cancels the native request and throws", async (t) => { + const s = stubNative("never"); + const src = CancellationToken.create(); + const p = awaitNativeRequest(s.native, { cancellationToken: src.token }); + src.cancel(); + await assert.rejects(p, /cancel/i); + assert.strictEqual(s.cancels, 1); +}); + +test("a successful request returns its value", async (t) => { + const s = stubNative<string>("resolve", "ok"); + const r = await awaitNativeRequest(s.native, { + timeout: Duration.fromMilliseconds(10000), + }); + assert.strictEqual(r, "ok"); +}); + +test("no cancellation-listener leak across failed requests", async (t) => { + const src = CancellationToken.create(); + const callbacks = () => (src.token as any)._callbacks.size as number; + const before = callbacks(); + for (let i = 0; i < 50; i++) { + const s = stubNative("reject"); + await awaitNativeRequest(s.native, { + timeout: Duration.fromMilliseconds(10000), + cancellationToken: src.token, + }).catch(() => undefined); + } + assert.strictEqual(callbacks(), before); +}); + +test("no armed-timer leak: the timeout does not fire after the request settled", async (t) => { + // On a failed request the timer must be cleared; otherwise it fires later and + // cancels an already-finished request. + const s = stubNative("reject"); + await awaitNativeRequest(s.native, { + timeout: Duration.fromMilliseconds(20), + }).catch(() => undefined); + await new Promise((r) => setTimeout(r, 60)); + assert.strictEqual(s.cancels, 0); +});