import { beforeEach, describe, expect, mock, test } from "bun:test"; import { Database } from "bun:sqlite"; import { readdirSync, readFileSync } from "node:fs"; import { join } from "node:path"; mock.module("cloudflare:workers", () => ({ env: { PUBLIC_URL: "https://forge.example.com" } })); mock.module("@tanstack/react-start/server", () => ({ getRequestHeader: () => undefined })); const db = new Database(":memory:"); const migrations = join(import.meta.dir, "../../migrations"); for (const file of readdirSync(migrations).sort()) db.exec(readFileSync(join(migrations, file), "utf8")); mock.module("./db", () => ({ first: async (sql: string, ...params: unknown[]) => db.query(sql).get(...(params as never[])) ?? null, all: async (sql: string, ...params: unknown[]) => db.query(sql).all(...(params as never[])), run: async (sql: string, ...params: unknown[]) => { const result = db.query(sql).run(...(params as never[])); return { meta: { changes: result.changes } }; }, batch: async (statements: { sql: string; params: unknown[] }[]) => { for (const statement of statements) db.query(statement.sql).run(...(statement.params as never[])); return []; }, stmt: (sql: string, ...params: unknown[]) => ({ sql, params }), placeholders: (count: number) => Array(count).fill("?").join(", "), chunks: (items: T[]) => [items], MAX_IN_LIST: 90, })); const { purgeFollowerIfRevoked, purgeInaccessibleFollowers, purgeOrgFollowers } = await import("./access-revocation"); const { repoPermission } = await import("./repos"); function seed(): void { db.exec("PRAGMA foreign_keys = ON"); db.exec(` DELETE FROM stars; DELETE FROM watches; DELETE FROM thread_subscriptions; DELETE FROM issues; DELETE FROM collaborators; DELETE FROM org_members; DELETE FROM repos; DELETE FROM users; DELETE FROM accounts; INSERT INTO accounts (id, name, kind, created_at) VALUES ('org', 'acme', 'org', 0), ('owner', 'owner', 'user', 0), ('member', 'member', 'user', 0), ('collab', 'collab', 'user', 0), ('stranger', 'stranger', 'user', 0), ('root', 'root', 'user', 0); INSERT INTO users (account_id, email, is_admin) VALUES ('owner', 'o@x', 0), ('member', 'm@x', 0), ('collab', 'c@x', 0), ('stranger', 's@x', 0), ('root', 'r@x', 1); INSERT INTO repos (id, owner_id, name, visibility, default_branch, artifacts_name, artifacts_remote, created_at, updated_at) VALUES ('repo', 'org', 'app', 'public', 'main', 'art', 'remote', 0, 0); INSERT INTO org_members (org_id, user_id, role, created_at) VALUES ('org', 'owner', 'owner', 0), ('org', 'member', 'member', 0); INSERT INTO collaborators (repo_id, user_id, permission, created_at) VALUES ('repo', 'collab', 'read', 0); INSERT INTO issues (id, repo_id, number, title, body, state, author_id, is_pull, created_at, updated_at) VALUES ('issue', 'repo', 1, 't', '', 'open', 'owner', 0, 0, 0); `); for (const user of ["owner", "member", "collab", "stranger", "root"]) { db.exec(`INSERT INTO stars VALUES ('${user}', 'repo', 0); INSERT INTO watches (user_id, repo_id, created_at) VALUES ('${user}', 'repo', 0); INSERT INTO thread_subscriptions VALUES ('${user}', 'issue', 1, 0);`); } } function followers(): string[] { return (db.query("SELECT user_id FROM stars ORDER BY user_id").all() as { user_id: string }[]).map( (row) => row.user_id, ); } function watchers(): string[] { return (db.query("SELECT user_id FROM watches ORDER BY user_id").all() as { user_id: string }[]).map( (row) => row.user_id, ); } function subscribers(): string[] { return (db.query("SELECT user_id FROM thread_subscriptions ORDER BY user_id").all() as { user_id: string }[]).map( (row) => row.user_id, ); } beforeEach(seed); describe("access revocation (AZ-A6)", () => { test("public -> private drops everyone without access and keeps owner, members, collaborators and admins", async () => { db.exec("UPDATE repos SET visibility = 'private'"); await purgeInaccessibleFollowers("repo"); expect(followers()).toEqual(["collab", "member", "owner", "root"]); expect(watchers()).toEqual(["collab", "member", "owner", "root"]); expect(subscribers()).toEqual(["collab", "member", "owner", "root"]); }); test("nothing is removed while the repository is public", async () => { await purgeInaccessibleFollowers("repo"); expect(followers()).toHaveLength(5); }); test("collaborator removal on a private repo purges only that user", async () => { db.exec("UPDATE repos SET visibility = 'private'; DELETE FROM collaborators WHERE user_id = 'collab'"); await purgeFollowerIfRevoked("repo", "collab"); expect(followers()).toEqual(["member", "owner", "root", "stranger"]); }); test("org membership removal purges the user across the org's private repos", async () => { db.exec("UPDATE repos SET visibility = 'private'; DELETE FROM org_members WHERE user_id = 'member'"); await purgeOrgFollowers("org", "member"); expect(followers()).toEqual(["collab", "owner", "root", "stranger"]); }); test("an org default permission of none revokes plain members (AZ-A8)", async () => { db.exec( "UPDATE repos SET visibility = 'private'; UPDATE accounts SET org_default_permission = 'none' WHERE id = 'org'", ); await purgeOrgFollowers("org", null); expect(followers()).toEqual(["collab", "owner", "root"]); }); }); describe("repoPermission honours the org default permission (AZ-A8)", () => { const repo = () => db.query("SELECT r.*, a.name AS owner_name FROM repos r JOIN accounts a ON a.id = r.owner_id").get() as never; const member = { id: "member", name: "member", displayName: null, email: "m@x", isAdmin: false }; test("members get read by default, never write", async () => { db.exec("UPDATE repos SET visibility = 'private'"); expect(await repoPermission(repo(), member)).toBe("read"); }); test("owners can raise members to write or drop them to none", async () => { db.exec( "UPDATE repos SET visibility = 'private'; UPDATE accounts SET org_default_permission = 'write' WHERE id = 'org'", ); expect(await repoPermission(repo(), member)).toBe("write"); db.exec("UPDATE accounts SET org_default_permission = 'none' WHERE id = 'org'"); expect(await repoPermission(repo(), member)).toBe("none"); expect(await repoPermission(repo(), { ...member, id: "owner", name: "owner" })).toBe("admin"); }); });