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

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

install.test.tsBlame273 lines · 1 contributor
46d6165Claude1/**
2 * Block L2 — one-command install.
3 *
4 * Coverage:
5 * - GET /install returns 200 + the bash script + correct headers
6 * - POST /api/v2/auth/install-token refuses Bearer-token callers
7 * - POST /api/v2/auth/install-token refuses unauthenticated callers
8 * - POST /api/v2/auth/install-token mints a PAT + writes an audit row when
9 * called over a real session cookie (DB-backed)
10 *
11 * The DB-backed test is gated on `DATABASE_URL` so the suite still runs in
12 * environments without Postgres — matching the convention used elsewhere
13 * (see `api-tokens.test.ts`).
14 */
15
16import { describe, it, expect } from "bun:test";
17import app from "../app";
18import { INSTALL_SCRIPT_SRC } from "../routes/install";
19
20const HAS_DB = Boolean(process.env.DATABASE_URL);
21
22// ---------------------------------------------------------------------------
23// 1. GET /install — the curl-able installer
24// ---------------------------------------------------------------------------
25
26describe("install — GET /install", () => {
27 it("returns 200 with the bash script body", async () => {
28 const res = await app.request("/install");
29 expect(res.status).toBe(200);
30 const body = await res.text();
31 expect(body.length).toBeGreaterThan(0);
32 expect(body.startsWith("#!")).toBe(true);
33 });
34
35 it("serves Content-Type: text/x-shellscript", async () => {
36 const res = await app.request("/install");
37 const ct = res.headers.get("content-type") || "";
38 expect(ct).toContain("text/x-shellscript");
39 });
40
41 it("serves a public Cache-Control so a CDN can absorb load", async () => {
42 const res = await app.request("/install");
43 const cc = res.headers.get("cache-control") || "";
44 expect(cc).toContain("public");
45 expect(cc).toContain("max-age=300");
46 });
47
48 it("script body contains the key install-flow markers", async () => {
49 const res = await app.request("/install");
50 const body = await res.text();
51 // Sanity-check the actual script (or fallback) loaded into memory.
52 expect(body).toContain("#!/usr/bin/env bash");
53 if (INSTALL_SCRIPT_SRC.includes("set -euo pipefail")) {
54 // Real script path — assert the user-facing flow it promises.
55 expect(body).toContain("set -euo pipefail");
56 expect(body).toContain("/api/v2/auth/install-token");
57 expect(body).toContain("claude_desktop_config.json");
58 expect(body).toContain("mcpServers");
59 }
60 });
61
62 it("INSTALL_SCRIPT_SRC exports a non-empty bash script", () => {
63 expect(INSTALL_SCRIPT_SRC.length).toBeGreaterThan(0);
64 expect(INSTALL_SCRIPT_SRC.startsWith("#!")).toBe(true);
65 });
66});
67
68// ---------------------------------------------------------------------------
69// 2. POST /api/v2/auth/install-token — auth contract
70// ---------------------------------------------------------------------------
71
72describe("install-token — auth contract", () => {
73 it("rejects Bearer tokens outright with 401 JSON", async () => {
74 // Unknown / unresolvable bearer — the apiAuth middleware short-circuits
75 // before our handler runs, but the contract for the caller is the same:
76 // 401 JSON with an `error` string. The whole point is that a Bearer
77 // caller *never* gets a 200 + a fresh PAT.
78 const res = await app.request("/api/v2/auth/install-token", {
79 method: "POST",
80 headers: {
81 "content-type": "application/json",
82 authorization: "Bearer glc_" + "a".repeat(64),
83 },
84 body: JSON.stringify({ name: "abuse", scope: "admin" }),
85 });
86 expect(res.status).toBe(401);
87 const body = await res.json();
88 expect(typeof body.error).toBe("string");
89 expect(body.error.length).toBeGreaterThan(0);
90 });
91
92 it("rejects malformed Bearer (no token) the same way", async () => {
93 const res = await app.request("/api/v2/auth/install-token", {
94 method: "POST",
95 headers: {
96 "content-type": "application/json",
97 authorization: "Bearer ",
98 },
99 body: JSON.stringify({}),
100 });
101 // Either the apiAuth middleware fails the bearer lookup (401) or we
102 // hit our own bearer-reject branch (401). Either is acceptable; the
103 // important invariant is "never 200".
104 expect(res.status).toBe(401);
105 });
106
107 it("rejects unauthenticated callers with 401 JSON", async () => {
108 const res = await app.request("/api/v2/auth/install-token", {
109 method: "POST",
110 headers: { "content-type": "application/json" },
111 body: JSON.stringify({}),
112 });
113 expect(res.status).toBe(401);
114 const body = await res.json();
115 expect(typeof body.error).toBe("string");
116 expect(JSON.stringify(body).toLowerCase()).toContain("session");
117 });
118
119 it("rejects an empty body without a session", async () => {
120 const res = await app.request("/api/v2/auth/install-token", {
121 method: "POST",
122 });
123 expect(res.status).toBe(401);
124 });
125});
126
127// ---------------------------------------------------------------------------
128// 3. POST /api/v2/auth/install-token — successful mint (DB-backed)
129// ---------------------------------------------------------------------------
130
131describe("install-token — successful mint", () => {
132 it.skipIf(!HAS_DB)(
133 "mints a glc_ PAT + writes auth.install_token.created audit row",
134 async () => {
135 const { db } = await import("../db");
136 const { users, sessions, apiTokens, auditLog } = await import(
137 "../db/schema"
138 );
139 const { eq, and, desc } = await import("drizzle-orm");
140
141 // Set up a one-off user + session purely for this test.
142 const uname = "install-test-" + Math.random().toString(36).slice(2, 10);
143 const [user] = await db
144 .insert(users)
145 .values({
146 username: uname,
147 email: `${uname}@example.com`,
148 passwordHash: "x",
149 })
150 .returning();
151
152 const sessionToken =
153 "sess_test_" + Math.random().toString(36).slice(2) + Date.now();
154 const expiresAt = new Date(Date.now() + 60_000);
155 await db.insert(sessions).values({
156 userId: user.id,
157 token: sessionToken,
158 expiresAt,
159 });
160
161 try {
162 const res = await app.request("/api/v2/auth/install-token", {
163 method: "POST",
164 headers: {
165 "content-type": "application/json",
166 cookie: `session=${sessionToken}`,
167 },
168 body: JSON.stringify({ name: "ci-install", scope: "admin" }),
169 });
170
171 expect(res.status).toBe(201);
172 const body = await res.json();
173 expect(typeof body.token).toBe("string");
174 expect(body.token.startsWith("glc_")).toBe(true);
175 expect(body.token.length).toBe("glc_".length + 64);
176 expect(body.name).toBe("ci-install");
177 expect(body.scope).toBe("admin");
178 expect(body.id).toBeDefined();
179
180 // PAT row exists with the right prefix + scopes.
181 const [row] = await db
182 .select()
183 .from(apiTokens)
184 .where(eq(apiTokens.id, body.id))
185 .limit(1);
186 expect(row).toBeDefined();
187 expect(row!.userId).toBe(user.id);
188 expect(row!.name).toBe("ci-install");
189 expect(row!.scopes).toContain("admin");
190 expect(row!.tokenPrefix).toBe(body.token.slice(0, 12));
191
192 // Audit row written under the expected action name.
193 const [audit] = await db
194 .select()
195 .from(auditLog)
196 .where(
197 and(
198 eq(auditLog.userId, user.id),
199 eq(auditLog.action, "auth.install_token.created")
200 )
201 )
202 .orderBy(desc(auditLog.createdAt))
203 .limit(1);
204 expect(audit).toBeDefined();
205 expect(audit!.targetType).toBe("api_token");
206 expect(audit!.targetId).toBe(body.id);
207 } finally {
208 // Best-effort cleanup. Cascade on users covers sessions + tokens.
209 try {
210 await db.delete(users).where(eq(users.id, user.id));
211 } catch {
212 /* ignore */
213 }
214 }
215 }
216 );
217
218 it.skipIf(!HAS_DB)(
219 "defaults name + scope when body is empty",
220 async () => {
221 const { db } = await import("../db");
222 const { users, sessions, apiTokens } = await import("../db/schema");
223 const { eq } = await import("drizzle-orm");
224
225 const uname = "install-test2-" + Math.random().toString(36).slice(2, 10);
226 const [user] = await db
227 .insert(users)
228 .values({
229 username: uname,
230 email: `${uname}@example.com`,
231 passwordHash: "x",
232 })
233 .returning();
234
235 const sessionToken =
236 "sess_test_" + Math.random().toString(36).slice(2) + Date.now();
237 await db.insert(sessions).values({
238 userId: user.id,
239 token: sessionToken,
240 expiresAt: new Date(Date.now() + 60_000),
241 });
242
243 try {
244 const res = await app.request("/api/v2/auth/install-token", {
245 method: "POST",
246 headers: {
247 "content-type": "application/json",
248 cookie: `session=${sessionToken}`,
249 },
250 body: "",
251 });
252 expect(res.status).toBe(201);
253 const body = await res.json();
254 expect(body.scope).toBe("admin");
255 expect(typeof body.name).toBe("string");
256 expect(body.name.startsWith("gluecron-install-")).toBe(true);
257
258 const [row] = await db
259 .select()
260 .from(apiTokens)
261 .where(eq(apiTokens.id, body.id))
262 .limit(1);
263 expect(row).toBeDefined();
264 } finally {
265 try {
266 await db.delete(users).where(eq(users.id, user.id));
267 } catch {
268 /* ignore */
269 }
270 }
271 }
272 );
273});