eleven / ocho Public

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

Plain git — no account needed to clone.

Download

Download this file

1// Tests for the saved-spaces list helpers (star + independent sort), run in Node.
2// Same harness style as home/test/crypto.test.mjs.
3// node home/test/serverlist.test.mjs
4import assert from "node:assert/strict";
5import {
6 partitionServers,
7 partitionArchived,
8 spaceRestoreURL,
9 setArchived,
10 toggleStar,
11 moveInGroup,
12 orderServers,
13 filterServers,
14 fuzzyMatch,
15 soleAdminHold,
16 addTombstone,
17 clearTombstone,
18 tombstonedAt,
19 mergeRemoved,
20 pruneTombstones,
21} from "../web/static/serverlist.js";
22
23let failures = 0;
24async function test(name, fn) {
25 try {
26 await fn();
27 console.log("PASS " + name);
28 } catch (e) {
29 console.log("FAIL " + name + " — " + (e && e.message));
30 failures++;
31 }
32}
33
34const urls = (list) => list.map((s) => s.url);
35
36await test("space links request automatic restore and preserve their query", () => {
37 assert.equal(spaceRestoreURL("https://chat.example.com"), "https://chat.example.com/?restore=1");
38 assert.equal(spaceRestoreURL("https://chat.example.com/?thread=7"), "https://chat.example.com/?thread=7&restore=1");
39 assert.equal(spaceRestoreURL("not a url"), "not a url");
40});
41
42await test("partitionServers splits starred from the rest, keeping array order", () => {
43 const servers = [
44 { url: "a", starred: true },
45 { url: "b" },
46 { url: "c", starred: true },
47 { url: "d" },
48 ];
49 const { starred, others } = partitionServers(servers);
50 assert.deepEqual(urls(starred), ["a", "c"]);
51 assert.deepEqual(urls(others), ["b", "d"]);
52});
53
54await test("partitionServers tolerates null/empty", () => {
55 assert.deepEqual(partitionServers(null), { starred: [], others: [] });
56 assert.deepEqual(partitionServers([]), { starred: [], others: [] });
57});
58
59await test("toggleStar adds then removes the flag, and doesn't mutate the input", () => {
60 const servers = [{ url: "a" }, { url: "b" }];
61 const on = toggleStar(servers, "a");
62 assert.equal(on.find((s) => s.url === "a").starred, true);
63 assert.equal(servers[0].starred, undefined, "input array/entry must be untouched");
64
65 const off = toggleStar(on, "a");
66 assert.equal("starred" in off.find((s) => s.url === "a"), false, "unstarred entry carries no flag");
67});
68
69await test("starring sends the item to the END of the Starred list", () => {
70 // x, y already starred; star 'b' → it lands after them (end of Starred).
71 const servers = [
72 { url: "x", starred: true },
73 { url: "y", starred: true },
74 { url: "a" },
75 { url: "b" },
76 { url: "c" },
77 ];
78 const { starred, others } = partitionServers(toggleStar(servers, "b"));
79 assert.deepEqual(urls(starred), ["x", "y", "b"], "b appended to Starred");
80 assert.deepEqual(urls(others), ["a", "c"], "others keep order minus b");
81});
82
83await test("unstarring sends the item to the TOP of the rest", () => {
84 // x, y starred; unstar 'x' → it becomes the FIRST of the rest.
85 const servers = [
86 { url: "x", starred: true },
87 { url: "y", starred: true },
88 { url: "a" },
89 { url: "b" },
90 ];
91 const { starred, others } = partitionServers(toggleStar(servers, "x"));
92 assert.deepEqual(urls(starred), ["y"], "x left Starred");
93 assert.deepEqual(urls(others), ["x", "a", "b"], "x is now the top of the rest");
94});
95
96await test("toggleStar leaves unknown urls unchanged", () => {
97 const servers = [{ url: "a", name: "A" }];
98 assert.deepEqual(toggleStar(servers, "zzz"), servers);
99});
100
101await test("moveInGroup reorders within a group and never disturbs the other", () => {
102 // starred: [a, c] others: [b, d] (interleaved in the master array)
103 const servers = [
104 { url: "a", starred: true },
105 { url: "b" },
106 { url: "c", starred: true },
107 { url: "d" },
108 ];
109 // Move starred 'c' up: it swaps with 'a' (its nearest starred neighbour),
110 // jumping over the non-starred 'b'. Others keep their order.
111 const moved = moveInGroup(servers, "c", -1);
112 assert.deepEqual(urls(partitionServers(moved).starred), ["c", "a"]);
113 assert.deepEqual(urls(partitionServers(moved).others), ["b", "d"], "other list unchanged");
114});
115
116await test("moveInGroup down within the 'others' group skips over starred entries", () => {
117 const servers = [
118 { url: "b" },
119 { url: "a", starred: true },
120 { url: "d" },
121 ];
122 const moved = moveInGroup(servers, "b", +1); // b swaps with d, over starred a
123 assert.deepEqual(urls(partitionServers(moved).others), ["d", "b"]);
124 assert.deepEqual(urls(partitionServers(moved).starred), ["a"]);
125});
126
127await test("moveInGroup at a group boundary is a no-op", () => {
128 const servers = [{ url: "a", starred: true }, { url: "b" }];
129 assert.deepEqual(moveInGroup(servers, "a", -1), servers, "top of its group can't move up");
130 assert.deepEqual(moveInGroup(servers, "b", +1), servers, "bottom of its group can't move down");
131});
132
133await test("orderServers reorders to match the given full order, preserving entries", () => {
134 const servers = [
135 { url: "a", starred: true },
136 { url: "b" },
137 { url: "c", starred: true },
138 ];
139 // A drag result: starred list became [c, a], others [b].
140 const next = orderServers(servers, ["c", "a", "b"]);
141 assert.deepEqual(urls(next), ["c", "a", "b"]);
142 assert.equal(next[0].starred, true, "entry objects (with starred) are preserved");
143 assert.equal(servers[0].url, "a", "input is not mutated");
144});
145
146await test("orderServers ignores unknown urls and keeps unlisted entries at the end", () => {
147 const servers = [{ url: "a" }, { url: "b" }, { url: "c" }];
148 const next = orderServers(servers, ["c", "zzz"]); // 'zzz' unknown; a,b unlisted
149 assert.deepEqual(urls(next), ["c", "a", "b"]);
150});
151
152await test("partitionArchived splits archived from active, keeping array order", () => {
153 const servers = [
154 { url: "a", starred: true },
155 { url: "b", archived: true },
156 { url: "c" },
157 { url: "d", archived: true },
158 ];
159 const { active, archived } = partitionArchived(servers);
160 assert.deepEqual(urls(active), ["a", "c"]);
161 assert.deepEqual(urls(archived), ["b", "d"]);
162});
163
164await test("partitionArchived tolerates null/empty", () => {
165 assert.deepEqual(partitionArchived(null), { active: [], archived: [] });
166 assert.deepEqual(partitionArchived([]), { active: [], archived: [] });
167});
168
169await test("setArchived archives: sets the flag, drops starred, doesn't mutate input", () => {
170 const servers = [{ url: "a", starred: true }, { url: "b" }];
171 const next = setArchived(servers, "a", true);
172 const entry = next.find((s) => s.url === "a");
173 assert.equal(entry.archived, true);
174 assert.equal("starred" in entry, false, "an archived space leaves the rail");
175 assert.equal(servers[0].starred, true, "input entry untouched");
176 assert.equal("archived" in servers[0], false, "input entry untouched");
177});
178
179await test("archiving sends the item to the TOP of the archived group, active kept in order", () => {
180 // x active, y already archived; archive 'x' → it lands above y (top of archive).
181 const servers = [
182 { url: "s", starred: true },
183 { url: "x" },
184 { url: "z" },
185 { url: "y", archived: true },
186 ];
187 const { active, archived } = partitionArchived(setArchived(servers, "x", true));
188 assert.deepEqual(urls(active), ["s", "z"], "active keeps order minus x");
189 assert.deepEqual(urls(archived), ["x", "y"], "x is the top of the archive");
190});
191
192await test("unarchiving sends the item to the BOTTOM of the active list", () => {
193 const servers = [
194 { url: "s", starred: true },
195 { url: "a" },
196 { url: "z", archived: true },
197 { url: "y", archived: true },
198 ];
199 const next = setArchived(servers, "z", false);
200 const entry = next.find((s) => s.url === "z");
201 assert.equal("archived" in entry, false, "unarchived entry carries no flag");
202 const { active, archived } = partitionArchived(next);
203 assert.deepEqual(urls(active), ["s", "a", "z"], "z rejoins at the bottom, after kept spaces");
204 assert.deepEqual(urls(archived), ["y"], "y stays archived");
205});
206
207await test("setArchived leaves unknown urls unchanged", () => {
208 const servers = [{ url: "a", name: "A" }];
209 assert.deepEqual(setArchived(servers, "zzz", true), servers);
210});
211
212await test("fuzzyMatch is a case-insensitive subsequence match", () => {
213 assert.equal(fuzzyMatch("fam", "family space"), true);
214 assert.equal(fuzzyMatch("fly", "family"), true, "f-l-y appear in order");
215 assert.equal(fuzzyMatch("xyz", "family"), false);
216 assert.equal(fuzzyMatch("", "anything"), true, "empty query matches");
217});
218
219await test("filterServers keeps fuzzy matches over name + host, order preserved", () => {
220 const servers = [
221 { url: "https://family.example.com", name: "Family", starred: true },
222 { url: "https://work.example.com", name: "Work" },
223 { url: "https://a4f.11ch.at" },
224 ];
225 assert.deepEqual(urls(filterServers(servers, "fam")), ["https://family.example.com"]);
226 assert.deepEqual(urls(filterServers(servers, "11ch")), ["https://a4f.11ch.at"], "matches the host");
227 assert.deepEqual(urls(filterServers(servers, "")), urls(servers), "empty query = everything");
228 assert.deepEqual(filterServers(servers, "zzzz"), [], "no matches");
229});
230
231await test("soleAdminHold engages only on an explicit sole-admin answer (fail-open)", () => {
232 assert.equal(soleAdminHold({ role: "admin", sole_admin: true }), true, "the one positive case");
233 assert.equal(soleAdminHold({ role: "admin", sole_admin: false }), false, "other admins exist");
234 assert.equal(soleAdminHold({ role: "admin" }), false, "older build without the field");
235 assert.equal(soleAdminHold({ role: "member", sole_admin: true }), false, "field without the role");
236 assert.equal(soleAdminHold({ role: "admin", sole_admin: "true" }), false, "malformed answer");
237 assert.equal(soleAdminHold(null), false, "no answer at all");
238});
239
240await test("addTombstone upserts newest-per-url without mutating the input", () => {
241 const removed = [{ url: "a", ts: 100 }];
242 const next = addTombstone(removed, "b", 200);
243 assert.deepEqual(
244 next.map((t) => t.url).sort(),
245 ["a", "b"],
246 );
247 assert.equal(removed.length, 1, "input untouched");
248 const bumped = addTombstone(next, "a", 300);
249 assert.equal(tombstonedAt(bumped, "a"), 300, "re-removal keeps the newest ts");
250 assert.equal(bumped.filter((t) => t.url === "a").length, 1, "one tombstone per url");
251});
252
253await test("clearTombstone removes the url; an emptied list drops out entirely", () => {
254 const removed = [{ url: "a", ts: 100 }, { url: "b", ts: 200 }];
255 assert.deepEqual(clearTombstone(removed, "a").map((t) => t.url), ["b"]);
256 assert.equal(clearTombstone([{ url: "a", ts: 100 }], "a"), undefined,
257 "empty list → undefined, so the blob key disappears");
258});
259
260await test("tombstonedAt returns the removal time, 0 when absent or malformed", () => {
261 assert.equal(tombstonedAt([{ url: "a", ts: 42 }], "a"), 42);
262 assert.equal(tombstonedAt([{ url: "a", ts: 42 }], "b"), 0);
263 assert.equal(tombstonedAt(null, "a"), 0);
264 assert.equal(tombstonedAt([{ url: "a", ts: "soon" }], "a"), 0, "malformed ts reads as never");
265});
266
267await test("mergeRemoved unions two lists, newest tombstone per url", () => {
268 const merged = mergeRemoved(
269 [{ url: "a", ts: 100 }, { url: "b", ts: 500 }],
270 [{ url: "a", ts: 300 }, { url: "c", ts: 200 }],
271 );
272 assert.equal(tombstonedAt(merged, "a"), 300, "newer side wins");
273 assert.equal(tombstonedAt(merged, "b"), 500);
274 assert.equal(tombstonedAt(merged, "c"), 200);
275 assert.equal(merged.length, 3);
276});
277
278await test("pruneTombstones drops year-old entries and caps the list at the newest 200", () => {
279 const now = 1_000_000_000_000;
280 const year = 365 * 24 * 60 * 60 * 1000;
281 const fresh = { url: "fresh", ts: now - 1000 };
282 const stale = { url: "stale", ts: now - year - 1 };
283 assert.deepEqual(pruneTombstones([fresh, stale], now), [fresh]);
284
285 const many = Array.from({ length: 250 }, (_, i) => ({ url: "u" + i, ts: now - i }));
286 const capped = pruneTombstones(many, now);
287 assert.equal(capped.length, 200);
288 assert.equal(tombstonedAt(capped, "u0"), now, "newest kept");
289 assert.equal(tombstonedAt(capped, "u249"), 0, "oldest dropped");
290});
291
292console.log(failures ? `\n${failures} FAILED` : "\nALL PASSED");
293process.exit(failures ? 1 : 0);
294