commit 9d4254f9708c83bc4771ff7ac0690295a0fa760f
parent 09a3d09ee6c718006176e78901d6c3291f105ebc
Author: Florian Dold <dold@taler.net>
Date: Thu, 3 Sep 2026 15:32:33 +0200
db: test that a dead helper and an aborted transaction fail
Diffstat:
2 files changed, 58 insertions(+), 1 deletion(-)
diff --git a/packages/idb-bridge/src/node-helper-sqlite3-impl.test.ts b/packages/idb-bridge/src/node-helper-sqlite3-impl.test.ts
@@ -112,3 +112,33 @@ test("sqlite3 helper", async (t) => {
await assert.rejects(db.prepare("SELECT 1"), /database is closed/);
await impl.shutdown();
});
+
+// The helper serves one connection at a time and exits on a second open;
+// callers must see that as an error rather than wait forever.
+test("sqlite3 helper exit rejects pending and later requests", async (t) => {
+ const impl = await createNodeHelperSqlite3Impl();
+ t.after(async () => await impl.shutdown().catch(() => {}));
+ const db = await impl.open(":memory:");
+ const withTimeout = <T>(p: Promise<T>): Promise<T | "timeout"> =>
+ Promise.race([
+ p,
+ new Promise<"timeout">((resolve) =>
+ setTimeout(() => resolve("timeout"), 5000).unref(),
+ ),
+ ]);
+ await assert.rejects(
+ withTimeout(impl.open(":memory:")).then((r) => {
+ if (r === "timeout") throw Error("request hung");
+ return r;
+ }),
+ (err: unknown) => {
+ assert(err instanceof Error);
+ assert.doesNotMatch(err.message, /hung/);
+ return true;
+ },
+ );
+ await assert.rejects(withTimeout(db.exec("SELECT 1")).then((r) => {
+ if (r === "timeout") throw Error("request hung");
+ return r;
+ }), /helper/);
+});
diff --git a/packages/taler-wallet-core/src/db/indexeddb/database.test.ts b/packages/taler-wallet-core/src/db/indexeddb/database.test.ts
@@ -14,7 +14,11 @@
GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/>
*/
-import { BridgeIDBFactory, createSqliteBackend } from "@gnu-taler/idb-bridge";
+import {
+ BridgeIDBFactory,
+ createSqliteBackend,
+ IDBDatabase,
+} from "@gnu-taler/idb-bridge";
import { createNodeHelperSqlite3Impl } from "@gnu-taler/idb-bridge/node-helper-sqlite3-impl";
import assert from "node:assert";
import { test } from "node:test";
@@ -23,6 +27,7 @@ import {
abortTalerDatabaseReplacement,
beginTalerDatabaseReplacement,
openTalerDatabase,
+ promiseFromTransaction,
publishTalerDatabaseReplacement,
} from "./database.js";
import { exportSingleDb } from "./dump.js";
@@ -271,3 +276,25 @@ test("export closes its database connection", async () => {
);
});
});
+
+test("an aborted transaction without pending requests settles its promise", async () => {
+ const handle = await makeIdbRunner();
+ try {
+ const db = (handle as any).idbHandle as IDBDatabase;
+ const tx = db.transaction(["config"], "readwrite");
+ const prom = promiseFromTransaction(tx);
+ tx.abort();
+ const outcome = await Promise.race([
+ prom.then(
+ () => "resolved",
+ () => "rejected",
+ ),
+ new Promise<string>((resolve) =>
+ setTimeout(() => resolve("hung"), 3000).unref(),
+ ),
+ ]);
+ assert.strictEqual(outcome, "rejected");
+ } finally {
+ await handle.close();
+ }
+});