Files
bitvid/tests/nostr/comment-events.test.mjs
T

357 lines
9.6 KiB
JavaScript

import test from "node:test";
import assert from "node:assert/strict";
import {
COMMENT_EVENT_KIND,
publishComment,
listVideoComments,
subscribeVideoComments,
} from "../../js/nostr/commentEvents.js";
function createMockClient({
actorPubkey = "actor-pubkey",
sessionPrivateKey = "session-private",
relays = ["wss://primary"],
publishResults = [],
} = {}) {
const publishCalls = [];
const pool = {
publish(urls, event) {
publishCalls.push({ urls, event });
const result = publishResults.shift();
return {
on(name, handler) {
if (name === "ok" && (!result || result.success !== false)) {
handler();
}
if (name === "failed" && result && result.success === false) {
handler(result.error || new Error("publish failed"));
}
},
};
},
list: async () => [],
sub: () => ({ on() {}, unsub() {} }),
};
const ensureSessionActorCalls = [];
const client = {
pool,
relays,
pubkey: actorPubkey,
sessionActor: {
pubkey: actorPubkey,
privateKey: sessionPrivateKey,
},
ensureSessionActor: async (forceRefresh = false) => {
ensureSessionActorCalls.push(forceRefresh);
if (forceRefresh && !client.sessionActor) {
client.sessionActor = {
pubkey: actorPubkey,
privateKey: sessionPrivateKey,
};
}
return actorPubkey;
},
ensureExtensionPermissions: async () => ({ ok: true }),
};
return { client, pool, publishCalls, ensureSessionActorCalls };
}
function buildCommentEventTags(event) {
return Array.isArray(event?.tags)
? event.tags.map((tag) => Array.isArray(tag) ? [...tag] : tag)
: [];
}
test("publishComment prefers active signer when available", async () => {
const {
client,
publishCalls,
} = createMockClient({
relays: ["wss://relay.one", "wss://relay.two"],
});
const signerCalls = [];
const signer = {
type: "extension",
pubkey: client.pubkey,
async signEvent(event) {
signerCalls.push(event);
return { ...event, id: "signed-by-active" };
},
};
let privateKeySignCalls = 0;
const result = await publishComment(
client,
{
videoEventId: "video-event-id",
videoEventRelay: "wss://video-relay",
videoDefinitionAddress: "30078:deadbeef:clip",
videoDefinitionRelay: "wss://definition-relay",
parentCommentId: "parent-comment-id",
threadParticipantPubkey: "thread-participant",
},
{
content: "Hello world",
relays: ["wss://custom"],
},
{
resolveActiveSigner: () => signer,
shouldRequestExtensionPermissions: () => true,
signEventWithPrivateKey: () => {
privateKeySignCalls += 1;
return null;
},
DEFAULT_NIP07_PERMISSION_METHODS: ["signEvent"],
},
);
assert.equal(result.ok, true, "publish should succeed when relay accepts the event");
assert.deepEqual(
result.acceptedRelays,
["wss://custom"],
"accepted relays should reflect the sanitized relay list",
);
assert.equal(
publishCalls.length,
1,
"exactly one publish call should be issued",
);
assert.deepEqual(
publishCalls[0].urls,
["wss://custom"],
"publish should target the caller supplied relays",
);
assert.equal(privateKeySignCalls, 0, "session key signing should be skipped when active signer succeeds");
assert.equal(signerCalls.length, 1, "active signer should sign the event once");
const eventTags = buildCommentEventTags(signerCalls[0]);
const eTags = eventTags.filter((tag) => Array.isArray(tag) && tag[0] === "e");
const aTags = eventTags.filter((tag) => Array.isArray(tag) && tag[0] === "a");
assert.deepEqual(
eTags,
[
["e", "video-event-id", "wss://video-relay"],
["e", "parent-comment-id"],
],
"comment event should include both video and parent pointers",
);
assert.deepEqual(
aTags,
[["a", "30078:deadbeef:clip", "wss://definition-relay"]],
"comment event should target the video definition via #a tag",
);
});
test("publishComment falls back to session signer when active signer is missing", async () => {
const {
client,
publishCalls,
ensureSessionActorCalls,
} = createMockClient();
client.sessionActor = null;
let privateKeySignCalls = 0;
const signEventWithPrivateKey = (event, key) => {
privateKeySignCalls += 1;
assert.equal(key, "session-private", "session key should be used when rehydrated");
return { ...event, id: "signed-by-session" };
};
const result = await publishComment(
client,
{
videoEventId: "event-1",
videoDefinitionAddress: "30078:abc:def",
},
{ content: "Fallback" },
{
resolveActiveSigner: () => null,
shouldRequestExtensionPermissions: () => false,
signEventWithPrivateKey,
DEFAULT_NIP07_PERMISSION_METHODS: [],
},
);
assert.equal(result.ok, true, "publish should succeed with session signer");
assert.equal(privateKeySignCalls, 1, "session signing should occur exactly once");
assert.equal(
ensureSessionActorCalls.includes(true),
true,
"ensureSessionActor should be invoked with force refresh",
);
assert.equal(publishCalls.length, 1, "publish should still occur once");
});
test("listVideoComments builds filters with both #e and #a tags", async () => {
const matchingEventLatest = {
id: "comment-2",
kind: COMMENT_EVENT_KIND,
created_at: 1700000200,
tags: [
["e", "video-1"],
["a", "30078:author:clip"],
["e", "parent-1"],
],
};
const matchingEventOlder = {
id: "comment-2",
kind: COMMENT_EVENT_KIND,
created_at: 1690000000,
tags: [
["e", "video-1"],
["a", "30078:author:clip"],
["e", "parent-1"],
],
};
const otherEvent = {
id: "comment-3",
kind: COMMENT_EVENT_KIND,
created_at: 1700000100,
tags: [["e", "video-1"]],
};
const {
client,
pool,
} = createMockClient();
let receivedFilters = null;
pool.list = async (relays, filters) => {
receivedFilters = { relays, filters };
return [[matchingEventLatest, matchingEventOlder, otherEvent]];
};
const events = await listVideoComments(
client,
{
videoEventId: "video-1",
videoDefinitionAddress: "30078:author:clip",
parentCommentId: "parent-1",
},
{ relays: ["wss://history"], since: 1700000000, limit: 10 },
);
assert.ok(Array.isArray(events), "listing should produce an array");
assert.equal(events.length, 1, "duplicate ids should be collapsed to the newest instance");
assert.equal(events[0].id, "comment-2", "newest event should be retained");
assert.ok(receivedFilters, "pool.list should receive filters");
assert.deepEqual(
receivedFilters.relays,
["wss://history"],
"list should use caller provided relays",
);
assert.equal(receivedFilters.filters.length, 1, "exactly one filter should be emitted");
const [filter] = receivedFilters.filters;
assert.equal(filter.kinds[0], COMMENT_EVENT_KIND, "filter should target comment kind");
assert.deepEqual(
filter["#e"],
["video-1", "parent-1"],
"filter should bind the video and parent ids via #e",
);
assert.deepEqual(
filter["#a"],
["30078:author:clip"],
"filter should bind the video definition via #a",
);
assert.equal(filter.since, 1700000000, "since option should propagate to filter");
assert.equal(filter.limit, 10, "limit option should propagate to filter");
});
test("subscribeVideoComments forwards matching events and cleans up unsubscribe", async () => {
const {
client,
pool,
} = createMockClient();
let handler = null;
let unsubCalls = 0;
let subscriptionArgs = null;
pool.sub = (relays, filters) => {
subscriptionArgs = { relays, filters };
handler = null;
return {
on(name, cb) {
if (name === "event") {
handler = cb;
}
},
unsub() {
unsubCalls += 1;
},
relays,
filters,
};
};
const receivedEvents = [];
const unsubscribe = subscribeVideoComments(
client,
{
videoEventId: "video-1",
videoDefinitionAddress: "30078:author:clip",
parentCommentId: "parent-1",
},
{
relays: ["wss://live"],
onEvent: (event) => {
receivedEvents.push(event);
},
},
);
assert.ok(typeof unsubscribe === "function", "subscribe should return an unsubscribe function");
assert.ok(handler, "subscription handler should be registered");
assert.ok(subscriptionArgs, "subscription should capture args");
assert.deepEqual(
subscriptionArgs.relays,
["wss://live"],
"subscription should use caller provided relays",
);
assert.equal(subscriptionArgs.filters.length, 1, "subscription should emit a single filter");
const [subFilter] = subscriptionArgs.filters;
assert.deepEqual(
subFilter["#e"],
["video-1", "parent-1"],
"subscription filter should bind video and parent ids",
);
assert.deepEqual(
subFilter["#a"],
["30078:author:clip"],
"subscription filter should bind the video definition",
);
handler({
id: "comment-accepted",
kind: COMMENT_EVENT_KIND,
tags: [
["e", "video-1"],
["a", "30078:author:clip"],
["e", "parent-1"],
],
});
handler({
id: "comment-rejected",
kind: COMMENT_EVENT_KIND,
tags: [["e", "video-1"]],
});
assert.equal(receivedEvents.length, 1, "only matching events should reach the callback");
assert.equal(receivedEvents[0].id, "comment-accepted", "matching event should be forwarded");
unsubscribe();
unsubscribe();
assert.equal(unsubCalls, 1, "underlying unsubscribe should only run once");
});