mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
Merge pull request #1801 from PR0M3TH3AN/ai/tests-nip46-moderation-13594149868051412958
Add unit tests for Nip46RequestQueue and ModerationActionController
This commit is contained in:
2 files changed
+339
No files matched your search
@@ -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");
|
||||
});
|
||||
Reference in new issue
Block a user