mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
- Fix circular dependency in `js/adminListStore.js` by importing from `js/nostrClientRegistry.js`. - Update `js/services/hashtagPreferencesService.js` to include legacy kind 30005 in filters. - Fix JSDOM mocking in `tests/modal-accessibility.test.mjs` (navigator getter) and `UploadModal` selectors. - Add `getRoot()` to `js/ui/components/UploadModal.js` for API consistency. - Update `tests/magnet-utils.test.mjs` expectations to reflect removed default tracker. - Mock `ensureActiveSignerForPubkey` in `tests/moderation/submit-report.test.mjs`. - Fix mock loader path in `tests/moderation/trust-seeds.test.mjs` to target `trustBootstrap.js`. - Fix duplicate pointer tags in `js/nostrEventSchemas.js`'s `buildCommentEvent`. - Update `tests/nostr/comment-events.test.mjs` and `tests/comment-event-builder.test.mjs` to match current code behavior (legacy kinds, unique tags).
802 lines
22 KiB
JavaScript
802 lines
22 KiB
JavaScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import {
|
|
COMMENT_EVENT_KIND,
|
|
LEGACY_COMMENT_KIND,
|
|
publishComment,
|
|
listVideoComments,
|
|
subscribeVideoComments,
|
|
__testExports,
|
|
} from "../../js/nostr/commentEvents.js";
|
|
|
|
const { normalizeCommentTarget, isVideoCommentEvent } = __testExports;
|
|
|
|
function createMockClient({
|
|
actorPubkey = "actor-pubkey",
|
|
relays = ["wss://primary"],
|
|
publishResults = [],
|
|
extensionPermissions = { ok: true },
|
|
} = {}) {
|
|
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 client = {
|
|
pool,
|
|
relays,
|
|
pubkey: actorPubkey,
|
|
ensureExtensionPermissions: async () => extensionPermissions,
|
|
};
|
|
|
|
return { client, pool, publishCalls };
|
|
}
|
|
|
|
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" };
|
|
},
|
|
};
|
|
|
|
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,
|
|
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(signerCalls.length, 1, "active signer should sign the event once");
|
|
|
|
const eventTags = buildCommentEventTags(signerCalls[0]);
|
|
assert.deepEqual(
|
|
eventTags,
|
|
[
|
|
["A", "30078:deadbeef:clip", "wss://definition-relay"],
|
|
["K", "30078"],
|
|
["P", "deadbeef", "wss://definition-relay"],
|
|
["E", "video-event-id", "wss://video-relay"],
|
|
["A", "30078:deadbeef:clip", "wss://definition-relay"],
|
|
["E", "parent-comment-id", "thread-participant"],
|
|
["K", String(COMMENT_EVENT_KIND)],
|
|
["P", "thread-participant"],
|
|
],
|
|
"comment event should include uppercase pointers and participant metadata",
|
|
);
|
|
});
|
|
|
|
test("publishComment rejects when active signer is unavailable", async () => {
|
|
const { client, publishCalls } = createMockClient();
|
|
|
|
client.sessionActor = {
|
|
pubkey: client.pubkey,
|
|
privateKey: "session-private",
|
|
};
|
|
|
|
const result = await publishComment(
|
|
client,
|
|
{
|
|
videoEventId: "event-1",
|
|
videoDefinitionAddress: "30078:abc:def",
|
|
},
|
|
{ content: "Fallback" },
|
|
{
|
|
resolveActiveSigner: () => null,
|
|
shouldRequestExtensionPermissions: () => false,
|
|
DEFAULT_NIP07_PERMISSION_METHODS: [],
|
|
},
|
|
);
|
|
|
|
assert.equal(result.ok, false, "publish should fail without an active signer");
|
|
assert.equal(result.error, "auth-required", "missing signer should yield auth-required");
|
|
assert.equal(publishCalls.length, 0, "publish should not be attempted without a signer");
|
|
});
|
|
|
|
test("publishComment accepts legacy targets with only an event id", async () => {
|
|
const { client, publishCalls } = createMockClient();
|
|
|
|
let signCalls = 0;
|
|
const signer = {
|
|
type: "extension",
|
|
pubkey: client.pubkey,
|
|
signEvent: (event) => {
|
|
signCalls += 1;
|
|
return { ...event, id: `signed-${signCalls}` };
|
|
},
|
|
};
|
|
|
|
const result = await publishComment(
|
|
client,
|
|
{
|
|
videoEventId: "legacy-event-id",
|
|
parentCommentId: "parent-legacy",
|
|
},
|
|
{ content: "Legacy comment" },
|
|
{
|
|
resolveActiveSigner: () => signer,
|
|
shouldRequestExtensionPermissions: () => false,
|
|
DEFAULT_NIP07_PERMISSION_METHODS: [],
|
|
},
|
|
);
|
|
|
|
assert.equal(result.ok, true, "publishing should succeed without a definition address");
|
|
assert.equal(signCalls, 1, "active signer should be used to sign the event");
|
|
assert.equal(publishCalls.length, 1, "event should be published exactly once");
|
|
|
|
const publishedEvent = publishCalls[0]?.event;
|
|
const tags = buildCommentEventTags(publishedEvent);
|
|
assert.deepEqual(
|
|
tags,
|
|
[
|
|
["E", "legacy-event-id"],
|
|
["K", String(COMMENT_EVENT_KIND)],
|
|
["E", "legacy-event-id"],
|
|
["E", "parent-legacy"],
|
|
["K", String(COMMENT_EVENT_KIND)],
|
|
],
|
|
"legacy publish should emit uppercase root metadata with lowercase fallbacks",
|
|
);
|
|
});
|
|
|
|
test("publishComment derives root and parent metadata from parent comment tags", async () => {
|
|
const { client } = createMockClient();
|
|
|
|
const parentEvent = {
|
|
id: "parent-comment",
|
|
kind: COMMENT_EVENT_KIND,
|
|
pubkey: "parentpk",
|
|
tags: [
|
|
["E", "video-event", "wss://root", "videopk"],
|
|
["K", "30078"],
|
|
["P", "videopk", "wss://author"],
|
|
["A", "30078:videopk:root", "wss://definition"],
|
|
["E", "video-event", "wss://root"],
|
|
["E", "parent-comment", "wss://parent", "parentpk"],
|
|
["K", String(COMMENT_EVENT_KIND)],
|
|
["P", "parentpk", "wss://parent"],
|
|
],
|
|
};
|
|
|
|
const signer = {
|
|
type: "extension",
|
|
pubkey: client.pubkey,
|
|
signEvent: (event) => ({ ...event, id: "signed" }),
|
|
};
|
|
|
|
const result = await publishComment(
|
|
client,
|
|
{ parentComment: parentEvent },
|
|
{ content: "Reply" },
|
|
{
|
|
resolveActiveSigner: () => signer,
|
|
shouldRequestExtensionPermissions: () => false,
|
|
DEFAULT_NIP07_PERMISSION_METHODS: [],
|
|
},
|
|
);
|
|
|
|
const tags = buildCommentEventTags(result.event);
|
|
assert.deepEqual(tags, [
|
|
["A", "30078:videopk:root", "wss://definition"],
|
|
["K", "30078"],
|
|
["P", "videopk", "wss://author"],
|
|
["E", "video-event", "wss://root"],
|
|
["A", "30078:videopk:root", "wss://definition"],
|
|
["E", "parent-comment", "wss://parent", "parentpk"],
|
|
["K", String(COMMENT_EVENT_KIND)],
|
|
["P", "parentpk", "wss://parent"],
|
|
]);
|
|
});
|
|
|
|
test("listVideoComments matches comments even when tag casing and whitespace differ", async () => {
|
|
const mismatchedEvent = {
|
|
id: "case-comment",
|
|
kind: COMMENT_EVENT_KIND,
|
|
created_at: 1700000300,
|
|
tags: [
|
|
[" A ", " 30078:AUTHOR:CLIP ", " WSS://Definition "],
|
|
["E", " VIDEO-1 "],
|
|
[" E ", " VIDEO-1 "],
|
|
["E", " PARENT-1 "],
|
|
["K", String(COMMENT_EVENT_KIND)],
|
|
["P", " THREAD-PARTICIPANT "],
|
|
],
|
|
};
|
|
|
|
const { client, pool } = createMockClient();
|
|
pool.list = async () => [[mismatchedEvent]];
|
|
|
|
const descriptorInput = {
|
|
videoEventId: " video-1 ",
|
|
videoDefinitionAddress: " 30078:author:clip ",
|
|
parentCommentId: " parent-1 ",
|
|
};
|
|
|
|
const events = await listVideoComments(client, descriptorInput);
|
|
assert.equal(events.length, 1, "case-insensitive descriptor should match comment");
|
|
assert.equal(events[0].id, "case-comment", "matched event should be returned");
|
|
|
|
const normalizedDescriptor = normalizeCommentTarget(descriptorInput);
|
|
assert.ok(normalizedDescriptor, "normalizeCommentTarget should produce a descriptor");
|
|
assert.equal(
|
|
isVideoCommentEvent(mismatchedEvent, normalizedDescriptor),
|
|
true,
|
|
"isVideoCommentEvent should treat tag casing and whitespace as insignificant",
|
|
);
|
|
});
|
|
|
|
test("listVideoComments matches uppercase definition addresses without lowering", async () => {
|
|
const uppercasePointer = "30078:AUTHOR:Clip42";
|
|
const matchingEvent = {
|
|
id: "uppercase-comment",
|
|
kind: COMMENT_EVENT_KIND,
|
|
created_at: 1700000400,
|
|
tags: [
|
|
["A", uppercasePointer],
|
|
["E", "video-1"],
|
|
],
|
|
};
|
|
|
|
const { client, pool } = createMockClient();
|
|
pool.list = async () => [[matchingEvent]];
|
|
|
|
const descriptorInput = {
|
|
videoEventId: "video-1",
|
|
videoDefinitionAddress: uppercasePointer,
|
|
};
|
|
|
|
const events = await listVideoComments(client, descriptorInput);
|
|
assert.equal(events.length, 1, "uppercase pointer should still match");
|
|
assert.equal(events[0].id, "uppercase-comment", "matched event should be returned");
|
|
|
|
const normalizedDescriptor = normalizeCommentTarget(descriptorInput);
|
|
assert.ok(normalizedDescriptor, "normalizeCommentTarget should produce a descriptor");
|
|
assert.equal(
|
|
isVideoCommentEvent(matchingEvent, normalizedDescriptor),
|
|
true,
|
|
"isVideoCommentEvent should respect descriptor casing for definition pointers",
|
|
);
|
|
});
|
|
|
|
test("publishComment emits only uppercase video event pointer when address and parent are absent", async () => {
|
|
const { client, publishCalls } = createMockClient();
|
|
|
|
const signer = {
|
|
type: "extension",
|
|
pubkey: client.pubkey,
|
|
signEvent: (event) => ({ ...event, id: "solo" }),
|
|
};
|
|
|
|
const result = await publishComment(
|
|
client,
|
|
{
|
|
videoEventId: "video-only",
|
|
},
|
|
{ content: "Solo" },
|
|
{
|
|
resolveActiveSigner: () => signer,
|
|
shouldRequestExtensionPermissions: () => false,
|
|
DEFAULT_NIP07_PERMISSION_METHODS: [],
|
|
},
|
|
);
|
|
|
|
assert.equal(result.ok, true, "publishing should succeed with only a video id");
|
|
assert.equal(publishCalls.length, 1, "comment should be published once");
|
|
|
|
const publishedEvent = publishCalls[0]?.event;
|
|
const tags = buildCommentEventTags(publishedEvent);
|
|
assert.deepEqual(
|
|
tags,
|
|
[
|
|
["E", "video-only"],
|
|
],
|
|
"event should include a single uppercase pointer",
|
|
);
|
|
});
|
|
|
|
test("listVideoComments builds filters with uppercase roots plus legacy fallbacks", async () => {
|
|
const matchingEventLatest = {
|
|
id: "comment-2",
|
|
kind: COMMENT_EVENT_KIND,
|
|
created_at: 1700000200,
|
|
tags: [
|
|
["A", "30078:author:clip"],
|
|
["K", "30078"],
|
|
["P", "author"],
|
|
["E", "parent-1"],
|
|
["K", String(COMMENT_EVENT_KIND)],
|
|
["P", "thread-participant"],
|
|
],
|
|
};
|
|
|
|
const matchingEventOlder = {
|
|
id: "comment-2",
|
|
kind: COMMENT_EVENT_KIND,
|
|
created_at: 1690000000,
|
|
tags: [
|
|
["A", "30078:author:clip"],
|
|
["E", "parent-1"],
|
|
],
|
|
};
|
|
|
|
const otherEvent = {
|
|
id: "comment-3",
|
|
kind: COMMENT_EVENT_KIND,
|
|
created_at: 1700000100,
|
|
tags: [["A", "30078:author:clip"]],
|
|
};
|
|
|
|
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",
|
|
parentKind: COMMENT_EVENT_KIND,
|
|
parentAuthorPubkey: "thread-participant",
|
|
},
|
|
{ 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,
|
|
4,
|
|
"event, uppercase, parent, and address filters should be emitted",
|
|
);
|
|
const [eventFilter, uppercaseFilter, parentFilter, definitionFilter] =
|
|
receivedFilters.filters;
|
|
assert.deepEqual(
|
|
eventFilter.kinds,
|
|
[COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"event filter should target comment kind",
|
|
);
|
|
assert.deepEqual(
|
|
eventFilter["#E"],
|
|
["video-1"],
|
|
"event filter should target the video via #E",
|
|
);
|
|
assert.deepEqual(
|
|
uppercaseFilter.kinds,
|
|
[COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"uppercase filter should target comment kind",
|
|
);
|
|
assert.deepEqual(
|
|
uppercaseFilter["#A"],
|
|
["30078:author:clip"],
|
|
"uppercase filter should target the video definition via #A",
|
|
);
|
|
assert.deepEqual(
|
|
uppercaseFilter["#K"],
|
|
["30078"],
|
|
"uppercase filter should scope the root kind via #K",
|
|
);
|
|
assert.deepEqual(
|
|
uppercaseFilter["#P"],
|
|
["author"],
|
|
"uppercase filter should scope the root author via #P",
|
|
);
|
|
assert.equal(
|
|
uppercaseFilter.since,
|
|
1700000000,
|
|
"since option should propagate to uppercase filter",
|
|
);
|
|
assert.equal(
|
|
uppercaseFilter.limit,
|
|
10,
|
|
"limit option should propagate to uppercase filter",
|
|
);
|
|
assert.deepEqual(
|
|
parentFilter,
|
|
{
|
|
kinds: [COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"#E": ["parent-1"],
|
|
since: 1700000000,
|
|
limit: 10,
|
|
},
|
|
"parent filter should scope the thread via #E",
|
|
);
|
|
assert.equal(
|
|
eventFilter.since,
|
|
1700000000,
|
|
"since option should propagate to event filter",
|
|
);
|
|
assert.equal(
|
|
eventFilter.limit,
|
|
10,
|
|
"limit option should propagate to event filter",
|
|
);
|
|
|
|
assert.deepEqual(
|
|
definitionFilter.kinds,
|
|
[COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"definition filter should target comment kind",
|
|
);
|
|
assert.deepEqual(
|
|
definitionFilter["#A"],
|
|
["30078:author:clip"],
|
|
"definition filter should bind the video definition via #A",
|
|
);
|
|
assert.deepEqual(
|
|
definitionFilter["#E"],
|
|
["parent-1"],
|
|
"definition filter should continue scoping to the parent comment",
|
|
);
|
|
assert.equal(
|
|
definitionFilter.since,
|
|
1700000000,
|
|
"since option should propagate to definition filter",
|
|
);
|
|
assert.equal(
|
|
definitionFilter.limit,
|
|
10,
|
|
"limit option should propagate to definition filter",
|
|
);
|
|
});
|
|
|
|
test("listVideoComments emits uppercase root filters when only the identifier is known", async () => {
|
|
const matchingEvent = {
|
|
id: "comment-root",
|
|
kind: COMMENT_EVENT_KIND,
|
|
tags: [
|
|
["I", "root-only"],
|
|
["P", "root-author"],
|
|
["E", "video-rooted"],
|
|
],
|
|
};
|
|
|
|
const { client, pool } = createMockClient();
|
|
|
|
let receivedFilters = null;
|
|
pool.list = async (relays, filters) => {
|
|
receivedFilters = { relays, filters };
|
|
return [[matchingEvent]];
|
|
};
|
|
|
|
const events = await listVideoComments(
|
|
client,
|
|
{
|
|
videoEventId: "video-rooted",
|
|
videoKind: COMMENT_EVENT_KIND,
|
|
videoAuthorPubkey: "root-author",
|
|
rootIdentifier: "root-only",
|
|
},
|
|
{ since: 1700000000, limit: 5 },
|
|
);
|
|
|
|
assert.equal(events.length, 1, "matching comment should be returned");
|
|
assert.ok(receivedFilters, "pool.list should receive filters");
|
|
assert.equal(
|
|
receivedFilters.filters.length,
|
|
2,
|
|
"root identifier requests should emit uppercase filters only",
|
|
);
|
|
|
|
const [eventFilter, uppercaseRootFilter] = receivedFilters.filters;
|
|
|
|
assert.deepEqual(
|
|
eventFilter,
|
|
{
|
|
kinds: [COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"#E": ["video-rooted"],
|
|
since: 1700000000,
|
|
limit: 5,
|
|
},
|
|
"event filter should target the video event id via #E",
|
|
);
|
|
|
|
assert.deepEqual(
|
|
uppercaseRootFilter.kinds,
|
|
[COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"uppercase root filter should continue targeting comment kind",
|
|
);
|
|
assert.deepEqual(
|
|
uppercaseRootFilter["#I"],
|
|
["root-only"],
|
|
"uppercase root filter should continue binding the root identifier via #I",
|
|
);
|
|
assert.deepEqual(
|
|
uppercaseRootFilter["#P"],
|
|
["root-author"],
|
|
"uppercase root filter should scope by author when available",
|
|
);
|
|
assert.equal(
|
|
uppercaseRootFilter.since,
|
|
1700000000,
|
|
"since option should propagate to uppercase root filter",
|
|
);
|
|
assert.equal(
|
|
uppercaseRootFilter.limit,
|
|
5,
|
|
"limit option should propagate to uppercase root filter",
|
|
);
|
|
});
|
|
|
|
test("listVideoComments supports legacy targets without a definition address", async () => {
|
|
const legacyEvent = {
|
|
id: "legacy-comment",
|
|
kind: COMMENT_EVENT_KIND,
|
|
tags: [["E", "legacy-video"]],
|
|
};
|
|
|
|
const { client, pool } = createMockClient();
|
|
|
|
let receivedFilters = null;
|
|
pool.list = async (relays, filters) => {
|
|
receivedFilters = { relays, filters };
|
|
return [[legacyEvent]];
|
|
};
|
|
|
|
const events = await listVideoComments(client, { videoEventId: "legacy-video" });
|
|
|
|
assert.ok(Array.isArray(events), "listing should still return an array");
|
|
assert.equal(events.length, 1, "legacy matching comment should be returned");
|
|
assert.equal(events[0].id, "legacy-comment", "returned event should match the pool payload");
|
|
|
|
assert.ok(receivedFilters, "pool.list should be invoked with filters");
|
|
assert.deepEqual(
|
|
receivedFilters.relays,
|
|
client.relays,
|
|
"legacy listing should fall back to client relays when none are provided",
|
|
);
|
|
assert.deepEqual(
|
|
receivedFilters.filters,
|
|
[
|
|
{
|
|
kinds: [COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"#E": ["legacy-video"],
|
|
},
|
|
{
|
|
kinds: [COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"#E": ["legacy-video"],
|
|
},
|
|
],
|
|
"legacy listing should target the video event id via #E",
|
|
);
|
|
});
|
|
|
|
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",
|
|
parentKind: COMMENT_EVENT_KIND,
|
|
parentAuthorPubkey: "thread-participant",
|
|
},
|
|
{
|
|
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.deepEqual(
|
|
subscriptionArgs.filters,
|
|
[
|
|
{
|
|
kinds: [COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"#E": ["video-1"],
|
|
},
|
|
{
|
|
kinds: [COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"#A": ["30078:author:clip"],
|
|
"#K": ["30078"],
|
|
"#P": ["author"],
|
|
},
|
|
{
|
|
kinds: [COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"#E": ["parent-1"],
|
|
},
|
|
{
|
|
kinds: [COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"#a": ["30078:author:clip"],
|
|
"#E": ["parent-1"],
|
|
},
|
|
],
|
|
"subscription should emit event, parent, and address filters",
|
|
);
|
|
|
|
handler({
|
|
id: "comment-accepted",
|
|
kind: COMMENT_EVENT_KIND,
|
|
tags: [
|
|
["A", "30078:author:clip"],
|
|
["K", "30078"],
|
|
["P", "author"],
|
|
["E", "parent-1"],
|
|
["K", String(COMMENT_EVENT_KIND)],
|
|
["P", "thread-participant"],
|
|
],
|
|
});
|
|
|
|
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");
|
|
});
|
|
|
|
test("subscribeVideoComments supports video targets without a definition address", async () => {
|
|
const { client, pool } = createMockClient();
|
|
|
|
let handler = null;
|
|
let subscriptionArgs = null;
|
|
pool.sub = (relays, filters) => {
|
|
subscriptionArgs = { relays, filters };
|
|
return {
|
|
on(name, cb) {
|
|
if (name === "event") {
|
|
handler = cb;
|
|
}
|
|
},
|
|
unsub() {},
|
|
};
|
|
};
|
|
|
|
const receivedEvents = [];
|
|
subscribeVideoComments(
|
|
client,
|
|
{ videoEventId: "legacy-video" },
|
|
{
|
|
onEvent: (event) => {
|
|
receivedEvents.push(event);
|
|
},
|
|
},
|
|
);
|
|
|
|
assert.ok(subscriptionArgs, "subscription should capture filters");
|
|
assert.deepEqual(
|
|
subscriptionArgs.filters,
|
|
[
|
|
{
|
|
kinds: [COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"#E": ["legacy-video"],
|
|
},
|
|
{
|
|
kinds: [COMMENT_EVENT_KIND, LEGACY_COMMENT_KIND],
|
|
"#E": ["legacy-video"],
|
|
},
|
|
],
|
|
"subscription should emit both legacy and uppercase video filters when address is absent",
|
|
);
|
|
|
|
assert.ok(handler, "event handler should be registered");
|
|
|
|
handler({
|
|
id: "legacy-comment",
|
|
kind: COMMENT_EVENT_KIND,
|
|
tags: [["E", "legacy-video"]],
|
|
});
|
|
|
|
assert.equal(receivedEvents.length, 1, "legacy comment should be forwarded");
|
|
assert.equal(receivedEvents[0].id, "legacy-comment", "comment payload should be delivered");
|
|
});
|