repoworkspace.test.ts

8.8 KB · raw

1// Covers §24's selected-repo workspace: the summary, detected and shown languages, where the2// description comes from, and the repo's releases.34import { Database } from "bun:sqlite";5import { beforeAll, describe, expect, test } from "bun:test";6import { mkdtemp, writeFile } from "node:fs/promises";7import { tmpdir } from "node:os";8import { join } from "node:path";9import { openKeprDb } from "../src/server/db.ts";10import { Browser, type Instance, instance } from "./browser.ts";11import { makeRepos, writeRelease } from "./fixture.ts";1213describe("the selected repo's workspace (§24)", () => {14	let inst: Instance;15	let browser: Browser;16	let bucketDir: string;17	const workspace = async (tab = "") =>18		(await browser.get(`/admin/repos?repo=app${tab && `&tab=${tab}`}`)).text();19	const save = (fields: Record<string, string>) =>20		browser.post("/admin/repos/app", { public: "1", ...fields });2122	beforeAll(async () => {23		const repos = await makeRepos(["app"]);24		const work = join(repos.bucketDir, "..", "work", "app");25		await writeFile(join(work, "main.ts"), "export const a = 1;\nexport const b = 2;\n");26		await writeFile(join(work, "tool.py"), "print(1)\n");27		repos.koh("app", "save", "add sources");28		repos.koh("app", "offer");29		const head = JSON.parse(repos.koh("app", "view", "--json")).head_id as string;30		for (const version of ["1.0.0", "1.1.0"]) {31			await writeRelease(repos.bucketDir, "app", {32				version,33				save: head,34				files: [{ platform: "linux-x86_64", name: "app", bytes: new Uint8Array([1]) }],35			});36		}37		bucketDir = repos.bucketDir;38		inst = await instance(repos.bucketDir);39		inst.kepr.run(40			`INSERT INTO issues (repo, number, title, body, type, filed_at)41			 VALUES ('app', 1, 'Broken', 'It broke', 'bug', 0)`,42		);43		browser = new Browser(inst.app);44		await browser.signIn();45		await save({});46		await inst.app.request("/app/releases/1.1.0/linux-x86_64");47		await inst.app.request("/app/releases/1.1.0/linux-x86_64");48	});4950	test("summarises saves, lines, the newest version, open issues and downloads", async () => {51		const page = await workspace();52		expect(page).toContain('class="admin-vitals"');53		expect(page).toMatch(/<strong>2<\/strong><span class="mono">saves<\/span>/);54		expect(page).toMatch(/<strong>3<\/strong><span class="mono">lines<\/span>/);55		expect(page).toMatch(/<strong>1\.1\.0<\/strong><span class="mono">newest version<\/span>/);56		expect(page).toMatch(/<strong>1<\/strong><span class="mono">open issues<\/span>/);57		expect(page).toMatch(/<strong>2<\/strong><span class="mono">downloads<\/span>/);58	});5960	test("splits into overview, settings, builds and archives tabs; overview is the default", async () => {61		const tabs = ["overview", "settings", "builds", "archives"];62		const marks: Record<string, string> = {63			overview: 'class="admin-vitals"',64			settings: 'name="hidden_paths"',65			builds: 'action="/admin/releases/app/1.1.0"',66			archives: 'action="/admin/repos/app/tarballs/delete"',67		};68		for (const tab of tabs) {69			const page = await workspace(tab);70			expect(page).toContain(`href="/admin/repos?repo=app&amp;tab=${tab}" aria-current="page"`);71			for (const other of tabs) {72				expect(page.includes(marks[other] as string)).toBe(other === tab);73			}74		}75		expect(await workspace()).toContain(marks.overview as string);76		expect(await workspace("nowhere")).toContain(marks.overview as string);77		const saved = await save({});78		expect(saved.headers.get("location")).toBe("/admin/repos?repo=app&tab=settings&saved=1");79		const refused = await save({ languages: "a, b, c, d, e, f" });80		expect(await refused.text()).toContain(marks.settings as string);81	});8283	test("lists the detected languages with their lines", async () => {84		const page = await workspace();85		expect(page).toContain('TypeScript <span class="muted">2 lines</span>');86		expect(page).toContain('Python <span class="muted">1 line</span>');87	});8889	test("edits the description and details; blank hands them back to the README", async () => {90		expect(await workspace("settings")).toMatch(91			/<textarea[^>]*name="description"[^>]*><\/textarea>/,92		);93		const saved = await save({ description: "Own *words*", details: "## Usage\r\nRun it." });94		expect(saved.status).toBe(303);95		const page = await workspace("settings");96		expect(page).toContain("Own *words*</textarea>");97		expect(page).toContain("## Usage\nRun it.</textarea>");98		const repo = await (await inst.app.request("/app")).text();99		expect(repo).toContain("Own <em>words</em>");100		expect(repo).toContain("Run it.");101		const long = await save({ details: "x".repeat(60_001) });102		expect(long.status).toBe(400);103		expect(await long.text()).toContain("Keep the details under 60,000 characters.");104		await save({ description: "", details: "" });105		expect(inst.kepr.query("SELECT description, details FROM repo_text").get()).toEqual({106			description: "",107			details: "",108		});109	});110111	test("edits its builds in place, with visibility and downloads", async () => {112		inst.kepr.run(113			"INSERT INTO release_settings (repo, version, hidden) VALUES ('app', '1.0.0', 1)",114		);115		const page = await workspace("builds");116		expect(page).toContain('action="/admin/releases/app/1.1.0"');117		expect(page).toContain("<span>shown</span><span>2 downloads</span>");118		expect(page).toContain("<span>hidden</span><span>0 downloads</span>");119		inst.kepr.run("DELETE FROM release_settings");120	});121122	test("shown languages replace the header's detected language; blank brings it back", async () => {123		const header = async () => (await inst.app.request("/app")).text();124		expect(await header()).toContain('<span class="lang-badge">TypeScript</span>');125		expect((await save({ languages: " Zig ,  TypeScript" })).status).toBe(303);126		const shown = await header();127		expect(shown).toContain('<span class="lang-badge">Zig</span>');128		expect(shown).toContain('<span class="lang-badge">TypeScript</span>');129		expect(await workspace("settings")).toContain('value="Zig, TypeScript"');130		expect((await save({ languages: "" })).status).toBe(303);131		const detected = await header();132		expect(detected).not.toContain('<span class="lang-badge">Zig</span>');133		expect(detected).toContain('<span class="lang-badge">TypeScript</span>');134	});135136	test("refuses more than five languages, a long name or a control character", async () => {137		const refusals: [string, string][] = [138			["a, b, c, d, e, f", "up to five languages."],139			["x".repeat(31), "a language name is at most 30 characters."],140			["Zig\u0007", "a language name is printable text."],141		];142		for (const [languages, error] of refusals) {143			const response = await save({ languages });144			expect(response.status).toBe(400);145			expect(await response.text()).toContain(error);146		}147		expect(await workspace("settings")).not.toContain('value="Zig');148	});149150	test("deletes the repo's kept tarballs from the bucket", async () => {151		await save({});152		const head = (await inst.app.request("/app/archive/HEAD.tar.gz")).headers.get("location");153		expect((await inst.app.request(head ?? "")).status).toBe(200);154		const kept = new Bun.Glob("**/*.tar.gz");155		const keptIn = () => Array.fromAsync(kept.scan(join(bucketDir, ".kepr", "tarballs", "app")));156		expect(await keptIn()).toHaveLength(1);157		expect(await workspace("archives")).toContain('action="/admin/repos/app/tarballs/delete"');158		const deleted = await browser.post("/admin/repos/app/tarballs/delete", {});159		expect(deleted.headers.get("location")).toBe("/admin/repos?repo=app&tab=archives&saved=1");160		expect(await keptIn()).toEqual([]);161		expect((await inst.app.request(head ?? "")).status).toBe(200);162	});163});164165test("an older kepr.db gains the shown-languages column when opened", async () => {166	const dir = await mkdtemp(join(tmpdir(), "kepr-old-db-"));167	const old = new Database(join(dir, "kepr.db"));168	old.run("CREATE TABLE repos (name TEXT PRIMARY KEY, public INTEGER NOT NULL DEFAULT 0)");169	old.run("INSERT INTO repos (name) VALUES ('app')");170	old.close();171	const db = openKeprDb(dir);172	expect(db.query<{ languages: string }, []>("SELECT languages FROM repos").get()?.languages).toBe(173		"",174	);175});176177test("an older kepr.db loses card order and install lines, keeping each repo", async () => {178	const dir = await mkdtemp(join(tmpdir(), "kepr-old-db-"));179	const old = new Database(join(dir, "kepr.db"));180	old.run(`CREATE TABLE repos (181		name TEXT PRIMARY KEY, public INTEGER NOT NULL DEFAULT 0, sort_order INTEGER NOT NULL DEFAULT 0,182		install1 TEXT NOT NULL DEFAULT '', install2 TEXT NOT NULL DEFAULT '',183		install3 TEXT NOT NULL DEFAULT '')`);184	old.run("INSERT INTO repos (name, public, install1) VALUES ('app', 1, 'nix profile add app')");185	old.close();186	const db = openKeprDb(dir);187	const columns = db.query<{ name: string }, []>("PRAGMA table_info(repos)").all();188	expect(columns.map((column) => column.name)).not.toContain("install1");189	expect(columns.map((column) => column.name)).not.toContain("sort_order");190	expect(db.query("SELECT name, public FROM repos").all()).toEqual([{ name: "app", public: 1 }]);191});