tinyrelayfile
tinyrelay/scripts/wiki-compose.test.mjs/

scripts/wiki-compose.test.mjs raw
1import test from "node:test";
2import assert from "node:assert/strict";
3import fs from "node:fs";
4import vm from "node:vm";
5import { finalizeEvent, generateSecretKey, getPublicKey, verifyEvent } from "nostr-tools";
6import { sharedSigning } from "./test-signing.mjs";
7
8const source = fs.readFileSync("tinyclient/components.js", "utf8");
9const tinySource = fs.readFileSync("tinyclient/tiny.js", "utf8");
10const wikiSource = source.slice(source.indexOf(" const publishSigned = "), source.indexOf(" // ApprovalItem reacts"));
11const isHex64 = value => /^[0-9a-f]{64}$/.test(value || "");
12
13const util = (() => {
14 const sandbox = {TextEncoder, TextDecoder, URL, crypto: {subtle: {}}, document: {}};
15 sandbox.globalThis = sandbox;
16 vm.runInNewContext(tinySource, sandbox);
17 return sandbox.tiny.util;
18})();
19
20test("wikiName follows the relay's NIP-54 normalization", () => {
21 for (const [title, name] of [
22 ["What's Up?", "whats-up"],
23 ["日本語 Article", "日本語-article"],
24 ["Москва", "москва"],
25 [" --Hello__World-- ", "hello-world"],
26 ["Release notes 1.4", "release-notes-14"],
27 ["Files & Private Repositories", "files-private-repositories"],
28 ["café au lait", "café-au-lait"],
29 ["***", ""],
30 ["", ""]
31 ]) {
32 assert.equal(util.wikiName(title), name, title);
33 }
34 assert.equal(util.wikiName(undefined), "");
35});
36
37// setup runs one element class from components.js against fakes for the
38// signer, the relay and the form, the way collaboration-compose.test does.
39function setup(name, attributes, values = {}, options = {}) {
40 const secret = generateSecretKey(), pubkey = getPublicKey(secret);
41 const sent = [], signed = [];
42 class FormElement {
43 getAttribute(key) { return attributes[key] ?? null; }
44 report(message) { this.message = message; }
45 }
46 const window = {NostrSigner: {verifyEvent}, nostr: {
47 getPublicKey: async () => pubkey,
48 signEvent: async event => {
49 signed.push(structuredClone(event));
50 if (options.change) event.content = "changed";
51 return finalizeEvent(structuredClone(event), secret);
52 }
53 }};
54 const tiny = {util, navigated: [], signedFetch: async (path, method, body) => {
55 sent.push({path, method, event: JSON.parse(body)});
56 return Response.json(options.results?.[sent.length - 1] ?? options.result ?? {accepted: true});
57 }, navigate: async href => { tiny.navigated.push(href); }};
58 tiny.signing = sharedSigning(window, tiny);
59 const Constructor = vm.runInNewContext(`${wikiSource}\n${name}`, {FormElement, window, tiny, URL, isHex64, Date, JSON, encodeURIComponent});
60 const component = new Constructor();
61 component.submitter = options.submitter ? {value: options.submitter, textContent: options.label || ""} : null;
62 const form = {elements: Object.fromEntries(Object.entries(values).map(([key, value]) => [key, {value}]))};
63 return {component, form, sent, signed, tiny, pubkey};
64}
65
66const author = "a".repeat(64), base = "b".repeat(64), mergeID = "c".repeat(64);
67const coordinate = `30818:${author}:release-notes-1-4`;
68const page = {name: "release-notes-1-4", author, coordinate, event: base};
69
70test("a new page signs d, title and summary and opens the page", async () => {
71 const s = setup("WikiCompose", {name: "", author: "", coordinate: "", event: ""}, {title: "What's Up?", name: "", summary: "A greeting", content: "# Hi\n\nHello."});
72 await s.component.submit(s.form);
73 assert.equal(s.sent.length, 1);
74 const event = s.sent[0].event;
75 assert.equal(s.sent[0].path, "/events");
76 assert.equal(event.kind, 30818);
77 assert.deepEqual(event.tags, [["d", "whats-up"], ["title", "What's Up?"], ["summary", "A greeting"]]);
78 assert.equal(event.content, "# Hi\n\nHello.");
79 assert.equal(verifyEvent(event), true);
80 assert.equal(s.component.message, "Published.");
81 assert.deepEqual(s.tiny.navigated, ["/wiki/whats-up"]);
82});
83
84test("the name field wins over the title and is normalized", async () => {
85 const s = setup("WikiCompose", {name: "", author: "", coordinate: "", event: ""}, {title: "Bitcoin", name: " BTC Coin ", content: "Money."});
86 await s.component.submit(s.form);
87 assert.deepEqual(s.sent[0].event.tags, [["d", "btc-coin"], ["title", "Bitcoin"]]);
88});
89
90test("another signer publishes a fork of the version in view", async () => {
91 const s = setup("WikiCompose", page, {title: "Release notes 1.4", name: "release-notes-1-4", summary: "", content: "Rewritten."});
92 await s.component.submit(s.form);
93 assert.equal(s.sent.length, 1);
94 assert.deepEqual(s.sent[0].event.tags, [["d", "release-notes-1-4"], ["title", "Release notes 1.4"], ["a", coordinate, "", "fork"], ["e", base, "", "fork"]]);
95 assert.notEqual(s.sent[0].event.pubkey, author);
96});
97
98test("propose publishes the fork and then a merge request to the author", async () => {
99 const s = setup("WikiCompose", page, {title: "Release notes 1.4", name: "release-notes-1-4", summary: "Fix the upload section", content: "Rewritten."}, {submitter: "propose"});
100 await s.component.submit(s.form);
101 assert.equal(s.sent.length, 2);
102 const [version, request] = s.sent.map(item => item.event);
103 assert.equal(version.kind, 30818);
104 assert.equal(request.kind, 818);
105 assert.deepEqual(request.tags, [["a", coordinate], ["e", version.id, "", "source"], ["e", base], ["p", author]]);
106 assert.equal(request.content, "Fix the upload section");
107 assert.equal(verifyEvent(request), true);
108 assert.match(s.component.message, /proposed to aaaaaaaaaaaa/);
109 assert.deepEqual(s.tiny.navigated, ["/wiki/release-notes-1-4"]);
110});
111
112test("the author's own edit carries no fork tags and cannot be proposed", async () => {
113 const s = setup("WikiCompose", page, {title: "Release notes 1.4", name: "release-notes-1-4", content: "Edited."});
114 s.component.getAttribute = key => ({...page, author: s.pubkey, coordinate: `30818:${s.pubkey}:release-notes-1-4`})[key] ?? null;
115 await s.component.submit(s.form);
116 assert.deepEqual(s.sent[0].event.tags, [["d", "release-notes-1-4"], ["title", "Release notes 1.4"]]);
117 const own = setup("WikiCompose", page, {title: "Release notes 1.4", name: "release-notes-1-4", content: "Edited."}, {submitter: "propose"});
118 own.component.getAttribute = key => ({...page, author: own.pubkey})[key] ?? null;
119 await assert.rejects(own.component.submit(own.form), /your own version/);
120 assert.equal(own.sent.length, 0);
121});
122
123test("missing fields, a changed signature and a relay refusal stop before or after one event", async () => {
124 for (const [values, message] of [[{title: "", name: "", content: "x"}, /title/], [{title: "***", name: "", content: "x"}, /letter or digit/], [{title: "T", name: "", content: " "}, /content/]]) {
125 const s = setup("WikiCompose", {name: "", author: "", coordinate: "", event: ""}, values);
126 await assert.rejects(s.component.submit(s.form), message);
127 assert.equal(s.signed.length, 0);
128 }
129 const changed = setup("WikiCompose", page, {title: "T", name: "t", content: "x"}, {change: true});
130 await assert.rejects(changed.component.submit(changed.form), /changed/);
131 assert.equal(changed.sent.length, 0);
132 const refused = setup("WikiCompose", page, {title: "T", name: "t", content: "x"}, {submitter: "propose", results: [{accepted: false, message: "blocked"}]});
133 await assert.rejects(refused.component.submit(refused.form), /blocked/);
134 assert.equal(refused.sent.length, 1);
135 assert.deepEqual(refused.tiny.navigated, []);
136});
137
138test("nostr-react signs the reaction the pressed button carries", async () => {
139 for (const [reaction, message] of [["+", "Accepted."], ["-", "Rejected."]]) {
140 const s = setup("NostrReact", {event: mergeID, pubkey: author, kind: "818"}, {}, {submitter: reaction, label: reaction === "+" ? "Accept" : "Reject"});
141 await s.component.submit(s.form);
142 const event = s.sent[0].event;
143 assert.equal(event.kind, 7);
144 assert.equal(event.content, reaction);
145 assert.deepEqual(event.tags, [["e", mergeID, "", author], ["p", author], ["k", "818"]]);
146 assert.equal(verifyEvent(event), true);
147 assert.equal(s.component.message, message);
148 }
149 const plain = setup("NostrReact", {event: mergeID, pubkey: author}, {}, {submitter: "+"});
150 await plain.component.submit(plain.form);
151 assert.deepEqual(plain.sent[0].event.tags, [["e", mergeID, "", author], ["p", author]]);
152});
153
154test("nostr-react refuses unknown reactions and bad addresses before signing", async () => {
155 for (const [attributes, submitter, message] of [
156 [{event: mergeID, pubkey: author}, "meh", /Approve or Deny/],
157 [{event: "short", pubkey: author}, "+", /missing/],
158 [{event: mergeID, pubkey: ""}, "-", /missing/]
159 ]) {
160 const s = setup("NostrReact", attributes, {}, {submitter});
161 await assert.rejects(s.component.submit(s.form), message);
162 assert.equal(s.signed.length, 0);
163 assert.equal(s.sent.length, 0);
164 }
165});

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs