repodelete.test.ts

4.9 KB · raw

1// Covers deleting a repo from the console (§24): the double confirmation, every key of that repo2// gone from the bucket with no other repo touched, and Kepr's own rows and mirror dropped with it.34import { beforeAll, describe, expect, test } from "bun:test";5import { existsSync } from "node:fs";6import { join } from "node:path";7import { isRepoKey, openBucket } from "../src/bucket/reader.ts";8import { removalRun } from "../src/server/remove.ts";9import { Browser, type Instance, instance } from "./browser.ts";10import { type FixtureRepos, makeRepos } from "./fixture.ts";1112describe("deleting a repo", () => {13	let inst: Instance;14	let browser: Browser;15	let repos: FixtureRepos;16	const keys = (prefix: string) => [...new Bun.Glob(`${prefix}/**`).scanSync(repos.bucketDir)];17	const count = (sql: string) => (inst.kepr.query<{ n: number }, []>(sql).get() ?? { n: 0 }).n;18	const remove = (fields: Record<string, string>) =>19		browser.post("/admin/repos/app/delete", fields);20	// The delete answers before it is done; startRemoval leaves the run to wait on.21	const settled = () => removalRun() ?? Promise.resolve();2223	beforeAll(async () => {24		repos = await makeRepos(["app", "other"]);25		inst = await instance(repos.bucketDir);26		browser = new Browser(inst.app);27		await browser.signIn();28		await browser.post("/admin/repos/app", { public: "1", category: "tools", hidden_paths: "" });29		inst.kepr.run(30			"INSERT INTO downloads (repo, version, platform, count) VALUES ('app', '1.0.0', 'linux-x64', 7)",31		);32		inst.kepr.run(33			`INSERT INTO issues (repo, number, title, body, type, filed_at)34VALUES ('app', 1, 'Crash', 'It broke.', 'bug', 0)`,35		);36		const issue = inst.kepr.query<{ id: number }, []>("SELECT id FROM issues").get()?.id ?? 0;37		inst.kepr.run(38			"INSERT INTO issue_entries (issue_id, author, body, created_at) VALUES (?, 'reporter', 'x', 0)",39			[issue],40		);41		inst.kepr.run(42			"INSERT INTO issue_followers (issue_id, email, created_at) VALUES (?, '[email protected]', 0)",43			[issue],44		);45		// Kepr's own files for the repo, which a delete has to take with it.46		await Bun.write(join(repos.bucketDir, ".kepr/hidden/app"), "notes.md\n");47		await Bun.write(join(repos.bucketDir, ".kepr/media/app/abc.png"), "png");48		await Bun.write(join(repos.bucketDir, ".kepr/tarballs/app/1/abcdef.tar.gz"), "tar");49	});5051	test("takes both the tick and the name before anything is deleted", async () => {52		expect((await remove({})).status).toBe(400);53		expect((await remove({ delete: "on" })).status).toBe(400);54		expect((await remove({ delete: "on", name: "other" })).status).toBe(400);55		expect((await remove({ name: "app" })).status).toBe(400);56		expect(keys("app").length).toBeGreaterThan(0);57		expect(count("SELECT COUNT(*) AS n FROM repos WHERE name = 'app'")).toBe(1);58	});5960	test("deletes only that repo's keys, its mirror and its rows", async () => {61		const other = keys("other").length;62		expect((await remove({ delete: "on", name: "app" })).status).toBe(303);63		await settled();64		expect(keys("app")).toEqual([]);65		// Kepr's own files for the repo go too; a folder remote may leave the empty folders behind,66		// which an S3 bucket never has and which no listing shows.67		expect(existsSync(join(repos.bucketDir, ".kepr/hidden/app"))).toBe(false);68		expect(keys(".kepr/media/app")).toEqual([]);69		expect(keys(".kepr/tarballs/app")).toEqual([]);70		// The other repo is untouched, keys and rows alike.71		expect(keys("other").length).toBe(other);72		expect(count("SELECT COUNT(*) AS n FROM repos WHERE name = 'other'")).toBe(1);73		// Kepr's own copy: the mirror, and every row that pointed at the repo.74		expect(existsSync(join(inst.dataDir, "mirror", "app.db"))).toBe(false);75		for (const sql of [76			"SELECT COUNT(*) AS n FROM repos WHERE name = 'app'",77			"SELECT COUNT(*) AS n FROM downloads WHERE repo = 'app'",78			"SELECT COUNT(*) AS n FROM issues WHERE repo = 'app'",79			"SELECT COUNT(*) AS n FROM issue_entries",80			"SELECT COUNT(*) AS n FROM issue_followers",81		]) {82			expect(`${sql} ${count(sql)}`).toBe(`${sql} 0`);83		}84	});8586	test("the delete path refuses any key that is not of that repo", async () => {87		const bucket = openBucket(inst.config.bucket);88		for (const key of [89			"other/head",90			"other/saves/abcdef",91			".kepr/media/other/x.png",92			"../app/head",93		]) {94			expect(`${key} ${isRepoKey("app", key)}`).toBe(`${key} false`);95			await expect(bucket.removeRepoKey("app", key)).rejects.toThrow();96		}97		expect(isRepoKey("app", "app/head")).toBe(true);98		expect(isRepoKey("app", ".kepr/tarballs/app/1/abcdef.tar.gz")).toBe(true);99		expect(isRepoKey("app", ".kepr/hidden/app")).toBe(true);100		expect(isRepoKey("", "head")).toBe(false);101	});102103	test("the console offers the delete with both confirmations", async () => {104		const page = await (await browser.get("/admin/repos?repo=other")).text();105		expect(page).toContain('action="/admin/repos/other/delete"');106		expect(page).toContain("type other to confirm");107		expect(page).toContain("nothing here can undo it");108	});109});