clone

nostr relays on demand at <name>.bind.ws: claim one with your key, pay in zaps past the free allowance

dependabot/github_actions/actions-9b61906d8b
bindws / test / object / features.test.ts
features.test.ts 7.4K
1// Features an owner switches off (settings.ts, features): the NIP-11 list2// loses the number, the door answers 404, the socket refuses the verb, and3// the full-text index follows the search mode.4import { describe, it, expect } from "vitest";5import { generateSecretKey, getPublicKey } from "nostr-tools/pure";6import { ev, rpc, info, get, post, pk } from "../helpers/relay.ts";7import { WS } from "../helpers/ws.ts";8import { upload } from "../helpers/media.ts";910const status = async (host: string, path: string) => (await get(host, path)).status;1112describe("features", () => {13 it("have explicit defaults, and setpolicy takes a map that survives export and import", async () => {14 const host = "feat-default.bind.ws";15 const owner = generateSecretKey();16 await rpc(host, owner, "claim");17 const p = (await rpc(host, owner, "getpolicy")).result;18 expect(p.features).toEqual({ search: "prose", sync: true, count: true, discovery: true, names: true, files: true, pages: true, signer: true, sites: { enabled: true, mirror: true }, marmot: false, grasp: false, grasp02: false, grasp03: false, grasp05: false, grasp06: false, push: false });19 for (const n of [5, 45, 46, 50, 66, 77]) expect((await info(host)).supported_nips).toContain(n);20 const r = await rpc(host, owner, "setpolicy", { features: { search: "full", sync: false, bogus: true, count: "no" } });21 expect(r.result.features).toMatchObject({ search: "full", sync: false, count: true });22 expect(r.result.features.bogus).toBeUndefined();23 const cfg = (await rpc(host, owner, "exportconfig")).result;24 expect(cfg.policy.features).toMatchObject({ search: "full", sync: false });25 const other = "feat-import.bind.ws";26 await rpc(other, owner, "claim");27 expect((await rpc(other, owner, "importconfig", cfg)).status).toBe(200);28 expect((await rpc(other, owner, "getpolicy")).result.features).toMatchObject({ search: "full", sync: false });29 const doc = await info(other);30 expect(doc.supported_nips).not.toContain(77);31 expect(doc.supported_nips).toContain(50);32 });3334 it("GRASP is off by default and survives policy export and import", async () => {35 const host = "feat-grasp.bind.ws";36 const owner = generateSecretKey();37 await rpc(host, owner, "claim");38 expect((await rpc(host, owner, "getpolicy")).result.features.grasp).toBe(false);39 const policy = await rpc(host, owner, "setpolicy", { features: { grasp: true } });40 expect(policy.result.features.grasp).toBe(true);41 const config = (await rpc(host, owner, "exportconfig")).result;42 const other = "feat-grasp-import.bind.ws";43 await rpc(other, owner, "claim");44 expect((await rpc(other, owner, "importconfig", config)).status).toBe(200);45 expect((await rpc(other, owner, "getpolicy")).result.features.grasp).toBe(true);46 });4748 it("search: prose indexes notes, full indexes reactions too, off refuses the filter", async () => {49 const host = "feat-search.bind.ws";50 const owner = generateSecretKey();51 const alice = generateSecretKey();52 await rpc(host, owner, "claim");53 const c = await WS.connect(host);54 expect((await c.ok(ev(alice, 1, "hello world"))).ok).toBe(true);55 expect((await c.ok(ev(alice, 7, "hello", [["e", "ab".repeat(32)]]))).ok).toBe(true);56 expect((await c.req({ search: "hello" })).map((e) => e.kind)).toEqual([1]);57 await rpc(host, owner, "setpolicy", { features: { search: "full" } });58 expect((await c.ok(ev(alice, 7, "hello again", [["e", "cd".repeat(32)]]))).ok).toBe(true);59 expect((await c.req({ search: "hello" })).map((e) => e.kind).sort()).toEqual([1, 7]);60 await rpc(host, owner, "setpolicy", { features: { search: "off" } });61 expect((await c.query({ search: "hello" })).closed).toMatch(/^unsupported: search/);62 expect((await c.req({ kinds: [1] })).length).toBe(1);63 expect((await info(host)).supported_nips).not.toContain(50);64 });6566 it("count and sync off are refused at the socket and leave the list", async () => {67 const host = "feat-socket.bind.ws";68 const owner = generateSecretKey();69 await rpc(host, owner, "claim");70 const c = await WS.connect(host);71 expect((await c.count({ kinds: [1] })).closed).toBe("");72 expect(await c.sync("s1", { kinds: [1] })).toBe("");73 await rpc(host, owner, "setpolicy", { features: { count: false, sync: false } });74 expect((await c.count({ kinds: [1] })).closed).toMatch(/^unsupported: COUNT/);75 expect(await c.sync("s2", { kinds: [1] })).toMatch(/^unsupported: sync/);76 const nips = (await info(host)).supported_nips;77 expect(nips).not.toContain(45);78 expect(nips).not.toContain(77);79 expect(nips).toContain(1);80 });8182 it("names, files and pages off answer 404 at their doors and refuse uploads", async () => {83 const host = "feat-doors.bind.ws";84 const owner = generateSecretKey();85 const alice = generateSecretKey();86 await rpc(host, owner, "claim");87 await rpc(host, owner, "setmember", pk(alice), { name: "alice" });88 const up = await upload(host, alice, "a file");89 expect(up.status).toBe(200);90 expect(await status(host, "/.well-known/nostr.json?name=alice")).toBe(200);91 expect(await status(host, `/${up.sha}`)).toBe(200);92 expect(await status(host, "/.well-known/nostr/nip96.json")).toBe(200);93 expect(await status(host, "/feed.xml")).toBe(200);94 await rpc(host, owner, "setpolicy", { features: { names: false, files: false, pages: false } });95 expect(await status(host, "/.well-known/nostr.json?name=alice")).toBe(404);96 expect(await status(host, `/${up.sha}`)).toBe(404);97 expect(await status(host, "/.well-known/nostr/nip96.json")).toBe(404);98 expect(await status(host, "/feed.xml")).toBe(404);99 const again = await upload(host, alice, "another file");100 expect(again.status).toBe(404);101 // The page, NIP-11 and management stay.102 expect(await status(host, "/")).toBe(200);103 expect((await info(host)).supported_nips).not.toContain(5);104 expect((await rpc(host, owner, "stats")).status).toBe(200);105 });106107 it("discovery off takes the record down, and on brings it back", async () => {108 const host = "feat-discovery.bind.ws";109 const owner = generateSecretKey();110 await rpc(host, owner, "claim");111 const self = (await info(host)).self as string;112 const records = async () => (await post(host, owner, "/query", [{ kinds: [30166], authors: [self] }])).body.length;113 expect(await records()).toBe(1);114 await rpc(host, owner, "setpolicy", { features: { discovery: false } });115 expect(await records()).toBe(0);116 expect((await info(host)).supported_nips).not.toContain(66);117 await rpc(host, owner, "setpolicy", { features: { discovery: true } });118 expect(await records()).toBe(1);119 });120121 it("signer off treats kind 24133 like any other event", async () => {122 const host = "feat-signer.bind.ws";123 const owner = generateSecretKey();124 const stranger = generateSecretKey();125 await rpc(host, owner, "claim");126 await rpc(host, owner, "setpolicy", { writes: "owner", reads: "auth" });127 const c = await WS.connect(host);128 const traffic = () => ev(stranger, 24133, "x", [["p", getPublicKey(owner)]]);129 expect((await c.ok(traffic())).ok).toBe(true);130 expect((await c.open("nc", { kinds: [24133] })).closed).toBe("");131 await rpc(host, owner, "setpolicy", { features: { signer: false } });132 expect((await c.ok(traffic())).msg).toMatch(/^restricted:/);133 expect((await c.open("nc2", { kinds: [24133] })).closed).toMatch(/^auth-required/);134 expect((await info(host)).supported_nips).not.toContain(46);135 });136});