mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Fully closes #11b's un-quarantine work: the QUARANTINE map is now EMPTY. dm-block-filter.test.mjs (node:test) PASSED all subtests then hung, so the node:test runner never exited. Diagnosed via process.getActiveResourcesInfo(): the "integration with loaded user block list" test's manager.loadBlocks() opens a LIVE block-list subscription through nostrClient's real SimplePool aimed at the test's fake relay URLs, leaking 4 TCP sockets + reconnect timers. It is NOT the decrypt-retry timer (verified absent). Nulling nostrClient.pool did not help — loadBlocks calls ensurePool() (userBlocks.js:967) to re-create+connect a real pool when pool is falsy. Fix (node:test-SAFE, no force-exit): make the test hermetic — (1) a TRUTHY stub pool so ensurePool() is skipped, (2) a no-op getSubscriptionManager so startBlockListSubscription opens nothing. The block-merge logic under test still runs fully through the already-stubbed fetchListIncrementally. Restored in finally. Verified BOTH directions: a clean run exits 0 (# pass 12), AND a deliberately broken assertion still exits 1 (# fail 1) — failures are NOT masked. A force process.exit in an after() hook was rejected precisely because it masked failures (process.exitCode isn't set when the hook fires). See TEST_INTEGRITY.md. NOTE: the full suite still has one SEPARATE, pre-existing red unrelated to #11b — tests/ui/uploadModal-reset.test.mjs (TODO #11): its "zombie callbacks" subtest feeds a plain object where webtorrent expects a Blob ("must be an instance of Blob") and leaks async activity after the test ends. Untouched here; tracked under #11 (harden flaky tests), not the #11b quarantine work. Co-Authored-By: Claude Opus 4.8 <[email protected]>
348 lines
11 KiB
JavaScript
348 lines
11 KiB
JavaScript
import "./test-helpers/setup-localstorage.mjs";
|
|
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import nostrService from "../js/services/nostrService.js";
|
|
import { userBlocks } from "../js/userBlocks.js";
|
|
import { nostrClient } from "../js/nostrClientFacade.js";
|
|
import { relayManager } from "../js/relayManager.js";
|
|
import {
|
|
BLOCK_LIST_IDENTIFIER,
|
|
KIND_MUTE_LIST,
|
|
} from "../js/nostrEventSchemas.js";
|
|
|
|
const ACTOR_PUBKEY = "aaaa".repeat(16);
|
|
const BLOCKED_PUBKEY = "bbbb".repeat(16);
|
|
const ALLOWED_PUBKEY = "cccc".repeat(16);
|
|
|
|
function makeDmMessage({ senderPubkey, receiverPubkey, eventId, direction }) {
|
|
return {
|
|
ok: true,
|
|
direction: direction || "incoming",
|
|
sender: { pubkey: senderPubkey },
|
|
recipients: [{ pubkey: receiverPubkey }],
|
|
remotePubkey:
|
|
direction === "outgoing" ? receiverPubkey : senderPubkey,
|
|
actorPubkey: direction === "outgoing" ? senderPubkey : receiverPubkey,
|
|
timestamp: Math.floor(Date.now() / 1000),
|
|
plaintext: "Hello",
|
|
event: {
|
|
id: eventId || `evt-${Math.random().toString(36).slice(2)}`,
|
|
kind: 4,
|
|
pubkey: senderPubkey,
|
|
created_at: Math.floor(Date.now() / 1000),
|
|
tags: [["p", receiverPubkey]],
|
|
},
|
|
message: {
|
|
kind: 4,
|
|
pubkey: senderPubkey,
|
|
tags: [["p", receiverPubkey]],
|
|
},
|
|
};
|
|
}
|
|
|
|
describe("DM block/mute filtering", () => {
|
|
beforeEach(() => {
|
|
nostrService.dmMessages = [];
|
|
nostrService.dmMessageIndex = new Map();
|
|
nostrService.dmActorPubkey = ACTOR_PUBKEY;
|
|
nostrService.dmHydratedFromSnapshot = false;
|
|
});
|
|
|
|
afterEach(() => {
|
|
nostrService.setDmBlockChecker(null);
|
|
nostrService.dmMessages = [];
|
|
nostrService.dmMessageIndex = new Map();
|
|
nostrService.dmActorPubkey = null;
|
|
mock.reset();
|
|
});
|
|
|
|
describe("setDmBlockChecker", () => {
|
|
it("should accept a function", () => {
|
|
nostrService.setDmBlockChecker(() => false);
|
|
assert.equal(typeof nostrService._dmBlockChecker, "function");
|
|
});
|
|
|
|
it("should clear checker when passed null", () => {
|
|
nostrService.setDmBlockChecker(() => true);
|
|
nostrService.setDmBlockChecker(null);
|
|
assert.equal(nostrService._dmBlockChecker, null);
|
|
});
|
|
|
|
it("should clear checker when passed non-function", () => {
|
|
nostrService.setDmBlockChecker(() => true);
|
|
nostrService.setDmBlockChecker("not-a-function");
|
|
assert.equal(nostrService._dmBlockChecker, null);
|
|
});
|
|
});
|
|
|
|
describe("applyDirectMessage with block checker", () => {
|
|
it("should drop messages from blocked senders", () => {
|
|
nostrService.setDmBlockChecker((pubkey) => pubkey === BLOCKED_PUBKEY);
|
|
|
|
const message = makeDmMessage({
|
|
senderPubkey: BLOCKED_PUBKEY,
|
|
receiverPubkey: ACTOR_PUBKEY,
|
|
eventId: "blocked-msg-1",
|
|
direction: "incoming",
|
|
});
|
|
|
|
nostrService.applyDirectMessage(message, { reason: "test" });
|
|
|
|
assert.equal(nostrService.dmMessages.length, 0);
|
|
assert.equal(nostrService.dmMessageIndex.has("blocked-msg-1"), false);
|
|
});
|
|
|
|
it("should allow messages from non-blocked senders", () => {
|
|
nostrService.setDmBlockChecker((pubkey) => pubkey === BLOCKED_PUBKEY);
|
|
|
|
const message = makeDmMessage({
|
|
senderPubkey: ALLOWED_PUBKEY,
|
|
receiverPubkey: ACTOR_PUBKEY,
|
|
eventId: "allowed-msg-1",
|
|
direction: "incoming",
|
|
});
|
|
|
|
nostrService.applyDirectMessage(message, { reason: "test" });
|
|
|
|
assert.equal(nostrService.dmMessages.length, 1);
|
|
});
|
|
|
|
it("should allow all messages when no block checker is set", () => {
|
|
const message = makeDmMessage({
|
|
senderPubkey: BLOCKED_PUBKEY,
|
|
receiverPubkey: ACTOR_PUBKEY,
|
|
eventId: "no-checker-msg-1",
|
|
direction: "incoming",
|
|
});
|
|
|
|
nostrService.applyDirectMessage(message, { reason: "test" });
|
|
|
|
assert.equal(nostrService.dmMessages.length, 1);
|
|
});
|
|
|
|
it("should block outgoing messages to blocked recipients", () => {
|
|
nostrService.setDmBlockChecker((pubkey) => pubkey === BLOCKED_PUBKEY);
|
|
|
|
const message = makeDmMessage({
|
|
senderPubkey: ACTOR_PUBKEY,
|
|
receiverPubkey: BLOCKED_PUBKEY,
|
|
eventId: "outgoing-blocked-1",
|
|
direction: "outgoing",
|
|
});
|
|
|
|
nostrService.applyDirectMessage(message, { reason: "test" });
|
|
|
|
assert.equal(nostrService.dmMessages.length, 0);
|
|
});
|
|
});
|
|
|
|
describe("_isDmRemoteBlocked", () => {
|
|
it("should return false when no checker is set", () => {
|
|
const message = makeDmMessage({
|
|
senderPubkey: BLOCKED_PUBKEY,
|
|
receiverPubkey: ACTOR_PUBKEY,
|
|
direction: "incoming",
|
|
});
|
|
assert.equal(nostrService._isDmRemoteBlocked(message), false);
|
|
});
|
|
|
|
it("should return true for blocked remote pubkey", () => {
|
|
nostrService.setDmBlockChecker((pubkey) => pubkey === BLOCKED_PUBKEY);
|
|
const message = makeDmMessage({
|
|
senderPubkey: BLOCKED_PUBKEY,
|
|
receiverPubkey: ACTOR_PUBKEY,
|
|
direction: "incoming",
|
|
});
|
|
assert.equal(nostrService._isDmRemoteBlocked(message), true);
|
|
});
|
|
|
|
it("should return false for allowed remote pubkey", () => {
|
|
nostrService.setDmBlockChecker((pubkey) => pubkey === BLOCKED_PUBKEY);
|
|
const message = makeDmMessage({
|
|
senderPubkey: ALLOWED_PUBKEY,
|
|
receiverPubkey: ACTOR_PUBKEY,
|
|
direction: "incoming",
|
|
});
|
|
assert.equal(nostrService._isDmRemoteBlocked(message), false);
|
|
});
|
|
|
|
it("should handle checker that throws", () => {
|
|
nostrService.setDmBlockChecker(() => {
|
|
throw new Error("checker error");
|
|
});
|
|
const message = makeDmMessage({
|
|
senderPubkey: BLOCKED_PUBKEY,
|
|
receiverPubkey: ACTOR_PUBKEY,
|
|
direction: "incoming",
|
|
});
|
|
assert.equal(nostrService._isDmRemoteBlocked(message), false);
|
|
});
|
|
});
|
|
|
|
describe("integration with loaded user block list", () => {
|
|
it("should block DM contact for pubkeys loaded from both standard and legacy lists", async () => {
|
|
const actor = ACTOR_PUBKEY;
|
|
const legacyBlockedPubkey = "dddd".repeat(16);
|
|
const standardBlockedPubkey = "eeee".repeat(16);
|
|
|
|
const UserBlockListManager = userBlocks.constructor;
|
|
const manager = new UserBlockListManager();
|
|
|
|
const originalNostr = globalThis.window?.nostr;
|
|
const originalRelays = Array.isArray(nostrClient.relays)
|
|
? [...nostrClient.relays]
|
|
: nostrClient.relays;
|
|
const originalFetchListIncrementally = nostrClient.fetchListIncrementally;
|
|
const originalRelayEntries = relayManager.getEntries();
|
|
const originalPool = nostrClient.pool;
|
|
const originalGetSubscriptionManager = nostrClient.getSubscriptionManager;
|
|
|
|
const relayUrls = ["wss://dm-block-regression.example"];
|
|
relayManager.setEntries(
|
|
relayUrls.map((url) => ({ url, mode: "both" })),
|
|
{ allowEmpty: false, updateClient: false },
|
|
);
|
|
nostrClient.relays = relayUrls;
|
|
|
|
// Keep this test hermetic. Otherwise loadBlocks() opens a LIVE block-list
|
|
// subscription through nostrClient's real SimplePool aimed at the fake relay
|
|
// above, leaking TCP sockets + reconnect timers that outlive the test and
|
|
// hang the node:test runner AFTER every assertion has already passed.
|
|
// (1) A truthy stub pool stops loadBlocks from calling ensurePool(), which
|
|
// otherwise re-creates and connects a real pool when pool is falsy
|
|
// (userBlocks.js) — so nulling the pool alone is not enough.
|
|
// (2) A no-op subscription manager makes startBlockListSubscription() a
|
|
// no-op instead of a real relay REQ.
|
|
// The block MERGE under test still runs entirely through the stubbed
|
|
// fetchListIncrementally below. Both are restored in finally.
|
|
nostrClient.pool = { __mockForDmBlockFilterTest: true };
|
|
nostrClient.getSubscriptionManager = () => ({
|
|
subscribe: () => ({ close() {} }),
|
|
});
|
|
|
|
if (typeof globalThis.window === "undefined") {
|
|
globalThis.window = {};
|
|
}
|
|
|
|
globalThis.window.nostr = {
|
|
...(originalNostr || {}),
|
|
nip04: {
|
|
...((originalNostr && originalNostr.nip04) || {}),
|
|
decrypt: async () =>
|
|
JSON.stringify({
|
|
blockedPubkeys: [legacyBlockedPubkey],
|
|
}),
|
|
},
|
|
};
|
|
|
|
const standardEvent = {
|
|
id: "event-standard-mute",
|
|
kind: KIND_MUTE_LIST,
|
|
created_at: 2_000,
|
|
pubkey: actor,
|
|
content: "",
|
|
tags: [["p", standardBlockedPubkey]],
|
|
};
|
|
|
|
const legacyEvent = {
|
|
id: "event-legacy-blocks",
|
|
kind: KIND_MUTE_LIST,
|
|
created_at: 1_000,
|
|
pubkey: actor,
|
|
content: "legacy-encrypted",
|
|
tags: [["d", BLOCK_LIST_IDENTIFIER]],
|
|
};
|
|
|
|
nostrClient.fetchListIncrementally = async ({
|
|
kind,
|
|
dTag,
|
|
} = {}) => {
|
|
if (kind === KIND_MUTE_LIST && dTag === BLOCK_LIST_IDENTIFIER) {
|
|
return [legacyEvent];
|
|
}
|
|
if (kind === KIND_MUTE_LIST && !dTag) {
|
|
return [standardEvent];
|
|
}
|
|
if (kind === 30002 && dTag === BLOCK_LIST_IDENTIFIER) {
|
|
return [];
|
|
}
|
|
return [];
|
|
};
|
|
|
|
try {
|
|
await manager.loadBlocks(actor, { allowPermissionPrompt: false });
|
|
|
|
const waitForMergedBlocks = async (timeoutMs = 250) => {
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
if (
|
|
manager.isBlocked(legacyBlockedPubkey) &&
|
|
manager.isBlocked(standardBlockedPubkey)
|
|
) {
|
|
return;
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
};
|
|
await waitForMergedBlocks();
|
|
|
|
nostrService.setDmBlockChecker((pubkey) => manager.isBlocked(pubkey));
|
|
|
|
nostrService.applyDirectMessage(
|
|
makeDmMessage({
|
|
senderPubkey: legacyBlockedPubkey,
|
|
receiverPubkey: actor,
|
|
eventId: "blocked-legacy",
|
|
direction: "incoming",
|
|
}),
|
|
{ reason: "integration-test" },
|
|
);
|
|
|
|
nostrService.applyDirectMessage(
|
|
makeDmMessage({
|
|
senderPubkey: standardBlockedPubkey,
|
|
receiverPubkey: actor,
|
|
eventId: "blocked-standard",
|
|
direction: "incoming",
|
|
}),
|
|
{ reason: "integration-test" },
|
|
);
|
|
|
|
nostrService.applyDirectMessage(
|
|
makeDmMessage({
|
|
senderPubkey: ALLOWED_PUBKEY,
|
|
receiverPubkey: actor,
|
|
eventId: "allowed-contact",
|
|
direction: "incoming",
|
|
}),
|
|
{ reason: "integration-test" },
|
|
);
|
|
|
|
assert.equal(
|
|
nostrService.dmMessages.length,
|
|
1,
|
|
"only non-blocked contacts should remain in DM message state",
|
|
);
|
|
assert.equal(
|
|
nostrService.dmMessages[0]?.remotePubkey,
|
|
ALLOWED_PUBKEY,
|
|
"allowed contact should pass block filter",
|
|
);
|
|
} finally {
|
|
relayManager.setEntries(originalRelayEntries, {
|
|
allowEmpty: false,
|
|
updateClient: false,
|
|
});
|
|
nostrClient.relays = originalRelays;
|
|
nostrClient.fetchListIncrementally = originalFetchListIncrementally;
|
|
nostrClient.pool = originalPool;
|
|
nostrClient.getSubscriptionManager = originalGetSubscriptionManager;
|
|
if (typeof globalThis.window === "undefined") {
|
|
globalThis.window = {};
|
|
}
|
|
globalThis.window.nostr = originalNostr;
|
|
}
|
|
});
|
|
});
|
|
});
|