mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-02 10:52:27 +00:00
Merge pull request #1461 from PR0M3TH3AN/ai/tests-core-coverage-20250302-14976230523453771489
Add unit tests for core modules
This commit is contained in:
4 files changed
+475
-234
No files matched your search
@@ -0,0 +1,123 @@
|
||||
// Run with: node scripts/run-targeted-tests.mjs tests/nostr/sign-request-queue.test.mjs
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { queueSignEvent } from "../../js/nostr/signRequestQueue.js";
|
||||
|
||||
// Helper to delay execution
|
||||
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
test("queueSignEvent: processes requests sequentially", async () => {
|
||||
const executionOrder = [];
|
||||
const signer = {
|
||||
signEvent: async (event) => {
|
||||
executionOrder.push(`start:${event.id}`);
|
||||
await delay(50);
|
||||
executionOrder.push(`end:${event.id}`);
|
||||
return { ...event, sig: "signature" };
|
||||
},
|
||||
};
|
||||
|
||||
const event1 = { id: "1", pubkey: "abc" };
|
||||
const event2 = { id: "2", pubkey: "abc" };
|
||||
|
||||
const p1 = queueSignEvent(signer, event1);
|
||||
const p2 = queueSignEvent(signer, event2);
|
||||
|
||||
await Promise.all([p1, p2]);
|
||||
|
||||
assert.deepEqual(executionOrder, [
|
||||
"start:1",
|
||||
"end:1",
|
||||
"start:2",
|
||||
"end:2",
|
||||
]);
|
||||
});
|
||||
|
||||
test("queueSignEvent: handles timeouts", async () => {
|
||||
const signer = {
|
||||
signEvent: async () => {
|
||||
await delay(100);
|
||||
return { sig: "slow" };
|
||||
},
|
||||
};
|
||||
|
||||
const event = { id: "timeout", pubkey: "abc" };
|
||||
|
||||
await assert.rejects(
|
||||
() => queueSignEvent(signer, event, { timeoutMs: 20 }),
|
||||
(err) => {
|
||||
assert.equal(err.code, "timeout");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("queueSignEvent: handles permission denied errors", async () => {
|
||||
const signer = {
|
||||
signEvent: async () => {
|
||||
const err = new Error("User rejected");
|
||||
err.message = "The user denied the permission";
|
||||
throw err;
|
||||
},
|
||||
};
|
||||
|
||||
const event = { id: "denied", pubkey: "abc" };
|
||||
|
||||
await assert.rejects(
|
||||
() => queueSignEvent(signer, event),
|
||||
(err) => {
|
||||
assert.equal(err.code, "permission-denied");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("queueSignEvent: handles signer disconnected", async () => {
|
||||
const signer = {
|
||||
signEvent: async () => {
|
||||
throw new Error("Extension disconnected");
|
||||
},
|
||||
};
|
||||
|
||||
const event = { id: "disconnect", pubkey: "abc" };
|
||||
|
||||
await assert.rejects(
|
||||
() => queueSignEvent(signer, event),
|
||||
(err) => {
|
||||
assert.equal(err.code, "signer-disconnected");
|
||||
return true;
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("queueSignEvent: fails if signer is missing or invalid", async () => {
|
||||
await assert.rejects(
|
||||
() => queueSignEvent(null, {}),
|
||||
(err) => err.code === "not-capable"
|
||||
);
|
||||
|
||||
await assert.rejects(
|
||||
() => queueSignEvent({}, {}),
|
||||
(err) => err.code === "not-capable"
|
||||
);
|
||||
});
|
||||
|
||||
test("queueSignEvent: continues queue processing after failure", async () => {
|
||||
const signer = {
|
||||
signEvent: async (event) => {
|
||||
if (event.id === "fail") {
|
||||
throw new Error("fail");
|
||||
}
|
||||
return { ...event, sig: "ok" };
|
||||
},
|
||||
};
|
||||
|
||||
const p1 = queueSignEvent(signer, { id: "fail" }).catch(() => "caught");
|
||||
const p2 = queueSignEvent(signer, { id: "success" });
|
||||
|
||||
const [res1, res2] = await Promise.all([p1, p2]);
|
||||
|
||||
assert.equal(res1, "caught");
|
||||
assert.equal(res2.sig, "ok");
|
||||
});
|
||||
+78
-234
@@ -1,254 +1,98 @@
|
||||
// Run with: node scripts/run-targeted-tests.mjs tests/nostr/toolkit.test.mjs
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
const originalCanonical = globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__;
|
||||
const originalNostrTools = globalThis.NostrTools;
|
||||
const originalNostrToolsReady = globalThis.nostrToolsReady;
|
||||
|
||||
class BootstrapSimplePool {
|
||||
sub() {}
|
||||
close() {}
|
||||
}
|
||||
|
||||
const bootstrapToolkit = {
|
||||
ok: true,
|
||||
nip19: {
|
||||
decode: (value) => value,
|
||||
npubEncode: (value) => value,
|
||||
},
|
||||
SimplePool: BootstrapSimplePool,
|
||||
};
|
||||
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = bootstrapToolkit;
|
||||
globalThis.NostrTools = bootstrapToolkit;
|
||||
globalThis.nostrToolsReady = Promise.resolve(bootstrapToolkit);
|
||||
|
||||
const toolkitModule = await import("../../js/nostr/toolkit.js");
|
||||
const {
|
||||
rememberNostrTools,
|
||||
getCachedNostrTools,
|
||||
ensureNostrTools,
|
||||
resolveSimplePoolConstructor,
|
||||
import {
|
||||
DEFAULT_RELAY_URLS,
|
||||
shimLegacySimplePoolMethods,
|
||||
} = toolkitModule;
|
||||
resolveSimplePoolConstructor,
|
||||
readToolkitFromScope,
|
||||
normalizeToolkitCandidate,
|
||||
} from "../../js/nostr/toolkit.js";
|
||||
|
||||
const baselineToolkit = getCachedNostrTools() || bootstrapToolkit;
|
||||
|
||||
test.after(() => {
|
||||
if (originalCanonical === undefined) {
|
||||
delete globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__;
|
||||
} else {
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = originalCanonical;
|
||||
}
|
||||
|
||||
if (originalNostrTools === undefined) {
|
||||
delete globalThis.NostrTools;
|
||||
} else {
|
||||
globalThis.NostrTools = originalNostrTools;
|
||||
}
|
||||
|
||||
if (originalNostrToolsReady === undefined) {
|
||||
delete globalThis.nostrToolsReady;
|
||||
} else {
|
||||
globalThis.nostrToolsReady = originalNostrToolsReady;
|
||||
}
|
||||
|
||||
rememberNostrTools(baselineToolkit);
|
||||
test("toolkit: DEFAULT_RELAY_URLS is frozen and contains valid URLs", () => {
|
||||
assert.ok(Array.isArray(DEFAULT_RELAY_URLS));
|
||||
assert.ok(Object.isFrozen(DEFAULT_RELAY_URLS));
|
||||
assert.ok(DEFAULT_RELAY_URLS.length > 0);
|
||||
assert.ok(DEFAULT_RELAY_URLS.every((url) => url.startsWith("wss://")));
|
||||
});
|
||||
|
||||
test("ensureNostrTools resolves the cached toolkit", async () => {
|
||||
class CachedSimplePool {
|
||||
sub() {}
|
||||
close() {}
|
||||
}
|
||||
|
||||
const cachedToolkit = {
|
||||
ok: true,
|
||||
nip19: {
|
||||
decode: (value) => value,
|
||||
npubEncode: (value) => value,
|
||||
},
|
||||
SimplePool: CachedSimplePool,
|
||||
};
|
||||
|
||||
const previousCanonicalForTest = globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__;
|
||||
const previousNostrToolsForTest = globalThis.NostrTools;
|
||||
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = cachedToolkit;
|
||||
globalThis.NostrTools = cachedToolkit;
|
||||
rememberNostrTools(cachedToolkit);
|
||||
|
||||
try {
|
||||
const ensured = await ensureNostrTools();
|
||||
assert.strictEqual(ensured, cachedToolkit);
|
||||
} finally {
|
||||
if (previousCanonicalForTest === undefined) {
|
||||
delete globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__;
|
||||
} else {
|
||||
globalThis.__BITVID_CANONICAL_NOSTR_TOOLS__ = previousCanonicalForTest;
|
||||
}
|
||||
|
||||
if (previousNostrToolsForTest === undefined) {
|
||||
delete globalThis.NostrTools;
|
||||
} else {
|
||||
globalThis.NostrTools = previousNostrToolsForTest;
|
||||
}
|
||||
|
||||
rememberNostrTools(baselineToolkit);
|
||||
}
|
||||
});
|
||||
|
||||
test("resolveSimplePoolConstructor handles supported permutations", () => {
|
||||
test("toolkit: resolveSimplePoolConstructor finds SimplePool", () => {
|
||||
class MockSimplePool {
|
||||
sub() {}
|
||||
close() {}
|
||||
}
|
||||
|
||||
const variations = [
|
||||
{ label: "tools.SimplePool", tools: { SimplePool: MockSimplePool } },
|
||||
{ label: "tools.pool.SimplePool", tools: { pool: { SimplePool: MockSimplePool } } },
|
||||
{ label: "tools.pool constructor", tools: { pool: MockSimplePool } },
|
||||
{
|
||||
label: "tools.SimplePool.SimplePool",
|
||||
tools: { SimplePool: { SimplePool: MockSimplePool } },
|
||||
},
|
||||
{
|
||||
label: "tools.SimplePool.default",
|
||||
tools: { SimplePool: { default: MockSimplePool } },
|
||||
},
|
||||
{ label: "tools.pool.default", tools: { pool: { default: MockSimplePool } } },
|
||||
{
|
||||
label: "tools.default.SimplePool",
|
||||
tools: { default: { SimplePool: MockSimplePool } },
|
||||
},
|
||||
{
|
||||
label: "tools.default.pool.SimplePool",
|
||||
tools: { default: { pool: { SimplePool: MockSimplePool } } },
|
||||
},
|
||||
{
|
||||
label: "tools.default.pool constructor",
|
||||
tools: { default: { pool: MockSimplePool } },
|
||||
},
|
||||
];
|
||||
const mockTools = {
|
||||
SimplePool: MockSimplePool,
|
||||
};
|
||||
|
||||
for (const { label, tools } of variations) {
|
||||
assert.strictEqual(
|
||||
resolveSimplePoolConstructor(tools),
|
||||
MockSimplePool,
|
||||
`expected ${label}`
|
||||
);
|
||||
}
|
||||
const Resolved = resolveSimplePoolConstructor(mockTools);
|
||||
assert.equal(Resolved, MockSimplePool);
|
||||
|
||||
assert.strictEqual(
|
||||
resolveSimplePoolConstructor({}, { SimplePool: MockSimplePool }),
|
||||
MockSimplePool,
|
||||
"scope.SimplePool fallback"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
resolveSimplePoolConstructor({}, { pool: { SimplePool: MockSimplePool } }),
|
||||
MockSimplePool,
|
||||
"scope.pool.SimplePool fallback"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
resolveSimplePoolConstructor(
|
||||
{},
|
||||
{ NostrTools: { pool: { SimplePool: MockSimplePool } } }
|
||||
),
|
||||
MockSimplePool,
|
||||
"scope.NostrTools.pool.SimplePool fallback"
|
||||
);
|
||||
|
||||
assert.strictEqual(
|
||||
resolveSimplePoolConstructor({}, {}),
|
||||
null,
|
||||
"returns null when no candidates are available"
|
||||
);
|
||||
});
|
||||
|
||||
test("shimLegacySimplePoolMethods adds legacy sub/list wrappers", async () => {
|
||||
let capturedRequests = null;
|
||||
let capturedParams = null;
|
||||
const closeReasons = [];
|
||||
|
||||
const mockPool = {
|
||||
subscribeMap(requests, params) {
|
||||
capturedRequests = requests;
|
||||
capturedParams = params;
|
||||
return {
|
||||
close(reason) {
|
||||
closeReasons.push(reason);
|
||||
},
|
||||
};
|
||||
const mockToolsNested = {
|
||||
pool: {
|
||||
SimplePool: MockSimplePool,
|
||||
},
|
||||
};
|
||||
|
||||
shimLegacySimplePoolMethods(mockPool);
|
||||
|
||||
assert.strictEqual(typeof mockPool.sub, "function");
|
||||
assert.strictEqual(typeof mockPool.list, "function");
|
||||
|
||||
const relays = ["wss://relay.one", "wss://relay.two"];
|
||||
const filters = [{ kinds: [1] }, { authors: ["abc"] }];
|
||||
|
||||
const sub = mockPool.sub(relays, filters, {
|
||||
alreadyHaveEvent(id) {
|
||||
return id === "skip";
|
||||
const ResolvedNested = resolveSimplePoolConstructor(mockToolsNested);
|
||||
assert.equal(ResolvedNested, MockSimplePool);
|
||||
});
|
||||
|
||||
test("toolkit: shimLegacySimplePoolMethods adds sub/list/map if missing", () => {
|
||||
const pool = {
|
||||
subscribeMany: () => () => {},
|
||||
};
|
||||
|
||||
shimLegacySimplePoolMethods(pool);
|
||||
|
||||
assert.equal(typeof pool.sub, "function");
|
||||
assert.equal(typeof pool.list, "function");
|
||||
|
||||
// Verify shim methods return expected interface
|
||||
const sub = pool.sub([], []);
|
||||
assert.equal(typeof sub.on, "function");
|
||||
assert.equal(typeof sub.off, "function");
|
||||
assert.equal(typeof sub.unsub, "function");
|
||||
});
|
||||
|
||||
test("toolkit: readToolkitFromScope finds NostrTools in global scope", () => {
|
||||
const mockScope = {
|
||||
NostrTools: {
|
||||
generateSecretKey: () => {},
|
||||
},
|
||||
};
|
||||
|
||||
const tools = readToolkitFromScope(mockScope);
|
||||
assert.ok(tools);
|
||||
assert.equal(typeof tools.generateSecretKey, "function");
|
||||
});
|
||||
|
||||
test("toolkit: normalizeToolkitCandidate validation", () => {
|
||||
assert.equal(normalizeToolkitCandidate(null), null);
|
||||
assert.equal(normalizeToolkitCandidate({ ok: false }), null);
|
||||
assert.equal(normalizeToolkitCandidate({ then: () => {} }), null); // Promise-like
|
||||
|
||||
const valid = { generateSecretKey: () => {} };
|
||||
const normalized = normalizeToolkitCandidate(valid);
|
||||
assert.deepEqual(normalized, valid);
|
||||
});
|
||||
|
||||
test("toolkit: shimLegacySimplePoolMethods handles simple list operation", async () => {
|
||||
const pool = {
|
||||
subscribeMany: (relays, filters, opts) => {
|
||||
setTimeout(() => {
|
||||
if (opts.onevent) opts.onevent({ id: "1" });
|
||||
if (opts.oneose) opts.oneose();
|
||||
}, 10);
|
||||
return () => {};
|
||||
},
|
||||
});
|
||||
|
||||
assert.ok(Array.isArray(capturedRequests));
|
||||
assert.strictEqual(capturedRequests.length, relays.length * filters.length);
|
||||
assert.ok(capturedRequests.every((entry) => entry && entry.url && entry.filter));
|
||||
assert.ok(capturedParams);
|
||||
assert.strictEqual(typeof capturedParams.onevent, "function");
|
||||
assert.strictEqual(typeof capturedParams.oneose, "function");
|
||||
assert.strictEqual(typeof capturedParams.onclose, "function");
|
||||
assert.strictEqual(typeof capturedParams.alreadyHaveEvent, "function");
|
||||
|
||||
const receivedEvents = [];
|
||||
const eosePromise = new Promise((resolve) => sub.on("eose", resolve));
|
||||
const closePromise = new Promise((resolve) => sub.on("close", resolve));
|
||||
sub.on("event", (event) => {
|
||||
receivedEvents.push(event.id);
|
||||
});
|
||||
|
||||
assert.strictEqual(capturedParams.alreadyHaveEvent("skip"), true);
|
||||
assert.strictEqual(capturedParams.alreadyHaveEvent("id-1"), false);
|
||||
assert.strictEqual(capturedParams.alreadyHaveEvent("id-1"), true);
|
||||
|
||||
capturedParams.onevent({ id: "id-1" });
|
||||
capturedParams.onevent({ id: "id-2" });
|
||||
capturedParams.oneose();
|
||||
capturedParams.onclose(["done"]);
|
||||
|
||||
await eosePromise;
|
||||
const closePayload = await closePromise;
|
||||
|
||||
assert.deepStrictEqual(receivedEvents, ["id-1", "id-2"]);
|
||||
assert.deepStrictEqual(closePayload, ["done"]);
|
||||
|
||||
sub.unsub();
|
||||
|
||||
closeReasons.length = 0;
|
||||
const immediateUnsub = mockPool.sub(["wss://relay.extra"], [{ kinds: [2] }]);
|
||||
immediateUnsub.unsub();
|
||||
assert.ok(closeReasons.includes("closed by caller"));
|
||||
|
||||
capturedParams = null;
|
||||
capturedRequests = null;
|
||||
closeReasons.length = 0;
|
||||
};
|
||||
|
||||
const listPromise = mockPool.list(["wss://relay.example"], [{ kinds: [1] }]);
|
||||
assert.ok(capturedParams);
|
||||
capturedParams.onevent({ id: "evt-1" });
|
||||
capturedParams.onevent({ id: "evt-2" });
|
||||
capturedParams.oneose();
|
||||
shimLegacySimplePoolMethods(pool);
|
||||
|
||||
const listed = await listPromise;
|
||||
assert.deepStrictEqual(
|
||||
listed.map((event) => event.id),
|
||||
["evt-1", "evt-2"]
|
||||
);
|
||||
const events = await pool.list(["wss://relay.com"], [{ kinds: [1] }]);
|
||||
assert.equal(events.length, 1);
|
||||
assert.equal(events[0].id, "1");
|
||||
});
|
||||
@@ -0,0 +1,166 @@
|
||||
// Run with: node scripts/run-targeted-tests.mjs tests/services/relay-health-service.test.mjs
|
||||
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import RelayHealthService from "../../js/services/relayHealthService.js";
|
||||
|
||||
// Mock SimpleEventEmitter for tests
|
||||
class SimpleEventEmitter {
|
||||
constructor() {
|
||||
this.listeners = new Map();
|
||||
}
|
||||
on(event, handler) {
|
||||
if (!this.listeners.has(event)) {
|
||||
this.listeners.set(event, []);
|
||||
}
|
||||
this.listeners.get(event).push(handler);
|
||||
}
|
||||
emit(event, ...args) {
|
||||
if (this.listeners.has(event)) {
|
||||
this.listeners.get(event).forEach((handler) => handler(...args));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("RelayHealthService: initializes with default values", () => {
|
||||
const service = new RelayHealthService();
|
||||
assert.equal(service.getTelemetryOptIn(), false);
|
||||
assert.equal(service.getRelayUrls().length, 0);
|
||||
});
|
||||
|
||||
test("RelayHealthService: manages telemetry opt-in", () => {
|
||||
const service = new RelayHealthService();
|
||||
service.setTelemetryOptIn(true);
|
||||
assert.equal(service.getTelemetryOptIn(), true);
|
||||
assert.equal(globalThis.localStorage.getItem("bitvid:relay-health-telemetry-opt-in"), "true");
|
||||
|
||||
service.setTelemetryOptIn(false);
|
||||
assert.equal(service.getTelemetryOptIn(), false);
|
||||
assert.equal(globalThis.localStorage.getItem("bitvid:relay-health-telemetry-opt-in"), "false");
|
||||
});
|
||||
|
||||
test("RelayHealthService: getRelayUrls fetches from relayManager", () => {
|
||||
const relayManager = {
|
||||
getEntries: () => [{ url: "wss://relay1.com" }, { url: "wss://relay2.com" }],
|
||||
};
|
||||
const service = new RelayHealthService({ relayManager });
|
||||
const urls = service.getRelayUrls();
|
||||
assert.deepEqual(urls, ["wss://relay1.com", "wss://relay2.com"]);
|
||||
});
|
||||
|
||||
test("RelayHealthService: ensureRelayState creates default state", () => {
|
||||
const service = new RelayHealthService();
|
||||
const state = service.ensureRelayState("wss://relay.com");
|
||||
assert.deepEqual(state, {
|
||||
url: "wss://relay.com",
|
||||
connected: false,
|
||||
lastLatencyMs: null,
|
||||
errorCount: 0,
|
||||
lastCheckedAt: null,
|
||||
lastErrorAt: null,
|
||||
});
|
||||
});
|
||||
|
||||
test("RelayHealthService: checkRelay success flow", async () => {
|
||||
const mockRelay = new SimpleEventEmitter();
|
||||
const nostrClient = {
|
||||
ensurePool: async () => {},
|
||||
pool: {
|
||||
ensureRelay: async (url) => {
|
||||
if (url === "wss://success.com") return mockRelay;
|
||||
return null;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const service = new RelayHealthService({ nostrClient });
|
||||
await service.checkRelay("wss://success.com");
|
||||
|
||||
// Simulate connection
|
||||
mockRelay.emit("connect");
|
||||
|
||||
const state = service.ensureRelayState("wss://success.com");
|
||||
assert.equal(state.connected, true);
|
||||
assert.notEqual(state.lastCheckedAt, null);
|
||||
assert.notEqual(state.lastLatencyMs, null);
|
||||
});
|
||||
|
||||
test("RelayHealthService: checkRelay failure flow", async () => {
|
||||
const nostrClient = {
|
||||
ensurePool: async () => {},
|
||||
pool: {
|
||||
ensureRelay: async () => {
|
||||
throw new Error("Connection failed");
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const service = new RelayHealthService({ nostrClient });
|
||||
await service.checkRelay("wss://fail.com");
|
||||
|
||||
const state = service.ensureRelayState("wss://fail.com");
|
||||
assert.equal(state.connected, false);
|
||||
assert.equal(state.errorCount, 1);
|
||||
assert.notEqual(state.lastErrorAt, null);
|
||||
});
|
||||
|
||||
test("RelayHealthService: checkRelay handles missing nostrClient", async () => {
|
||||
const service = new RelayHealthService(); // No client
|
||||
await service.checkRelay("wss://test.com");
|
||||
|
||||
const state = service.ensureRelayState("wss://test.com");
|
||||
assert.equal(state.connected, false);
|
||||
assert.equal(state.errorCount, 1);
|
||||
});
|
||||
|
||||
test("RelayHealthService: refresh checks all relays", async () => {
|
||||
const relayManager = {
|
||||
getEntries: () => [{ url: "wss://r1.com" }, { url: "wss://r2.com" }],
|
||||
};
|
||||
const nostrClient = {
|
||||
ensurePool: async () => {},
|
||||
pool: {
|
||||
ensureRelay: async (url) => {
|
||||
const relay = new SimpleEventEmitter();
|
||||
setTimeout(() => relay.emit("connect"), 10);
|
||||
return relay;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const service = new RelayHealthService({ relayManager, nostrClient });
|
||||
const snapshot = await service.refresh();
|
||||
|
||||
assert.equal(snapshot.length, 2);
|
||||
assert.equal(snapshot[0].url, "wss://r1.com");
|
||||
assert.equal(snapshot[1].url, "wss://r2.com");
|
||||
});
|
||||
|
||||
test("RelayHealthService: emits telemetry if opted in", async () => {
|
||||
let telemetryPayload = null;
|
||||
const telemetryEmitter = (event, payload) => {
|
||||
if (event === "relay_health_snapshot") {
|
||||
telemetryPayload = payload;
|
||||
}
|
||||
};
|
||||
|
||||
const relayManager = {
|
||||
getEntries: () => [{ url: "wss://t1.com" }],
|
||||
};
|
||||
const nostrClient = {
|
||||
ensurePool: async () => {},
|
||||
pool: {
|
||||
ensureRelay: async () => new SimpleEventEmitter(),
|
||||
},
|
||||
};
|
||||
|
||||
const service = new RelayHealthService({ relayManager, nostrClient, telemetryEmitter });
|
||||
service.setTelemetryOptIn(true);
|
||||
|
||||
await service.refresh();
|
||||
|
||||
assert.ok(telemetryPayload);
|
||||
assert.equal(telemetryPayload.relays.length, 1);
|
||||
assert.equal(telemetryPayload.relays[0].url, "wss://t1.com");
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
// Run with: node scripts/run-targeted-tests.mjs tests/state/profile-cache.test.mjs
|
||||
|
||||
import "../test-helpers/setup-localstorage.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import test, { beforeEach } from "node:test";
|
||||
import { profileCache } from "../../js/state/profileCache.js";
|
||||
|
||||
beforeEach(() => {
|
||||
if (globalThis.localStorage && typeof globalThis.localStorage.clear === "function") {
|
||||
globalThis.localStorage.clear();
|
||||
}
|
||||
// Reset singleton state
|
||||
if (profileCache.activePubkey) {
|
||||
profileCache.clearMemoryCache(profileCache.activePubkey);
|
||||
profileCache.activePubkey = null;
|
||||
}
|
||||
profileCache.memoryCache.clear();
|
||||
});
|
||||
|
||||
test("ProfileCache: normalizeHexPubkey validates and cleans inputs", () => {
|
||||
assert.equal(profileCache.normalizeHexPubkey(null), null);
|
||||
assert.equal(profileCache.normalizeHexPubkey("invalid"), null);
|
||||
|
||||
const valid = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
assert.equal(profileCache.normalizeHexPubkey(valid), valid);
|
||||
assert.equal(profileCache.normalizeHexPubkey(` ${valid.toUpperCase()} `), valid);
|
||||
});
|
||||
|
||||
test("ProfileCache: resolves storage keys correctly", () => {
|
||||
const pubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
|
||||
// Test watchHistory (kind:pubkey:d)
|
||||
const historyKey = profileCache.resolveAddressKey("watchHistory", { pubkey });
|
||||
assert.match(historyKey, new RegExp(`bitvid:profile:${pubkey}:watchHistory:watch-history:v1`));
|
||||
|
||||
// Test profile (simple)
|
||||
const profileKey = profileCache.resolveAddressKey("profile", { pubkey });
|
||||
assert.match(profileKey, new RegExp(`bitvid:profile:${pubkey}:profileMetadata:v1`));
|
||||
});
|
||||
|
||||
test("ProfileCache: persists data to localStorage and memory", () => {
|
||||
const pubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
const data = { foo: "bar" };
|
||||
|
||||
profileCache.setProfileData(pubkey, "test-section", data);
|
||||
|
||||
// Check memory
|
||||
const memData = profileCache.getProfileData(pubkey, "test-section");
|
||||
assert.deepEqual(memData, data);
|
||||
|
||||
// Check storage
|
||||
const key = profileCache.getStorageKey(pubkey, "test-section");
|
||||
const stored = globalThis.localStorage.getItem(key);
|
||||
assert.ok(stored);
|
||||
assert.deepEqual(JSON.parse(stored), data);
|
||||
});
|
||||
|
||||
test("ProfileCache: loads from localStorage if memory miss", () => {
|
||||
const pubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
const data = { loaded: true };
|
||||
const key = profileCache.getStorageKey(pubkey, "test-load");
|
||||
|
||||
// Pre-seed storage
|
||||
globalThis.localStorage.setItem(key, JSON.stringify(data));
|
||||
|
||||
// Clear memory to force load
|
||||
profileCache.clearMemoryCache(pubkey);
|
||||
|
||||
const result = profileCache.getProfileData(pubkey, "test-load");
|
||||
assert.deepEqual(result, data);
|
||||
});
|
||||
|
||||
test("ProfileCache: handles active profile switching", () => {
|
||||
const pubkey1 = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
const pubkey2 = "fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210";
|
||||
|
||||
profileCache.setActiveProfile(pubkey1);
|
||||
profileCache.set("section", { p: 1 });
|
||||
|
||||
assert.deepEqual(profileCache.get("section"), { p: 1 });
|
||||
|
||||
profileCache.setActiveProfile(pubkey2);
|
||||
|
||||
// Should not access pubkey1's data via active convenience methods
|
||||
assert.equal(profileCache.get("section"), null);
|
||||
|
||||
// But pubkey1 data should still exist in persistence if saved
|
||||
const p1Data = profileCache.getProfileData(pubkey1, "section");
|
||||
assert.deepEqual(p1Data, { p: 1 });
|
||||
});
|
||||
|
||||
test("ProfileCache: setProfile normalizes and saves", () => {
|
||||
const pubkey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
|
||||
const rawProfile = {
|
||||
name: " Tester ",
|
||||
picture: "https://example.com/pic.jpg",
|
||||
about: "I am a tester",
|
||||
};
|
||||
|
||||
const entry = profileCache.setProfile(pubkey, rawProfile);
|
||||
|
||||
assert.equal(entry.profile.name, "Tester");
|
||||
assert.equal(entry.profile.picture, "https://example.com/pic.jpg");
|
||||
assert.ok(entry.timestamp);
|
||||
|
||||
const stored = profileCache.getProfile(pubkey);
|
||||
assert.deepEqual(stored, entry.profile);
|
||||
});
|
||||
Reference in new issue
Block a user