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

green-ecosystem.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.

green-ecosystem.test.tsBlame225 lines · 1 contributor
3ef4c9dClaude1/**
2 * Tests for the green ecosystem: secret scanner, codeowners parser,
3 * auto-repair helpers, notification + audit log helpers, health routes,
4 * and rate limiting.
5 *
6 * These unit-level tests avoid DB + git subprocess work so they run fast.
7 */
8
9import { describe, it, expect } from "bun:test";
10import app from "../app";
11import { scanForSecrets, SECRET_PATTERNS } from "../lib/security-scan";
12import {
13 parseCodeowners,
14 ownersForPath,
15} from "../lib/codeowners";
16import { generateCommitMessage } from "../lib/ai-generators";
17import { isAiAvailable } from "../lib/ai-client";
18
19describe("secret scanner", () => {
20 it("detects AWS access keys", () => {
21 const findings = scanForSecrets([
22 {
23 path: "config.env",
24 content: "AWS_ACCESS_KEY=AKIAZ2J4NPQR5LTMWXYZ\n",
25 },
26 ]);
27 expect(findings.length).toBeGreaterThan(0);
28 expect(findings.some((f) => /AWS/i.test(f.type))).toBe(true);
29 });
30
31 it("detects Anthropic API keys", () => {
32 const findings = scanForSecrets([
33 {
34 path: "app.ts",
35 content:
36 'const key = "sk-ant-api03-QQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQQ-AAAAAA";',
37 },
38 ]);
39 expect(findings.some((f) => /anthropic/i.test(f.type))).toBe(true);
40 });
41
42 it("ignores binary/lock paths", () => {
43 const findings = scanForSecrets([
44 {
45 path: "package-lock.json",
46 content: "AKIAZ2J4NPQR5LTMWXYZ secret content",
47 },
48 ]);
49 expect(findings.length).toBe(0);
50 });
51
52 it("does not match placeholder strings in test fixtures", () => {
53 const findings = scanForSecrets([
54 {
55 path: "test.js",
56 content:
57 '// example: AKIA" + "XAMPLE_PLACEHOLDER_KEY_FIXTURE"\nconst k = "FAKE_TEST_PLACEHOLDER";',
58 },
59 ]);
60 // all findings should be filtered by the placeholder heuristic
61 expect(findings.every((f) => !/placeholder|fixture/i.test(f.snippet))).toBe(true);
62 });
63
64 it("has a rich library of rules", () => {
65 expect(SECRET_PATTERNS.length).toBeGreaterThanOrEqual(10);
66 });
67});
68
69describe("codeowners parser", () => {
70 it("parses simple rules", () => {
71 const rules = parseCodeowners(
72 "# top-level owner\n* @alice\nsrc/api/** @bob @carol\n/docs/ @alice\n"
73 );
74 expect(rules.length).toBe(3);
75 expect(rules[0].owners).toEqual(["alice"]);
76 expect(rules[1].owners).toEqual(["bob", "carol"]);
77 });
78
79 it("resolves last-matching rule wins", () => {
80 const rules = parseCodeowners("* @alice\nsrc/api/** @bob\n");
81 expect(ownersForPath("README.md", rules)).toEqual(["alice"]);
82 expect(ownersForPath("src/api/users.ts", rules)).toEqual(["bob"]);
83 });
84
85 it("anchors leading-slash patterns to repo root", () => {
86 const rules = parseCodeowners("/docs/ @alice\n");
87 expect(ownersForPath("docs/readme.md", rules)).toEqual(["alice"]);
88 expect(ownersForPath("src/docs/readme.md", rules)).toEqual([]);
89 });
90
91 it("ignores comments and blank lines", () => {
92 const rules = parseCodeowners(
93 "# comment\n\n \n# another\n* @ghost # trailing comment\n"
94 );
95 expect(rules.length).toBe(1);
96 expect(rules[0].owners).toEqual(["ghost"]);
97 });
98});
99
100describe("AI generator fallbacks", () => {
101 it("returns a safe fallback commit message when AI is unavailable", async () => {
102 if (isAiAvailable()) {
103 // API key is set — skip fallback assertion
104 return;
105 }
106 const msg = await generateCommitMessage("");
107 expect(msg.length).toBeGreaterThan(0);
108 expect(msg).toMatch(/^\S+/);
109 });
110});
111
112describe("health + metrics endpoints", () => {
113 it("GET /healthz returns 200", async () => {
114 const res = await app.request("/healthz");
115 expect(res.status).toBe(200);
116 const body = await res.json();
117 expect(body.ok).toBe(true);
118 });
119
120 it("GET /metrics returns process metrics", async () => {
121 const res = await app.request("/metrics");
122 expect(res.status).toBe(200);
123 const body = await res.json();
124 expect(typeof body.uptimeMs).toBe("number");
125 expect(typeof body.heapUsed).toBe("number");
126 });
127
128 it("response carries X-Request-Id header", async () => {
129 const res = await app.request("/healthz");
130 expect(res.headers.get("X-Request-Id")).toBeTruthy();
131 });
132});
133
134describe("rate limiting", () => {
135 it("rate-limit headers appear on /api requests", async () => {
136 const res = await app.request("/api/users/nonexistent/repos");
137 // Headers should exist even though user is missing
138 const limit = res.headers.get("X-RateLimit-Limit");
139 expect(limit).toBeTruthy();
140 });
141});
142
143describe("shortcuts + search page", () => {
144 it("GET /shortcuts is public", async () => {
145 const res = await app.request("/shortcuts");
146 expect(res.status).toBe(200);
147 const html = await res.text();
148 expect(html).toContain("Keyboard shortcuts");
149 });
150
151 it("GET /search with no query shows the type tabs", async () => {
152 const res = await app.request("/search");
153 expect(res.status).toBe(200);
154 const html = await res.text();
155 expect(html).toContain("Repositories");
156 expect(html).toContain("Users");
157 });
158});
ad6d4adClaude159
160describe("GateTest inbound hook", () => {
161 it("GET /api/hooks/ping is unauthenticated and reports service", async () => {
162 const res = await app.request("/api/hooks/ping");
163 expect(res.status).toBe(200);
164 const body = await res.json();
165 expect(body.ok).toBe(true);
166 expect(body.service).toBe("gluecron");
167 expect(Array.isArray(body.hooks)).toBe(true);
168 });
169
170 it("POST /api/hooks/gatetest rejects when no secret configured", async () => {
171 const prev = process.env.GATETEST_CALLBACK_SECRET;
172 const prevH = process.env.GATETEST_HMAC_SECRET;
173 delete process.env.GATETEST_CALLBACK_SECRET;
174 delete process.env.GATETEST_HMAC_SECRET;
175 const res = await app.request("/api/hooks/gatetest", {
176 method: "POST",
177 headers: { "content-type": "application/json" },
178 body: JSON.stringify({ repository: "a/b", sha: "x", status: "passed" }),
179 });
180 expect(res.status).toBe(401);
181 if (prev) process.env.GATETEST_CALLBACK_SECRET = prev;
182 if (prevH) process.env.GATETEST_HMAC_SECRET = prevH;
183 });
184
185 it("POST /api/hooks/gatetest rejects bad bearer token", async () => {
186 const prev = process.env.GATETEST_CALLBACK_SECRET;
187 process.env.GATETEST_CALLBACK_SECRET = "real-secret-abc123";
188 const res = await app.request("/api/hooks/gatetest", {
189 method: "POST",
190 headers: {
191 "content-type": "application/json",
192 authorization: "Bearer wrong-token",
193 },
194 body: JSON.stringify({ repository: "a/b", sha: "x", status: "passed" }),
195 });
196 expect(res.status).toBe(401);
197 if (prev === undefined) delete process.env.GATETEST_CALLBACK_SECRET;
198 else process.env.GATETEST_CALLBACK_SECRET = prev;
199 });
200
201 it("POST /api/hooks/gatetest rejects malformed payload even when authed", async () => {
202 const prev = process.env.GATETEST_CALLBACK_SECRET;
203 process.env.GATETEST_CALLBACK_SECRET = "real-secret-abc123";
204 const res = await app.request("/api/hooks/gatetest", {
205 method: "POST",
206 headers: {
207 "content-type": "application/json",
208 authorization: "Bearer real-secret-abc123",
209 },
210 body: "not-json",
211 });
212 expect(res.status).toBe(400);
213 if (prev === undefined) delete process.env.GATETEST_CALLBACK_SECRET;
214 else process.env.GATETEST_CALLBACK_SECRET = prev;
215 });
216
217 it("POST /api/v1/gate-runs (backup) rejects without bearer", async () => {
218 const res = await app.request("/api/v1/gate-runs", {
219 method: "POST",
220 headers: { "content-type": "application/json" },
221 body: JSON.stringify({ repository: "a/b", sha: "x", status: "passed" }),
222 });
223 expect(res.status).toBe(401);
224 });
225});