1import { afterAll, beforeAll, describe, expect, setSystemTime, test } from "bun:test";2import { fileIssue } from "../src/issues/store.ts";3import { Browser, type Instance, instance } from "./browser.ts";4import { makeRepos, writeRelease } from "./fixture.ts";56let clock = Date.parse("2026-09-26T12:00:00Z");7const tick = (seconds: number) => {8 clock += seconds * 1000;9 setSystemTime(new Date(clock));10};1112describe("repos and visibility", () => {13 let inst: Instance;14 let browser: Browser;15 let bucketDir: string;16 let save: string;17 const publicPaths = () => [18 "/app",19 "/app/files/README.md",20 "/app/raw/README.md",21 `/app/saves/${save}`,22 `/app/archive/${save}.tar.gz`,23 "/app/archive/HEAD.tar.gz",24 "/app/archives",25 "/app/releases",26 "/app/releases/latest",27 "/app/releases/latest.json",28 "/app/releases/history.json",29 "/app/releases/latest/version.txt",30 "/app/releases/latest/linux-x86_64",31 "/app/releases/1.0.0/checksums.txt",32 "/app/releases/1.0.0/linux-x86_64",33 "/app/install.sh",34 "/api/app/releases/latest",35 "/api/app/releases/history",36 "/app/update-info",37 "/app/meta",38 "/app/issues",39 "/app/issues/new",40 "/app/issues/1",41 ];42 const statuses = async () => {43 const out: string[] = [];44 for (const path of publicPaths()) out.push(`${path} ${(await inst.app.request(path)).status}`);45 return out;46 };47 const text = async (path: string) => (await inst.app.request(path)).text();48 const publish = (repo: string, fields: Record<string, string>) =>49 browser.post(`/admin/repos/${repo}`, { sort_order: "0", ...fields });5051 beforeAll(async () => {52 setSystemTime(new Date(clock));53 const repos = await makeRepos(["app", "other"]);54 bucketDir = repos.bucketDir;55 save = repos.saves.app as string;56 await writeRelease(bucketDir, "app", {57 version: "1.0.0",58 save,59 files: [{ platform: "linux-x86_64", name: "app", bytes: new Uint8Array([1]) }],60 });61 inst = await instance(bucketDir);62 inst.kepr.run("INSERT INTO settings (key, value) VALUES ('handle', 'june')");63 fileIssue(inst.kepr, {64 repo: "app",65 title: "It broke",66 body: "b",67 type: "bug",68 version: "",69 reporterName: "",70 contactEmail: "",71 ipHash: "x",72 filedAt: 1,73 });74 browser = new Browser(inst.app);75 await browser.signIn();76 });77 afterAll(() => setSystemTime());7879 test("every repo the sync found is listed, private, with its last sync", async () => {80 const page = await (await browser.get("/admin/repos")).text();81 expect(page).toContain('id="repo-app"');82 expect(page).toContain('id="repo-other"');83 expect(page.match(/<span>private<\/span>/g)?.length).toBe(2);84 expect(page).toContain("last sync 2026-09-26 12:00 UTC");85 expect((await statuses()).every((line) => line.endsWith(" 404"))).toBe(true);86 });8788 test("bad settings are refused and change nothing", async () => {89 for (const fields of <Record<string, string>[]>[90 { homepage: "javascript:alert(1)" },91 { sort_order: "first" },92 { install1: "one\ntwo" },93 { category: "x".repeat(501) },94 ]) {95 expect((await publish("app", { public: "1", ...fields })).status).toBe(400);96 }97 expect((await inst.app.request("/app")).status).toBe(404);98 expect((await browser.post("/admin/repos/nowhere", { public: "1" })).status).toBe(404);99 });100101 test("publishing opens every public surface, with the settings shown", async () => {102 await publish("other", { public: "1" });103 const response = await publish("app", {104 public: "1",105 category: "tools",106 homepage: "https://app.example",107 install1: "nix profile add app",108 });109 expect(response.status).toBe(303);110 const lines = await statuses();111 expect(lines.filter((line) => line.endsWith(" 404"))).toEqual([]);112 expect(await text("/app")).toContain("https://app.example");113 expect(await text("/app")).toContain("nix profile add app");114 expect(await text("/")).toContain('href="/app"');115 expect(await text("/browse")).toContain('href="/app"');116 expect(await text("/@june")).toContain('href="/app"');117 expect(await text("/sitemap.xml")).toContain("/app</loc>");118 });119120 test("flipping a repo private 404s every surface and drops it from lists and counts", async () => {121 const stat = async () =>122 [...(await text("/@june")).matchAll(/profile-stat-number">([^<]*)</g)].map((m) => m[1]);123 124 expect(await stat()).toEqual(["2", "2", "1", "2"]);125 await publish("app", { category: "tools" });126 expect((await statuses()).filter((line) => !line.endsWith(" 404"))).toEqual([]);127 for (const path of ["/", "/browse", "/@june"]) expect(await text(path)).not.toContain("/app");128 expect(await text("/sitemap.xml")).not.toContain("/app<");129 expect(await text("/")).not.toContain("tools");130 expect(await stat()).toEqual(["1", "1", "0", "0"]);131 132 expect(await (await browser.get("/admin/repos")).text()).toContain('value="tools"');133 });134135 test("sync now reads the bucket at once and records the time", async () => {136 await writeRelease(bucketDir, "other", {137 version: "2.0.0",138 save: save,139 files: [{ platform: "linux-x86_64", name: "other", bytes: new Uint8Array([2]) }],140 });141 tick(60);142 expect((await inst.app.request("/other/releases/latest/version.txt")).status).toBe(404);143 const response = await browser.post("/admin/sync", {});144 expect(response.status).toBe(303);145 expect(await text("/other/releases/latest/version.txt")).toBe("2.0.0");146 expect(await (await browser.get("/admin/repos")).text()).toContain(147 "last sync 2026-09-26 12:01 UTC",148 );149 });150});