kepr2.test.ts

4.2 KB · raw

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	// The redirects read only the visibility flag, so no mirror is needed.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});