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

scripts/collaboration-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, verifyEvent } from "nostr-tools";
6import { sharedSigning } from "./test-signing.mjs";
7
8const root = "a".repeat(64), author = "b".repeat(64), parent = "c".repeat(64), parentAuthor = "d".repeat(64);
9const coordinate = `30617:${author}:test`;
10const source = fs.readFileSync("tinyclient/components.js", "utf8");
11const composeSource = source.slice(source.indexOf(" class NostrCompose "), source.indexOf(" class NostrKey "));
12function setup(attributes, values = {}, options = {}) {
13 const sent = [], signed = [];
14 let resets = 0;
15 class FormElement {
16 getAttribute(name) { return attributes[name] ?? null; }
17 report(message) { this.message = message; }
18 }
19 const window = {NostrSigner: {verifyEvent}, nostr: {signEvent: async event => {
20 signed.push(structuredClone(event));
21 if (options.change) event.content = "changed";
22 return finalizeEvent(structuredClone(event), generateSecretKey());
23 }}};
24 const tiny = {signedFetch: async (path, method, body) => {
25 sent.push({path, method, event: JSON.parse(body)});
26 return Response.json(options.result ?? {accepted: true});
27 }};
28 tiny.signing = sharedSigning(window, tiny);
29 const Constructor = vm.runInNewContext(`${composeSource}\nNostrCompose`, {FormElement, window, tiny, URL, isHex64: value => /^[0-9a-f]{64}$/.test(value || "")});
30 const component = new Constructor();
31 const form = {elements: Object.fromEntries(Object.entries(values).map(([key, value]) => [key, {value}])), reset: () => { resets++; }};
32 return {component, form, sent, signed, resets: () => resets};
33}
34const rootAttrs = {coordinate, root, "root-pubkey": author, "root-kind": "1621"};
35
36test("issue composition signs subject, deduplicated labels and body", async () => {
37 const s = setup({kind: "1621", coordinate}, {title: " Bug ", labels: "ui, bug, ui", content: "Markdown **body**"});
38 await s.component.submit(s.form);
39 assert.deepEqual(s.sent[0].event.tags, [["a", coordinate], ["p", author], ["subject", "Bug"], ["t", "ui"], ["t", "bug"]]);
40 assert.equal(s.sent[0].path, "/events");
41 assert.equal(s.sent[0].event.kind, 1621);
42 assert.equal(verifyEvent(s.sent[0].event), true);
43 assert.equal(s.resets(), 1);
44});
45
46test("nested NIP-22 replies preserve the root and address the immediate parent", async () => {
47 const s = setup({...rootAttrs, kind: "1111", parent, "parent-pubkey": parentAuthor, "parent-kind": "1111"}, {content: "A reply"});
48 await s.component.submit(s.form);
49 assert.deepEqual(s.sent[0].event.tags, [["a", coordinate], ["E", root, "", author], ["K", "1621"], ["P", author], ["e", parent, "", parentAuthor], ["k", "1111"], ["p", parentAuthor]]);
50});
51
52test("review comments carry file and line tags from hidden inputs or attributes", async () => {
53 const pullAttrs = {coordinate, root, "root-pubkey": author, "root-kind": "1618"};
54 const fromInputs = setup({...pullAttrs, kind: "1111"}, {content: "Prefer two", file: "README", line: "2", side: "new"});
55 await fromInputs.component.submit(fromInputs.form);
56 assert.deepEqual(fromInputs.sent[0].event.tags.slice(-2), [["file", "README"], ["line", "2", "new"]]);
57 const fromAttributes = setup({...pullAttrs, kind: "1111", file: "src/app.js", line: "14", side: "old"}, {content: "Keep this"});
58 await fromAttributes.component.submit(fromAttributes.form);
59 assert.deepEqual(fromAttributes.sent[0].event.tags.slice(-2), [["file", "src/app.js"], ["line", "14", "old"]]);
60 const plain = setup({...pullAttrs, kind: "1111"}, {content: "General note"});
61 await plain.component.submit(plain.form);
62 assert.equal(plain.sent[0].event.tags.some(tag => tag[0] === "file" || tag[0] === "line"), false);
63});
64
65test("incomplete or misplaced line references are rejected before signing", async () => {
66 const pullAttrs = {coordinate, root, "root-pubkey": author, "root-kind": "1618"};
67 for (const [attrs, values] of [
68 [{...pullAttrs, kind: "1111"}, {content: "x", file: "README", line: "0", side: "new"}],
69 [{...pullAttrs, kind: "1111"}, {content: "x", file: "README", line: "2", side: "left"}],
70 [{...pullAttrs, kind: "1111"}, {content: "x", line: "2", side: "new"}],
71 [{...pullAttrs, kind: "1111"}, {content: "x", file: "README"}],
72 [{...rootAttrs, kind: "1111"}, {content: "x", file: "README", line: "2", side: "new"}]
73 ]) {
74 const s = setup(attrs, values);
75 await assert.rejects(s.component.submit(s.form), /line|pull request/);
76 assert.equal(s.signed.length, 0);
77 assert.equal(s.sent.length, 0);
78 }
79});
80
81test("status selector signs a NIP-34 root marker with an optional empty body", async () => {
82 const s = setup({...rootAttrs, kind: "status"}, {status: "1632", content: ""});
83 await s.component.submit(s.form);
84 assert.equal(s.sent[0].event.kind, 1632);
85 assert.deepEqual(s.sent[0].event.tags, [["a", coordinate], ["e", root, "", "root"], ["p", author]]);
86});
87
88test("PRs publish the commit, clone and merge-base tags", async () => {
89 const commit = "1".repeat(40), base = "2".repeat(40);
90 const s = setup({kind: "1618", coordinate}, {title: "Change", commit, "merge-base": base, clone: "https://example.test/project.git", content: "Why"});
91 await s.component.submit(s.form);
92 assert.deepEqual(s.sent[0].event.tags.slice(-3), [["c", commit], ["clone", "https://example.test/project.git"], ["merge-base", base]]);
93});
94
95test("relay rejection and signer substitution retain the draft", async () => {
96 for (const options of [{result: {accepted: false, message: "blocked"}}, {change: true}]) {
97 const s = setup({kind: "1621", coordinate}, {title: "Bug", content: "Details"}, options);
98 await assert.rejects(s.component.submit(s.form), /blocked|changed/);
99 assert.equal(s.resets(), 0);
100 if (options.change) assert.equal(s.sent.length, 0);
101 }
102});
103
104test("invalid conversation addresses and PR commit IDs are rejected before signing", async () => {
105 for (const [attrs, values] of [[{...rootAttrs, kind: "1111", parent: "bad"}, {content: "Reply"}], [{kind: "1618", coordinate}, {title: "PR", commit: "main", clone: "https://example.test/repo"}]]) {
106 const s = setup(attrs, values);
107 await assert.rejects(s.component.submit(s.form));
108 assert.equal(s.signed.length, 0);
109 assert.equal(s.sent.length, 0);
110 }
111});

About

ownerc01503e8035f
visibilitypublic
headd730679639bf

Maintainers

  • c01503e8035f owner
  • ba1b5beed5b5 agent hermes

Clone

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

Refs