CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
push-policy.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.
| 5dc83ed | 1 | /** |
| 2 | * Tests for src/lib/push-policy.ts. | |
| 3 | * | |
| 4 | * The DB-touching paths (matchProtectedTag, listRulesetsForRepo, | |
| 5 | * canBypassProtectedTag) require a live DB; we don't have one in unit | |
| 6 | * tests. Instead we cover: | |
| 7 | * | |
| 8 | * - The pure formatPolicyError formatter — exhaustive. | |
| 9 | * - The fail-open guarantee on bad input (empty refs, missing repo id). | |
| 10 | * - Module shape (imports don't throw, expected exports are present). | |
| 11 | * | |
| 12 | * Real end-to-end coverage of the policy decisions runs in the existing | |
| 13 | * rulesets + protected-tags test suites (which exercise evaluatePush and | |
| 14 | * matchGlob directly). This file verifies that the new wrapper preserves | |
| 15 | * fail-open semantics, which is the property that protects production | |
| 16 | * pushes from a Postgres hiccup. | |
| 17 | */ | |
| 18 | ||
| 19 | import { describe, it, expect } from "bun:test"; | |
| 20 | import { | |
| 21 | evaluatePushPolicy, | |
| 22 | formatPolicyError, | |
| 23 | ZERO_SHA, | |
| 24 | } from "../lib/push-policy"; | |
| 25 | ||
| 26 | describe("ZERO_SHA constant", () => { | |
| 27 | it("is exactly 40 zeros", () => { | |
| 28 | expect(ZERO_SHA).toBe("0000000000000000000000000000000000000000"); | |
| 29 | expect(ZERO_SHA.length).toBe(40); | |
| 30 | }); | |
| 31 | }); | |
| 32 | ||
| 33 | describe("formatPolicyError", () => { | |
| 34 | it("returns a generic message when violations is empty", () => { | |
| 35 | expect(formatPolicyError([])).toBe("Push rejected by Gluecron policy."); | |
| 36 | }); | |
| 37 | ||
| 38 | it("returns a generic message when violations is null/undefined", () => { | |
| 39 | expect(formatPolicyError(null as any)).toBe( | |
| 40 | "Push rejected by Gluecron policy." | |
| 41 | ); | |
| 42 | expect(formatPolicyError(undefined as any)).toBe( | |
| 43 | "Push rejected by Gluecron policy." | |
| 44 | ); | |
| 45 | }); | |
| 46 | ||
| 47 | it("renders one violation as a bulleted list", () => { | |
| 48 | const out = formatPolicyError(['tag "v1.0" is protected']); | |
| 49 | expect(out).toContain("Push rejected by Gluecron policy:"); | |
| 50 | expect(out).toContain(' - tag "v1.0" is protected'); | |
| 51 | }); | |
| 52 | ||
| 53 | it("renders multiple violations on separate lines", () => { | |
| 54 | const out = formatPolicyError([ | |
| 55 | 'tag "v1.0" is protected', | |
| 56 | 'ruleset "no-prod-pushes" rule branch_name_pattern: blocked', | |
| 57 | ]); | |
| 58 | const lines = out.trimEnd().split("\n"); | |
| 59 | expect(lines[0]).toBe("Push rejected by Gluecron policy:"); | |
| 60 | expect(lines[1]).toBe(' - tag "v1.0" is protected'); | |
| 61 | expect(lines[2]).toBe( | |
| 62 | ' - ruleset "no-prod-pushes" rule branch_name_pattern: blocked' | |
| 63 | ); | |
| 64 | }); | |
| 65 | ||
| 66 | it("ends with a newline (so git surfaces the body cleanly)", () => { | |
| 67 | expect(formatPolicyError(["one violation"]).endsWith("\n")).toBe(true); | |
| 68 | }); | |
| 69 | }); | |
| 70 | ||
| 71 | describe("evaluatePushPolicy — fail-open on bad input", () => { | |
| 72 | it("returns allowed=true for an empty refs list", async () => { | |
| 73 | const r = await evaluatePushPolicy({ | |
| 74 | repositoryId: "repo-1", | |
| 75 | refs: [], | |
| 76 | pusherUserId: null, | |
| 77 | }); | |
| 78 | expect(r.allowed).toBe(true); | |
| 79 | expect(r.violations).toEqual([]); | |
| 80 | }); | |
| 81 | ||
| 82 | it("returns allowed=true for missing repositoryId", async () => { | |
| 83 | const r = await evaluatePushPolicy({ | |
| 84 | repositoryId: "" as any, | |
| 85 | refs: [ | |
| 86 | { | |
| 87 | oldSha: ZERO_SHA, | |
| 88 | newSha: "a".repeat(40), | |
| 89 | refName: "refs/tags/v1.0", | |
| 90 | }, | |
| 91 | ], | |
| 92 | pusherUserId: null, | |
| 93 | }); | |
| 94 | expect(r.allowed).toBe(true); | |
| 95 | }); | |
| 96 | ||
| 97 | it("returns allowed=true when DB is unreachable (refs target a nonexistent repo)", async () => { | |
| 98 | // No real repo exists with id "definitely-not-a-real-repo-id"; the | |
| 99 | // matchProtectedTag + listRulesetsForRepo callers catch their own | |
| 100 | // errors and return empty arrays, so the wrapper returns allowed. | |
| 101 | const r = await evaluatePushPolicy({ | |
| 102 | repositoryId: "definitely-not-a-real-repo-id", | |
| 103 | refs: [ | |
| 104 | { | |
| 105 | oldSha: ZERO_SHA, | |
| 106 | newSha: "a".repeat(40), | |
| 107 | refName: "refs/heads/main", | |
| 108 | }, | |
| 109 | ], | |
| 110 | pusherUserId: null, | |
| 111 | }); | |
| 112 | expect(r.allowed).toBe(true); | |
| 113 | }); | |
| 114 | }); | |
| 115 | ||
| 116 | describe("evaluatePushPolicy — module shape", () => { | |
| 117 | it("exports the functions the route depends on", async () => { | |
| 118 | const mod = await import("../lib/push-policy"); | |
| 119 | expect(typeof mod.evaluatePushPolicy).toBe("function"); | |
| 120 | expect(typeof mod.formatPolicyError).toBe("function"); | |
| 121 | expect(typeof mod.ZERO_SHA).toBe("string"); | |
| 122 | }); | |
| 123 | }); |