import { expect, test } from "bun:test"; import { cp, mkdtemp } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createApp } from "../src/server/app.ts"; import { openKeprDb } from "../src/server/db.ts"; import { Browser, instance } from "./browser.ts"; import { makeKohBucket } from "./fixture.ts"; async function app() { const dataDir = await mkdtemp(join(tmpdir(), "kepr-kepr2-")); const config = { domain: "kepr.example", dataDir, adminPasswordHash: "$argon2id$x", bucket: { kind: "folder" as const, path: dataDir }, }; const kepr = openKeprDb(dataDir); // The redirects read only the visibility flag, so no mirror is needed. kepr.run("INSERT INTO repos (name, public) VALUES ('demo', 1), ('hidden', 0)"); return createApp(config, kepr); } const MOVED = [ "?panel=issues", "?panel=trust", "/releases/2.1.0", "/file?path=src/main.ts", "/bundle", "/bundle.blake3", ]; test("§28: Kepr 2's moved addresses answer 301 with the new one", async () => { const kepr = await app(); const moves: [string, string][] = [ ["/demo?panel=issues", "/demo/issues"], ["/demo?panel=builds", "/demo/releases"], ["/demo?panel=archives", "/demo/archives"], ["/demo?panel=trust", "/demo"], ["/demo/releases/2.1.0", "/demo/releases#2.1.0"], ["/demo/file?path=src/main.ts", "/demo/files/src/main.ts"], ["/demo/file?path=docs/a%20b.md", "/demo/files/docs/a%20b.md"], ["/demo/bundle", "/demo/archive/HEAD.tar.gz"], ["/demo/bundle.blake3", "/demo/archive/HEAD.tar.gz"], ["/source", "/kepr"], ]; for (const [from, to] of moves) { const response = await kepr.request(from); expect([from, response.status, response.headers.get("location")]).toEqual([from, 301, to]); } }); test("§20: a private or unknown repo's old addresses 404 instead of redirecting", async () => { const kepr = await app(); for (const repo of ["hidden", "nowhere"]) { for (const rest of MOVED) { const path = `/${repo}${rest}`; expect([path, (await kepr.request(path)).status]).toEqual([path, 404]); } } }); test("§28: Kepr 2's retired addresses answer 410 Gone, for any method", async () => { const kepr = await app(); const gone = [ "/tickets", "/tickets/new", "/tickets/3", "/drafts", "/drafts/abc", "/join", "/cli-auth", "/api/cli-auth/exchange", "/api/cli-auth/session", "/api/repos", "/api/repos/demo/offer/negotiate", "/api/repos/demo/offer/complete", "/api/repos/demo/snapshot", "/api/releases/upload", "/protocol/v1/protocol.zig", "/feed", "/demo/feed", ]; for (const path of gone) { expect([path, (await kepr.request(path)).status]).toEqual([path, 410]); const post = await kepr.request(path, { method: "POST" }); expect([path, post.status]).toEqual([path, 410]); } }); test("the redirects shadow none of §22.1's routes, and refuse unsafe input", async () => { const kepr = await app(); for (const path of [ "/demo/releases/latest", "/demo/releases/latest.json", "/demo/releases/history.json", "/demo", "/demo?panel=nope", ]) { const status = (await kepr.request(path)).status; expect([path, status === 301 || status >= 500]).toEqual([path, false]); } for (const path of [ "/demo/file", "/demo/file?path=../etc/passwd", "/demo/file?path=a//b", "/demo/releases/bad_version", ]) { expect([path, (await kepr.request(path)).status]).toEqual([path, 404]); } }); test("a bucket repo named like one of Kepr's own routes is not synced, and the console says why", async () => { const fx = await makeKohBucket(); await cp(join(fx.bucketDir, fx.repo), join(fx.bucketDir, "browse"), { recursive: true }); const inst = await instance(fx.bucketDir); const rows = inst.kepr.query("SELECT name, last_sync_error FROM repos ORDER BY name").all(); const clash = `not synced: the name "browse" clashes with Kepr's own /browse route; rename the repo`; expect(rows).toEqual([ { name: "browse", last_sync_error: clash }, { name: "demo", last_sync_error: null }, ]); expect(await Bun.file(join(inst.dataDir, "mirror/browse.db")).exists()).toBe(false); const browser = new Browser(inst.app); await browser.signIn(); expect(await (await browser.get("/admin/repos")).text()).toContain( `sync error: ${clash.replaceAll('"', """).replaceAll("'", "'")}`, ); });