import { beforeEach, describe, expect, mock, test } from "bun:test"; import type { Viewer } from "~/lib/types"; import type { RequestCredential } from "../token-scope"; class StubEntrypoint {} mock.module("cloudflare:workers", () => ({ env: { PUBLIC_URL: "https://forge.example.com" }, waitUntil: () => undefined, WorkflowEntrypoint: StubEntrypoint, DurableObject: StubEntrypoint, WorkerEntrypoint: StubEntrypoint, })); const HEAD = "a".repeat(40); const people: Record = Object.fromEntries( ["owner", "writer", "author", "stranger"].map((name) => [ name, { id: `u-${name}`, name, displayName: null, email: `${name}@example.com`, isAdmin: false, emailVerified: true }, ]), ); const baseRepo = { id: "repo-1", owner_id: "u-owner", owner_name: "owner", name: "app", visibility: "public", fork_of_id: null, is_archived: 0, default_branch: "main", artifacts_name: "art-1", description: null, }; const pullRow = { issue_id: "issue-1", repo_id: "repo-1", number: 1, title: "Feature", body: "", state: "open", author_id: "u-author", locked: 0, head_repo_id: "repo-1", head_ref: "feature", head_sha: HEAD, base_ref: "main", base_sha: "b".repeat(40), merge_base_sha: null, draft: 1, mergeable: "unknown", merged_at: null, merged_by: null, merge_commit_sha: null, merge_method: null, }; const ruleRow = { id: "rule-1", repo_id: "repo-1", pattern: "main", require_pull_request: 1, required_approvals: 1, required_checks: "[]", dismiss_stale_reviews: 0, allow_deletions: 0, allow_force_pushes: 0, enforce_admins: 0, require_code_owner_review: 0, created_at: 1, }; const writes: string[] = []; const createdRules = new Set(); function fakeFirst(sql: string, ...params: unknown[]): unknown { if (sql.includes("FROM pull_requests")) return params[0] === "repo-1" && params[1] === 1 ? pullRow : null; if (sql.includes("WHERE a.name = ? AND r.name = ?")) { return params[0] === "owner" && params[1] === "app" ? baseRepo : null; } if (sql.includes("WHERE r.id = ?")) return params[0] === "repo-1" ? baseRepo : null; if (sql.includes("FROM org_members")) return null; if (sql.includes("FROM collaborators")) return params[1] === "u-writer" ? { permission: "write" } : null; if (sql.includes("FROM access_token_repos")) return null; if (sql.includes("FROM review_comments")) { return params[0] === "c-1" ? { path: "a.ts", line: 3, side: "RIGHT", start_line: null, start_side: null, author_id: "u-writer" } : null; } if (sql.includes("FROM reviews WHERE id")) return params[0] === "r-1" ? { author_id: "u-writer", state: "approved" } : null; if (sql.includes("FROM branch_protections WHERE id")) { return params[0] === "rule-1" || createdRules.has(params[0]) ? { ...ruleRow, id: params[0] } : null; } if (sql.includes("COUNT(*)")) return { count: 0 }; if (sql.includes("FROM accounts WHERE id")) return { name: "author" }; if (sql.includes("JOIN users u ON u.account_id = a.id WHERE a.name = ?")) { return params[0] === "owner" ? { id: "u-owner" } : null; } return null; } const fakeDb = { first: async (sql: string, ...params: unknown[]) => fakeFirst(sql, ...params), all: async (sql: string, ...params: unknown[]) => { if (sql.includes("FROM branch_protections")) return [ruleRow]; if (sql.includes("WHERE a.name IN")) { return params .filter((name) => name === "stranger" || name === "writer") .map((name) => ({ id: `u-${name}`, name })); } if (sql.includes("EXISTS (SELECT 1 FROM collaborators c WHERE c.repo_id")) { return params.includes("u-writer") ? [{ id: "u-writer" }] : []; } return []; }, run: async (sql: string, ...params: unknown[]) => { writes.push(sql.trim().split(/\s+/).slice(0, 3).join(" ")); if (sql.includes("INSERT INTO branch_protections")) createdRules.add(params[0]); return { meta: { changes: sql.includes("DELETE FROM branch_protections") && params[0] !== "rule-1" ? 0 : 1 } }; }, batch: async () => { writes.push("batch"); return []; }, }; mock.module("../db", () => ({ ...fakeDb, stmt: () => ({}), placeholders: (count: number) => Array.from({ length: count }, () => "?").join(", "), chunks: (items: T[]) => [items], MAX_IN_LIST: 90, })); // Directory and repository tables share the fake: the routing under test doesn't depend on where rows live. mock.module("../scoped-db", () => ({ repoDb: (scopeId: string) => ({ scopeId, ...fakeDb }), accountDb: (scopeId: string) => ({ scopeId, ...fakeDb }), stmt: (sql: string, ...params: unknown[]) => ({ sql, params }), })); mock.module("../repo-provisioning", () => ({ describeProvisioningError: () => "", markProvisioningFailed: async () => undefined, startProvisioning: async () => undefined, RepoProvisionWorkflow: StubEntrypoint, })); const actual = { limits: await import("../limits"), emit: await import("../events/emit"), automation: await import("../merge-automation"), queue: await import("../merge-queue"), requests: await import("../review-requests"), events: await import("../pull-events"), refs: await import("../git/refs"), scope: await import("../token-scope"), }; const noop = async () => undefined; mock.module("../limits", () => ({ ...actual.limits, limitWrite: noop, limitHeavy: noop, limitBrowse: noop })); mock.module("../events/emit", () => ({ ...actual.emit, emitWebhook: noop, emitNotification: noop, recordActivity: noop, })); mock.module("../merge-automation", () => ({ ...actual.automation, kickBranch: noop, kickMergeAutomation: noop })); mock.module("../merge-queue", () => ({ ...actual.queue, enqueuePull: async () => writes.push("enqueue"), dequeuePull: async () => writes.push("dequeue"), })); mock.module("../review-requests", () => ({ ...actual.requests, rerequestReview: async () => true })); mock.module("../pull-events", () => ({ ...actual.events, afterPullChange: () => undefined })); mock.module("../git/refs", () => ({ ...actual.refs, listRefs: async () => ({ head: "main", branches: [{ name: "main", sha: "b".repeat(40) }], tags: [] }), branchSha: (refs: { branches: Array<{ name: string; sha: string }> }, name: string) => refs.branches.find((branch) => branch.name === name)?.sha ?? null, })); const credentials: Record = { "full-owner": { viewer: people.owner, grant: { kind: "fine", tokenId: "t-owner", allRepos: true, permissions: { contents: "write", issues: "write", pulls: "write", actions: "write", admin: "write" }, }, }, "full-writer": { viewer: people.writer, grant: { kind: "fine", tokenId: "t-writer", allRepos: true, permissions: { contents: "write", issues: "write", pulls: "write", actions: "write", admin: "write" }, }, }, "full-author": { viewer: people.author, grant: { kind: "fine", tokenId: "t-author", allRepos: true, permissions: { contents: "write", issues: "write", pulls: "write", actions: "write", admin: "write" }, }, }, "full-stranger": { viewer: people.stranger, grant: { kind: "fine", tokenId: "t-stranger", allRepos: true, permissions: { contents: "write", issues: "write", pulls: "write", actions: "write", admin: "write" }, }, }, "fine-writer-contents-only": { viewer: people.writer, grant: { kind: "fine", tokenId: "t-fine-w", allRepos: true, permissions: { contents: "write", pulls: "read" } }, }, "fine-writer-pulls-only": { viewer: people.writer, grant: { kind: "fine", tokenId: "t-fine-p", allRepos: true, permissions: { pulls: "write", contents: "read" } }, }, "fine-owner-no-admin": { viewer: people.owner, grant: { kind: "fine", tokenId: "t-fine-o", allRepos: true, permissions: { contents: "write", pulls: "write" } }, }, }; mock.module("../token-scope", () => ({ ...actual.scope, tokenCredential: async (request: Request) => { const token = (request.headers.get("authorization") ?? "").replace(/^Bearer /, ""); return credentials[token] ?? null; }, })); const { handleApiV1 } = await import("./router"); const { HttpError } = await import("../errors"); async function call(token: string, method: string, path: string, body?: unknown): Promise { const url = new URL(`https://forge.example.com/api/v1${path}`); const request = new Request(url, { method, headers: { authorization: `Bearer ${token}`, "content-type": "application/json" }, body: body === undefined ? undefined : JSON.stringify(body), }); try { return (await handleApiV1(request, url)).status; } catch (error) { if (error instanceof HttpError) return error.status; throw error; } } const PR = "/repos/owner/app/pulls/1"; const FORBIDDEN = 403; type Case = { name: string; method: string; path: string; body?: unknown; allowed: [string, number]; scopeDenied: string; outsider: number; }; const cases: Case[] = [ { name: "submit review", method: "POST", path: `${PR}/reviews`, body: { event: "APPROVE", body: "lgtm", commit_id: HEAD }, allowed: ["full-writer", 201], scopeDenied: "fine-writer-contents-only", outsider: 403, }, { name: "reply to review comment", method: "POST", path: `${PR}/comments/c-1/replies`, body: { body: "done" }, allowed: ["full-writer", 201], scopeDenied: "fine-writer-contents-only", outsider: 201, }, { name: "resolve thread", method: "PUT", path: `${PR}/threads/c-1`, body: { resolved: true }, allowed: ["full-writer", 204], scopeDenied: "fine-writer-contents-only", outsider: 403, }, { name: "request reviewers", method: "POST", path: `${PR}/requested_reviewers`, body: { reviewers: ["owner"] }, allowed: ["full-writer", 204], scopeDenied: "fine-writer-contents-only", outsider: 403, }, { name: "dismiss review", method: "PUT", path: `${PR}/reviews/r-1/dismissals`, body: { message: "stale" }, allowed: ["full-writer", 204], scopeDenied: "fine-writer-contents-only", outsider: 403, }, { name: "re-request review", method: "POST", path: `${PR}/requested_reviewers/rerequest`, body: { user_id: "u-owner" }, allowed: ["full-writer", 204], scopeDenied: "fine-writer-contents-only", outsider: 403, }, { name: "enable auto-merge", method: "PUT", path: `${PR}/auto_merge`, body: { method: "squash", expected_head: HEAD }, allowed: ["full-writer", 204], scopeDenied: "fine-writer-pulls-only", outsider: 403, }, { name: "disable auto-merge", method: "DELETE", path: `${PR}/auto_merge`, allowed: ["full-writer", 204], scopeDenied: "fine-writer-pulls-only", outsider: 403, }, { name: "dequeue", method: "DELETE", path: `${PR}/merge_queue`, allowed: ["full-writer", 204], scopeDenied: "fine-writer-pulls-only", outsider: 403, }, { name: "mark ready / change base", method: "PATCH", path: PR, body: { draft: false, base: "main" }, allowed: ["full-writer", 200], scopeDenied: "fine-writer-contents-only", outsider: 403, }, { name: "list branch protections", method: "GET", path: "/repos/owner/app/branch-protections", allowed: ["full-owner", 200], scopeDenied: "fine-owner-no-admin", outsider: 403, }, { name: "create branch protection", method: "POST", path: "/repos/owner/app/branch-protections", body: { pattern: "release/*", allow_force_pushes: false }, allowed: ["full-owner", 201], scopeDenied: "fine-owner-no-admin", outsider: 403, }, { name: "update branch protection", method: "PUT", path: "/repos/owner/app/branch-protections/rule-1", body: { pattern: "main", allow_force_pushes: true }, allowed: ["full-owner", 200], scopeDenied: "fine-owner-no-admin", outsider: 403, }, { name: "delete branch protection", method: "DELETE", path: "/repos/owner/app/branch-protections/rule-1", allowed: ["full-owner", 204], scopeDenied: "fine-owner-no-admin", outsider: 403, }, ]; beforeEach(() => { writes.length = 0; }); describe("PR-write parity endpoints", () => { for (const item of cases) { test(`${item.name}: allowed`, async () => { expect(await call(item.allowed[0], item.method, item.path, item.body)).toBe(item.allowed[1]); }); test(`${item.name}: token scope denied`, async () => { expect(await call(item.scopeDenied, item.method, item.path, item.body)).toBe(403); expect(writes).toEqual([]); }); test(`${item.name}: non-collaborator`, async () => { expect(await call("full-stranger", item.method, item.path, item.body)).toBe(item.outsider); if (item.outsider === FORBIDDEN) expect(writes).toEqual([]); }); } test("archived repositories reject review writes but still serve reads (AZ-A10)", async () => { baseRepo.is_archived = 1; try { expect(await call("full-writer", "PUT", `${PR}/threads/c-1`, { resolved: true })).toBe(403); expect(writes).toEqual([]); expect(await call("full-owner", "GET", "/repos/owner/app/branch-protections")).toBe(200); } finally { baseRepo.is_archived = 0; } }); test("locked pull requests reject review comments from readers (AZ-A9)", async () => { pullRow.locked = 1; try { expect(await call("full-stranger", "POST", `${PR}/comments/c-1/replies`, { body: "hi" })).toBe(403); expect(await call("full-writer", "POST", `${PR}/comments/c-1/replies`, { body: "hi" })).toBe(201); } finally { pullRow.locked = 0; } }); test("requested reviewers must be collaborators or org members (AZ-A12)", async () => { expect(await call("full-writer", "POST", `${PR}/requested_reviewers`, { reviewers: ["stranger"] })).toBe(400); expect(writes).toEqual([]); expect(await call("full-writer", "POST", `${PR}/requested_reviewers`, { reviewers: ["writer"] })).toBe(204); }); test("the API never authenticates via cookies or Basic", async () => { const url = new URL("https://forge.example.com/api/v1/repos/owner/app/pulls/1/reviews"); const attempts: Array> = [ {}, { cookie: "session=abc; __Host-session=abc" }, { authorization: `Basic ${btoa("writer:full-writer")}` }, ]; for (const headers of attempts) { const request = new Request(url, { method: "POST", headers: { ...headers, "content-type": "application/json" }, body: JSON.stringify({ event: "COMMENT", body: "hi", commit_id: HEAD }), }); const status = await handleApiV1(request, url).then( (response) => response.status, (error: unknown) => (error instanceof HttpError ? error.status : 500), ); expect(status).toBe(401); } expect(writes).toEqual([]); }); test("queueing requires the current head", async () => { expect(await call("full-writer", "PUT", `${PR}/merge_queue`, { expected_head: "c".repeat(40) })).toBe(400); expect(writes).toEqual([]); }); test("auto-merge with a stale head is rejected", async () => { expect(await call("full-writer", "PUT", `${PR}/auto_merge`, { expected_head: "c".repeat(40) })).toBe(409); }); test("the pull author may mark their own draft ready", async () => { expect(await call("full-author", "PATCH", PR, { draft: false })).toBe(200); }); test("PATCH refuses fields it cannot change", async () => { expect(await call("full-writer", "PATCH", PR, { title: "x" })).toBe(422); }); test("re-request accepts a login, and rejects ambiguous or unknown input", async () => { expect(await call("full-writer", "POST", `${PR}/requested_reviewers/rerequest`, { login: "owner" })).toBe(204); expect(await call("full-writer", "POST", `${PR}/requested_reviewers/rerequest`, { login: "nobody" })).toBe(404); expect( await call("full-writer", "POST", `${PR}/requested_reviewers/rerequest`, { login: "owner", user_id: "u-owner", }), ).toBe(422); }); test("branch protection can require code owner review", async () => { const body = { pattern: "main", require_code_owner_review: true }; expect(await call("full-owner", "PUT", "/repos/owner/app/branch-protections/rule-1", body)).toBe(200); expect(writes).toContain("UPDATE branch_protections SET"); expect(writes.filter((entry) => entry === "UPDATE branch_protections SET")).toHaveLength(2); }); test("updating or deleting an unknown rule is a 404", async () => { expect(await call("full-owner", "PUT", "/repos/owner/app/branch-protections/nope", { pattern: "x" })).toBe(404); expect(await call("full-owner", "DELETE", "/repos/owner/app/branch-protections/nope")).toBe(404); }); });