Merge pull request #1801 from PR0M3TH3AN/ai/tests-nip46-moderation-13594149868051412958

Add unit tests for Nip46RequestQueue and ModerationActionController
This commit is contained in:
thePR0M3TH3AN authored and GitHub committed 2026-01-27 08:10:59 -05:00
commit 5bea3590bd
2 files changed
+339

No files matched your search

+136
View File
@@ -0,0 +1,136 @@
import test from "node:test";
import assert from "node:assert/strict";
import { Nip46RequestQueue, NIP46_PRIORITY } from "../js/nostr/nip46Queue.js";
test("Nip46RequestQueue: processes tasks in FIFO order for same priority", async () => {
const queue = new Nip46RequestQueue({ minDelayMs: 0 });
const results = [];
const task1 = () => {
results.push(1);
return Promise.resolve(1);
};
const task2 = () => {
results.push(2);
return Promise.resolve(2);
};
const task3 = () => {
results.push(3);
return Promise.resolve(3);
};
await Promise.all([
queue.enqueue(task1),
queue.enqueue(task2),
queue.enqueue(task3),
]);
assert.deepEqual(results, [1, 2, 3]);
});
test("Nip46RequestQueue: respects priority levels", async () => {
const queue = new Nip46RequestQueue({ minDelayMs: 0 });
const results = [];
// Enqueue tasks. The queue starts processing immediately, so we need to
// delay the start slightly or ensure they are added before the first one finishes?
// Actually, since tasks are async, the first one starts running.
// But subsequent ones will be queued.
// To ensure they are all in the queue before processing picks the next one,
// we can make the first task slow.
let releaseFirst;
const firstTaskPromise = new Promise(resolve => releaseFirst = resolve);
const task1 = async () => {
await firstTaskPromise;
results.push("low");
return "low";
};
const task2 = async () => {
results.push("normal");
return "normal";
};
const task3 = async () => {
results.push("high");
return "high";
};
const p1 = queue.enqueue(task1, NIP46_PRIORITY.LOW);
// Give a tiny moment for p1 to start running (and block on firstTaskPromise)
await new Promise(r => setTimeout(r, 10));
const p2 = queue.enqueue(task2, NIP46_PRIORITY.NORMAL);
const p3 = queue.enqueue(task3, NIP46_PRIORITY.HIGH);
releaseFirst();
await Promise.all([p1, p2, p3]);
// task1 started first. While it was running, task2 and task3 were added.
// task3 has higher priority than task2, so it should run after task1 finishes.
assert.deepEqual(results, ["low", "high", "normal"]);
});
test("Nip46RequestQueue: respects minDelayMs", async () => {
const minDelayMs = 50;
const queue = new Nip46RequestQueue({ minDelayMs });
const start = Date.now();
const times = [];
const task = async () => {
times.push(Date.now());
};
await Promise.all([
queue.enqueue(task),
queue.enqueue(task),
queue.enqueue(task),
]);
assert.equal(times.length, 3);
const diff1 = times[1] - times[0];
const diff2 = times[2] - times[1];
// Allow some margin for execution time
assert.ok(diff1 >= minDelayMs - 10, `Delay ${diff1}ms should be >= ${minDelayMs}ms`);
assert.ok(diff2 >= minDelayMs - 10, `Delay ${diff2}ms should be >= ${minDelayMs}ms`);
});
test("Nip46RequestQueue: clear() rejects pending tasks", async () => {
const queue = new Nip46RequestQueue({ minDelayMs: 100 });
const results = [];
const errors = [];
// Start a task that blocks the queue for a bit
queue.enqueue(async () => {
await new Promise(r => setTimeout(r, 50));
results.push("finished");
});
// Add tasks that will be pending
const p2 = queue.enqueue(async () => {
results.push("should not run");
}).catch(e => errors.push(e.message));
const p3 = queue.enqueue(async () => {
results.push("should not run either");
}).catch(e => errors.push(e.message));
// Clear the queue immediately
queue.clear(new Error("Queue cleared manually"));
await Promise.allSettled([p2, p3]);
// wait for the first task to finish naturally (it was already running)
await new Promise(r => setTimeout(r, 100));
assert.ok(results.includes("finished"));
assert.ok(!results.includes("should not run"));
assert.equal(errors.length, 2);
assert.equal(errors[0], "Queue cleared manually");
});
@@ -0,0 +1,203 @@
import test from "node:test";
import assert from "node:assert/strict";
import ModerationActionController from "../../js/services/moderationActionController.js";
test("ModerationActionController: handles moderation override", async () => {
const overrides = [];
const events = [];
const mockServices = {
setModerationOverride: (descriptor, opts) => overrides.push({ descriptor, opts }),
clearModerationOverride: () => {},
userBlocks: {},
};
const mockSelectors = {
getVideoById: (id) => ({ id, moderation: { hidden: true } }),
getCurrentVideo: () => ({ id: "video1", moderation: { hidden: true } }),
};
const mockUi = {
refreshCardModerationUi: () => {},
dispatchModerationEvent: (name, detail) => events.push({ name, detail }),
};
const mockActions = {
decorateVideoModeration: (video) => { video.decorated = true; },
resumePlayback: () => {},
};
const mockAuth = {
normalizePubkey: (k) => k,
};
const controller = new ModerationActionController({
services: mockServices,
selectors: mockSelectors,
ui: mockUi,
actions: mockActions,
auth: mockAuth,
});
const video = { id: "video1", pubkey: "author1" };
const result = controller.handleOverride({ video });
assert.equal(result, true);
assert.equal(overrides.length, 1);
assert.equal(overrides[0].descriptor.eventId, "video1");
assert.equal(overrides[0].descriptor.authorPubkey, "author1");
assert.equal(events.length, 1);
assert.equal(events[0].name, "video:moderation-override");
assert.equal(events[0].detail.video.id, "video1");
});
test("ModerationActionController: blocks user and updates state", async () => {
const blocks = [];
const events = [];
const errors = [];
const mockServices = {
userBlocks: {
ensureLoaded: async () => {},
isBlocked: () => false,
addBlock: async (target, viewer) => {
blocks.push({ target, viewer });
return { already: false };
},
},
clearModerationOverride: () => {},
};
const mockAuth = {
isLoggedIn: () => true,
getViewerPubkey: () => "viewer1",
normalizePubkey: (k) => k,
};
const mockUi = {
refreshCardModerationUi: () => {},
dispatchModerationEvent: (name, detail) => events.push({ name, detail }),
};
const mockActions = {
showStatus: () => {},
showError: (msg) => errors.push(msg),
refreshVideos: async () => {},
decorateVideoModeration: () => {},
};
const controller = new ModerationActionController({
services: mockServices,
auth: mockAuth,
ui: mockUi,
actions: mockActions,
});
const video = { id: "video1", pubkey: "spammer" };
const result = await controller.handleBlock({ video });
assert.equal(result, true);
assert.equal(blocks.length, 1);
assert.equal(blocks[0].target, "spammer");
assert.equal(blocks[0].viewer, "viewer1");
// Should trigger block and hide events
assert.ok(events.find(e => e.name === "video:moderation-block"));
assert.ok(events.find(e => e.name === "video:moderation-hide"));
assert.equal(errors.length, 0);
});
test("ModerationActionController: prevents blocking self", async () => {
const errors = [];
const mockAuth = {
isLoggedIn: () => true,
getViewerPubkey: () => "me",
normalizePubkey: (k) => k,
};
const mockServices = {
userBlocks: { ensureLoaded: async () => {} },
};
const mockActions = {
showError: (msg) => errors.push(msg),
};
const controller = new ModerationActionController({
services: mockServices,
auth: mockAuth,
actions: mockActions,
});
const video = { id: "video1", pubkey: "me" };
const result = await controller.handleBlock({ video });
assert.equal(result, false);
assert.ok(errors[0].includes("cannot block yourself"));
});
test("ModerationActionController: requires login to block", async () => {
const statuses = [];
const mockAuth = {
isLoggedIn: () => false,
};
const mockServices = {
userBlocks: {},
};
const mockActions = {
showStatus: (msg) => statuses.push(msg),
};
const controller = new ModerationActionController({
services: mockServices,
auth: mockAuth,
actions: mockActions,
});
const video = { id: "video1", pubkey: "someone" };
const result = await controller.handleBlock({ video });
assert.equal(result, false);
assert.ok(statuses[0].includes("Log in"));
});
test("ModerationActionController: handles hide action", async () => {
const events = [];
const mockServices = {
clearModerationOverride: () => {},
};
const mockSelectors = {
getVideoById: (id) => ({ id }),
getCurrentVideo: () => null,
};
const mockUi = {
refreshCardModerationUi: () => {},
dispatchModerationEvent: (name, detail) => events.push({ name, detail }),
};
const mockActions = {
decorateVideoModeration: () => {},
};
const controller = new ModerationActionController({
services: mockServices,
selectors: mockSelectors,
ui: mockUi,
actions: mockActions,
});
const video = { id: "video1" };
const result = controller.handleHide({ video });
assert.equal(result, true);
assert.equal(events.length, 1);
assert.equal(events[0].name, "video:moderation-hide");
});