Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
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.

rulesets.test.tsBlame368 lines · 1 contributor
9ff7128Claude1/**
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
8import { describe, it, expect } from "bun:test";
9import app from "../app";
10import {
11 RULE_TYPES,
12 __internal,
13 evaluatePush,
14 globToRegex,
15 parseParams,
16} from "../lib/rulesets";
17
18describe("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
40describe("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
53function 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
75describe("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
161describe("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
213describe("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
257describe("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
293describe("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
305describe("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
345describe("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});