tinyrelayfile
tinyrelay/scripts/profile-form.test.mjs/

scripts/profile-form.test.mjs raw
1// The profile form merges the fields it shows over the profile the relay
2// holds, signs a kind 0, publishes it here and then to the listed relays.
3import test from "node:test";
4import assert from "node:assert/strict";
5import fs from "node:fs";
6import vm from "node:vm";
7import { finalizeEvent, generateSecretKey, getPublicKey, verifyEvent } from "nostr-tools/pure";
8import { sharedSigning } from "./test-signing.mjs";
9
10const source = fs.readFileSync("tinyclient/components.js", "utf8");
11const formSource = source.slice(source.indexOf(" // ProfileForm publishes"), source.indexOf(" // AgentGrant signs"));
12const plain = value => JSON.parse(JSON.stringify(value));
13
14function setup({attributes = {}, result, pool, noSigner = false, otherKey = false} = {}) {
15 const secret = generateSecretKey(), pubkey = getPublicKey(secret);
16 const sent = [], published = [], reports = [];
17 class FormElement {
18 getAttribute(name) { return attributes[name] ?? (name === "pubkey" ? (otherKey ? "f".repeat(64) : pubkey) : null); }
19 report(text, error) { reports.push({text, error: Boolean(error)}); }
20 }
21 const store = {};
22 const localStorage = {getItem: key => store[key] ?? null, setItem: (key, value) => { store[key] = value; }};
23 const window = {
24 nostr: noSigner ? undefined : {signEvent: async event => finalizeEvent(structuredClone(event), secret)},
25 NostrSigner: {verifyEvent, SimplePool: class { publish(relays, event) { published.push({relays, event}); return relays.map(relay => (pool?.[relay] === "fail" ? Promise.reject(Error("closed")) : Promise.resolve("ok"))); } close() {} }},
26 tinyNames: {cacheKey: "tiny:names:v1"}
27 };
28 const tiny = {
29 util: {relayURL: value => (/^wss?:\/\/[a-z0-9.-]+(?::\d+)?\/?$/i.test(String(value).trim()) ? String(value).trim().replace(/\/$/, "") : null)},
30 signedFetch: async (path, method, body) => { sent.push({path, method, event: JSON.parse(body)}); return Response.json(result ?? {accepted: true}); }
31 };
32 tiny.signing = sharedSigning(window, tiny);
33 const Constructor = vm.runInNewContext(`${formSource}\nProfileForm`, {FormElement, window, tiny, localStorage, JSON, Date, Error, Promise, setTimeout, Object, Array, String});
34 const form = values => ({elements: Object.fromEntries(Object.entries(values).map(([key, value]) => [key, {value}]))});
35 return {Constructor, component: new Constructor(), form, sent, published, reports, pubkey, store};
36}
37
38test("content merges shown fields over the held profile and validates links", () => {
39 const {Constructor} = setup();
40 const merged = Constructor.content({name: " dami ", display_name: "", about: "hi", picture: "https://x/p.png"}, JSON.stringify({name: "old", display_name: "Old Name", website: "https://kept.example", custom: {deep: true}}));
41 assert.deepEqual(plain(merged), {name: "dami", about: "hi", picture: "https://x/p.png", website: "https://kept.example", custom: {deep: true}});
42 assert.deepEqual(plain(Constructor.content({name: "x"}, "not json")), {name: "x"});
43 assert.throws(() => Constructor.content({picture: "javascript:alert(1)"}, "{}"), /http or https/);
44});
45
46test("relays parse from any separator, dedupe and cap at twelve", () => {
47 const {Constructor} = setup();
48 assert.deepEqual(plain(Constructor.relays("wss://a.example/\nwss://b.example, wss://a.example ftp://no.example")), ["wss://a.example", "wss://b.example"]);
49 assert.throws(() => Constructor.relays(Array.from({length: 13}, (_, i) => "wss://r" + i + ".example").join(" ")), /at most 12/);
50});
51
52test("submit publishes here first, then to the listed relays, and forgets the cached name", async () => {
53 const s = setup({pool: {"wss://b.example": "fail"}});
54 s.store["tiny:names:v1"] = JSON.stringify({[s.pubkey]: {event: null, at: 1}, other: {at: 2}});
55 await s.component.submit(s.form({name: "dami", about: "", relays: "wss://a.example\nwss://b.example"}));
56 assert.equal(s.sent.length, 1);
57 assert.equal(s.sent[0].path, "/events");
58 const event = s.sent[0].event;
59 assert.equal(event.kind, 0);
60 assert.deepEqual(plain(JSON.parse(event.content)), {name: "dami"});
61 assert.equal(verifyEvent(event), true);
62 assert.deepEqual(plain(s.published[0].relays), ["wss://a.example", "wss://b.example"]);
63 assert.equal(s.published[0].event.id, event.id);
64 const last = s.reports.at(-1).text.split("\n");
65 assert.deepEqual(last, ["Published here.", "wss://a.example: ok", "wss://b.example: failed, closed"]);
66 assert.deepEqual(Object.keys(JSON.parse(s.store["tiny:names:v1"])), ["other"]);
67});
68
69test("submit refuses without a signer, with another key, or when the relay rejects", async () => {
70 await assert.rejects(setup({noSigner: true}).component.submit(setup().form({name: "x"})), /Connect a signer/);
71 const other = setup({otherKey: true});
72 await assert.rejects(other.component.submit(other.form({name: "x"})), /different key/);
73 assert.equal(other.sent.length, 0);
74 const refused = setup({result: {accepted: false, message: "blocked: no"}});
75 await assert.rejects(refused.component.submit(refused.form({name: "x"})), /blocked: no/);
76 assert.equal(refused.published.length, 0);
77});

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

  • https://012.run/npub1cq2s86qrtadf36eqq3epm38wmhdynrrcwxf84eldk3f94dkv7ttsrulrz5/tinyrelay.git

Refs