diff --git a/tests/moderation/mocks/bootstrap-trust-seeds-loader.mjs b/tests/moderation/mocks/bootstrap-trust-seeds-loader.mjs new file mode 100644 index 00000000..b820ddcf --- /dev/null +++ b/tests/moderation/mocks/bootstrap-trust-seeds-loader.mjs @@ -0,0 +1,36 @@ +const ACCESS_CONTROL_MOCK_URL = "bitvid:tests:access-control"; +const MODERATION_SERVICE_MOCK_URL = "bitvid:tests:moderation-service"; +const BOOTSTRAP_PATH_SUFFIX = "/js/bootstrap.js"; + +export function resolve(specifier, context, defaultResolve) { + const parent = context?.parentURL || ""; + if (parent.endsWith(BOOTSTRAP_PATH_SUFFIX)) { + if (specifier === "./accessControl.js") { + return { url: ACCESS_CONTROL_MOCK_URL, shortCircuit: true }; + } + if (specifier === "./services/moderationService.js") { + return { url: MODERATION_SERVICE_MOCK_URL, shortCircuit: true }; + } + } + return defaultResolve(specifier, context, defaultResolve); +} + +export async function load(url, context, defaultLoad) { + if (url === ACCESS_CONTROL_MOCK_URL) { + const source = `const mock = globalThis.__bootstrapAccessControlMock || null; +export const accessControl = mock; +export default accessControl; +`; + return { format: "module", source, shortCircuit: true }; + } + + if (url === MODERATION_SERVICE_MOCK_URL) { + const source = `const mock = globalThis.__bootstrapModerationServiceMock || {}; +export const ModerationService = class {}; +export default mock; +`; + return { format: "module", source, shortCircuit: true }; + } + + return defaultLoad(url, context, defaultLoad); +} diff --git a/tests/moderation/trust-seeds.test.mjs b/tests/moderation/trust-seeds.test.mjs index 0fef25df..06901be9 100644 --- a/tests/moderation/trust-seeds.test.mjs +++ b/tests/moderation/trust-seeds.test.mjs @@ -7,6 +7,8 @@ import { createModerationServiceHarness, createReportEvent, } from "../helpers/moderation-test-helpers.mjs"; +import { ADMIN_SUPER_NPUB } from "../../js/config.js"; +import { register } from "node:module"; const [PRIMARY_SEED_NPUB] = DEFAULT_TRUST_SEED_NPUBS; @@ -85,22 +87,137 @@ test("trusted seeds contribute to trusted report counts", (t) => { assert.deepEqual(reporters.map((entry) => entry.pubkey), [seedHex]); }); -test("whitelist entries augment trusted seeds", (t) => { +test("moderator seeds populate trusted contacts", (t) => { withMockedNostrTools(t); - const whitelistHex = "a".repeat(64); - const whitelistNpub = `npub${whitelistHex}`; + const moderatorHex = "d".repeat(64); + const moderatorNpub = `npub${moderatorHex}`; const { service } = createModerationServiceHarness(t); - service.setTrustedSeeds([...DEFAULT_TRUST_SEED_NPUBS, whitelistNpub]); + service.setTrustedSeeds([moderatorNpub]); assert.ok( - service.trustedSeedContacts.has(whitelistHex), - "whitelist npubs should populate trusted seed contacts" + service.trustedSeedContacts.has(moderatorHex), + "moderator seeds should populate trusted seed contacts" ); assert.ok( - service.trustedContacts.has(whitelistHex), - "whitelist npubs should populate trusted contacts" + service.trustedContacts.has(moderatorHex), + "moderator seeds should populate trusted contacts" + ); +}); + +test("bootstrap seeds track editor roster and ignore whitelist-only changes", async (t) => { + register(new URL("./mocks/bootstrap-trust-seeds-loader.mjs", import.meta.url)); + + t.after(() => { + delete globalThis.__bootstrapAccessControlMock; + delete globalThis.__bootstrapModerationServiceMock; + }); + + let currentEditors = [ + "npub1moderatorexamplemoderatorexamplemoderatorexample1", + ]; + const whitelistListeners = new Set(); + const editorListeners = new Set(); + let ensureReadyCalls = 0; + let getEditorsCalls = 0; + + const accessControlMock = { + ensureReady: async () => { + ensureReadyCalls += 1; + }, + getEditors: () => { + getEditorsCalls += 1; + return currentEditors; + }, + onWhitelistChange: (listener) => { + if (typeof listener === "function") { + whitelistListeners.add(listener); + } + return () => { + whitelistListeners.delete(listener); + }; + }, + onEditorsChange: (listener) => { + if (typeof listener === "function") { + editorListeners.add(listener); + } + return () => { + editorListeners.delete(listener); + }; + }, + }; + + const setTrustedSeedsCalls = []; + const moderationServiceMock = { + setTrustedSeeds: (seeds) => { + const snapshot = []; + if (seeds && typeof seeds[Symbol.iterator] === "function") { + for (const value of seeds) { + snapshot.push(value); + } + } + setTrustedSeedsCalls.push(snapshot); + }, + }; + + globalThis.__bootstrapAccessControlMock = accessControlMock; + globalThis.__bootstrapModerationServiceMock = moderationServiceMock; + + await import("../../js/bootstrap.js"); + + assert.equal(ensureReadyCalls, 1); + assert.ok( + getEditorsCalls >= 1, + "bootstrap should query editors during initialization" + ); + + assert.equal(setTrustedSeedsCalls.length, 1); + const [initialSeeds] = setTrustedSeedsCalls; + assert.ok( + initialSeeds.includes(ADMIN_SUPER_NPUB), + "super admin should be included in trusted seed roster" + ); + assert.ok( + initialSeeds.some((value) => currentEditors.includes(value)), + "initial editor roster should seed trust" + ); + + const whitelistListener = Array.from(whitelistListeners)[0]; + if (typeof whitelistListener === "function") { + whitelistListener(["npub1whitelistentrywhitelistentrywhitelistentry"]); + } + + assert.equal( + setTrustedSeedsCalls.length, + 2, + "whitelist change should retrigger trusted seed derivation" + ); + const [, whitelistSeeds] = setTrustedSeedsCalls; + assert.deepEqual( + new Set(whitelistSeeds), + new Set(initialSeeds), + "whitelist updates should not introduce new trusted seeds" + ); + + currentEditors = [ + "npub1newmoderatorexamplemoderatorexamplemoderator", + "npub1secondmoderatorexamplemoderatorexamplemod", + ]; + for (const listener of editorListeners) { + listener(currentEditors); + } + + assert.equal( + setTrustedSeedsCalls.length, + 3, + "editor change should recompute trusted seed roster" + ); + const latestSeeds = setTrustedSeedsCalls[2]; + assert.deepEqual( + new Set(latestSeeds), + new Set([ADMIN_SUPER_NPUB, ...currentEditors]), + "trusted seeds should match super admin plus latest editors" ); });