eleven / messenger Public

Clone
git clone https://amadan.net/eleven/messenger

Plain git — no account needed to clone.

Download

Download this file

1// Tests for the mini-app static validator. node test/check-app.test.mjs
2import assert from "node:assert/strict";
3import { spawnSync } from "node:child_process";
4import { mkdtempSync, writeFileSync } from "node:fs";
5import { tmpdir } from "node:os";
6import { join } from "node:path";
7
8const CHECKER = new URL("../tools/check-app.mjs", import.meta.url).pathname;
9const dir = mkdtempSync(join(tmpdir(), "check-app-"));
10let n = 0;
11
12// Write `source` to a temp app file and run the checker on it.
13function run(source) {
14 const file = join(dir, `app-${n++}.js`);
15 writeFileSync(file, source);
16 const res = spawnSync(process.execPath, [CHECKER, file], { encoding: "utf8" });
17 return { status: res.status, out: res.stdout + res.stderr };
18}
19
20let failures = 0;
21async function test(name, fn) {
22 try {
23 await fn();
24 console.log("PASS " + name);
25 } catch (e) {
26 console.log("FAIL " + name + " β€” " + (e && e.message));
27 failures++;
28 }
29}
30
31// A minimal valid app the good-path test uses; bad-path tests mutate it.
32const GOOD = `import { defineApp, addItem, removeItem } from "../apps.js";
33
34export const app = defineApp({
35 name: "note",
36 title: "Notes",
37 glyph: "πŸ“",
38 places: ["you"],
39 initialState: () => ({ items: [] }),
40 events: {
41 "note.add": (s, e) => (e.text ? { items: addItem(s.items, { id: e.id, text: e.text }) } : s),
42 "note.remove": (s, e) => ({ items: removeItem(s.items, e.id) }),
43 },
44 render(state, el, ctx) {
45 const d = document.createElement("div");
46 d.textContent = "static label";
47 d.innerHTML = "<b>static markup is fine</b>";
48 el.appendChild(d);
49 ctx.interval(() => {}, 1000);
50 },
51});
52`;
53
54await test("a valid app passes", () => {
55 const r = run(GOOD);
56 assert.equal(r.status, 0, r.out);
57 assert.ok(r.out.includes("all checks passed"), r.out);
58});
59
60await test("a syntax error fails the parse check", () => {
61 const r = run("import { defineApp } from \"../apps.js\";\nconst x = {;\n");
62 assert.notEqual(r.status, 0);
63 assert.ok(r.out.includes("does not parse"), r.out);
64});
65
66await test("zero and two defineApp calls both fail", () => {
67 const none = run("export const x = 1;\n");
68 assert.notEqual(none.status, 0);
69 assert.ok(none.out.includes("found 0"), none.out);
70 const twice = run(GOOD + "\ndefineApp({});\n");
71 assert.notEqual(twice.status, 0);
72 assert.ok(twice.out.includes("found 2"), twice.out);
73});
74
75await test("a missing required field fails", () => {
76 const r = run(GOOD.replace('glyph: "πŸ“",\n', ""));
77 assert.notEqual(r.status, 0);
78 assert.ok(r.out.includes('required field "glyph" missing'), r.out);
79});
80
81await test("places must be a non-empty valid subset β€” any combination, you not required", () => {
82 const badEntry = run(GOOD.replace('places: ["you"]', 'places: ["you", "everywhere"]'));
83 assert.notEqual(badEntry.status, 0);
84 assert.ok(badEntry.out.includes("everywhere"), badEntry.out);
85 const empty = run(GOOD.replace('places: ["you"]', "places: []"));
86 assert.notEqual(empty.status, 0);
87 assert.ok(empty.out.includes("at least one place"), empty.out);
88 const chatToo = run(GOOD.replace('places: ["you"]', 'places: ["you", "chat"]'));
89 assert.equal(chatToo.status, 0, chatToo.out);
90 // A space-only app (one shared copy for everyone, gif-style) is legal.
91 const spaceOnly = run(GOOD.replace('places: ["you"]', 'places: ["space"]'));
92 assert.equal(spaceOnly.status, 0, spaceOnly.out);
93});
94
95await test("legacy scope: personal still passes; missing both fails; scope: shared is retired", () => {
96 const legacy = run(GOOD.replace('places: ["you"]', 'scope: "personal"'));
97 assert.equal(legacy.status, 0, legacy.out);
98 assert.ok(legacy.out.includes("legacy scope"), legacy.out);
99 const neither = run(GOOD.replace('places: ["you"],\n', ""));
100 assert.notEqual(neither.status, 0);
101 assert.ok(neither.out.includes("declare places"), neither.out);
102 const shared = run(GOOD.replace('places: ["you"]', 'scope: "shared"'));
103 assert.notEqual(shared.status, 0);
104 assert.ok(shared.out.includes("declare places"), shared.out);
105});
106
107await test("event types must carry the app-name prefix", () => {
108 const r = run(GOOD.replace('"note.remove"', '"other.remove"'));
109 assert.notEqual(r.status, 0);
110 assert.ok(r.out.includes("other.remove"), r.out);
111});
112
113await test("fetch is forbidden", () => {
114 const r = run(GOOD.replace("el.appendChild(d);", 'fetch("/api/x");'));
115 assert.notEqual(r.status, 0);
116 assert.ok(r.out.includes("fetch("), r.out);
117});
118
119await test("localStorage and document.cookie are forbidden", () => {
120 const r = run(GOOD.replace("el.appendChild(d);", 'localStorage.setItem("a", "b"); const c = document.cookie;'));
121 assert.notEqual(r.status, 0);
122 assert.ok(r.out.includes("localStorage"), r.out);
123 assert.ok(r.out.includes("document.cookie"), r.out);
124});
125
126await test("bare setInterval is flagged; ctx.interval is not", () => {
127 const good = run(GOOD); // GOOD uses ctx.interval
128 assert.equal(good.status, 0, good.out);
129 const bad = run(GOOD.replace("ctx.interval(() => {}, 1000);", "setInterval(() => {}, 1000);"));
130 assert.notEqual(bad.status, 0);
131 assert.ok(bad.out.includes("setInterval"), bad.out);
132});
133
134await test("a forbidden token in a comment or string does not fail", () => {
135 const r = run(GOOD.replace("el.appendChild(d);", 'el.appendChild(d); // never call setInterval( or fetch(\nconst hint = "no localStorage here";'));
136 assert.equal(r.status, 0, r.out);
137});
138
139await test("innerHTML of dynamic content is flagged; a static literal is not", () => {
140 const good = run(GOOD); // GOOD assigns a static literal
141 assert.equal(good.status, 0, good.out);
142 const bad = run(GOOD.replace('d.innerHTML = "<b>static markup is fine</b>";', "d.innerHTML = item.text;"));
143 assert.notEqual(bad.status, 0);
144 assert.ok(bad.out.includes("innerHTML"), bad.out);
145 const tpl = run(GOOD.replace('d.innerHTML = "<b>static markup is fine</b>";', "d.innerHTML = `<b>${state.x}</b>`;"));
146 assert.notEqual(tpl.status, 0);
147 assert.ok(tpl.out.includes("innerHTML"), tpl.out);
148});
149
150await test("imports outside ../apps.js and ../format.js are flagged", () => {
151 const format = run(GOOD.replace('from "../apps.js";', 'from "../apps.js";\nimport { firstLink } from "../format.js";'));
152 assert.equal(format.status, 0, format.out);
153 const bad = run(GOOD.replace('from "../apps.js";', 'from "../apps.js";\nimport { x } from "./helpers.js";'));
154 assert.notEqual(bad.status, 0);
155 assert.ok(bad.out.includes("./helpers.js"), bad.out);
156});
157
158await test("dynamic import() is flagged", () => {
159 const r = run(GOOD.replace("el.appendChild(d);", 'import("../shell.js");'));
160 assert.notEqual(r.status, 0);
161 assert.ok(r.out.includes("dynamic import"), r.out);
162});
163
164if (failures) {
165 console.error(`\n${failures} test(s) failed`);
166 process.exit(1);
167}
168console.log("\nAll check-app tests passed");
169