mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-30 17:32:26 +00:00
102 lines
3.6 KiB
JavaScript
102 lines
3.6 KiB
JavaScript
import "../test-helpers/setup-localstorage.mjs";
|
|
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { NostrClient } from "../../js/nostr/client.js";
|
|
|
|
// Mock global.window if missing
|
|
if (typeof globalThis.window === "undefined") {
|
|
globalThis.window = {};
|
|
}
|
|
|
|
function deferred() {
|
|
let resolve;
|
|
const promise = new Promise((done) => { resolve = done; });
|
|
return { promise, resolve };
|
|
}
|
|
|
|
test("NostrClient initialization race condition: concurrent init() calls", async (t) => {
|
|
const client = new NostrClient();
|
|
const relay = "wss://race.example.invalid";
|
|
client.relays = [relay];
|
|
const poolEntered = deferred();
|
|
const releasePool = deferred();
|
|
let poolReady = false;
|
|
let subscription;
|
|
let initPromiseA;
|
|
const subscriptions = [];
|
|
|
|
// Teardown runs even if an assertion fails before init or subscription setup.
|
|
t.after(async () => {
|
|
releasePool.resolve();
|
|
try {
|
|
await initPromiseA;
|
|
} finally {
|
|
subscription?.unsub();
|
|
client.resetRelayReconnectState();
|
|
}
|
|
});
|
|
|
|
// Virtualize the real pool transport boundary; keep init, L1 subscription
|
|
// management and the returned feed subscription's cleanup real.
|
|
const fakePool = {
|
|
sub: (relays, filters) => {
|
|
assert.equal(poolReady, true, "Subscription must use the initialized pool");
|
|
const record = { relays, filters, closed: 0 };
|
|
subscriptions.push(record);
|
|
return {
|
|
on: () => {},
|
|
unsub: () => { record.closed += 1; },
|
|
};
|
|
},
|
|
get: async () => null,
|
|
list: async () => [],
|
|
ensureRelay: async () => {},
|
|
};
|
|
t.mock.method(client.connectionManager, "ensurePool", async () => {
|
|
poolEntered.resolve();
|
|
await releasePool.promise;
|
|
client.connectionManager.pool = fakePool;
|
|
poolReady = true;
|
|
return fakePool;
|
|
});
|
|
// An empty result means connection failure and starts real reconnect timers.
|
|
// Model a successful connection at the manager boundary used by init/retry.
|
|
t.mock.method(client.connectionManager, "connectToRelays", async () => [
|
|
{ url: relay, success: true },
|
|
]);
|
|
t.mock.method(client, "restoreLocalData", async () => true);
|
|
t.mock.method(client, "scheduleStoredRemoteSignerRestore", async () => {});
|
|
|
|
initPromiseA = client.init();
|
|
const initPromiseB = client.init();
|
|
assert.ok(initPromiseB instanceof Promise, "Second init() call should return a Promise, not undefined");
|
|
|
|
await poolEntered.promise;
|
|
let secondInitSettled = false;
|
|
void initPromiseB.then(() => { secondInitSettled = true; });
|
|
await Promise.resolve();
|
|
assert.equal(secondInitSettled, false, "Second caller must wait for pool initialization");
|
|
|
|
releasePool.resolve();
|
|
await initPromiseB;
|
|
assert.equal(poolReady, true, "Pool should be ready after awaiting second init() call");
|
|
await initPromiseA;
|
|
assert.equal(client.connectionManager.relayReconnectTimer, null, "Successful fixture initialization must not schedule relay retries");
|
|
|
|
try {
|
|
subscription = client.subscribeVideos(() => {});
|
|
} catch (e) {
|
|
assert.fail("Should be able to subscribe after awaiting init()");
|
|
}
|
|
assert.equal(subscriptions.length, 1, "Post-init feed subscription must reach the pool transport");
|
|
assert.ok(subscriptions[0].relays.length > 0, "Feed must subscribe to a nonempty relay set");
|
|
assert.ok(subscriptions[0].filters.some((filter) =>
|
|
filter.kinds.includes(30078) && filter["#t"].includes("video")),
|
|
"Feed must request video events");
|
|
|
|
subscription.unsub();
|
|
assert.equal(subscriptions[0].closed, 1, "Feed cleanup must close its transport subscription");
|
|
subscription.unsub();
|
|
assert.equal(subscriptions[0].closed, 1, "Feed cleanup must be idempotent");
|
|
});
|