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, })); mock.module("../repo-provisioning", () => ({ describeProvisioningError: () => "", markProvisioningFailed: async () => undefined, startProvisioning: async () => undefined, RepoProvisionWorkflow: StubEntrypoint, })); const people: Record = Object.fromEntries( ["owner", "writer", "stranger"].map((name) => [ name, { id: `u-${name}`, name, displayName: null, email: `${name}@example.com`, isAdmin: false, emailVerified: true }, ]), ); const repo = { 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 PARENT_EXPIRY = Date.now() + 10 * 24 * 60 * 60 * 1000; const inserted: string[] = []; const existing = new Set(["label-1", "rule-1", "deploy-1"]); function fakeFirst(sql: string, ...params: unknown[]): unknown { if (sql.includes("SELECT token_hash FROM deploy_tokens")) return existing.has(String(params[0])) ? { token_hash: `hash-${String(params[0])}` } : null; if (sql.includes("WHERE a.name = ? AND r.name = ?")) return params[0] === "owner" && params[1] === "app" ? repo : null; if (sql.includes("WHERE r.id = ?")) return params[0] === "repo-1" ? repo : 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 params[0] === "t-scoped" && params[1] === "repo-1" ? { ok: 1 } : null; if (sql.includes("FROM labels WHERE id")) { return existing.has(String(params[0])) || inserted.includes(String(params[0])) ? { id: params[0], name: "bug", color: "ff0000", description: null } : null; } if (sql.includes("FROM branch_protections WHERE id")) { return existing.has(String(params[0])) ? { id: params[0], repo_id: "repo-1", pattern: "main", require_pull_request: 0, required_approvals: 0, required_checks: "[]", dismiss_stale_reviews: 0, allow_deletions: 0, allow_force_pushes: 0, enforce_admins: 0, require_code_owner_review: 0, created_at: 1, } : null; } if (sql.includes("SELECT t.expires_at, t.parent_token_id")) { return { expires_at: params[0] === "t-expiring" ? PARENT_EXPIRY : null, parent_token_id: params[0] === "t-child" ? "t-admin" : null, delegated: 0, scopes: params[0] === "t-converted" ? "converted-classic" : "fine-grained", }; } if (sql.includes("COUNT(*)")) return { count: 0 }; return null; } const d1 = { first: async (sql: string, ...params: unknown[]) => fakeFirst(sql, ...params), all: async (sql: string, ...params: unknown[]) => { if (sql.includes("FROM access_token_repos") && params[0] === "t-scoped") return [{ repo_id: "repo-1" }]; if (sql.includes("FROM repos r JOIN accounts") && params[0] === "repo-1") return [{ full_name: "owner/app" }]; return []; }, run: async (sql: string, ...params: unknown[]) => { const words = sql.trim().split(/\s+/); inserted.push(`${words[0]} ${words[1] === "INTO" || words[1] === "FROM" ? words[2] : words[1]}`); if (sql.startsWith("INSERT")) inserted.push(String(params[0])); return { meta: { changes: sql.startsWith("DELETE") && !existing.has(String(params[0])) ? 0 : 1 } }; }, batch: async () => { inserted.push("batch"); return []; }, stmt: () => ({}), placeholders: (count: number) => Array.from({ length: count }, () => "?").join(", "), chunks: (items: T[]) => [items], MAX_IN_LIST: 90, }; mock.module("../db", () => d1); // Repository and account objects answer from the same fake; locator writes are not recorded. const scoped = { first: d1.first, all: d1.all, run: d1.run, batch: d1.batch }; mock.module("../scoped-db", () => ({ repoDb: () => scoped, accountDb: () => scoped, stmt: d1.stmt })); const actualLocators = await import("../scoped-db/locators"); mock.module("../scoped-db/locators", () => ({ ...actualLocators, locate: async () => null, registerLocator: async () => undefined, forgetLocator: async () => undefined, forgetScope: async () => undefined, })); const actualLimits = await import("../limits"); mock.module("../limits", () => ({ ...actualLimits, limitWrite: async () => undefined, limitBrowse: async () => undefined, })); const credentials: Record = { owner: { viewer: people.owner, grant: { kind: "fine", tokenId: "t-owner", allRepos: true, permissions: { contents: "write", issues: "write", pulls: "write", actions: "write", admin: "write" }, }, }, writer: { viewer: people.writer, grant: { kind: "fine", tokenId: "t-writer", allRepos: true, permissions: { contents: "write", issues: "write", pulls: "write", actions: "write", admin: "write" }, }, }, stranger: { viewer: people.stranger, grant: { kind: "fine", tokenId: "t-stranger", allRepos: true, permissions: { contents: "write", issues: "write", pulls: "write", actions: "write", admin: "write" }, }, }, "writer-no-issues": { viewer: people.writer, grant: { kind: "fine", tokenId: "t-ni", allRepos: true, permissions: { contents: "write", issues: "read" } }, }, "owner-no-admin": { viewer: people.owner, grant: { kind: "fine", tokenId: "t-na", allRepos: true, permissions: { contents: "write", issues: "write" } }, }, converted: { viewer: people.owner, grant: { kind: "fine", tokenId: "t-converted", allRepos: true, permissions: { contents: "write", issues: "write", pulls: "write", actions: "write", admin: "write" }, }, }, "owner-admin": { viewer: people.owner, grant: { kind: "fine", tokenId: "t-admin", allRepos: true, permissions: { admin: "write", contents: "write" } }, }, child: { viewer: people.owner, grant: { kind: "fine", tokenId: "t-child", allRepos: true, permissions: { admin: "write", contents: "write" } }, }, "owner-admin-contents-read": { viewer: people.owner, grant: { kind: "fine", tokenId: "t-acr", allRepos: true, permissions: { admin: "write", contents: "read" } }, }, scoped: { viewer: people.writer, grant: { kind: "fine", tokenId: "t-scoped", allRepos: false, permissions: { contents: "read", pulls: "write" } }, }, expiring: { viewer: people.writer, grant: { kind: "fine", tokenId: "t-expiring", allRepos: true, permissions: { contents: "write" } }, }, }; const actualScope = await import("../token-scope"); mock.module("../token-scope", () => ({ ...actualScope, tokenCredential: async (request: Request) => credentials[(request.headers.get("authorization") ?? "").replace(/^Bearer /, "")] ?? 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; } } beforeEach(() => { inserted.length = 0; }); const R = "/repos/owner/app"; type Case = { name: string; method: string; path: string; body?: unknown; allowed: [string, number]; denied: string }; const cases: Case[] = [ { name: "create label", method: "POST", path: `${R}/labels`, body: { name: "bug", color: "ff0000" }, allowed: ["writer", 201], denied: "writer-no-issues", }, { name: "update label", method: "PATCH", path: `${R}/labels/label-1`, body: { name: "bug", color: "00ff00" }, allowed: ["writer", 200], denied: "writer-no-issues", }, { name: "delete label", method: "DELETE", path: `${R}/labels/label-1`, allowed: ["writer", 204], denied: "writer-no-issues", }, { name: "toggle code owner review", method: "PUT", path: `${R}/branch-protections/rule-1/code-owner-review`, body: { enabled: true }, allowed: ["owner", 204], denied: "owner-no-admin", }, { name: "create deploy token", method: "POST", path: `${R}/deploy-tokens`, body: { name: "ci", permission: "read", expires_in_days: 30 }, allowed: ["owner-admin", 201], denied: "owner-no-admin", }, { name: "revoke deploy token", method: "DELETE", path: `${R}/deploy-tokens/deploy-1`, allowed: ["owner", 204], denied: "owner-no-admin", }, ]; describe("label, code-owner and deploy-token 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.denied, item.method, item.path, item.body)).toBe(403); expect(inserted).toEqual([]); }); test(`${item.name}: non-collaborator`, async () => { expect(await call("stranger", item.method, item.path, item.body)).toBe(403); expect(inserted).toEqual([]); }); } test("a write deploy token needs contents:write on the caller's token", async () => { const body = { name: "ci", permission: "write", expires_in_days: 30 }; expect(await call("owner-admin-contents-read", "POST", `${R}/deploy-tokens`, body)).toBe(403); expect(await call("owner-admin", "POST", `${R}/deploy-tokens`, body)).toBe(201); }); test("deploy tokens minted by a token must expire, and converted classic tokens can't mint them (AUTH-A1)", async () => { expect( await call("owner-admin", "POST", `${R}/deploy-tokens`, { name: "ci", permission: "read", expires_in_days: 0 }), ).toBe(403); expect( await call("converted", "POST", `${R}/deploy-tokens`, { name: "ci", permission: "read", expires_in_days: 30 }), ).toBe(403); expect(inserted).toEqual([]); }); test("unknown labels and deploy tokens are 404", async () => { expect(await call("writer", "DELETE", `${R}/labels/nope`)).toBe(404); expect(await call("owner", "DELETE", `${R}/deploy-tokens/nope`)).toBe(404); }); }); describe("POST /user/tokens", () => { const base = { name: "child", expires_in_days: 5, all_repos: true, repos: [], permissions: { contents: "read" } }; test("a first-generation token can mint tokens; one converted from a classic token cannot (AUTH-A1)", async () => { expect(await call("converted", "POST", "/user/tokens", base)).toBe(403); expect(inserted).toEqual([]); expect(await call("owner-admin", "POST", "/user/tokens", base)).toBe(201); }); test("a token minted by a token cannot mint further, and children must expire", async () => { expect(await call("child", "POST", "/user/tokens", base)).toBe(403); expect(await call("owner-admin", "POST", "/user/tokens", { ...base, expires_in_days: 0 })).toBe(403); expect(inserted).toEqual([]); }); test("a token cannot grant an area above its own", async () => { expect( await call("scoped", "POST", "/user/tokens", { ...base, all_repos: false, repos: ["owner/app"], permissions: { contents: "write" }, }), ).toBe(403); expect( await call("scoped", "POST", "/user/tokens", { ...base, all_repos: false, repos: ["owner/app"], permissions: { issues: "read" }, }), ).toBe(403); expect(inserted).toEqual([]); }); test("a repo-limited token cannot mint all-repos or other repos", async () => { expect(await call("scoped", "POST", "/user/tokens", base)).toBe(403); expect(await call("scoped", "POST", "/user/tokens", { ...base, all_repos: false, repos: ["other/repo"] })).toBe( 403, ); expect(await call("scoped", "POST", "/user/tokens", { ...base, all_repos: false, repos: ["owner/app"] })).toBe(201); }); test("a child cannot outlive its parent", async () => { expect(await call("expiring", "POST", "/user/tokens", { ...base, expires_in_days: 0 })).toBe(403); expect(await call("expiring", "POST", "/user/tokens", { ...base, expires_in_days: 30 })).toBe(403); expect(await call("expiring", "POST", "/user/tokens", { ...base, expires_in_days: 5 })).toBe(201); }); test("unknown permission areas are rejected", async () => { expect(await call("owner-admin", "POST", "/user/tokens", { ...base, permissions: { root: "write" } })).toBe(422); }); });