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('

demo

'); // The README opens with an HTML comment and a raw

; 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(/builds'); }); test("the issues tab counts only this repo's visible open issues and hides zero", async () => { kepr.run("BEGIN"); try { kepr.run(`INSERT INTO issues (repo, number, title, body, type, filed_at, hidden, status) VALUES ('demo', 1, 'one', 'body', 'bug', 1, 0, 'open'), ('demo', 2, 'two', 'body', 'bug', 1, 0, 'open'), ('demo', 3, 'three', 'body', 'bug', 1, 0, 'open'), ('demo', 4, 'hidden', 'body', 'bug', 1, 1, 'open'), ('demo', 5, 'closed', 'body', 'bug', 1, 0, 'closed'), ('large', 1, 'another repo', 'body', 'bug', 1, 0, 'open')`); expect(await body("/demo")).toContain( '3', ); kepr.run("UPDATE issues SET status = 'closed' WHERE repo = 'demo' AND number = 1"); expect(await body("/demo")).toContain( '2', ); kepr.run("UPDATE issues SET status = 'closed' WHERE repo = 'demo' AND number = 2"); expect(await body("/demo")).toContain( '1', ); kepr.run("UPDATE issues SET status = 'closed' WHERE repo = 'demo'"); expect(await body("/demo")).not.toContain('class="repo-issue-count"'); } finally { kepr.run("ROLLBACK"); } }); test("a page past the last, or too large to bind, is page 1", async () => { for (const query of ["?page=2", `?page=${"9".repeat(400)}`, "?page=1e999", "?page=x"]) { const response = await get(`/demo${query}`); expect([query, response.status]).toEqual([query, 200]); expect(historyIds(await response.text()).length).toBe(3); } }); test("checkpoints never appear", async () => { expect(await body("/demo")).not.toContain(fx.saveIds[1] as string); }); test("the README points relative links and images at raw files and drops unsafe links", async () => { const page = await body("/demo"); expect(page).toContain('the guide'); expect(page).toContain('logo'); expect(page).toContain('the site'); expect(page).not.toContain('href="javascript:'); expect(page).toContain("<script>alert"); expect(page).not.toContain("