import type { Database } from "bun:sqlite"; import { beforeAll, describe, expect, test } from "bun:test"; import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { Hono } from "hono"; import { openBucket } from "../src/bucket/reader.ts"; import { syncAll } from "../src/bucket/sync.ts"; import { fileAt, objectBytes, type Save } from "../src/mirror/queries.ts"; import { openMirror } from "../src/mirror/store.ts"; import { timelineSaves, timelineSvg } from "../src/pages/timeline.ts"; import { createApp } from "../src/server/app.ts"; import { openKeprDb } from "../src/server/db.ts"; import { type Fixture, makeKohBucket } from "./fixture.ts"; let fx: Fixture; let app: Hono; let kepr: Database; let mirrorDir: string; let tailSaves: string[]; const get = (path: string) => app.request(path); const body = async (path: string) => (await get(path)).text(); // History's IDs only, not the Atlas picker's, which lists every save. const historyIds = (page: string) => { const list = /data-list="history">(.*?)<\/div>/s.exec(page)?.[1] ?? ""; return [...list.matchAll(/class="history-id">([0-9a-f]+) m[1]); }; beforeAll(async () => { fx = await makeKohBucket(); const secretRepo = await makeKohBucket("hidden"); const large = await makeKohBucket("large"); // Over 1 MB each: text with a two-byte "é" split by the 1 MB mark, and invalid UTF-8 with no NUL. await writeFile( join(large.repoDir, "big.txt"), `${"a".repeat(1024 * 1024 - 1)}é${"b".repeat(1000)}\n`, ); await writeFile(join(large.repoDir, "big.bin"), new Uint8Array(1024 * 1024 + 500).fill(0xff)); Bun.spawnSync(["koh", "save", "add large files"], { cwd: large.repoDir, env: { PATH: process.env.PATH, HOME: join(large.repoDir, "../../home"), NO_COLOR: "1" }, }); // A file that gains its final newline, then loses it again. const tail = await makeKohBucket("tail"); const tailEnv = { PATH: process.env.PATH, HOME: join(tail.repoDir, "../../home"), NO_COLOR: "1" }; for (const [text, message] of [ ["a\nb", "add tail"], ["a\nb\n", "end tail with a newline"], ["a\nb", "drop the newline"], ] as const) { await writeFile(join(tail.repoDir, "tail.txt"), text); Bun.spawnSync(["koh", "save", message], { cwd: tail.repoDir, env: tailEnv }); } tailSaves = JSON.parse( Bun.spawnSync(["koh", "view", "--json"], { cwd: tail.repoDir, env: tailEnv, }).stdout.toString(), ).saves.map((save: { id: string }) => save.id); const dataDir = await mkdtemp(join(tmpdir(), "kepr-pages-")); await mkdir(join(dataDir, "assets")); await writeFile(join(dataDir, "assets", "plain.txt"), "asset\n"); kepr = openKeprDb(dataDir); mirrorDir = join(dataDir, "mirror"); for (const bucketDir of [fx.bucketDir, secretRepo.bucketDir, large.bucketDir, tail.bucketDir]) { const bucket = openBucket({ kind: "folder", path: bucketDir }); await syncAll({ bucket, kepr, mirrorDir }); } kepr.run("UPDATE repos SET public = 1, homepage = 'https://demo.example' WHERE name = 'demo'"); kepr.run("UPDATE repos SET public = 1 WHERE name IN ('large', 'tail')"); app = createApp( { domain: "kepr.example", dataDir, adminPasswordHash: "$argon2id$x", bucket: { kind: "folder", path: fx.bucketDir }, }, kepr, ); }); const [head, , second, first] = [0, 1, 2, 3].map((i) => () => fx.saveIds[i] as string); describe("the visibility gate", () => { test("a private repo and an unknown one are the same 404, everywhere", async () => { for (const suffix of ["", "/files/src/main.ts", "/raw/src/main.ts"]) { const hidden = await get(`/hidden${suffix}`); const missing = await get(`/nowhere${suffix}`); expect(hidden.status).toBe(404); expect(missing.status).toBe(404); expect((await hidden.text()).replaceAll("/hidden", "/nowhere")).toBe(await missing.text()); } }); }); describe("the repo page", () => { test("renders header, timeline, history and README at HEAD with a short cache", async () => { const response = await get("/demo"); expect(response.status).toBe(200); expect(response.headers.get("cache-control")).toBe("public, max-age=60"); const page = await response.text(); expect(page).toContain('
; neither is the intro. expect(/
(.*?)<\/p>/.exec(page)?.[1]).toBe(
"A small repo for Kepr's tests.",
);
expect(page).toContain(
'',
);
expect(page).toContain('href="https://demo.example"');
expect(page).toContain('TypeScript');
expect(page).toContain("3 saves");
expect(page).not.toContain('class="install-line"');
expect(page.match(/
');
expect(page).toContain('the site');
expect(page).not.toContain('href="javascript:');
expect(page).toContain("<script>alert");
expect(page).not.toContain("