Merge pull request #706 from PR0M3TH3AN/codex/expand-tests-for-moderation-and-reporting

Expand moderation coverage and add video card override test
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2025-10-20 10:50:07 -04:00
commit 6ac5d87886
4 files changed
+580 -89

No files matched your search

+205
View File
@@ -0,0 +1,205 @@
import "../test-helpers/setup-localstorage.mjs";
import { ModerationService } from "../../js/services/moderationService.js";
const DEFAULT_CONTACT_OWNER = "f".repeat(64);
export function withMockedNostrTools(t) {
const hadWindow = typeof globalThis.window !== "undefined";
const previousWindow = hadWindow ? globalThis.window : undefined;
if (!hadWindow) {
globalThis.window = {};
}
const previousGlobalTools = globalThis.NostrTools;
const previousWindowTools = globalThis.window?.NostrTools;
const nip19 = {
npubEncode(hex) {
if (typeof hex !== "string" || !hex) {
throw new Error("invalid hex");
}
return `npub${hex}`;
},
decode(value) {
if (typeof value !== "string" || !value.startsWith("npub")) {
throw new Error("invalid npub");
}
return { type: "npub", data: value.slice(4) };
},
};
globalThis.NostrTools = { nip19 };
if (globalThis.window) {
globalThis.window.NostrTools = globalThis.NostrTools;
}
t.after(() => {
if (typeof previousGlobalTools === "undefined") {
delete globalThis.NostrTools;
} else {
globalThis.NostrTools = previousGlobalTools;
}
if (globalThis.window) {
if (typeof previousWindowTools === "undefined") {
delete globalThis.window.NostrTools;
} else {
globalThis.window.NostrTools = previousWindowTools;
}
}
if (!hadWindow) {
delete globalThis.window;
} else {
globalThis.window = previousWindow;
}
});
}
export function createModerationServiceHarness(t, { userBlocks, accessControl } = {}) {
const service = new ModerationService({
logger: () => {},
userBlocks:
userBlocks || {
async ensureLoaded() {},
isBlocked() {
return false;
},
},
accessControl: accessControl || null,
});
const tracked = new Map();
function capture(eventName) {
if (!tracked.has(eventName)) {
tracked.set(eventName, []);
}
const records = tracked.get(eventName);
const unsubscribe = service.on(eventName, (detail) => {
records.push(detail);
});
if (typeof unsubscribe === "function") {
t.after(() => {
try {
unsubscribe();
} catch {
// noop for tests
}
});
}
return records;
}
return { service, capture, trackedEvents: tracked };
}
export function createReportEvent({
id,
reporter,
eventId,
createdAt = Math.floor(Date.now() / 1000),
type = "nudity",
} = {}) {
const reportId =
typeof id === "string" && id.length
? id
: `${Math.random().toString(16).slice(2)}${"0".repeat(64)}`.slice(0, 64);
const reporterPubkey = typeof reporter === "string" && reporter ? reporter : "";
const targetEventId = typeof eventId === "string" && eventId ? eventId : "";
const normalizedType = typeof type === "string" && type ? type : "nudity";
return {
kind: 1984,
id: reportId,
pubkey: reporterPubkey,
created_at: Number.isFinite(createdAt) ? Math.floor(createdAt) : Math.floor(Date.now() / 1000),
tags: [
["e", targetEventId],
["report", normalizedType],
],
content: "fixture report",
};
}
export function createContactEvent({
owner = DEFAULT_CONTACT_OWNER,
contacts = [],
createdAt = Math.floor(Date.now() / 1000),
id,
} = {}) {
const eventId =
typeof id === "string" && id.length
? id
: `${Math.random().toString(16).slice(2)}${"1".repeat(64)}`.slice(0, 64);
const ownerHex = typeof owner === "string" && owner ? owner : DEFAULT_CONTACT_OWNER;
const tags = contacts
.filter((value) => typeof value === "string" && value)
.map((value) => ["p", value]);
return {
kind: 3,
id: eventId,
pubkey: ownerHex,
created_at: Number.isFinite(createdAt) ? Math.floor(createdAt) : Math.floor(Date.now() / 1000),
tags,
content: "",
};
}
export function applyTrustedContacts(service, contacts = []) {
const event = createContactEvent({ contacts });
service.applyContactEvent(event);
return event;
}
let cachedApplicationClass = null;
async function getApplicationClass() {
if (!cachedApplicationClass) {
if (typeof globalThis.self === "undefined") {
globalThis.self = globalThis;
}
if (typeof globalThis.WebSocket === "undefined") {
globalThis.WebSocket = class {
constructor() {}
close() {}
addEventListener() {}
removeEventListener() {}
send() {}
};
}
const module = await import("../../js/app.js");
cachedApplicationClass = module.Application || module.default;
}
return cachedApplicationClass;
}
export async function createModerationAppHarness() {
const Application = await getApplicationClass();
const app = Object.create(Application.prototype);
Object.defineProperty(app, "__profiles", {
value: new Map(),
writable: true,
configurable: true,
});
app.getProfileCacheEntry = (pubkey) => {
if (!pubkey || typeof pubkey !== "string") {
return null;
}
return app.__profiles.get(pubkey) || null;
};
app.decorateVideoModeration = Application.prototype.decorateVideoModeration;
app.deriveModerationReportType = Application.prototype.deriveModerationReportType;
app.deriveModerationTrustedCount = Application.prototype.deriveModerationTrustedCount;
app.getReporterDisplayName = Application.prototype.getReporterDisplayName;
app.handleModerationOverride = Application.prototype.handleModerationOverride;
app.videosMap = new Map();
app.currentVideo = null;
return app;
}
+129
View File
@@ -166,3 +166,132 @@ test("moderation stage annotates trusted mute metadata", async () => {
assert.ok(trustedMuteReason);
assert.equal(trustedMuteReason.videoId, "muted");
});
test("moderation stage propagates whitelist, muters, and threshold updates", async () => {
const whitelistedHex = "a".repeat(64);
const mutedHex = "b".repeat(64);
const muterHex = "c".repeat(64);
const state = {
whitelistHex: new Set([whitelistedHex]),
summaryById: new Map([
[
"muted-video",
{
eventId: "muted-video",
totalTrusted: 3,
types: { nudity: { trusted: 3, total: 3, latest: 1_700_000_000 } },
updatedAt: 1_700_000_100,
},
],
]),
trustedCounts: new Map([
["whitelisted-video", 4],
["muted-video", 3],
]),
reportersById: new Map([
["muted-video", [{ pubkey: muterHex, latest: 1_700_000_050 }]],
]),
mutedAuthors: new Set([mutedHex]),
mutersByAuthor: new Map([[mutedHex, [muterHex]]]),
};
const service = {
async refreshViewerFromClient() {},
async setActiveEventIds() {},
getAdminListSnapshot() {
return {
whitelist: new Set([`npub${whitelistedHex}`]),
whitelistHex: new Set(state.whitelistHex),
blacklist: new Set(),
blacklistHex: new Set(),
};
},
getAccessControlStatus(identifier) {
const normalized = typeof identifier === "string" ? identifier.trim().toLowerCase() : "";
return {
hex: normalized,
whitelisted: state.whitelistHex.has(normalized),
blacklisted: false,
};
},
getTrustedReportSummary(videoId) {
return state.summaryById.get(videoId) || null;
},
trustedReportCount(videoId) {
return state.trustedCounts.get(videoId) || 0;
},
getTrustedReporters(videoId) {
return state.reportersById.get(videoId) || [];
},
isAuthorMutedByTrusted(pubkey) {
return state.mutedAuthors.has(pubkey);
},
getTrustedMutersForAuthor(pubkey) {
return state.mutersByAuthor.get(pubkey) || [];
},
};
const stage = createModerationStage({
service,
autoplayThreshold: 2,
blurThreshold: 3,
reportType: "nudity",
});
const items = [
{ video: { id: "whitelisted-video", pubkey: whitelistedHex }, metadata: {} },
{ video: { id: "muted-video", pubkey: mutedHex }, metadata: {} },
];
const reasons = [];
const context = {
feedName: "discovery",
addWhy(detail) {
reasons.push(detail);
return detail;
},
log() {},
};
const firstPass = await stage(items, context);
const whitelistedItem = firstPass.find((entry) => entry.video.id === "whitelisted-video");
const mutedItem = firstPass.find((entry) => entry.video.id === "muted-video");
assert.ok(whitelistedItem);
assert.ok(mutedItem);
assert.equal(whitelistedItem.metadata.moderation.adminWhitelist, true);
assert.equal(whitelistedItem.metadata.moderation.blockAutoplay, false);
assert.equal(whitelistedItem.metadata.moderation.blurThumbnail, false);
assert.equal(whitelistedItem.video.moderation.adminWhitelistBypass, true);
assert.equal(mutedItem.metadata.moderation.trustedMuted, true);
assert.deepEqual(mutedItem.metadata.moderation.trustedMuters, [muterHex]);
assert.equal(mutedItem.metadata.moderation.blockAutoplay, true);
assert.equal(mutedItem.metadata.moderation.blurThumbnail, true);
assert.equal(mutedItem.video.moderation.trustedMuted, true);
assert.deepEqual(mutedItem.video.moderation.trustedMuters, [muterHex]);
state.summaryById.set("muted-video", {
eventId: "muted-video",
totalTrusted: 1,
types: { nudity: { trusted: 1, total: 1, latest: 1_700_000_500 } },
updatedAt: 1_700_000_600,
});
state.trustedCounts.set("muted-video", 1);
state.mutersByAuthor.set(mutedHex, []);
state.mutedAuthors.delete(mutedHex);
const secondPass = await stage(firstPass, context);
const updatedMuted = secondPass.find((entry) => entry.video.id === "muted-video");
assert.ok(updatedMuted);
assert.equal(updatedMuted.metadata.moderation.trustedMuted, false);
assert.deepEqual(updatedMuted.metadata.moderation.trustedMuters, []);
assert.equal(updatedMuted.metadata.moderation.blockAutoplay, false);
assert.equal(updatedMuted.metadata.moderation.blurThumbnail, false);
assert.equal(updatedMuted.video.moderation.trustedMuted, false);
assert.equal(updatedMuted.video.moderation.blockAutoplay, false);
assert.equal(updatedMuted.video.moderation.blurThumbnail, false);
});
+82 -89
View File
@@ -1,71 +1,14 @@
import test from "node:test";
import assert from "node:assert/strict";
import { ModerationService } from "../../js/services/moderationService.js";
import {
withMockedNostrTools,
createModerationServiceHarness,
createReportEvent,
applyTrustedContacts,
} from "../helpers/moderation-test-helpers.mjs";
const nip19 = {
npubEncode(hex) {
if (typeof hex !== "string" || !hex) {
throw new Error("invalid hex");
}
return `npub${hex}`;
},
decode(value) {
if (typeof value !== "string" || !value.startsWith("npub")) {
throw new Error("invalid npub");
}
return { type: "npub", data: value.slice(4) };
},
};
function withMockedNostrTools(t) {
const hadWindow = typeof globalThis.window !== "undefined";
const previousWindow = hadWindow ? globalThis.window : undefined;
if (!hadWindow) {
globalThis.window = {};
}
const previousGlobalTools = globalThis.NostrTools;
const previousWindowTools = globalThis.window?.NostrTools;
globalThis.NostrTools = { nip19 };
if (globalThis.window) {
globalThis.window.NostrTools = globalThis.NostrTools;
}
t.after(() => {
if (typeof previousGlobalTools === "undefined") {
delete globalThis.NostrTools;
} else {
globalThis.NostrTools = previousGlobalTools;
}
if (globalThis.window) {
if (typeof previousWindowTools === "undefined") {
delete globalThis.window.NostrTools;
} else {
globalThis.window.NostrTools = previousWindowTools;
}
}
if (!hadWindow) {
delete globalThis.window;
} else {
globalThis.window = previousWindow;
}
});
}
function createReport({ id, reporter, eventId, createdAt, type = "nudity" }) {
return {
kind: 1984,
id,
pubkey: reporter,
created_at: createdAt,
tags: [
["e", eventId],
["report", type],
],
content: "fixture report",
};
function createReport(options) {
return createReportEvent(options);
}
test("trustedReportCount dedupes multiple reports from the same trusted reporter", (t) => {
@@ -74,17 +17,9 @@ test("trustedReportCount dedupes multiple reports from the same trusted reporter
const eventId = "f".repeat(64);
const reporterHex = "a".repeat(64);
const service = new ModerationService({
logger: () => {},
userBlocks: {
async ensureLoaded() {},
isBlocked() {
return false;
},
},
});
const { service } = createModerationServiceHarness(t);
service.trustedContacts = new Set([reporterHex]);
applyTrustedContacts(service, [reporterHex]);
const firstReport = createReport({
id: "1".repeat(64),
@@ -116,17 +51,16 @@ test("trustedReportCount ignores reports from muted or blocked reporters", (t) =
const mutedReporter = "b".repeat(64);
const trustedReporter = "c".repeat(64);
const service = new ModerationService({
logger: () => {},
userBlocks: {
async ensureLoaded() {},
isBlocked(pubkey) {
return pubkey === mutedReporter;
},
const userBlocks = {
async ensureLoaded() {},
isBlocked(pubkey) {
return pubkey === mutedReporter;
},
});
};
service.trustedContacts = new Set([mutedReporter, trustedReporter]);
const { service } = createModerationServiceHarness(t, { userBlocks });
applyTrustedContacts(service, [mutedReporter, trustedReporter]);
const mutedReport = createReport({
id: "3".repeat(64),
@@ -182,12 +116,9 @@ test("blocking and unblocking reporters recomputes trusted summaries", async (t)
},
};
const service = new ModerationService({
logger: () => {},
userBlocks: userBlocksMock,
});
const { service } = createModerationServiceHarness(t, { userBlocks: userBlocksMock });
service.trustedContacts = new Set([reporterHex]);
applyTrustedContacts(service, [reporterHex]);
const report = createReport({
id: "b".repeat(64),
@@ -235,3 +166,65 @@ test("blocking and unblocking reporters recomputes trusted summaries", async (t)
assert.equal(service.trustedReportCount(eventId, "nudity"), 1);
assert.deepEqual(setActiveCalls, [[eventId], [eventId]]);
});
test("trustedReportCount only counts eligible F1 reporters and admin whitelist", (t) => {
withMockedNostrTools(t);
const eventId = "c".repeat(64);
const viewerBlocked = "1".repeat(64);
const f1Reporter = "2".repeat(64);
const f2Reporter = "3".repeat(64);
const adminWhitelisted = "4".repeat(64);
const adminBlacklisted = "5".repeat(64);
const userBlocks = {
async ensureLoaded() {},
isBlocked(pubkey) {
return pubkey === viewerBlocked;
},
};
const accessControl = {
getWhitelist() {
return [`npub${adminWhitelisted}`];
},
getBlacklist() {
return [`npub${adminBlacklisted}`];
},
};
const { service } = createModerationServiceHarness(t, { userBlocks, accessControl });
applyTrustedContacts(service, [f1Reporter, viewerBlocked]);
const reports = [
{ id: "r1".repeat(32), reporter: f1Reporter, createdAt: 1_700_000_000 },
{ id: "r2".repeat(32), reporter: viewerBlocked, createdAt: 1_700_000_050 },
{ id: "r3".repeat(32), reporter: f2Reporter, createdAt: 1_700_000_100 },
{ id: "r4".repeat(32), reporter: adminWhitelisted, createdAt: 1_700_000_200 },
{ id: "r5".repeat(32), reporter: adminBlacklisted, createdAt: 1_700_000_300 },
];
for (const entry of reports) {
service.ingestReportEvent(
createReport({
id: entry.id.padEnd(64, entry.id[0]),
reporter: entry.reporter,
eventId,
createdAt: entry.createdAt,
}),
);
}
const summary = service.getTrustedReportSummary(eventId);
assert.equal(summary.totalTrusted, 2);
assert.equal(summary.types.nudity.total, 3);
assert.equal(summary.types.nudity.trusted, 2);
assert.equal(service.trustedReportCount(eventId, "nudity"), 2);
const reporters = service.getTrustedReporters(eventId, "nudity");
assert.deepEqual(
reporters.map((entry) => entry.pubkey).sort(),
[adminWhitelisted, f1Reporter].sort(),
);
});
+164
View File
@@ -0,0 +1,164 @@
import test from "node:test";
import assert from "node:assert/strict";
import { JSDOM } from "jsdom";
import "../test-helpers/setup-localstorage.mjs";
import { VideoCard } from "../../js/ui/components/VideoCard.js";
import { clearModerationOverride, setModerationOverride } from "../../js/state/cache.js";
import {
withMockedNostrTools,
createModerationAppHarness,
} from "../helpers/moderation-test-helpers.mjs";
function setupDom(t) {
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
url: "https://example.com",
pretendToBeVisual: true,
});
const { window } = dom;
const previous = new Map();
const keys = [
"window",
"document",
"HTMLElement",
"Element",
"Node",
"MouseEvent",
"CustomEvent",
"ResizeObserver",
"requestAnimationFrame",
"cancelAnimationFrame",
"navigator",
];
keys.forEach((key) => {
const hadValue = Object.prototype.hasOwnProperty.call(globalThis, key);
previous.set(key, hadValue ? globalThis[key] : Symbol.for("__undefined__"));
if (key === "requestAnimationFrame" && typeof window[key] !== "function") {
window.requestAnimationFrame = (cb) => setTimeout(() => cb(Date.now()), 16);
} else if (key === "cancelAnimationFrame" && typeof window[key] !== "function") {
window.cancelAnimationFrame = (id) => clearTimeout(id);
} else if (key === "ResizeObserver" && typeof window[key] !== "function") {
window.ResizeObserver = class {
observe() {}
unobserve() {}
disconnect() {}
};
}
globalThis[key] = window[key];
});
if (!window.requestAnimationFrame) {
window.requestAnimationFrame = (cb) => setTimeout(() => cb(Date.now()), 16);
globalThis.requestAnimationFrame = window.requestAnimationFrame;
}
if (!window.cancelAnimationFrame) {
window.cancelAnimationFrame = (id) => clearTimeout(id);
globalThis.cancelAnimationFrame = window.cancelAnimationFrame;
}
if (!window.ResizeObserver) {
class ResizeObserverStub {
observe() {}
unobserve() {}
disconnect() {}
}
window.ResizeObserver = ResizeObserverStub;
globalThis.ResizeObserver = ResizeObserverStub;
}
t.after(() => {
dom.window.close();
keys.forEach((key) => {
const previousValue = previous.get(key);
if (previousValue === Symbol.for("__undefined__")) {
delete globalThis[key];
} else {
globalThis[key] = previousValue;
}
});
});
return { window, document: window.document };
}
test("VideoCard renders moderation badges and respects viewer override", async (t) => {
const { document } = setupDom(t);
withMockedNostrTools(t);
const app = await createModerationAppHarness();
const videoId = "a".repeat(64);
const video = {
id: videoId,
title: "Blurred Clip",
pubkey: "d".repeat(64),
moderation: {
blockAutoplay: true,
blurThumbnail: true,
reportType: "nudity",
trustedCount: 3,
summary: {
eventId: videoId,
totalTrusted: 3,
types: { nudity: { trusted: 3, total: 3, latest: 1_700_000_000 } },
updatedAt: 1_700_000_100,
},
trustedReporters: [{ pubkey: "e".repeat(64), latest: 1_700_000_050 }],
adminWhitelist: false,
trustedMuted: false,
},
};
t.after(() => {
clearModerationOverride(video.id, { persist: false });
});
app.videosMap.set(video.id, video);
app.currentVideo = video;
app.decorateVideoModeration(video);
const card = new VideoCard({
document,
video,
formatters: {
formatTimeAgo: () => "moments ago",
},
helpers: {
isMagnetSupported: () => false,
},
});
document.body.appendChild(card.getRoot());
assert.equal(card.getRoot().dataset.autoplayPolicy, "blocked");
assert.equal(card.thumbnailEl.dataset.thumbnailState, "blurred");
assert.equal(card.getRoot().dataset.moderationReportCount, "3");
assert.ok(card.moderationBadgeEl);
assert.equal(card.moderationBadgeEl.dataset.moderationState, "blocked");
assert.ok(card.moderationActionButton);
card.onModerationOverride = ({ video: overrideVideo }) => {
app.videosMap.set(overrideVideo.id, overrideVideo);
app.currentVideo = overrideVideo;
setModerationOverride(overrideVideo.id, { showAnyway: true, updatedAt: Date.now() }, { persist: false });
app.decorateVideoModeration(overrideVideo);
card.refreshModerationUi();
return true;
};
card.moderationActionButton.click();
await new Promise((resolve) => setTimeout(resolve, 0));
const contextAfterOverride = card.getModerationContext();
assert.equal(contextAfterOverride.overrideActive, true);
assert.equal(contextAfterOverride.activeBlockAutoplay, false);
assert.equal(card.getRoot().dataset.autoplayPolicy, undefined);
assert.equal(card.getRoot().dataset.moderationOverride, "show-anyway");
assert.equal(card.thumbnailEl.dataset.thumbnailState, undefined);
assert.equal(card.moderationBadgeEl.dataset.moderationState, "override");
assert.ok(card.moderationBadgeEl.textContent.includes("Showing despite"));
});