CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
commit-statuses.test.ts
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 0cdfd89 | 1 | /** |
| 2 | * Block J8 — Commit status API tests. Pure helpers + route-auth smokes. | |
| 3 | * DB-backed CRUD is covered in integration. | |
| 4 | */ | |
| 5 | ||
| 6 | import { describe, it, expect } from "bun:test"; | |
| 7 | import app from "../app"; | |
| 8 | import { | |
| 9 | STATUS_STATES, | |
| 10 | isValidSha, | |
| 11 | isValidState, | |
| 12 | reduceCombined, | |
| 13 | sanitiseContext, | |
| 14 | __internal, | |
| 15 | } from "../lib/commit-statuses"; | |
| 16 | ||
| 17 | describe("commit-statuses — isValidSha", () => { | |
| 18 | it("accepts 4 to 40 hex chars", () => { | |
| 19 | expect(isValidSha("abcd")).toBe(true); | |
| 20 | expect(isValidSha("a".repeat(40))).toBe(true); | |
| 21 | expect(isValidSha("DEADBEEF")).toBe(true); | |
| 22 | }); | |
| 23 | ||
| 24 | it("rejects too short / too long / bad chars / empty / null", () => { | |
| 25 | expect(isValidSha("abc")).toBe(false); | |
| 26 | expect(isValidSha("a".repeat(41))).toBe(false); | |
| 27 | expect(isValidSha("xyz1")).toBe(false); | |
| 28 | expect(isValidSha("")).toBe(false); | |
| 29 | expect(isValidSha(null)).toBe(false); | |
| 30 | expect(isValidSha(undefined)).toBe(false); | |
| 31 | }); | |
| 32 | }); | |
| 33 | ||
| 34 | describe("commit-statuses — isValidState", () => { | |
| 35 | it("accepts the four canonical states", () => { | |
| 36 | for (const s of STATUS_STATES) expect(isValidState(s)).toBe(true); | |
| 37 | }); | |
| 38 | ||
| 39 | it("rejects anything else", () => { | |
| 40 | expect(isValidState("")).toBe(false); | |
| 41 | expect(isValidState("passed")).toBe(false); | |
| 42 | expect(isValidState("ok")).toBe(false); | |
| 43 | expect(isValidState(null)).toBe(false); | |
| 44 | expect(isValidState(42)).toBe(false); | |
| 45 | expect(isValidState(undefined)).toBe(false); | |
| 46 | }); | |
| 47 | }); | |
| 48 | ||
| 49 | describe("commit-statuses — sanitiseContext", () => { | |
| 50 | it("defaults to 'default' on empty / nullish", () => { | |
| 51 | expect(sanitiseContext("")).toBe("default"); | |
| 52 | expect(sanitiseContext(null)).toBe("default"); | |
| 53 | expect(sanitiseContext(undefined)).toBe("default"); | |
| 54 | expect(sanitiseContext(" ")).toBe("default"); | |
| 55 | }); | |
| 56 | ||
| 57 | it("trims and caps length", () => { | |
| 58 | expect(sanitiseContext(" ci/build ")).toBe("ci/build"); | |
| 59 | const long = "x".repeat(500); | |
| 60 | expect(sanitiseContext(long).length).toBe(__internal.CONTEXT_MAX); | |
| 61 | }); | |
| 62 | }); | |
| 63 | ||
| 64 | describe("commit-statuses — reduceCombined", () => { | |
| 65 | it("empty list rolls up as success (no blockers)", () => { | |
| 66 | expect(reduceCombined([])).toBe("success"); | |
| 67 | }); | |
| 68 | ||
| 69 | it("any failure dominates", () => { | |
| 70 | expect(reduceCombined(["success", "failure"])).toBe("failure"); | |
| 71 | expect(reduceCombined(["pending", "failure"])).toBe("failure"); | |
| 72 | }); | |
| 73 | ||
| 74 | it("any error dominates (bucketed as failure)", () => { | |
| 75 | expect(reduceCombined(["success", "error"])).toBe("failure"); | |
| 76 | expect(reduceCombined(["pending", "error"])).toBe("failure"); | |
| 77 | }); | |
| 78 | ||
| 79 | it("no failure + any pending → pending", () => { | |
| 80 | expect(reduceCombined(["pending"])).toBe("pending"); | |
| 81 | expect(reduceCombined(["success", "pending", "success"])).toBe("pending"); | |
| 82 | }); | |
| 83 | ||
| 84 | it("all success → success", () => { | |
| 85 | expect(reduceCombined(["success", "success", "success"])).toBe("success"); | |
| 86 | }); | |
| 87 | }); | |
| 88 | ||
| 89 | describe("commit-statuses — __internal.clamp", () => { | |
| 90 | it("returns null for empty / nullish", () => { | |
| 91 | expect(__internal.clamp("", 10)).toBeNull(); | |
| 92 | expect(__internal.clamp(null, 10)).toBeNull(); | |
| 93 | expect(__internal.clamp(undefined, 10)).toBeNull(); | |
| 94 | }); | |
| 95 | ||
| 96 | it("caps at max", () => { | |
| 97 | expect(__internal.clamp("abcdef", 3)).toBe("abc"); | |
| 98 | expect(__internal.clamp("ok", 10)).toBe("ok"); | |
| 99 | }); | |
| 100 | }); | |
| 101 | ||
| 102 | // --------------------------------------------------------------------------- | |
| 103 | // Route auth — these run without a DB so we only assert unauthenticated | |
| 104 | // behaviour. Authenticated paths (owner check, actual upsert) belong in | |
| 105 | // integration tests against a live DB. | |
| 106 | // --------------------------------------------------------------------------- | |
| 107 | ||
| 108 | describe("commit-statuses — route auth", () => { | |
| 109 | it("POST without auth → 302 /login redirect", async () => { | |
| 110 | const res = await app.request( | |
| 111 | "/api/v1/repos/alice/repo/statuses/abc1234", | |
| 112 | { | |
| 113 | method: "POST", | |
| 114 | headers: { "content-type": "application/json" }, | |
| 115 | body: JSON.stringify({ state: "success" }), | |
| 116 | } | |
| 117 | ); | |
| 118 | expect(res.status).toBe(302); | |
| 119 | expect(res.headers.get("location") || "").toContain("/login"); | |
| 120 | }); | |
| 121 | ||
| 122 | it("POST with invalid bearer token → 401 JSON", async () => { | |
| 123 | const res = await app.request( | |
| 124 | "/api/v1/repos/alice/repo/statuses/abc1234", | |
| 125 | { | |
| 126 | method: "POST", | |
| 127 | headers: { | |
| 128 | "content-type": "application/json", | |
| 129 | authorization: "Bearer glc_not_a_real_token", | |
| 130 | }, | |
| 131 | body: JSON.stringify({ state: "success" }), | |
| 132 | } | |
| 133 | ); | |
| 134 | expect(res.status).toBe(401); | |
| 135 | }); | |
| 136 | ||
| 137 | it("GET list returns JSON (200 / 404 / 500 depending on env)", async () => { | |
| 138 | const res = await app.request( | |
| 139 | "/api/v1/repos/alice/repo/commits/abc1234/statuses" | |
| 140 | ); | |
| 141 | expect([200, 404, 500]).toContain(res.status); | |
| 142 | }); | |
| 143 | ||
| 144 | it("GET combined status returns JSON", async () => { | |
| 145 | const res = await app.request( | |
| 146 | "/api/v1/repos/alice/repo/commits/abc1234/status" | |
| 147 | ); | |
| 148 | expect([200, 404, 500]).toContain(res.status); | |
| 149 | }); | |
| 150 | ||
| 151 | it("GET with obviously invalid sha → 400", async () => { | |
| 152 | const res = await app.request( | |
| 153 | "/api/v1/repos/alice/repo/commits/NOT_HEX/status" | |
| 154 | ); | |
| 155 | expect(res.status).toBe(400); | |
| 156 | const body = await res.json(); | |
| 157 | expect(body.error).toContain("sha"); | |
| 158 | }); | |
| 159 | }); |