mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-01 10:22:25 +00:00
357 lines
9.6 KiB
JavaScript
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");
|
|
});
|