Files
bitvid/tests/nip71-mirror-service.test.mjs
T
thePR0M3TH3ANandClaude Opus 4.8 58f1909570 feat(nip71): Phase 1b — mirror teardown (NIP-09 delete + empty-replace tombstone)
Adds remove(video, options) to the mirror service for delete / toggle-off:
- NIP-09 kind-5 delete referencing BOTH addressable kinds
  (34235:pub:root + 34236:pub:root, with k tags) so it's robust to a
  since-changed orientation.
- empty-replace tombstone (same d-tag, title, NO imeta) so clients that cache
  addressable events but ignore NIP-09 still see the entry cleared; uses the
  short kind for portrait videos.
Published to the write/outbox relay set; unavailable without a signer.

tests/nip71-mirror-service.test.mjs: remove publishes delete (both kinds) +
tombstone; portrait => 34236 tombstone; unavailable without signer.
Mutation-verified (tombstone emission + both-kind a-tags load-bearing).

Note: full unit suite green apart from the documented order-dependent flakes
(uploadModal-reset; a transient login-modal clipboard global-state flake) — both
pass in isolation and are unrelated to this change.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-06-22 09:18:31 -04:00

148 lines
5.4 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 (both addressable kinds) AND a tombstone", async () => {
const { service, relay } = makeService();
const result = await service.remove(baseVideo());
assert.equal(result.ok, true);
assert.equal(relay.events.length, 2, "delete + empty-replace tombstone");
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 tomb = relay.events.find((e) => e.kind === 34235 || e.kind === 34236);
assert.ok(tomb, "must publish an empty-replace tombstone");
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,
"tombstone has no playable imeta",
);
});
test("remove() tombstone uses the short kind for portrait videos", async () => {
const { service, relay } = makeService();
await service.remove(baseVideo({ width: 1080, height: 1920 }));
const tomb = relay.events.find((e) => e.kind === 34235 || e.kind === 34236);
assert.equal(tomb.kind, 34236, "portrait => 34236 tombstone");
});
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");
});