1234import { beforeAll, describe, expect, test } from "bun:test";5import { mkdir, writeFile } from "node:fs/promises";6import { join } from "node:path";7import { openBucket } from "../src/bucket/reader.ts";8import { syncAll } from "../src/bucket/sync.ts";9import { openMirror } from "../src/mirror/store.ts";10import { Browser, type Instance, instance } from "./browser.ts";11import { makeRepos } from "./fixture.ts";1213describe("hidden paths", () => {14 let inst: Instance;15 let browser: Browser;16 let repos: Awaited<ReturnType<typeof makeRepos>>;17 let work: string;18 const status = async (path: string) => (await inst.app.request(path)).status;19 const text = async (path: string) => (await inst.app.request(path)).text();20 const hide = (hidden_paths: string) =>21 browser.post("/admin/repos/app", { public: "1", hidden_paths });22 const sync = () =>23 syncAll({24 bucket: openBucket({ kind: "folder", path: repos.bucketDir }),25 kepr: inst.kepr,26 mirrorDir: join(inst.dataDir, "mirror"),27 });2829 beforeAll(async () => {30 repos = await makeRepos(["app"]);31 work = join(repos.bucketDir, "..", "work", "app");32 await mkdir(join(work, "private"));33 await writeFile(join(work, "private", "notes.md"), "# private\n");34 await writeFile(join(work, "tool.py"), "print(1)\n");35 await writeFile(join(work, "main.ts"), "export const a = 1;\n");36 repos.koh("app", "save", "add sources");37 repos.koh("app", "offer");38 inst = await instance(repos.bucketDir);39 browser = new Browser(inst.app);40 await browser.signIn();41 await hide("");42 });4344 test("refuses unsafe paths and changes nothing", async () => {45 for (const path of ["../x", "/etc", "a//b", "x".repeat(501)]) {46 expect((await hide(path)).status).toBe(400);47 }48 expect(await status("/app/files/tool.py")).toBe(200);49 });5051 test("a hidden file or folder is gone everywhere and handed to Koh", async () => {52 const saved = await hide("tool.py\r\nprivate/\n");53 expect(saved.status).toBe(303);54 for (const path of ["/app/files/tool.py", "/app/raw/tool.py", "/app/raw/private/notes.md"]) {55 expect(`${path} ${await status(path)}`).toBe(`${path} 404`);56 }57 expect(await status("/app/files/main.ts")).toBe(200);58 const page = await text("/app");59 expect(page).not.toContain("tool.py");60 expect(page).not.toContain("notes.md");61 expect(page).not.toContain("Python");62 const head = (await inst.app.request("/app/archive/HEAD.tar.gz")).headers.get("location");63 const tarball = new Uint8Array(await (await inst.app.request(head ?? "")).arrayBuffer());64 const listed = Bun.spawnSync(["tar", "-tzf", "-"], { stdin: tarball }).stdout.toString();65 expect(listed).toContain("main.ts");66 expect(listed).not.toContain("tool.py");67 expect(await Bun.file(join(repos.bucketDir, ".kepr/hidden/app")).text()).toBe(68 "tool.py\nprivate\n",69 );70 expect(await (await browser.get("/admin/repos?repo=app&tab=settings")).text()).toContain(71 ">tool.py\nprivate</textarea>",72 );73 });7475 test("stays hidden in saves that arrive later", async () => {76 await writeFile(join(work, "tool.py"), "print(2)\n");77 repos.koh("app", "save", "change the tool");78 repos.koh("app", "offer");79 await sync();80 expect(await status("/app/files/tool.py")).toBe(404);81 expect(await text("/app")).toContain("change the tool");82 });8384 test("unhiding brings the files back", async () => {85 expect((await hide("")).status).toBe(303);86 expect(await status("/app/files/tool.py")).toBe(200);87 expect(await text("/app/raw/tool.py")).toBe("print(2)\n");88 expect(await status("/app/raw/private/notes.md")).toBe(200);89 expect(await Bun.file(join(repos.bucketDir, ".kepr/hidden/app")).text()).toBe("");90 });9192 test("an old kept tarball is never served once a path is hidden, even if cleanup failed", async () => {93 const head = (await inst.app.request("/app/archive/HEAD.tar.gz")).headers.get("location") ?? "";94 await inst.app.request(head);95 const keptDir = join(repos.bucketDir, ".kepr/tarballs/app");96 const old = new Map<string, Uint8Array>();97 for (const path of new Bun.Glob("**/*.tar.gz").scanSync(keptDir)) {98 old.set(path, await Bun.file(join(keptDir, path)).bytes());99 }100 expect(old.size).toBeGreaterThan(0);101 await hide("tool.py");102 for (const [path, bytes] of old) await Bun.write(join(keptDir, path), bytes);103 const tarball = new Uint8Array(await (await inst.app.request(head)).arrayBuffer());104 const listed = Bun.spawnSync(["tar", "-tzf", "-"], { stdin: tarball }).stdout.toString();105 expect(listed).toContain("main.ts");106 expect(listed).not.toContain("tool.py");107 await hide("");108 });109110 test("rebuilding a mirror keeps handles that requests already hold usable", async () => {111 const mirror = openMirror(join(inst.dataDir, "mirror"), "app");112 const count = () =>113 mirror114 .query<{ n: number }, []>("SELECT COUNT(*) AS n FROM save_files WHERE path = 'tool.py'")115 .get()?.n;116 await hide("tool.py");117 expect(count()).toBe(0);118 await hide("");119 expect(count()).toBeGreaterThan(0);120 });121});