mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
602 lines
17 KiB
JavaScript
602 lines
17 KiB
JavaScript
import "./test-helpers/setup-localstorage.mjs";
|
|
import assert from "node:assert/strict";
|
|
|
|
const { userBlocks, USER_BLOCK_EVENTS } = await import("../js/userBlocks.js");
|
|
const {
|
|
nostrClient,
|
|
setActiveSigner,
|
|
clearActiveSigner,
|
|
getActiveSigner,
|
|
} = await import("../js/nostr.js");
|
|
|
|
if (typeof globalThis.window === "undefined") {
|
|
globalThis.window = {};
|
|
}
|
|
|
|
await (async () => {
|
|
const actor = "f".repeat(64);
|
|
const blocked = "e".repeat(64);
|
|
const relays = [
|
|
"wss://relay-one.example",
|
|
"wss://relay-two.example",
|
|
"wss://relay-three.example",
|
|
];
|
|
|
|
const originalNostr = window.nostr;
|
|
const originalPool = nostrClient.pool;
|
|
const originalRelays = Array.isArray(nostrClient.relays)
|
|
? [...nostrClient.relays]
|
|
: nostrClient.relays;
|
|
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
|
|
? [...nostrClient.writeRelays]
|
|
: nostrClient.writeRelays;
|
|
const originalBlocked = new Set(userBlocks.blockedPubkeys);
|
|
const originalBlockEventId = userBlocks.blockEventId;
|
|
const originalLoaded = userBlocks.loaded;
|
|
|
|
const calls = [];
|
|
const resolvers = [];
|
|
|
|
window.nostr = {
|
|
...(originalNostr || {}),
|
|
nip04: {
|
|
...((originalNostr && originalNostr.nip04) || {}),
|
|
decrypt: async () =>
|
|
JSON.stringify({
|
|
blockedPubkeys: [blocked],
|
|
}),
|
|
},
|
|
};
|
|
|
|
const pool = originalPool ? { ...originalPool } : {};
|
|
pool.list = (targets) => {
|
|
const relay = Array.isArray(targets) ? targets[0] : undefined;
|
|
calls.push(relay);
|
|
let resolve;
|
|
const promise = new Promise((res) => {
|
|
resolve = res;
|
|
});
|
|
resolvers.push({ relay, resolve });
|
|
return promise;
|
|
};
|
|
|
|
nostrClient.pool = pool;
|
|
nostrClient.relays = relays;
|
|
nostrClient.writeRelays = relays;
|
|
|
|
assert.equal(
|
|
typeof nostrClient.pool?.list,
|
|
"function",
|
|
"nostrClient.pool.list should be stubbed for block list tests",
|
|
);
|
|
|
|
userBlocks.blockedPubkeys = new Set();
|
|
userBlocks.blockEventId = null;
|
|
userBlocks.loaded = false;
|
|
|
|
try {
|
|
const loadPromise = userBlocks.loadBlocks(actor);
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
assert.deepEqual(
|
|
calls,
|
|
relays,
|
|
"loadBlocks should initiate queries for all relays concurrently"
|
|
);
|
|
|
|
const responses = {
|
|
[relays[0]]: [
|
|
{
|
|
id: "event-old",
|
|
created_at: 1700,
|
|
pubkey: actor,
|
|
content: "ciphertext-old",
|
|
},
|
|
],
|
|
[relays[1]]: [
|
|
{
|
|
id: "event-new",
|
|
created_at: 1800,
|
|
pubkey: actor,
|
|
content: "ciphertext-new",
|
|
},
|
|
],
|
|
[relays[2]]: [],
|
|
};
|
|
|
|
for (const { relay, resolve } of resolvers) {
|
|
resolve(responses[relay]);
|
|
}
|
|
|
|
await loadPromise;
|
|
|
|
assert.equal(
|
|
userBlocks.blockEventId,
|
|
"event-new",
|
|
"latest event should win after aggregating relay responses"
|
|
);
|
|
assert.deepEqual(
|
|
userBlocks.getBlockedPubkeys(),
|
|
[blocked],
|
|
"decrypted block list should hydrate from newest event"
|
|
);
|
|
} finally {
|
|
userBlocks.blockedPubkeys = originalBlocked;
|
|
userBlocks.blockEventId = originalBlockEventId;
|
|
userBlocks.loaded = originalLoaded;
|
|
nostrClient.pool = originalPool;
|
|
nostrClient.relays = originalRelays;
|
|
nostrClient.writeRelays = originalWriteRelays;
|
|
window.nostr = originalNostr;
|
|
}
|
|
})();
|
|
|
|
await (async () => {
|
|
const actor = "a".repeat(64);
|
|
const initialBlocked = "b".repeat(64);
|
|
const backgroundBlocked = "c".repeat(64);
|
|
const relays = [
|
|
"wss://fast-one.example",
|
|
"wss://fast-two.example",
|
|
"wss://fast-three.example",
|
|
"wss://background.example",
|
|
];
|
|
|
|
const originalNostr = window.nostr;
|
|
const originalPool = nostrClient.pool;
|
|
const originalRelays = Array.isArray(nostrClient.relays)
|
|
? [...nostrClient.relays]
|
|
: nostrClient.relays;
|
|
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
|
|
? [...nostrClient.writeRelays]
|
|
: nostrClient.writeRelays;
|
|
const originalBlocked = new Set(userBlocks.blockedPubkeys);
|
|
const originalBlockEventId = userBlocks.blockEventId;
|
|
const originalBlockEventCreatedAt = userBlocks.blockEventCreatedAt;
|
|
const originalLoaded = userBlocks.loaded;
|
|
|
|
const calls = [];
|
|
const resolvers = [];
|
|
|
|
window.nostr = {
|
|
...(originalNostr || {}),
|
|
nip04: {
|
|
...((originalNostr && originalNostr.nip04) || {}),
|
|
decrypt: async () =>
|
|
JSON.stringify({
|
|
blockedPubkeys: [backgroundBlocked],
|
|
}),
|
|
},
|
|
};
|
|
|
|
const pool = originalPool ? { ...originalPool } : {};
|
|
pool.list = (targets) => {
|
|
const relay = Array.isArray(targets) ? targets[0] : undefined;
|
|
calls.push(relay);
|
|
let resolve;
|
|
const promise = new Promise((res) => {
|
|
resolve = res;
|
|
});
|
|
resolvers.push({ relay, resolve });
|
|
return promise;
|
|
};
|
|
|
|
nostrClient.pool = pool;
|
|
nostrClient.relays = relays;
|
|
nostrClient.writeRelays = relays;
|
|
|
|
assert.equal(
|
|
typeof nostrClient.pool?.list,
|
|
"function",
|
|
"nostrClient.pool.list should be stubbed for background relay tests",
|
|
);
|
|
|
|
userBlocks.blockedPubkeys = new Set([initialBlocked]);
|
|
userBlocks.blockEventId = "existing-event";
|
|
userBlocks.blockEventCreatedAt = 1_000;
|
|
userBlocks.loaded = false;
|
|
|
|
const statusEvents = [];
|
|
const changeEvents = [];
|
|
const unsubscribeStatus = userBlocks.on(USER_BLOCK_EVENTS.STATUS, (detail) => {
|
|
statusEvents.push(detail);
|
|
});
|
|
const unsubscribeChange = userBlocks.on(USER_BLOCK_EVENTS.CHANGE, (detail) => {
|
|
changeEvents.push(detail);
|
|
});
|
|
|
|
try {
|
|
const loadPromise = userBlocks.loadBlocks(actor);
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
assert.deepEqual(
|
|
calls,
|
|
relays,
|
|
"loadBlocks should query fast and background relays concurrently",
|
|
);
|
|
|
|
for (const { relay, resolve } of resolvers) {
|
|
if (relay !== relays[3]) {
|
|
resolve([]);
|
|
}
|
|
}
|
|
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
assert.deepEqual(
|
|
userBlocks.getBlockedPubkeys(),
|
|
[initialBlocked],
|
|
"existing blocked list should be preserved while waiting for background relays",
|
|
);
|
|
|
|
const backgroundResolver = resolvers.find((entry) => entry.relay === relays[3]);
|
|
backgroundResolver.resolve([
|
|
{
|
|
id: "background-event",
|
|
pubkey: actor,
|
|
created_at: 2_000,
|
|
content: "ciphertext-background",
|
|
},
|
|
]);
|
|
|
|
await loadPromise;
|
|
|
|
assert.equal(
|
|
userBlocks.blockEventId,
|
|
"background-event",
|
|
"background relay event should be applied after fast relay failures",
|
|
);
|
|
assert.deepEqual(
|
|
userBlocks.getBlockedPubkeys(),
|
|
[backgroundBlocked],
|
|
"block list should hydrate from background relay payload",
|
|
);
|
|
|
|
assert(
|
|
statusEvents.some((detail) => detail?.status === "awaiting-background"),
|
|
"status events should indicate when background relays are pending",
|
|
);
|
|
assert(
|
|
statusEvents.some((detail) => detail?.status === "applied"),
|
|
"status events should report when the background payload is applied",
|
|
);
|
|
assert(
|
|
changeEvents.some(
|
|
(detail) =>
|
|
detail?.action === "sync" &&
|
|
Array.isArray(detail?.blockedPubkeys) &&
|
|
detail.blockedPubkeys.includes(backgroundBlocked),
|
|
),
|
|
"change events should emit sync updates after applying background payloads",
|
|
);
|
|
} finally {
|
|
unsubscribeStatus?.();
|
|
unsubscribeChange?.();
|
|
userBlocks.blockedPubkeys = originalBlocked;
|
|
userBlocks.blockEventId = originalBlockEventId;
|
|
userBlocks.blockEventCreatedAt = originalBlockEventCreatedAt;
|
|
userBlocks.loaded = originalLoaded;
|
|
nostrClient.pool = originalPool;
|
|
nostrClient.relays = originalRelays;
|
|
nostrClient.writeRelays = originalWriteRelays;
|
|
window.nostr = originalNostr;
|
|
}
|
|
})();
|
|
|
|
await (async () => {
|
|
const actor = "1".repeat(64);
|
|
const seedOneHex = "2".repeat(64);
|
|
const seedTwoHex = "3".repeat(64);
|
|
const seedOneNpub = "npub1seedone";
|
|
const seedTwoNpub = "npub1seedtwo";
|
|
|
|
const originalEnsureLoaded = userBlocks.ensureLoaded;
|
|
const originalPublishBlockList = userBlocks.publishBlockList;
|
|
const originalBlocked = new Set(userBlocks.blockedPubkeys);
|
|
const originalBlockEventId = userBlocks.blockEventId;
|
|
const originalBlockEventCreatedAt = userBlocks.blockEventCreatedAt;
|
|
const originalLastPublishedCreatedAt = userBlocks.lastPublishedCreatedAt;
|
|
const originalLoaded = userBlocks.loaded;
|
|
const originalSeedStateCache = userBlocks.seedStateCache;
|
|
const originalNostrTools = window.NostrTools;
|
|
|
|
const publishCalls = [];
|
|
|
|
localStorage.clear();
|
|
|
|
const decodeMap = new Map([
|
|
[seedOneNpub, seedOneHex],
|
|
[seedTwoNpub, seedTwoHex],
|
|
]);
|
|
|
|
window.NostrTools = {
|
|
...(originalNostrTools || {}),
|
|
nip19: {
|
|
...((originalNostrTools && originalNostrTools.nip19) || {}),
|
|
decode(value) {
|
|
if (decodeMap.has(value)) {
|
|
return { type: "npub", data: decodeMap.get(value) };
|
|
}
|
|
throw new Error(`unexpected npub decode: ${value}`);
|
|
},
|
|
},
|
|
};
|
|
|
|
userBlocks.ensureLoaded = async () => {
|
|
userBlocks.loaded = true;
|
|
};
|
|
|
|
userBlocks.publishBlockList = async (pubkey) => {
|
|
publishCalls.push({
|
|
pubkey,
|
|
blocked: Array.from(userBlocks.blockedPubkeys),
|
|
});
|
|
userBlocks.blockEventId = `event-${publishCalls.length}`;
|
|
userBlocks.blockEventCreatedAt = 1_000 + publishCalls.length;
|
|
userBlocks.lastPublishedCreatedAt = userBlocks.blockEventCreatedAt;
|
|
return { id: userBlocks.blockEventId, created_at: userBlocks.blockEventCreatedAt };
|
|
};
|
|
|
|
userBlocks.blockedPubkeys = new Set();
|
|
userBlocks.blockEventId = null;
|
|
userBlocks.blockEventCreatedAt = null;
|
|
userBlocks.lastPublishedCreatedAt = null;
|
|
userBlocks.loaded = false;
|
|
userBlocks.seedStateCache = new Map();
|
|
|
|
try {
|
|
const seedResult = await userBlocks.seedWithNpubs(actor, [
|
|
seedOneNpub,
|
|
seedTwoNpub,
|
|
seedTwoNpub,
|
|
]);
|
|
|
|
assert.equal(
|
|
seedResult.seeded,
|
|
true,
|
|
"initial seeding should publish the aggregated blacklist",
|
|
);
|
|
assert.equal(
|
|
publishCalls.length,
|
|
1,
|
|
"seeding should publish exactly one block list event",
|
|
);
|
|
assert.deepEqual(
|
|
Array.from(userBlocks.blockedPubkeys).sort(),
|
|
[seedOneHex, seedTwoHex],
|
|
"seeding should populate the local block list with decoded hex pubkeys",
|
|
);
|
|
|
|
const secondSeed = await userBlocks.seedWithNpubs(actor, [seedOneNpub, seedTwoNpub]);
|
|
assert.equal(
|
|
secondSeed.seeded,
|
|
false,
|
|
"subsequent seeding attempts should be a no-op",
|
|
);
|
|
assert.equal(
|
|
publishCalls.length,
|
|
1,
|
|
"subsequent seeding attempts should not republish the block list",
|
|
);
|
|
|
|
await userBlocks.removeBlock(seedTwoHex, actor);
|
|
assert.equal(
|
|
publishCalls.length,
|
|
2,
|
|
"removing a seeded pubkey should republish the block list",
|
|
);
|
|
assert.deepEqual(
|
|
Array.from(userBlocks.blockedPubkeys),
|
|
[seedOneHex],
|
|
"removal should persist in the local block list",
|
|
);
|
|
|
|
await userBlocks.removeBlock(seedOneHex, actor);
|
|
assert.equal(
|
|
publishCalls.length,
|
|
3,
|
|
"removing the final baseline entry should publish again",
|
|
);
|
|
assert.equal(
|
|
userBlocks.blockedPubkeys.size,
|
|
0,
|
|
"block list should become empty after removing all seeded entries",
|
|
);
|
|
|
|
const removalState = userBlocks._getSeedState(actor);
|
|
assert(removalState.removals.has(seedOneHex), "seed removals should be recorded for the actor");
|
|
assert(removalState.removals.has(seedTwoHex), "seed removals should include every removed pubkey");
|
|
|
|
const afterRemovalSeed = await userBlocks.seedWithNpubs(actor, [
|
|
seedOneNpub,
|
|
seedTwoNpub,
|
|
]);
|
|
assert.equal(
|
|
afterRemovalSeed.seeded,
|
|
false,
|
|
"seeding should stay disabled after removals",
|
|
);
|
|
assert.equal(
|
|
publishCalls.length,
|
|
3,
|
|
"no additional publish calls should occur after removals",
|
|
);
|
|
assert.equal(
|
|
userBlocks.blockedPubkeys.size,
|
|
0,
|
|
"removed entries should not be re-applied by seeding",
|
|
);
|
|
} finally {
|
|
userBlocks.ensureLoaded = originalEnsureLoaded;
|
|
userBlocks.publishBlockList = originalPublishBlockList;
|
|
userBlocks.blockedPubkeys = originalBlocked;
|
|
userBlocks.blockEventId = originalBlockEventId;
|
|
userBlocks.blockEventCreatedAt = originalBlockEventCreatedAt;
|
|
userBlocks.lastPublishedCreatedAt = originalLastPublishedCreatedAt;
|
|
userBlocks.loaded = originalLoaded;
|
|
userBlocks.seedStateCache = originalSeedStateCache;
|
|
window.NostrTools = originalNostrTools;
|
|
localStorage.clear();
|
|
}
|
|
})();
|
|
|
|
await (async () => {
|
|
const actor = "4".repeat(64);
|
|
const target = "5".repeat(64);
|
|
|
|
const originalBlocked = new Set(userBlocks.blockedPubkeys);
|
|
const originalBlockEventId = userBlocks.blockEventId;
|
|
const originalBlockEventCreatedAt = userBlocks.blockEventCreatedAt;
|
|
const originalLastPublishedCreatedAt = userBlocks.lastPublishedCreatedAt;
|
|
const originalLoaded = userBlocks.loaded;
|
|
const originalPool = nostrClient.pool;
|
|
const originalRelays = Array.isArray(nostrClient.relays)
|
|
? [...nostrClient.relays]
|
|
: nostrClient.relays;
|
|
const originalWriteRelays = Array.isArray(nostrClient.writeRelays)
|
|
? [...nostrClient.writeRelays]
|
|
: nostrClient.writeRelays;
|
|
const originalEnsurePermissions = nostrClient.ensureExtensionPermissions;
|
|
const originalSignerState = getActiveSigner();
|
|
clearActiveSigner();
|
|
const originalNostr = window.nostr;
|
|
|
|
let permissionRequests = 0;
|
|
nostrClient.ensureExtensionPermissions = async () => {
|
|
permissionRequests += 1;
|
|
return { ok: true };
|
|
};
|
|
|
|
let eventCounter = 0;
|
|
let latestCiphertext = "cipher:[]";
|
|
let latestEventId = "event-initial";
|
|
let latestCreatedAt = 1_000;
|
|
|
|
const decryptPayloads = new Map([
|
|
[latestCiphertext, JSON.stringify({ blockedPubkeys: [] })],
|
|
]);
|
|
|
|
nostrClient.relays = ["wss://direct-signer.example"];
|
|
nostrClient.writeRelays = nostrClient.relays;
|
|
nostrClient.pool = {
|
|
publish: (_targets, event) => {
|
|
latestCiphertext = event.content;
|
|
latestEventId = event.id || `event-${Date.now()}`;
|
|
latestCreatedAt = event.created_at || Math.floor(Date.now() / 1000);
|
|
return {
|
|
on(eventName, handler) {
|
|
if (eventName === "ok") {
|
|
setImmediate(handler);
|
|
return true;
|
|
}
|
|
return false;
|
|
},
|
|
};
|
|
},
|
|
list: async () => [
|
|
{
|
|
id: latestEventId,
|
|
created_at: latestCreatedAt,
|
|
pubkey: actor,
|
|
content: latestCiphertext,
|
|
},
|
|
],
|
|
};
|
|
|
|
setActiveSigner({
|
|
type: "private-key",
|
|
pubkey: actor,
|
|
nip04Decrypt: async (pubkey, ciphertext) => {
|
|
assert.equal(pubkey, actor, "decrypt should target the actor pubkey");
|
|
if (!decryptPayloads.has(ciphertext)) {
|
|
throw new Error(`unexpected ciphertext ${ciphertext}`);
|
|
}
|
|
return decryptPayloads.get(ciphertext);
|
|
},
|
|
nip04Encrypt: async (pubkey, plaintext) => {
|
|
assert.equal(pubkey, actor, "encrypt should target the actor pubkey");
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(plaintext);
|
|
} catch (error) {
|
|
throw new Error(`unexpected payload ${plaintext}`);
|
|
}
|
|
const blocked = Array.isArray(parsed?.blockedPubkeys)
|
|
? [...parsed.blockedPubkeys].sort()
|
|
: [];
|
|
const cipher = `cipher:${JSON.stringify(blocked)}`;
|
|
decryptPayloads.set(cipher, plaintext);
|
|
return cipher;
|
|
},
|
|
signEvent: async (event) => {
|
|
eventCounter += 1;
|
|
return {
|
|
...event,
|
|
id: `event-${eventCounter}`,
|
|
created_at: event.created_at ?? Math.floor(Date.now() / 1000),
|
|
};
|
|
},
|
|
});
|
|
|
|
userBlocks.blockedPubkeys = new Set();
|
|
userBlocks.blockEventId = null;
|
|
userBlocks.blockEventCreatedAt = null;
|
|
userBlocks.lastPublishedCreatedAt = null;
|
|
userBlocks.loaded = false;
|
|
|
|
try {
|
|
window.nostr = undefined;
|
|
|
|
await userBlocks.loadBlocks(actor);
|
|
assert.deepEqual(
|
|
userBlocks.getBlockedPubkeys(),
|
|
[],
|
|
"direct signer should load block list without extension decryptor",
|
|
);
|
|
assert.equal(
|
|
permissionRequests,
|
|
0,
|
|
"loadBlocks should not request extension permissions when signer decryptor exists",
|
|
);
|
|
|
|
const blockResult = await userBlocks.addBlock(target, actor);
|
|
assert.equal(blockResult?.ok, true, "addBlock should succeed with direct signer");
|
|
assert(userBlocks.blockedPubkeys.has(target), "target should be in the block list");
|
|
|
|
const unblockResult = await userBlocks.removeBlock(target, actor);
|
|
assert.equal(
|
|
unblockResult?.ok,
|
|
true,
|
|
"removeBlock should succeed with direct signer",
|
|
);
|
|
assert.equal(
|
|
userBlocks.blockedPubkeys.has(target),
|
|
false,
|
|
"target should be removed from the block list",
|
|
);
|
|
|
|
assert.equal(
|
|
permissionRequests,
|
|
0,
|
|
"direct signer flows should never trigger extension permission prompts",
|
|
);
|
|
} finally {
|
|
userBlocks.blockedPubkeys = originalBlocked;
|
|
userBlocks.blockEventId = originalBlockEventId;
|
|
userBlocks.blockEventCreatedAt = originalBlockEventCreatedAt;
|
|
userBlocks.lastPublishedCreatedAt = originalLastPublishedCreatedAt;
|
|
userBlocks.loaded = originalLoaded;
|
|
nostrClient.pool = originalPool;
|
|
nostrClient.relays = originalRelays;
|
|
nostrClient.writeRelays = originalWriteRelays;
|
|
nostrClient.ensureExtensionPermissions = originalEnsurePermissions;
|
|
window.nostr = originalNostr;
|
|
clearActiveSigner();
|
|
if (originalSignerState) {
|
|
setActiveSigner(originalSignerState);
|
|
}
|
|
}
|
|
})();
|