1import { expect, test } from "bun:test";2import { cp, mkdtemp } from "node:fs/promises";3import { tmpdir } from "node:os";4import { join } from "node:path";5import { createApp } from "../src/server/app.ts";6import { openKeprDb } from "../src/server/db.ts";7import { Browser, instance } from "./browser.ts";8import { makeKohBucket } from "./fixture.ts";910async function app() {11 const dataDir = await mkdtemp(join(tmpdir(), "kepr-kepr2-"));12 const config = {13 domain: "kepr.example",14 dataDir,15 adminPasswordHash: "$argon2id$x",16 bucket: { kind: "folder" as const, path: dataDir },17 };18 const kepr = openKeprDb(dataDir);19 20 kepr.run("INSERT INTO repos (name, public) VALUES ('demo', 1), ('hidden', 0)");21 return createApp(config, kepr);22}2324const MOVED = [25 "?panel=issues",26 "?panel=trust",27 "/releases/2.1.0",28 "/file?path=src/main.ts",29 "/bundle",30 "/bundle.blake3",31];3233test("§28: Kepr 2's moved addresses answer 301 with the new one", async () => {34 const kepr = await app();35 const moves: [string, string][] = [36 ["/demo?panel=issues", "/demo/issues"],37 ["/demo?panel=builds", "/demo/releases"],38 ["/demo?panel=archives", "/demo/archives"],39 ["/demo?panel=trust", "/demo"],40 ["/demo/releases/2.1.0", "/demo/releases#2.1.0"],41 ["/demo/file?path=src/main.ts", "/demo/files/src/main.ts"],42 ["/demo/file?path=docs/a%20b.md", "/demo/files/docs/a%20b.md"],43 ["/demo/bundle", "/demo/archive/HEAD.tar.gz"],44 ["/demo/bundle.blake3", "/demo/archive/HEAD.tar.gz"],45 ["/source", "/kepr"],46 ];47 for (const [from, to] of moves) {48 const response = await kepr.request(from);49 expect([from, response.status, response.headers.get("location")]).toEqual([from, 301, to]);50 }51});5253test("§20: a private or unknown repo's old addresses 404 instead of redirecting", async () => {54 const kepr = await app();55 for (const repo of ["hidden", "nowhere"]) {56 for (const rest of MOVED) {57 const path = `/${repo}${rest}`;58 expect([path, (await kepr.request(path)).status]).toEqual([path, 404]);59 }60 }61});6263test("§28: Kepr 2's retired addresses answer 410 Gone, for any method", async () => {64 const kepr = await app();65 const gone = [66 "/tickets",67 "/tickets/new",68 "/tickets/3",69 "/drafts",70 "/drafts/abc",71 "/join",72 "/cli-auth",73 "/api/cli-auth/exchange",74 "/api/cli-auth/session",75 "/api/repos",76 "/api/repos/demo/offer/negotiate",77 "/api/repos/demo/offer/complete",78 "/api/repos/demo/snapshot",79 "/api/releases/upload",80 "/protocol/v1/protocol.zig",81 "/feed",82 "/demo/feed",83 ];84 for (const path of gone) {85 expect([path, (await kepr.request(path)).status]).toEqual([path, 410]);86 const post = await kepr.request(path, { method: "POST" });87 expect([path, post.status]).toEqual([path, 410]);88 }89});9091test("the redirects shadow none of §22.1's routes, and refuse unsafe input", async () => {92 const kepr = await app();93 for (const path of [94 "/demo/releases/latest",95 "/demo/releases/latest.json",96 "/demo/releases/history.json",97 "/demo",98 "/demo?panel=nope",99 ]) {100 const status = (await kepr.request(path)).status;101 expect([path, status === 301 || status >= 500]).toEqual([path, false]);102 }103 for (const path of [104 "/demo/file",105 "/demo/file?path=../etc/passwd",106 "/demo/file?path=a//b",107 "/demo/releases/bad_version",108 ]) {109 expect([path, (await kepr.request(path)).status]).toEqual([path, 404]);110 }111});112113test("a bucket repo named like one of Kepr's own routes is not synced, and the console says why", async () => {114 const fx = await makeKohBucket();115 await cp(join(fx.bucketDir, fx.repo), join(fx.bucketDir, "browse"), { recursive: true });116 const inst = await instance(fx.bucketDir);117 const rows = inst.kepr.query("SELECT name, last_sync_error FROM repos ORDER BY name").all();118 const clash = `not synced: the name "browse" clashes with Kepr's own /browse route; rename the repo`;119 expect(rows).toEqual([120 { name: "browse", last_sync_error: clash },121 { name: "demo", last_sync_error: null },122 ]);123 expect(await Bun.file(join(inst.dataDir, "mirror/browse.db")).exists()).toBe(false);124 const browser = new Browser(inst.app);125 await browser.signIn();126 expect(await (await browser.get("/admin/repos")).text()).toContain(127 `sync error: ${clash.replaceAll('"', """).replaceAll("'", "'")}`,128 );129});