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

environments.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.

environments.test.tsBlame196 lines · 1 contributor
25a91a6Claude1/**
2 * Tests for Block C4 — environments + deployment approvals.
3 *
4 * Unit tests exercise the pure-function glob matcher + the single-approver
5 * semantics of computeApprovalState. Route-level tests verify that settings
6 * CRUD and approve/reject endpoints are properly guarded — they tolerate
7 * DB-less test environments (302/303/307/404/503) rather than asserting
8 * a single happy-path status.
9 */
10
11import { describe, it, expect } from "bun:test";
12import app from "../app";
13import {
14 matchGlob,
15 reduceApprovalState,
16 reviewerIdsOf,
17 allowedBranchesOf,
18} from "../lib/environments";
19import type { Environment, DeploymentApproval } from "../db/schema";
20
21const envFixture = (overrides: Partial<Environment> = {}): Environment =>
22 ({
23 id: "env-1",
24 repositoryId: "repo-1",
25 name: "production",
26 requireApproval: true,
27 reviewers: "[]",
28 waitTimerMinutes: 0,
29 allowedBranches: "[]",
30 createdAt: new Date(),
31 updatedAt: new Date(),
32 ...overrides,
33 }) as Environment;
34
35describe("matchGlob", () => {
36 it("matches exact literals", () => {
37 expect(matchGlob("main", "main")).toBe(true);
38 });
39
40 it("does not match mismatched literals", () => {
41 expect(matchGlob("main", "release/*")).toBe(false);
42 });
43
44 it("supports single-segment * wildcards", () => {
45 expect(matchGlob("release/1.0", "release/*")).toBe(true);
46 expect(matchGlob("release/foo/bar", "release/*")).toBe(false);
47 });
48
49 it("supports ** for any path", () => {
50 expect(matchGlob("release/foo/bar", "release/**")).toBe(true);
51 expect(matchGlob("main", "**")).toBe(true);
52 });
53
54 it("strips refs/heads/ prefix on both sides", () => {
55 expect(matchGlob("refs/heads/main", "main")).toBe(true);
56 expect(matchGlob("main", "refs/heads/main")).toBe(true);
57 });
58
59 it("does not match unrelated branches", () => {
60 expect(matchGlob("feature/x", "release/*")).toBe(false);
61 expect(matchGlob("develop", "main")).toBe(false);
62 });
63});
64
65describe("reviewerIdsOf / allowedBranchesOf", () => {
66 it("parses valid JSON arrays", () => {
67 const env = envFixture({
68 reviewers: JSON.stringify(["u1", "u2"]),
69 allowedBranches: JSON.stringify(["main", "release/*"]),
70 });
71 expect(reviewerIdsOf(env)).toEqual(["u1", "u2"]);
72 expect(allowedBranchesOf(env)).toEqual(["main", "release/*"]);
73 });
74
75 it("returns [] for empty/invalid", () => {
76 expect(reviewerIdsOf(envFixture({ reviewers: "" }))).toEqual([]);
77 expect(reviewerIdsOf(envFixture({ reviewers: "not-json" }))).toEqual([]);
78 expect(allowedBranchesOf(envFixture({ allowedBranches: "[]" }))).toEqual([]);
79 });
80});
81
82const mkApproval = (
83 decision: "approved" | "rejected",
84 userId = "u1"
85): DeploymentApproval =>
86 ({
87 id: `a-${Math.random()}`,
88 deploymentId: "d1",
89 userId,
90 decision,
91 comment: null,
92 createdAt: new Date(),
93 }) as DeploymentApproval;
94
95describe("reduceApprovalState (single-approver semantics)", () => {
96 it("approved=true when any approval exists and no rejection", () => {
97 const state = reduceApprovalState([mkApproval("approved")]);
98 expect(state.approved).toBe(true);
99 expect(state.rejected).toBe(false);
100 expect(state.decided.length).toBe(1);
101 });
102
103 it("rejected=true when any rejection exists (overrides approval)", () => {
104 const state = reduceApprovalState([
105 mkApproval("approved", "u1"),
106 mkApproval("rejected", "u2"),
107 ]);
108 expect(state.rejected).toBe(true);
109 expect(state.approved).toBe(false);
110 });
111
112 it("neither approved nor rejected when no decisions", () => {
113 const state = reduceApprovalState([]);
114 expect(state.approved).toBe(false);
115 expect(state.rejected).toBe(false);
116 });
117});
118
119describe("environments routes — unauthed guards", () => {
120 const ok = [301, 302, 303, 307, 401, 404, 503];
121
122 it("GET /:owner/:repo/settings/environments redirects to login when unauthed", async () => {
123 const res = await app.request("/alice/project/settings/environments");
124 expect(ok).toContain(res.status);
125 });
126
127 it("POST /:owner/:repo/settings/environments requires auth", async () => {
128 const res = await app.request("/alice/project/settings/environments", {
129 method: "POST",
130 headers: { "content-type": "application/x-www-form-urlencoded" },
131 body: "name=staging",
132 });
133 expect(ok).toContain(res.status);
134 });
135
136 it("POST /:owner/:repo/settings/environments/:envId requires auth", async () => {
137 const res = await app.request(
138 "/alice/project/settings/environments/env-1",
139 {
140 method: "POST",
141 headers: { "content-type": "application/x-www-form-urlencoded" },
142 body: "",
143 }
144 );
145 expect(ok).toContain(res.status);
146 });
147
148 it("POST /:owner/:repo/settings/environments/:envId/delete requires auth", async () => {
149 const res = await app.request(
150 "/alice/project/settings/environments/env-1/delete",
151 {
152 method: "POST",
153 headers: { "content-type": "application/x-www-form-urlencoded" },
154 body: "",
155 }
156 );
157 expect(ok).toContain(res.status);
158 });
159
160 it("POST /:owner/:repo/deployments/:id/approve requires auth", async () => {
161 const res = await app.request(
162 "/alice/project/deployments/dep-1/approve",
163 {
164 method: "POST",
165 headers: { "content-type": "application/x-www-form-urlencoded" },
166 body: "",
167 }
168 );
169 expect(ok).toContain(res.status);
170 });
171
172 it("POST /:owner/:repo/deployments/:id/reject requires auth", async () => {
173 const res = await app.request(
174 "/alice/project/deployments/dep-1/reject",
175 {
176 method: "POST",
177 headers: { "content-type": "application/x-www-form-urlencoded" },
178 body: "",
179 }
180 );
181 expect(ok).toContain(res.status);
182 });
183
184 it("bearer auth with bogus token on settings POST returns 401", async () => {
185 const res = await app.request("/alice/project/settings/environments", {
186 method: "POST",
187 headers: {
188 "content-type": "application/x-www-form-urlencoded",
189 authorization: "Bearer glct_definitely-not-valid",
190 },
191 body: "name=staging",
192 });
193 // 401 from requireAuth with invalid bearer; 404/503 tolerated pre-route.
194 expect([401, 404, 503]).toContain(res.status);
195 });
196});