Files
bitvid/tests/playlist-facade.test.mjs
thePR0M3TH3ANandClaude Opus 4.8 0caf468c36 playlists(#37): fetch/publish facade over the live client
Network wiring on top of the pure playlistService core:
- fetchCreatorPlaylists(pubkey) — lists kind-30082 by author via the L1
  SubscriptionManager, collapses to newest-per-`d` (NIP-33 replaceable), drops
  empty playlists from the listing (a cleared playlist republishes empty),
  sorts newest-first.
- fetchPlaylist(pubkey, id) — newest matching event, parsed.
- publishPlaylist(playlist) — build + sign + publish via
  nostrClient.signAndPublishEvent (active signer), replacing the same `d`.
All entry points take an injectable client for hermetic testing.

tests/playlist-facade.test.mjs (5): newest-per-id + empty/other-author drop +
sort, includeEmpty, blank/no-relay guards, fetch-one newest, publish emits a
kind-30082 event with the right d/title/ordered a-refs under the "playlist"
context. Mock client; no relays/signer. Lint clean; 5/5.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-07-06 12:51:43 -04:00

139 lines
5.0 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import {
fetchCreatorPlaylists,
fetchPlaylist,
publishPlaylist,
} from "../js/playlists/playlistFacade.js";
import {
buildPlaylistEvent,
PLAYLIST_KIND,
} from "../js/nostrEventSchemas.js";
const AUTHOR = "a".repeat(64);
const OTHER = "c".repeat(64);
const coord = (d) => `30078:${"b".repeat(64)}:${d}`;
function playlistEvent({ pubkey = AUTHOR, id, title, coords = [], created_at }) {
const event = buildPlaylistEvent({
pubkey,
created_at,
dTagValue: id,
title,
videoCoordinates: coords,
});
event.id = `id-${id}-${created_at}`;
return event;
}
// Mock client: a subscription manager whose list() filters an in-memory event
// set by kinds/authors/#d, plus a signAndPublishEvent that captures the event.
function mockClient({ events = [], onPublish } = {}) {
return {
relays: ["wss://mock.relay"],
getSubscriptionManager: () => ({
list: async ({ filters }) => {
const f = filters[0] || {};
return events.filter((ev) => {
if (f.kinds && !f.kinds.includes(ev.kind)) return false;
if (f.authors && !f.authors.includes(ev.pubkey)) return false;
if (f["#d"]) {
const d = ev.tags.find((t) => t[0] === "d")?.[1];
if (!f["#d"].includes(d)) return false;
}
return true;
});
},
}),
signAndPublishEvent: async (event, options) => {
if (onPublish) onPublish(event, options);
return { signedEvent: { ...event, id: "signed-id", sig: "sig" } };
},
};
}
test("fetchCreatorPlaylists returns newest-per-id, drops empties, sorts newest-first", async () => {
const events = [
// Same id "a" published twice — the newer (created_at 200) must win.
playlistEvent({ id: "a", title: "A old", coords: [coord("v1")], created_at: 100 }),
playlistEvent({ id: "a", title: "A new", coords: [coord("v1"), coord("v2")], created_at: 200 }),
playlistEvent({ id: "b", title: "B", coords: [coord("v9")], created_at: 300 }),
// Empty playlist — dropped from the listing by default.
playlistEvent({ id: "empty", title: "Empty", coords: [], created_at: 400 }),
// Another author's playlist — must not appear.
playlistEvent({ pubkey: OTHER, id: "x", title: "Other", coords: [coord("v0")], created_at: 500 }),
];
const playlists = await fetchCreatorPlaylists(AUTHOR, { client: mockClient({ events }) });
assert.deepEqual(playlists.map((p) => p.id), ["b", "a"], "sorted newest-first, empties + other author dropped");
const a = playlists.find((p) => p.id === "a");
assert.equal(a.title, "A new", "newest copy wins");
assert.equal(a.items.length, 2);
});
test("fetchCreatorPlaylists can include empty playlists when asked", async () => {
const events = [playlistEvent({ id: "empty", title: "Empty", coords: [], created_at: 1 })];
const playlists = await fetchCreatorPlaylists(AUTHOR, {
client: mockClient({ events }),
includeEmpty: true,
});
assert.deepEqual(playlists.map((p) => p.id), ["empty"]);
});
test("fetchCreatorPlaylists returns [] for blank pubkey or no relays", async () => {
assert.deepEqual(await fetchCreatorPlaylists("", { client: mockClient({}) }), []);
const noRelays = { ...mockClient({}), relays: [] };
assert.deepEqual(await fetchCreatorPlaylists(AUTHOR, { client: noRelays }), []);
});
test("fetchPlaylist returns the newest matching playlist parsed", async () => {
const events = [
playlistEvent({ id: "list", title: "old", coords: [coord("v1")], created_at: 10 }),
playlistEvent({ id: "list", title: "new", coords: [coord("v1"), coord("v2")], created_at: 20 }),
playlistEvent({ id: "other", title: "nope", coords: [coord("v3")], created_at: 99 }),
];
const parsed = await fetchPlaylist(AUTHOR, "list", { client: mockClient({ events }) });
assert.equal(parsed.title, "new");
assert.equal(parsed.id, "list");
assert.equal(parsed.items.length, 2);
assert.equal(await fetchPlaylist(AUTHOR, "missing", { client: mockClient({ events }) }), null);
});
test("publishPlaylist builds a kind-30082 event and publishes with the playlist context", async () => {
let published = null;
let ctx = null;
const client = mockClient({
onPublish: (event, options) => {
published = event;
ctx = options?.context;
},
});
const signed = await publishPlaylist(
{
pubkey: AUTHOR,
id: "new-list",
title: "Fresh",
items: [
{ type: "a", value: coord("v1") },
{ type: "a", value: coord("v2") },
],
created_at: 777,
},
{ client },
);
assert.equal(published.kind, PLAYLIST_KIND);
assert.equal(ctx, "playlist");
assert.equal(published.tags.find((t) => t[0] === "d")?.[1], "new-list");
assert.equal(published.tags.find((t) => t[0] === "title")?.[1], "Fresh");
assert.deepEqual(
published.tags.filter((t) => t[0] === "a").map((t) => t[1]),
[coord("v1"), coord("v2")],
);
assert.equal(signed.id, "signed-id", "returns the signed event");
});