CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
rulesets.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.
| 9ff7128 | 1 | /** |
| 2 | * Block J6 — Ruleset evaluator + route-auth tests. | |
| 3 | * | |
| 4 | * Evaluator is pure, so most of the coverage is in this file. DB-backed CRUD | |
| 5 | * relies on a live DB so it's covered in integration. | |
| 6 | */ | |
| 7 | ||
| 8 | import { describe, it, expect } from "bun:test"; | |
| 9 | import app from "../app"; | |
| 10 | import { | |
| 11 | RULE_TYPES, | |
| 12 | __internal, | |
| 13 | evaluatePush, | |
| 14 | globToRegex, | |
| 15 | parseParams, | |
| 16 | } from "../lib/rulesets"; | |
| 17 | ||
| 18 | describe("rulesets — globToRegex", () => { | |
| 19 | it("handles literal paths", () => { | |
| 20 | expect(globToRegex("README.md").test("README.md")).toBe(true); | |
| 21 | expect(globToRegex("README.md").test("other.md")).toBe(false); | |
| 22 | }); | |
| 23 | ||
| 24 | it("* matches one segment", () => { | |
| 25 | expect(globToRegex("src/*.ts").test("src/app.ts")).toBe(true); | |
| 26 | expect(globToRegex("src/*.ts").test("src/a/b.ts")).toBe(false); | |
| 27 | }); | |
| 28 | ||
| 29 | it("** matches anything including slashes", () => { | |
| 30 | expect(globToRegex("docs/**").test("docs/a/b/c.md")).toBe(true); | |
| 31 | expect(globToRegex("**/secret.txt").test("a/b/secret.txt")).toBe(true); | |
| 32 | }); | |
| 33 | ||
| 34 | it("escapes regex specials", () => { | |
| 35 | expect(globToRegex("a+b.txt").test("a+b.txt")).toBe(true); | |
| 36 | expect(globToRegex("a+b.txt").test("axb.txt")).toBe(false); | |
| 37 | }); | |
| 38 | }); | |
| 39 | ||
| 40 | describe("rulesets — parseParams", () => { | |
| 41 | it("round-trips JSON", () => { | |
| 42 | expect(parseParams('{"pattern":"^feat:"}')).toEqual({ | |
| 43 | pattern: "^feat:", | |
| 44 | }); | |
| 45 | }); | |
| 46 | ||
| 47 | it("returns {} on garbage", () => { | |
| 48 | expect(parseParams("not json")).toEqual({}); | |
| 49 | expect(parseParams("")).toEqual({}); | |
| 50 | }); | |
| 51 | }); | |
| 52 | ||
| 53 | function rs( | |
| 54 | enforcement: "active" | "evaluate" | "disabled", | |
| 55 | rules: Array<{ ruleType: string; params: any }> | |
| 56 | ) { | |
| 57 | return { | |
| 58 | id: "rs-1", | |
| 59 | repositoryId: "r", | |
| 60 | name: "n", | |
| 61 | enforcement, | |
| 62 | createdBy: null, | |
| 63 | createdAt: new Date(), | |
| 64 | updatedAt: new Date(), | |
| 65 | rules: rules.map((r, i) => ({ | |
| 66 | id: `rule-${i}`, | |
| 67 | rulesetId: "rs-1", | |
| 68 | ruleType: r.ruleType, | |
| 69 | params: JSON.stringify(r.params), | |
| 70 | createdAt: new Date(), | |
| 71 | })), | |
| 72 | } as any; | |
| 73 | } | |
| 74 | ||
| 75 | describe("rulesets — evaluatePush commit_message_pattern", () => { | |
| 76 | it("require=true blocks non-matching messages under active", () => { | |
| 77 | const result = evaluatePush( | |
| 78 | [ | |
| 79 | rs("active", [ | |
| 80 | { | |
| 81 | ruleType: "commit_message_pattern", | |
| 82 | params: { pattern: "^(feat|fix|chore):" }, | |
| 83 | }, | |
| 84 | ]), | |
| 85 | ], | |
| 86 | { | |
| 87 | kind: "push", | |
| 88 | refType: "branch", | |
| 89 | refName: "main", | |
| 90 | commits: [{ sha: "abc123", message: "random junk" }], | |
| 91 | } | |
| 92 | ); | |
| 93 | expect(result.allowed).toBe(false); | |
| 94 | expect(result.violations[0].ruleType).toBe("commit_message_pattern"); | |
| 95 | }); | |
| 96 | ||
| 97 | it("evaluate mode warns but allows", () => { | |
| 98 | const result = evaluatePush( | |
| 99 | [ | |
| 100 | rs("evaluate", [ | |
| 101 | { | |
| 102 | ruleType: "commit_message_pattern", | |
| 103 | params: { pattern: "^(feat|fix|chore):" }, | |
| 104 | }, | |
| 105 | ]), | |
| 106 | ], | |
| 107 | { | |
| 108 | kind: "push", | |
| 109 | refType: "branch", | |
| 110 | refName: "main", | |
| 111 | commits: [{ sha: "abc123", message: "random" }], | |
| 112 | } | |
| 113 | ); | |
| 114 | expect(result.allowed).toBe(true); | |
| 115 | expect(result.violations.length).toBe(1); | |
| 116 | expect(result.violations[0].enforcement).toBe("evaluate"); | |
| 117 | }); | |
| 118 | ||
| 119 | it("disabled mode produces zero violations", () => { | |
| 120 | const result = evaluatePush( | |
| 121 | [ | |
| 122 | rs("disabled", [ | |
| 123 | { | |
| 124 | ruleType: "commit_message_pattern", | |
| 125 | params: { pattern: "^feat:" }, | |
| 126 | }, | |
| 127 | ]), | |
| 128 | ], | |
| 129 | { | |
| 130 | kind: "push", | |
| 131 | refType: "branch", | |
| 132 | refName: "main", | |
| 133 | commits: [{ sha: "abc", message: "anything" }], | |
| 134 | } | |
| 135 | ); | |
| 136 | expect(result.allowed).toBe(true); | |
| 137 | expect(result.violations.length).toBe(0); | |
| 138 | }); | |
| 139 | ||
| 140 | it("require=false blocks when pattern matches (forbidden)", () => { | |
| 141 | const result = evaluatePush( | |
| 142 | [ | |
| 143 | rs("active", [ | |
| 144 | { | |
| 145 | ruleType: "commit_message_pattern", | |
| 146 | params: { pattern: "wip", flags: "i", require: false }, | |
| 147 | }, | |
| 148 | ]), | |
| 149 | ], | |
| 150 | { | |
| 151 | kind: "push", | |
| 152 | refType: "branch", | |
| 153 | refName: "main", | |
| 154 | commits: [{ sha: "abc", message: "WIP push" }], | |
| 155 | } | |
| 156 | ); | |
| 157 | expect(result.allowed).toBe(false); | |
| 158 | }); | |
| 159 | }); | |
| 160 | ||
| 161 | describe("rulesets — evaluatePush branch / tag patterns", () => { | |
| 162 | it("branch must match required pattern", () => { | |
| 163 | const r = evaluatePush( | |
| 164 | [ | |
| 165 | rs("active", [ | |
| 166 | { | |
| 167 | ruleType: "branch_name_pattern", | |
| 168 | params: { pattern: "^release/" }, | |
| 169 | }, | |
| 170 | ]), | |
| 171 | ], | |
| 172 | { | |
| 173 | kind: "push", | |
| 174 | refType: "branch", | |
| 175 | refName: "feature/x", | |
| 176 | commits: [], | |
| 177 | } | |
| 178 | ); | |
| 179 | expect(r.allowed).toBe(false); | |
| 180 | }); | |
| 181 | ||
| 182 | it("branch rule is a no-op for tag pushes", () => { | |
| 183 | const r = evaluatePush( | |
| 184 | [ | |
| 185 | rs("active", [ | |
| 186 | { | |
| 187 | ruleType: "branch_name_pattern", | |
| 188 | params: { pattern: "^release/" }, | |
| 189 | }, | |
| 190 | ]), | |
| 191 | ], | |
| 192 | { kind: "push", refType: "tag", refName: "v1.0", commits: [] } | |
| 193 | ); | |
| 194 | expect(r.allowed).toBe(true); | |
| 195 | }); | |
| 196 | ||
| 197 | it("tag must match semver-ish", () => { | |
| 198 | const r = evaluatePush( | |
| 199 | [ | |
| 200 | rs("active", [ | |
| 201 | { | |
| 202 | ruleType: "tag_name_pattern", | |
| 203 | params: { pattern: "^v\\d+\\.\\d+\\.\\d+$" }, | |
| 204 | }, | |
| 205 | ]), | |
| 206 | ], | |
| 207 | { kind: "push", refType: "tag", refName: "v1.0", commits: [] } | |
| 208 | ); | |
| 209 | expect(r.allowed).toBe(false); | |
| 210 | }); | |
| 211 | }); | |
| 212 | ||
| 213 | describe("rulesets — evaluatePush blocked_file_paths", () => { | |
| 214 | it("blocks changes to matching paths", () => { | |
| 215 | const r = evaluatePush( | |
| 216 | [ | |
| 217 | rs("active", [ | |
| 218 | { | |
| 219 | ruleType: "blocked_file_paths", | |
| 220 | params: { paths: ["secrets/**", "*.pem"] }, | |
| 221 | }, | |
| 222 | ]), | |
| 223 | ], | |
| 224 | { | |
| 225 | kind: "push", | |
| 226 | refType: "branch", | |
| 227 | refName: "main", | |
| 228 | commits: [ | |
| 229 | { sha: "a", message: "x", changedPaths: ["secrets/db.env"] }, | |
| 230 | ], | |
| 231 | } | |
| 232 | ); | |
| 233 | expect(r.allowed).toBe(false); | |
| 234 | }); | |
| 235 | ||
| 236 | it("allows unrelated path changes", () => { | |
| 237 | const r = evaluatePush( | |
| 238 | [ | |
| 239 | rs("active", [ | |
| 240 | { | |
| 241 | ruleType: "blocked_file_paths", | |
| 242 | params: { paths: ["secrets/**"] }, | |
| 243 | }, | |
| 244 | ]), | |
| 245 | ], | |
| 246 | { | |
| 247 | kind: "push", | |
| 248 | refType: "branch", | |
| 249 | refName: "main", | |
| 250 | commits: [{ sha: "a", message: "x", changedPaths: ["src/app.ts"] }], | |
| 251 | } | |
| 252 | ); | |
| 253 | expect(r.allowed).toBe(true); | |
| 254 | }); | |
| 255 | }); | |
| 256 | ||
| 257 | describe("rulesets — evaluatePush max_file_size + force push", () => { | |
| 258 | it("blocks oversize blobs", () => { | |
| 259 | const r = evaluatePush( | |
| 260 | [ | |
| 261 | rs("active", [ | |
| 262 | { | |
| 263 | ruleType: "max_file_size", | |
| 264 | params: { bytes: 1024 }, | |
| 265 | }, | |
| 266 | ]), | |
| 267 | ], | |
| 268 | { | |
| 269 | kind: "push", | |
| 270 | refType: "branch", | |
| 271 | refName: "main", | |
| 272 | commits: [{ sha: "a", message: "x", maxBlobSize: 2048 }], | |
| 273 | } | |
| 274 | ); | |
| 275 | expect(r.allowed).toBe(false); | |
| 276 | }); | |
| 277 | ||
| 278 | it("blocks force push when configured", () => { | |
| 279 | const r = evaluatePush( | |
| 280 | [rs("active", [{ ruleType: "forbid_force_push", params: {} }])], | |
| 281 | { | |
| 282 | kind: "push", | |
| 283 | refType: "branch", | |
| 284 | refName: "main", | |
| 285 | commits: [], | |
| 286 | forcePush: true, | |
| 287 | } | |
| 288 | ); | |
| 289 | expect(r.allowed).toBe(false); | |
| 290 | }); | |
| 291 | }); | |
| 292 | ||
| 293 | describe("rulesets — RULE_TYPES surface", () => { | |
| 294 | it("exports all rule types", () => { | |
| 295 | expect(RULE_TYPES).toContain("commit_message_pattern"); | |
| 296 | expect(RULE_TYPES).toContain("branch_name_pattern"); | |
| 297 | expect(RULE_TYPES).toContain("tag_name_pattern"); | |
| 298 | expect(RULE_TYPES).toContain("blocked_file_paths"); | |
| 299 | expect(RULE_TYPES).toContain("max_file_size"); | |
| 300 | expect(RULE_TYPES).toContain("forbid_force_push"); | |
| 301 | expect(RULE_TYPES.length).toBe(6); | |
| 302 | }); | |
| 303 | }); | |
| 304 | ||
| 305 | describe("rulesets — __internal evalRule edge cases", () => { | |
| 306 | it("empty pattern is a no-op", () => { | |
| 307 | const msgs = __internal.evalRule( | |
| 308 | { | |
| 309 | id: "r1", | |
| 310 | rulesetId: "s", | |
| 311 | ruleType: "commit_message_pattern", | |
| 312 | params: JSON.stringify({ pattern: "" }), | |
| 313 | createdAt: new Date(), | |
| 314 | } as any, | |
| 315 | { | |
| 316 | kind: "push", | |
| 317 | refType: "branch", | |
| 318 | refName: "main", | |
| 319 | commits: [{ sha: "a", message: "nothing" }], | |
| 320 | } | |
| 321 | ); | |
| 322 | expect(msgs.length).toBe(0); | |
| 323 | }); | |
| 324 | ||
| 325 | it("invalid regex is a no-op", () => { | |
| 326 | const msgs = __internal.evalRule( | |
| 327 | { | |
| 328 | id: "r1", | |
| 329 | rulesetId: "s", | |
| 330 | ruleType: "commit_message_pattern", | |
| 331 | params: JSON.stringify({ pattern: "(" }), | |
| 332 | createdAt: new Date(), | |
| 333 | } as any, | |
| 334 | { | |
| 335 | kind: "push", | |
| 336 | refType: "branch", | |
| 337 | refName: "main", | |
| 338 | commits: [{ sha: "a", message: "x" }], | |
| 339 | } | |
| 340 | ); | |
| 341 | expect(msgs.length).toBe(0); | |
| 342 | }); | |
| 343 | }); | |
| 344 | ||
| 345 | describe("rulesets — route auth", () => { | |
| 346 | it("GET /:o/:r/settings/rulesets without auth → 302 /login", async () => { | |
| 347 | const res = await app.request("/alice/repo/settings/rulesets"); | |
| 348 | expect(res.status).toBe(302); | |
| 349 | expect(res.headers.get("location") || "").toContain("/login"); | |
| 350 | }); | |
| 351 | ||
| 352 | it("POST create without auth → 302 /login", async () => { | |
| 353 | const res = await app.request("/alice/repo/settings/rulesets", { | |
| 354 | method: "POST", | |
| 355 | }); | |
| 356 | expect(res.status).toBe(302); | |
| 357 | expect(res.headers.get("location") || "").toContain("/login"); | |
| 358 | }); | |
| 359 | ||
| 360 | it("POST delete without auth → 302 /login", async () => { | |
| 361 | const res = await app.request( | |
| 362 | "/alice/repo/settings/rulesets/00000000-0000-0000-0000-000000000000/delete", | |
| 363 | { method: "POST" } | |
| 364 | ); | |
| 365 | expect(res.status).toBe(302); | |
| 366 | expect(res.headers.get("location") || "").toContain("/login"); | |
| 367 | }); | |
| 368 | }); |