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 { Browser, type Instance, instance } from "./browser.ts";10import { makeRepos } from "./fixture.ts";1112describe("hidden paths", () => {13 let inst: Instance;14 let browser: Browser;15 let repos: Awaited<ReturnType<typeof makeRepos>>;16 let work: string;17 const status = async (path: string) => (await inst.app.request(path)).status;18 const text = async (path: string) => (await inst.app.request(path)).text();19 const hide = (hidden_paths: string) =>20 browser.post("/admin/repos/app", { public: "1", hidden_paths });21 const sync = () =>22 syncAll({23 bucket: openBucket({ kind: "folder", path: repos.bucketDir }),24 kepr: inst.kepr,25 mirrorDir: join(inst.dataDir, "mirror"),26 });2728 beforeAll(async () => {29 repos = await makeRepos(["app"]);30 work = join(repos.bucketDir, "..", "work", "app");31 await mkdir(join(work, "private"));32 await writeFile(join(work, "private", "notes.md"), "# private\n");33 await writeFile(join(work, "tool.py"), "print(1)\n");34 await writeFile(join(work, "main.ts"), "export const a = 1;\n");35 repos.koh("app", "save", "add sources");36 repos.koh("app", "offer");37 inst = await instance(repos.bucketDir);38 browser = new Browser(inst.app);39 await browser.signIn();40 await hide("");41 });4243 test("refuses unsafe paths and changes nothing", async () => {44 for (const path of ["../x", "/etc", "a//b", "x".repeat(501)]) {45 expect((await hide(path)).status).toBe(400);46 }47 expect(await status("/app/files/tool.py")).toBe(200);48 });4950 test("a hidden file or folder is gone everywhere and handed to Koh", async () => {51 const saved = await hide("tool.py\r\nprivate/\n");52 expect(saved.status).toBe(303);53 for (const path of ["/app/files/tool.py", "/app/raw/tool.py", "/app/raw/private/notes.md"]) {54 expect(`${path} ${await status(path)}`).toBe(`${path} 404`);55 }56 expect(await status("/app/files/main.ts")).toBe(200);57 const page = await text("/app");58 expect(page).not.toContain("tool.py");59 expect(page).not.toContain("notes.md");60 expect(page).not.toContain("Python");61 const head = (await inst.app.request("/app/archive/HEAD.tar.gz")).headers.get("location");62 const tarball = new Uint8Array(await (await inst.app.request(head ?? "")).arrayBuffer());63 const listed = Bun.spawnSync(["tar", "-tzf", "-"], { stdin: tarball }).stdout.toString();64 expect(listed).toContain("main.ts");65 expect(listed).not.toContain("tool.py");66 expect(await Bun.file(join(repos.bucketDir, ".kepr/hidden/app")).text()).toBe(67 "tool.py\nprivate\n",68 );69 expect(await (await browser.get("/admin/repos?repo=app")).text()).toContain(70 ">tool.py\nprivate</textarea>",71 );72 });7374 test("stays hidden in saves that arrive later", async () => {75 await writeFile(join(work, "tool.py"), "print(2)\n");76 repos.koh("app", "save", "change the tool");77 repos.koh("app", "offer");78 await sync();79 expect(await status("/app/files/tool.py")).toBe(404);80 expect(await text("/app")).toContain("change the tool");81 });8283 test("unhiding brings the files back", async () => {84 expect((await hide("")).status).toBe(303);85 expect(await status("/app/files/tool.py")).toBe(200);86 expect(await text("/app/raw/tool.py")).toBe("print(2)\n");87 expect(await status("/app/raw/private/notes.md")).toBe(200);88 expect(await Bun.file(join(repos.bucketDir, ".kepr/hidden/app")).text()).toBe("");89 });90});