Files
bitgate/packages/nostr/test/reports.test.js
T
thePR0M3TH3AN 5e0fffb32e feat(nostr): implement Phase 3 codecs
Event formats live in codecs so the core never learns which kind stores roles,
which d identifier is used, or how a legacy list is shaped.

- bech32/NIP-19 decoding, verified against the NIP-19 test vector, so the core
  can stay hex-only and dependency-free
- canonical v1 codec on kind:30078 with namespaced d identifiers, role rosters
  carrying both role labels and explicit capabilities, and size-bounded policy
  documents that return null rather than throwing on malformed JSON
- NIP-56 reports, resolving the report type across the three tag layouts
  clients use and excluding relay hints from the type position
- NIP-51 mute lists; encrypted entries are flagged, never decoded
- legacy admin-list codec supporting all five migration stages, with editors
  surfaced separately so a UI permission cannot silently become signing authority
- replaceable selection with a deterministic same-timestamp tiebreak, without
  which two relays returning events in different orders would produce different
  state on different clients

129 codec tests; 402 total.
2026-08-09 17:39:37 -04:00

159 lines
4.8 KiB
JavaScript

import { describe, expect, it } from "vitest";
import {
REPORT_KIND,
decodeReport,
encodeReport,
extractReportType,
isRelayHint,
normalizeCategory,
} from "../src/reports.js";
const REPORTER = "a1".repeat(32);
const CREATOR = "d4".repeat(32);
const EVENT_ID = "1b".repeat(32);
/**
* @param {Partial<import('../src/replaceable.js').NostrEvent>} parts
* @returns {import('../src/replaceable.js').NostrEvent}
*/
const event = (parts) => ({
id: "00".repeat(32),
pubkey: REPORTER,
kind: REPORT_KIND,
created_at: 1000,
tags: [],
content: "",
...parts,
});
describe("isRelayHint", () => {
it("detects relay URLs", () => {
expect(isRelayHint("wss://relay.example")).toBe(true);
expect(isRelayHint("https://relay.example")).toBe(true);
});
it("does not treat a category as a relay", () => {
expect(isRelayHint("spam")).toBe(false);
expect(isRelayHint(undefined)).toBe(false);
});
});
describe("normalizeCategory", () => {
it("lowercases and trims", () => {
expect(normalizeCategory(" SPAM ")).toBe("spam");
});
it("returns empty for non-strings", () => {
expect(normalizeCategory(null)).toBe("");
});
});
describe("extractReportType", () => {
it("prefers an explicit report tag", () => {
expect(extractReportType(event({ tags: [["report", "nudity"], ["t", "spam"]] }))).toBe("nudity");
});
it("accepts a type tag", () => {
expect(extractReportType(event({ tags: [["type", "malware"]] }))).toBe("malware");
});
it("falls back to the matching target tag", () => {
expect(extractReportType(event({ tags: [["e", EVENT_ID, "spam"]] }), EVENT_ID)).toBe("spam");
});
it("ignores a relay hint in the type position", () => {
expect(extractReportType(event({ tags: [["e", EVENT_ID, "wss://relay.example"]] }), EVENT_ID)).toBe(
"",
);
});
it("falls back to a t hashtag", () => {
expect(extractReportType(event({ tags: [["t", "impersonation"]] }))).toBe("impersonation");
});
it("returns empty when nothing names a type", () => {
expect(extractReportType(event({ tags: [["e", EVENT_ID]] }), EVENT_ID)).toBe("");
});
});
describe("decodeReport", () => {
it("decodes an event report", () => {
const [report] = decodeReport(event({ tags: [["e", EVENT_ID, "spam"]] }));
expect(report).toEqual({
reporter: REPORTER,
target: { type: "event", id: EVENT_ID },
category: "spam",
createdAt: 1000,
});
});
it("decodes a user report", () => {
const [report] = decodeReport(event({ tags: [["p", CREATOR, "impersonation"]] }));
expect(report.target).toEqual({ type: "user", pubkey: CREATOR });
});
it("prefers the event target when both are present", () => {
const reports = decodeReport(
event({ tags: [["report", "spam"], ["e", EVENT_ID], ["p", CREATOR]] }),
);
expect(reports).toHaveLength(1);
expect(reports[0].target.type).toBe("event");
});
it("decodes multiple event targets", () => {
const other = "2c".repeat(32);
const reports = decodeReport(
event({ tags: [["report", "spam"], ["e", EVENT_ID], ["e", other]] }),
);
expect(reports).toHaveLength(2);
});
it("rejects the wrong kind", () => {
expect(decodeReport(event({ kind: 1 }))).toEqual([]);
});
it("skips a report with no resolvable category", () => {
expect(decodeReport(event({ tags: [["e", EVENT_ID]] }))).toEqual([]);
});
it("skips malformed target identifiers", () => {
expect(decodeReport(event({ tags: [["report", "spam"], ["e", "short"]] }))).toEqual([]);
});
it("rejects a malformed reporter", () => {
expect(decodeReport(event({ pubkey: "nope", tags: [["e", EVENT_ID, "spam"]] }))).toEqual([]);
});
});
describe("encodeReport", () => {
it("encodes an event report round-trip", () => {
const template = encodeReport({ type: "event", id: EVENT_ID, author: CREATOR }, "spam");
const [decoded] = decodeReport(event(template));
expect(decoded.category).toBe("spam");
expect(decoded.target).toEqual({ type: "event", id: EVENT_ID });
});
it("includes the author as a p tag for event reports", () => {
const template = encodeReport({ type: "event", id: EVENT_ID, author: CREATOR }, "spam");
expect(template.tags).toContainEqual(["p", CREATOR]);
});
it("encodes a user report", () => {
const template = encodeReport({ type: "user", pubkey: CREATOR }, "impersonation");
expect(template.tags).toContainEqual(["p", CREATOR, "impersonation"]);
});
it("encodes an address report", () => {
const template = encodeReport(
{ type: "address", kind: "30078", pubkey: CREATOR, identifier: "sku" },
"scam",
);
expect(template.tags).toContainEqual(["a", `30078:${CREATOR}:sku`, "scam"]);
});
it("rejects an empty category", () => {
expect(() => encodeReport({ type: "user", pubkey: CREATOR }, " ")).toThrow(/non-empty/);
});
});