1234import { 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 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 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 66 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 71 expect(keys("other").length).toBe(other);72 expect(count("SELECT COUNT(*) AS n FROM repos WHERE name = 'other'")).toBe(1);73 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});