Files
bitvid/tests/nip71-mirror-service.test.mjs

266 lines
10 KiB
JavaScript

// Phase 1: the NIP-71 mirror publish service. Scenarios assert the observable
// outcomes at the publish boundary, especially the site-wide NSFW moderation gate
// (ALLOW_NSFW_CONTENT) — an instance that won't surface NSFW must not publish it
// outward — plus the private / url / availability gates and a real publish.
import "./test-helpers/setup-localstorage.mjs";
import assert from "node:assert/strict";
import test from "node:test";
import { createNip71MirrorService } from "../js/services/nip71MirrorService.js";
import {
publishEventToRelays,
summarizePublishResults,
} from "../js/nostrPublish.js";
const PUBKEY = "a".repeat(64);
function baseVideo(overrides = {}) {
return {
videoRootId: "root-1",
pubkey: PUBKEY,
title: "Hello",
description: "desc",
url: "https://cdn.example.com/v.mp4",
thumbnail: "https://cdn.example.com/t.jpg",
isPrivate: false,
isNsfw: false,
...overrides,
};
}
// Faithful relay twin: stores published events; thenable publish => success path.
function makeRelay() {
const events = [];
return {
events,
publish(_urls, event) {
events.push(event);
return Promise.resolve();
},
};
}
function makeService({ allowNsfw = false, available = true } = {}) {
const relay = makeRelay();
const signer = available
? { signEvent: async (tpl) => ({ ...tpl, id: "id", sig: "sig" }) }
: null;
const service = createNip71MirrorService({
getActivePubkey: () => (available ? PUBKEY : ""),
getSigner: () => signer,
getWriteRelays: () => ["wss://write.relay"],
getPool: () => relay,
publishEventToRelays,
summarizePublishResults,
signEvent: signer ? signer.signEvent : async () => { throw new Error("no signer"); },
allowNsfw: () => allowNsfw === true,
});
return { service, relay };
}
test("publishes a public video as an addressable 34235 mirror to the write relays", async () => {
const { service, relay } = makeService();
const result = await service.publish(baseVideo());
assert.equal(result.ok, true);
assert.equal(result.accepted, 1);
assert.equal(relay.events.length, 1);
assert.equal(relay.events[0].kind, 34235);
assert.equal(relay.events[0].tags.find((t) => t[0] === "d")[1], "root-1");
});
test("NSFW video is NOT mirrored when the instance forbids NSFW (ALLOW_NSFW_CONTENT=false)", async () => {
const { service, relay } = makeService({ allowNsfw: false });
const result = await service.publish(baseVideo({ isNsfw: true }));
assert.equal(result.ok, false);
assert.equal(result.reason, "nsfw-blocked");
assert.equal(relay.events.length, 0, "must not publish NSFW outward");
assert.deepEqual(service.canMirror(baseVideo({ isNsfw: true })), {
ok: false,
reason: "nsfw-blocked",
});
});
test("NSFW video IS mirrored when the instance allows NSFW", async () => {
const { service, relay } = makeService({ allowNsfw: true });
const result = await service.publish(baseVideo({ isNsfw: true, nsfwReason: "mature" }));
assert.equal(result.ok, true);
assert.equal(relay.events.length, 1);
assert.equal(
relay.events[0].tags.find((t) => t[0] === "content-warning")[1],
"mature",
);
});
test("private and url-less videos are refused (builder rules)", async () => {
const { service, relay } = makeService();
assert.equal((await service.publish(baseVideo({ isPrivate: true }))).reason, "private");
assert.equal((await service.publish(baseVideo({ url: "" }))).reason, "no-url");
assert.equal(relay.events.length, 0);
});
test("publish is unavailable without a signer / pubkey", async () => {
const { service } = makeService({ available: false });
assert.equal(service.isAvailable(), false);
const result = await service.publish(baseVideo());
assert.equal(result.ok, false);
assert.equal(result.error, "unavailable");
});
test("remove() publishes a NIP-09 delete and tombstones both addressable kinds", async () => {
const { service, relay } = makeService();
const result = await service.remove(baseVideo());
assert.equal(result.ok, true);
assert.equal(relay.events.length, 3, "delete + empty replacements for both kinds");
const del = relay.events.find((e) => e.kind === 5);
assert.ok(del, "must publish a NIP-09 kind-5 delete");
const aTags = del.tags.filter((t) => t[0] === "a").map((t) => t[1]).sort();
assert.deepEqual(aTags, [
`34235:${PUBKEY}:root-1`,
`34236:${PUBKEY}:root-1`,
], "delete references both addressable kinds (orientation-robust)");
const kTags = del.tags.filter((t) => t[0] === "k").map((t) => t[1]).sort();
assert.deepEqual(kTags, ["34235", "34236"]);
const tombs = relay.events.filter((e) => e.kind === 34235 || e.kind === 34236);
assert.deepEqual(tombs.map((event) => event.kind).sort(), [34235, 34236]);
for (const tomb of tombs) {
assert.equal(tomb.tags.find((t) => t[0] === "d")[1], "root-1", "same d-tag");
assert.equal(tomb.tags.some((t) => t[0] === "imeta"), false, "not playable");
}
});
test("remove() tombstone uses the short kind for portrait videos", async () => {
const { service, relay } = makeService();
await service.remove(baseVideo({ width: 1080, height: 1920 }));
const tombs = relay.events.filter((e) => e.kind === 34235 || e.kind === 34236);
assert.deepEqual(tombs.map((event) => event.kind).sort(), [34235, 34236]);
});
test("remove() is unavailable without a signer", async () => {
const { service } = makeService({ available: false });
const result = await service.remove(baseVideo());
assert.equal(result.ok, false);
assert.equal(result.error, "unavailable");
});
// #34: idempotent re-mirror. The mirror KIND is inferred from dimensions
// (34236 short for portrait, else 34235), but (kind,pubkey,d) is the addressable
// identity — so a kind flip between mirror attempts produced a DUPLICATE in NIP-71
// clients. publish() must reuse the kind of any existing mirror instead.
function makeServiceWithExisting(existing, { now = () => Date.now() } = {}) {
const relay = makeRelay();
const signer = { signEvent: async (tpl) => ({ ...tpl, id: "id", sig: "sig" }) };
const service = createNip71MirrorService({
getActivePubkey: () => PUBKEY,
getSigner: () => signer,
getWriteRelays: () => ["wss://write.relay"],
getPool: () => relay,
publishEventToRelays,
summarizePublishResults,
signEvent: signer.signEvent,
allowNsfw: () => false,
fetchExistingMirrors: async () => existing,
now,
});
return { service, relay };
}
test("re-mirror reuses the existing mirror's kind (idempotent — no cross-kind duplicate)", async () => {
// An existing SHORT (34236) mirror exists; the video object has no dims, so the
// inference would otherwise pick 34235 (normal) → a duplicate. Must reuse 34236.
const { service, relay } = makeServiceWithExisting([
{ kind: 34236, created_at: 100 },
]);
const result = await service.publish(baseVideo());
assert.equal(result.ok, true);
assert.equal(result.reusedExistingKind, true);
assert.equal(relay.events.length, 1, "replaces the same coordinate, no second event");
assert.equal(relay.events[0].kind, 34236, "reused the existing kind, not the inferred 34235");
});
test("self-heals a pre-existing cross-kind duplicate by NIP-09-deleting the stale kind", async () => {
// Both kinds already exist (the bug's aftermath). Reuse the newest (34236) and
// tear down the stale 34235 so the video stops showing twice.
const { service, relay } = makeServiceWithExisting([
{ kind: 34236, created_at: 200 },
{ kind: 34235, created_at: 100 },
]);
const result = await service.publish(baseVideo());
assert.equal(result.ok, true);
assert.equal(result.healedStaleKind, true);
assert.equal(relay.events.length, 2, "the mirror + a delete for the stale kind");
assert.equal(relay.events[0].kind, 34236);
const del = relay.events[1];
assert.equal(del.kind, 5, "NIP-09 delete");
assert.ok(
del.tags.some((t) => t[0] === "a" && t[1] === `34235:${PUBKEY}:root-1`),
"delete targets the stale 34235 coordinate",
);
});
test("an explicit options.short override is respected over any existing mirror", async () => {
const { service, relay } = makeServiceWithExisting([{ kind: 34236, created_at: 100 }]);
const result = await service.publish(baseVideo(), { short: false });
assert.equal(result.ok, true);
assert.equal(result.reusedExistingKind, false);
assert.equal(relay.events[0].kind, 34235, "explicit override wins");
});
test("re-mirror advances past an existing addressable timestamp", async () => {
const { service, relay } = makeServiceWithExisting(
[{ kind: 34235, created_at: 500 }],
{ now: () => 500_000 },
);
const result = await service.publish(baseVideo());
assert.equal(result.ok, true);
assert.equal(relay.events[0].created_at, 501, "mirror replacement must win its coordinate");
});
test("remove advances its delete and tombstones past an existing mirror", async () => {
const { service, relay } = makeServiceWithExisting(
[{ kind: 34235, created_at: 500 }],
{ now: () => 500_000 },
);
const result = await service.remove(baseVideo());
assert.equal(result.ok, true);
assert.deepEqual(relay.events.map((event) => event.created_at), [501, 501, 501]);
});
test("findMirrors resolves a library in one batched relay lookup", async () => {
let batchCalls = 0;
const service = createNip71MirrorService({
getActivePubkey: () => PUBKEY,
fetchMirrorsByRoots: async ({ roots }) => {
batchCalls += 1;
assert.deepEqual(roots.sort(), ["root-a", "root-b"]);
return [
{ root: "root-a", kind: 34235, created_at: 10 },
{ root: "root-b", kind: 34236, created_at: 11 },
];
},
});
const states = await service.findMirrors([
{ videoRootId: "root-a" },
{ videoRootId: "root-b" },
]);
assert.equal(batchCalls, 1);
assert.deepEqual(states.get("root-a"), { mirrored: true, kinds: [34235], duplicate: false });
assert.deepEqual(states.get("root-b"), { mirrored: true, kinds: [34236], duplicate: false });
});
test("findMirror derives mirror state from relays (truth), flagging duplicates", async () => {
const dup = makeServiceWithExisting([{ kind: 34235 }, { kind: 34236 }]).service;
assert.deepEqual(await dup.findMirror(baseVideo()), {
mirrored: true,
kinds: [34235, 34236],
duplicate: true,
});
const none = makeServiceWithExisting([]).service;
assert.deepEqual(await none.findMirror(baseVideo()), {
mirrored: false,
kinds: [],
duplicate: false,
});
});