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 return createApp(config, openKeprDb(dataDir));19}2021test("§28: Kepr 2's moved addresses answer 301 with the new one", async () => {22 const kepr = await app();23 const moves: [string, string][] = [24 ["/demo?panel=issues", "/demo/issues"],25 ["/demo?panel=builds", "/demo/releases"],26 ["/demo?panel=archives", "/demo/archives"],27 ["/demo?panel=trust", "/demo"],28 ["/demo/releases/2.1.0", "/demo/releases#2.1.0"],29 ["/demo/file?path=src/main.ts", "/demo/files/src/main.ts"],30 ["/demo/file?path=docs/a%20b.md", "/demo/files/docs/a%20b.md"],31 ["/demo/bundle", "/demo/archive/HEAD.tar.gz"],32 ["/demo/bundle.blake3", "/demo/archive/HEAD.tar.gz"],33 ["/source", "/kepr"],34 ];35 for (const [from, to] of moves) {36 const response = await kepr.request(from);37 expect([from, response.status, response.headers.get("location")]).toEqual([from, 301, to]);38 }39});4041test("§28: Kepr 2's retired addresses answer 410 Gone, for any method", async () => {42 const kepr = await app();43 const gone = [44 "/tickets",45 "/tickets/new",46 "/tickets/3",47 "/drafts",48 "/drafts/abc",49 "/join",50 "/cli-auth",51 "/api/cli-auth/exchange",52 "/api/cli-auth/session",53 "/api/repos",54 "/api/repos/demo/offer/negotiate",55 "/api/repos/demo/offer/complete",56 "/api/repos/demo/snapshot",57 "/api/releases/upload",58 "/protocol/v1/protocol.zig",59 "/feed",60 "/demo/feed",61 ];62 for (const path of gone) {63 expect([path, (await kepr.request(path)).status]).toEqual([path, 410]);64 const post = await kepr.request(path, { method: "POST" });65 expect([path, post.status]).toEqual([path, 410]);66 }67});6869test("the redirects shadow none of §22.1's routes, and refuse unsafe input", async () => {70 const kepr = await app();71 for (const path of [72 "/demo/releases/latest",73 "/demo/releases/latest.json",74 "/demo/releases/history.json",75 "/demo",76 "/demo?panel=nope",77 "/demo/file",78 "/demo/file?path=../etc/passwd",79 "/demo/file?path=a//b",80 "/demo/releases/bad_version",81 ]) {82 expect([path, (await kepr.request(path)).status]).toEqual([path, 404]);83 }84});8586test("a bucket repo named like one of Kepr's own routes is not synced, and the console says why", async () => {87 const fx = await makeKohBucket();88 await cp(join(fx.bucketDir, fx.repo), join(fx.bucketDir, "browse"), { recursive: true });89 const inst = await instance(fx.bucketDir);90 const rows = inst.kepr.query("SELECT name, last_sync_error FROM repos ORDER BY name").all();91 const clash = `not synced: the name "browse" clashes with Kepr's own /browse route; rename the repo`;92 expect(rows).toEqual([93 { name: "browse", last_sync_error: clash },94 { name: "demo", last_sync_error: null },95 ]);96 expect(await Bun.file(join(inst.dataDir, "mirror/browse.db")).exists()).toBe(false);97 const browser = new Browser(inst.app);98 await browser.signIn();99 expect(await (await browser.get("/admin/repos")).text()).toContain(100 `sync error: ${clash.replaceAll('"', """).replaceAll("'", "'")}`,101 );102});