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

env-health.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.

env-health.test.tsBlame167 lines · 1 contributor
bc519aeClaude1/**
2 * Environment / feature health tests (src/lib/env-health.ts).
3 *
4 * Pure-function checks against synthetic env objects — no DB, no real
5 * process.env mutation. Also smoke-tests the /admin/env-health route's
6 * auth gate against the standalone Hono module (the route is mounted in
7 * app.tsx, but testing the module directly keeps this independent of
8 * mount order).
9 */
10
11import { describe, it, expect } from "bun:test";
12import {
13 collectEnvHealth,
14 groupBySeverity,
15 SEVERITY_ORDER,
16 type EnvHealthItem,
17} from "../lib/env-health";
18import envHealthRoutes from "../routes/admin-env-health";
19
20/** A fully-wired synthetic env — every feature should report configured. */
21const FULL_ENV: NodeJS.ProcessEnv = {
22 ANTHROPIC_API_KEY: "sk-ant-secret-aaa",
23 EMAIL_PROVIDER: "resend",
24 RESEND_API_KEY: "re_secret_bbb",
25 APP_BASE_URL: "https://gluecron.com",
26 VOYAGE_API_KEY: "pa-secret-ccc",
27 GATETEST_URL: "https://gatetest.ai/api/events/push",
28 GATETEST_API_KEY: "gt_secret_ddd",
29 GLUECRON_WEBHOOK_SECRET: "whsec_secret_eee",
30 PREVIEW_DOMAIN: "https://previews.gluecron.com",
31 SENTRY_DSN: "https://abc@o1.ingest.sentry.io/1",
32 ERROR_WEBHOOK_URL: "https://hooks.example.com/errors",
33 SSH_HOST_KEY: "-----BEGIN OPENSSH PRIVATE KEY-----secret-fff",
34 REDIS_URL: "redis://:redis-secret-ggg@localhost:6379",
35 GOOGLE_OAUTH_CLIENT_ID: "abc.apps.googleusercontent.com",
36 GOOGLE_OAUTH_CLIENT_SECRET: "google-secret-hhh",
37 AI_AUTO_ISSUES: "1",
38 DEPENDENCY_SCAN_ENABLED: "1",
39};
40
41describe("collectEnvHealth — configured detection", () => {
42 it("reports every feature missing on an empty env", () => {
43 const items = collectEnvHealth({});
44 expect(items.length).toBeGreaterThanOrEqual(12);
45 expect(items.every((i) => i.configured === false)).toBe(true);
46 });
47
48 it("reports every feature configured on a fully-wired env", () => {
49 const items = collectEnvHealth(FULL_ENV);
50 expect(items.every((i) => i.configured === true)).toBe(true);
51 });
52
53 it("email needs EMAIL_PROVIDER=resend, not just the key", () => {
54 const find = (env: NodeJS.ProcessEnv) =>
55 collectEnvHealth(env).find((i) => i.envVars.includes("RESEND_API_KEY"))!;
56 // Key alone: still the dev "log" provider — mail goes to stderr.
57 expect(find({ RESEND_API_KEY: "re_x" }).configured).toBe(false);
58 // Provider alone: nothing to authenticate with.
59 expect(find({ EMAIL_PROVIDER: "resend" }).configured).toBe(false);
60 expect(
61 find({ EMAIL_PROVIDER: "resend", RESEND_API_KEY: "re_x" }).configured
62 ).toBe(true);
63 });
64
65 it("APP_BASE_URL pointing at localhost does not count", () => {
66 const find = (env: NodeJS.ProcessEnv) =>
67 collectEnvHealth(env).find((i) => i.envVars.includes("APP_BASE_URL"))!;
68 expect(find({}).configured).toBe(false);
69 expect(find({ APP_BASE_URL: "http://localhost:3000" }).configured).toBe(
70 false
71 );
72 expect(find({ APP_BASE_URL: "https://gluecron.com" }).configured).toBe(
73 true
74 );
75 });
76
77 it("either-or pairs: REDIS_URL/VALKEY_URL and SENTRY_DSN/ERROR_WEBHOOK_URL", () => {
78 const sse = (env: NodeJS.ProcessEnv) =>
79 collectEnvHealth(env).find((i) => i.envVars.includes("REDIS_URL"))!;
80 expect(sse({ REDIS_URL: "redis://x" }).configured).toBe(true);
81 expect(sse({ VALKEY_URL: "redis://y" }).configured).toBe(true);
82 expect(sse({}).configured).toBe(false);
83
84 const errs = (env: NodeJS.ProcessEnv) =>
85 collectEnvHealth(env).find((i) => i.envVars.includes("SENTRY_DSN"))!;
86 expect(errs({ SENTRY_DSN: "https://x" }).configured).toBe(true);
87 expect(errs({ ERROR_WEBHOOK_URL: "https://y" }).configured).toBe(true);
88 expect(errs({}).configured).toBe(false);
89 });
90
91 it("opt-in flags require the literal \"1\"", () => {
92 const auto = (env: NodeJS.ProcessEnv) =>
93 collectEnvHealth(env).find((i) => i.envVars.includes("AI_AUTO_ISSUES"))!;
94 expect(auto({ AI_AUTO_ISSUES: "1" }).configured).toBe(true);
95 expect(auto({ AI_AUTO_ISSUES: "true" }).configured).toBe(false);
96 expect(auto({ AI_AUTO_ISSUES: "0" }).configured).toBe(false);
97 });
98
99 it("whitespace-only values count as unset", () => {
100 const items = collectEnvHealth({ ANTHROPIC_API_KEY: " " });
101 const ai = items.find((i) => i.envVars.includes("ANTHROPIC_API_KEY"))!;
102 expect(ai.configured).toBe(false);
103 });
104});
105
106describe("collectEnvHealth — shape + severity", () => {
107 it("every item has the full shape and a valid severity", () => {
108 for (const item of collectEnvHealth(FULL_ENV)) {
109 expect(typeof item.feature).toBe("string");
110 expect(item.feature.length).toBeGreaterThan(0);
111 expect(Array.isArray(item.envVars)).toBe(true);
112 expect(item.envVars.length).toBeGreaterThan(0);
113 expect(typeof item.configured).toBe("boolean");
114 expect(typeof item.impact).toBe("string");
115 expect(item.impact.length).toBeGreaterThan(0);
116 expect(SEVERITY_ORDER).toContain(item.severity);
117 }
118 });
119
120 it("groupBySeverity returns critical → recommended → optional, no empties", () => {
121 const groups = groupBySeverity(collectEnvHealth({}));
122 expect(groups.map((g) => g.severity)).toEqual([
123 "critical",
124 "recommended",
125 "optional",
126 ]);
127 for (const g of groups) {
128 expect(g.items.length).toBeGreaterThan(0);
129 expect(g.items.every((i) => i.severity === g.severity)).toBe(true);
130 }
131 });
132
133 it("groupBySeverity drops empty buckets", () => {
134 const only: EnvHealthItem[] = [
135 {
136 feature: "x",
137 envVars: ["X"],
138 configured: false,
139 impact: "y",
140 severity: "optional",
141 },
142 ];
143 const groups = groupBySeverity(only);
144 expect(groups.length).toBe(1);
145 expect(groups[0]!.severity).toBe("optional");
146 });
147});
148
149describe("collectEnvHealth — never leaks values", () => {
150 it("no secret value from the env appears anywhere in the output", () => {
151 const serialized = JSON.stringify(collectEnvHealth(FULL_ENV));
152 // Short non-secret knobs ("1", "resend") legitimately appear in the
153 // impact prose — only assert on values long enough to be secrets/URLs.
154 for (const value of Object.values(FULL_ENV)) {
155 if (!value || value.length < 8) continue;
156 expect(serialized).not.toContain(value);
157 }
158 });
159});
160
161describe("/admin/env-health — auth gate", () => {
162 it("GET without auth → 302 /login", async () => {
163 const res = await envHealthRoutes.request("/admin/env-health");
164 expect(res.status).toBe(302);
165 expect(res.headers.get("location") || "").toContain("/login");
166 });
167});