hidden.test.ts

5.0 KB · raw

1// Covers per-repo hidden paths (June, 2026-09-30): gone from every page, the stats and new2// tarballs, kept hidden through later syncs, handed to Koh under `.kepr/hidden/`, and back on unhide.34import { 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});