server.test.ts

3.3 KB · raw

1import { Database } from "bun:sqlite";2import { expect, test } from "bun:test";3import { mkdtempSync } from "node:fs";4import { tmpdir } from "node:os";5import { join } from "node:path";6import { createApp } from "../src/server/app.ts";7import type { Config } from "../src/server/config.ts";8import { openKeprDb } from "../src/server/db.ts";9import { RESERVED_NAMES } from "../src/server/reserved.ts";1011const config: Config = {12	domain: "kepr.example",13	dataDir: "data",14	adminPasswordHash: "$argon2id$x",15	bucket: {16		kind: "s3",17		endpoint: "https://s3.fr-par.scw.cloud",18		name: "koh",19		region: "fr-par",20		accessKeyId: "read",21		secretAccessKey: "only",22	},23};2425const registered = () =>26	createApp(config, new Database(":memory:"))27		.routes.filter((route) => !(route.method === "ALL" && route.path === "/*"))28		.map((route) => `${route.method} ${route.path}`);2930test("the app registers exactly the routes in docs/routes.txt, both ways (§26)", async () => {31	const listed = (await Bun.file("docs/routes.txt").text())32		.split("\n")33		.filter((line) => line.trim() !== "" && !line.startsWith("#"));34	const routes = new Set(registered());35	expect(registered().filter((route) => !listed.includes(route))).toEqual([]);36	expect(listed.filter((route) => !routes.has(route))).toEqual([]);37});3839test("every literal first segment of a route is a reserved repo name", () => {40	const firsts = registered()41		.map((route) => route.split(" ")[1]?.split("/")[1] ?? "")42		.filter((segment) => segment !== "" && !segment.startsWith(":"));43	expect([...new Set(firsts)].sort()).toEqual([...RESERVED_NAMES].sort());44});4546test("responses carry the §25 content security policy", async () => {47	const kepr = openKeprDb(mkdtempSync(join(tmpdir(), "kepr-csp-")));48	const response = await createApp(config, kepr).request("/");49	const csp = response.headers.get("content-security-policy") ?? "";50	expect(csp).toContain("script-src 'self'");51	expect(csp).not.toContain("unsafe-inline");52	expect(csp).toContain("img-src 'self' https://s3.fr-par.scw.cloud");53	expect(csp).toContain("frame-ancestors 'none'");54	expect(response.headers.get("x-content-type-options")).toBe("nosniff");55});5657test("request bodies over 64 KB are refused", async () => {58	const app = createApp(config, openKeprDb(mkdtempSync(join(tmpdir(), "kepr-body-"))));59	app.post("/echo", async (c) => c.text(String((await c.req.text()).length)));60	const post = (bytes: number) => app.request("/echo", { method: "POST", body: "x".repeat(bytes) });61	expect((await post(64 * 1024)).status).toBe(200);62	expect((await post(64 * 1024 + 1)).status).toBe(413);63});6465test("an issue or reply may carry its attachments, up to 34 MB, and nothing else may", async () => {66	const app = createApp(config, openKeprDb(mkdtempSync(join(tmpdir(), "kepr-body-"))));67	app.post("/app/issues/7/x", (c) => c.text("ok"));68	const post = (path: string, bytes: number) =>69		app.request(path, { method: "POST", body: "x".repeat(bytes) });70	// The repo is unknown, so a body the cap lets through is answered 404.71	expect((await post("/app/issues/new", 1024 * 1024)).status).toBe(404);72	expect((await post("/app/issues/7", 34 * 1024 * 1024)).status).toBe(404);73	expect((await post("/app/issues/7", 34 * 1024 * 1024 + 1)).status).toBe(413);74	expect((await post("/app/issues/7/x", 64 * 1024 + 1)).status).toBe(413);75});