Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

crontech-client.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.

crontech-client.test.tsBlame235 lines · 1 contributor
a6d8fd5Claude1/**
2 * Block K — Crontech client tests.
3 *
4 * Exercises config toggle, auth headers, and offline short-circuits. When a
5 * key is present, we mock globalThis.fetch to confirm graceful degradation.
6 */
7
8import { describe, it, expect, beforeEach, afterEach } from "bun:test";
9import {
10 buildAuthHeaders,
11 getDeploymentForCommit,
12 isConfigured,
13 rollbackDeployment,
14 triggerRedeploy,
15 watchDeployment,
16} from "../lib/crontech-client";
17
18const ENV_KEYS = ["CRONTECH_API_KEY", "CRONTECH_BASE_URL"] as const;
19
20let savedEnv: Record<string, string | undefined> = {};
21let savedFetch: typeof fetch;
22
23beforeEach(() => {
24 savedEnv = {};
25 for (const k of ENV_KEYS) savedEnv[k] = process.env[k];
26 for (const k of ENV_KEYS) delete process.env[k];
27 savedFetch = globalThis.fetch;
28});
29
30afterEach(() => {
31 for (const k of ENV_KEYS) {
32 const v = savedEnv[k];
33 if (v === undefined) delete process.env[k];
34 else process.env[k] = v;
35 }
36 globalThis.fetch = savedFetch;
37});
38
39// ---------------------------------------------------------------------------
40// isConfigured + buildAuthHeaders
41// ---------------------------------------------------------------------------
42
43describe("crontech-client — config", () => {
44 it("isConfigured is false when no API key is set", () => {
45 expect(isConfigured()).toBe(false);
46 });
47
48 it("isConfigured flips true once CRONTECH_API_KEY is set", () => {
49 process.env.CRONTECH_API_KEY = "ct-test";
50 expect(isConfigured()).toBe(true);
51 });
52
53 it("buildAuthHeaders omits Authorization when no key is present", () => {
54 const h = buildAuthHeaders();
55 expect(h["Content-Type"]).toBe("application/json");
56 expect(h["Authorization"]).toBeUndefined();
57 });
58
59 it("buildAuthHeaders includes a Bearer token when the key is present", () => {
60 process.env.CRONTECH_API_KEY = "ct-live";
61 const h = buildAuthHeaders();
62 expect(h["Authorization"]).toBe("Bearer ct-live");
63 });
64});
65
66// ---------------------------------------------------------------------------
67// getDeploymentForCommit
68// ---------------------------------------------------------------------------
69
70describe("crontech-client — getDeploymentForCommit", () => {
71 it("returns null in offline mode (no key)", async () => {
72 globalThis.fetch = (() => {
73 throw new Error("fetch must not run offline");
74 }) as unknown as typeof fetch;
75 const result = await getDeploymentForCommit({
76 repo: "o/r",
77 commitSha: "deadbeef",
78 });
79 expect(result).toBeNull();
80 });
81
82 it("returns null on a 500 response", async () => {
83 process.env.CRONTECH_API_KEY = "ct";
84 globalThis.fetch = (async () =>
85 new Response("", { status: 500 })) as unknown as typeof fetch;
86 const result = await getDeploymentForCommit({
87 repo: "o/r",
88 commitSha: "deadbeef",
89 });
90 expect(result).toBeNull();
91 });
92
93 it("parses a valid deployment body on 200", async () => {
94 process.env.CRONTECH_API_KEY = "ct";
95 globalThis.fetch = (async () =>
96 new Response(
97 JSON.stringify({
98 deployId: "dep_1",
99 commitSha: "deadbeef",
100 status: "live",
101 environment: "production",
102 startedAt: "2026-01-01T00:00:00Z",
103 }),
104 { status: 200, headers: { "Content-Type": "application/json" } }
105 )) as unknown as typeof fetch;
106 const result = await getDeploymentForCommit({
107 repo: "o/r",
108 commitSha: "deadbeef",
109 });
110 expect(result).not.toBeNull();
111 expect(result?.deployId).toBe("dep_1");
112 expect(result?.status).toBe("live");
113 });
114});
115
116// ---------------------------------------------------------------------------
117// triggerRedeploy
118// ---------------------------------------------------------------------------
119
120describe("crontech-client — triggerRedeploy", () => {
121 it("returns null in offline mode (no key)", async () => {
122 const result = await triggerRedeploy({
123 repo: "o/r",
124 commitSha: "cafef00d",
125 });
126 expect(result).toBeNull();
127 });
128
129 it("returns null on a 500 response", async () => {
130 process.env.CRONTECH_API_KEY = "ct";
131 globalThis.fetch = (async () =>
132 new Response("", { status: 500 })) as unknown as typeof fetch;
133 const result = await triggerRedeploy({
134 repo: "o/r",
135 commitSha: "cafef00d",
136 environment: "staging",
137 });
138 expect(result).toBeNull();
139 });
140});
141
142// ---------------------------------------------------------------------------
143// rollbackDeployment
144// ---------------------------------------------------------------------------
145
146describe("crontech-client — rollbackDeployment", () => {
147 it("returns false in offline mode (no key)", async () => {
148 const ok = await rollbackDeployment({ repo: "o/r", deployId: "dep_1" });
149 expect(ok).toBe(false);
150 });
151
152 it("returns false on a 500 response", async () => {
153 process.env.CRONTECH_API_KEY = "ct";
154 globalThis.fetch = (async () =>
155 new Response("", { status: 500 })) as unknown as typeof fetch;
156 const ok = await rollbackDeployment({ repo: "o/r", deployId: "dep_1" });
157 expect(ok).toBe(false);
158 });
159
160 it("returns false when deployId is empty even with a key", async () => {
161 process.env.CRONTECH_API_KEY = "ct";
162 const ok = await rollbackDeployment({ repo: "o/r", deployId: "" });
163 expect(ok).toBe(false);
164 });
165});
166
167// ---------------------------------------------------------------------------
168// watchDeployment
169// ---------------------------------------------------------------------------
170
171describe("crontech-client — watchDeployment", () => {
172 it("returns offline:true immediately when no key is set", async () => {
173 globalThis.fetch = (() => {
174 throw new Error("fetch must not run offline");
175 }) as unknown as typeof fetch;
176 const start = Date.now();
177 const result = await watchDeployment({
178 repo: "o/r",
179 deployId: "dep_1",
180 maxWaitMs: 60_000,
181 });
182 const elapsed = Date.now() - start;
183 expect(result.offline).toBe(true);
184 expect(result.finalStatus).toBe("failed");
185 expect(result.errors).toEqual([]);
186 // Should short-circuit in well under a second.
187 expect(elapsed).toBeLessThan(500);
188 });
189
190 it("resolves with finalStatus=live when the poll returns terminal status", async () => {
191 process.env.CRONTECH_API_KEY = "ct";
192 let call = 0;
193 globalThis.fetch = (async (input: unknown) => {
194 call++;
195 const url = String(input);
196 if (url.endsWith("/status")) {
197 return new Response(
198 JSON.stringify({ status: "live" }),
199 { status: 200, headers: { "Content-Type": "application/json" } }
200 );
201 }
202 if (url.endsWith("/errors")) {
203 return new Response(
204 JSON.stringify({ errors: [] }),
205 { status: 200, headers: { "Content-Type": "application/json" } }
206 );
207 }
208 return new Response("", { status: 404 });
209 }) as unknown as typeof fetch;
210
211 const result = await watchDeployment({
212 repo: "o/r",
213 deployId: "dep_1",
214 maxWaitMs: 5_000,
215 pollIntervalMs: 50,
216 });
217 expect(result.offline).toBe(false);
218 expect(result.finalStatus).toBe("live");
219 expect(call).toBeGreaterThanOrEqual(2); // status + errors
220 });
221
222 it("bails to offline after repeated 500 status polls", async () => {
223 process.env.CRONTECH_API_KEY = "ct";
224 globalThis.fetch = (async () =>
225 new Response("", { status: 500 })) as unknown as typeof fetch;
226 const result = await watchDeployment({
227 repo: "o/r",
228 deployId: "dep_1",
229 maxWaitMs: 5_000,
230 pollIntervalMs: 10,
231 });
232 expect(result.offline).toBe(true);
233 expect(result.finalStatus).toBe("failed");
234 });
235});