1// Presets: Haven's four relays as one-click rule bundles, and the owner's2// own lists landing whatever the kind rules say.3import { describe, it, expect } from "vitest";4import { generateSecretKey, getPublicKey, type Event } from "nostr-tools/pure";5import { PRESETS } from "../../src/presets.ts";6import { ev, rpc, post } from "../helpers/relay.ts";78describe("presets", () => {9 it("each preset sets writes, reads, directory, kind rules and retention as its bundle says", async () => {10 const host = "presets.bind.ws";11 const owner = generateSecretKey();12 await rpc(host, owner, "claim");13 const list = (await rpc(host, owner, "listpresets")).result;14 expect(list.map((p: any) => p.name)).toEqual(["default", "outbox", "inbox", "private", "chat", "media", "search", "articles", "dm", "quiet", "site", "marmot", "grasp", "marmot-members", "home"]);15 for (const p of list) expect(p.about.length).toBeGreaterThan(10);16 expect(list.find((p: any) => p.name === "search").source).toBe("required");17 expect(list.find((p: any) => p.name === "articles").source).toBe("optional");18 expect(list.find((p: any) => p.name === "media").source).toBeUndefined();19 for (const preset of PRESETS) {20 const r = await rpc(host, owner, "applypreset", preset.name, preset.source === "required" ? { source: "wss://elsewhere.bind.ws" } : undefined);21 expect(r.status, preset.name + " " + JSON.stringify(r)).toBe(200);22 expect(r.result.writes).toBe(preset.writes);23 expect(r.result.reads).toBe(preset.reads);24 expect(r.result.directoryPublic).toBe(preset.directoryPublic);25 expect((await rpc(host, owner, "listallowedkinds")).result).toEqual([...preset.allow].sort((a, b) => a - b));26 expect((await rpc(host, owner, "listblockedkinds")).result).toEqual([...preset.block].sort((a, b) => a - b));27 expect((await rpc(host, owner, "listretention")).result).toEqual(preset.retention);28 }29 // Default clears kind and retention rules; omitted feature settings stay.30 await rpc(host, owner, "applypreset", "chat");31 const d = await rpc(host, owner, "applypreset", "default");32 expect(d.result.writes).toBe("open");33 expect((await rpc(host, owner, "listallowedkinds")).result).toEqual([]);34 expect((await rpc(host, owner, "listretention")).result).toEqual([]);35 expect((await rpc(host, owner, "applypreset", "haven")).status).toBe(400);36 });3738 it("a replica preset needs a source and keeps one standing pull of its kinds", async () => {39 const host = "presets-replica.bind.ws";40 const owner = generateSecretKey();41 await rpc(host, owner, "claim");42 let r = await rpc(host, owner, "applypreset", "search");43 expect(r.status).toBe(400);44 expect(r.error).toMatch(/needs a source/);45 r = await rpc(host, owner, "applypreset", "search", { source: "wss://" + host });46 expect(r.status).toBe(400);47 expect(r.error).toMatch(/itself/);48 r = await rpc(host, owner, "applypreset", "media", { source: "wss://elsewhere.bind.ws" });49 expect(r.status).toBe(400);50 expect(r.error).toMatch(/does not mirror/);51 r = await rpc(host, owner, "applypreset", "search", { source: "wss://elsewhere.bind.ws" });52 expect(r.status, JSON.stringify(r)).toBe(200);53 expect(r.result.job.kind).toBe("pull");54 expect(r.result.job.label).toBe("replica");55 expect(r.result.job.every).toBe(6);56 expect(r.result.job.relays).toEqual(["wss://elsewhere.bind.ws"]);57 expect(r.result.job.filter.kinds).toEqual([0, 1, 11, 1111, 9802, 30023, 30818]);58 expect((await rpc(host, owner, "listallowedkinds")).result).toEqual([0, 1, 11, 1111, 9802, 30023, 30818]);59 // Applying again replaces the standing job rather than adding one.60 r = await rpc(host, owner, "applypreset", "articles", { source: "wss://other.bind.ws" });61 expect(r.status).toBe(200);62 expect(r.result.job.every).toBe(24);63 const jobs = (await rpc(host, owner, "listjobs")).result.filter((j: any) => j.label === "replica");64 expect(jobs.length).toBe(1);65 expect(jobs[0].filter.kinds).toEqual([0, 30023]);66 // Articles without a source is a plain rule bundle; the replica job goes away.67 r = await rpc(host, owner, "applypreset", "articles");68 expect(r.status).toBe(200);69 expect(r.result.job).toBeNull();70 expect((await rpc(host, owner, "listjobs")).result.filter((j: any) => j.label === "replica")).toEqual([]);71 });7273 it("needs the rules action: a moderator may list presets but not apply one", async () => {74 const host = "presets-mod.bind.ws";75 const owner = generateSecretKey();76 const mod = generateSecretKey();77 await rpc(host, owner, "claim");78 await rpc(host, owner, "setmember", getPublicKey(mod), { role: "moderator" });79 expect((await rpc(host, mod, "listpresets")).status).toBe(200);80 const denied = await rpc(host, mod, "applypreset", "outbox");81 expect(denied.status).toBe(403);82 expect((await rpc(host, owner, "getpolicy")).result.writes).toBe("open");83 });8485 it("the owner's own lists land under an allow list that excludes them; a stranger's do not", async () => {86 const host = "presets-lists.bind.ws";87 const owner = generateSecretKey();88 const stranger = generateSecretKey();89 await rpc(host, owner, "claim");90 await rpc(host, owner, "applypreset", "inbox");91 const mine = ev(owner, 10002, "", [["r", "wss://" + host]]);92 let r = await post(host, owner, "/events", mine);93 expect(r.body.accepted, r.body.message).toBe(true);94 const theirs = ev(stranger, 10002, "", [["r", "wss://" + host]]);95 r = await post(host, stranger, "/events", theirs);96 expect(r.body.accepted).toBe(false);97 expect(r.body.message).toMatch(/does not accept kind 10002/);98 // A stranger's note is what the inbox is for.99 r = await post(host, stranger, "/events", ev(stranger, 1, "hello", [["p", getPublicKey(owner)]]));100 expect(r.body.accepted).toBe(true);101 const q = await post(host, owner, "/query", [{ kinds: [10002], authors: [getPublicKey(owner)] }]);102 expect(q.body.map((e: Event) => e.id)).toEqual([mine.id]);103 });104});