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

server-targets-crypto.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.

server-targets-crypto.test.tsBlame135 lines · 1 contributor
783dd46Claude1/**
2 * Pure-function tests for src/lib/server-targets-crypto.ts.
3 *
4 * Mirror of the workflow-secrets-crypto suite: round-trip, IV randomness,
5 * tamper detection, env-name validation, dotenv rendering.
6 */
7
8import { describe, it, expect, afterEach } from "bun:test";
9import {
10 encryptValue,
11 decryptValue,
12 getMasterKey,
13 isValidEnvName,
14 renderDotenv,
15} from "../lib/server-targets-crypto";
16
17const TEST_KEY =
18 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
19const original = process.env.SERVER_TARGETS_KEY;
20
21afterEach(() => {
22 if (original === undefined) delete process.env.SERVER_TARGETS_KEY;
23 else process.env.SERVER_TARGETS_KEY = original;
24});
25
26describe("getMasterKey", () => {
27 it("returns null when env is unset", () => {
28 delete process.env.SERVER_TARGETS_KEY;
29 expect(getMasterKey()).toBeNull();
30 });
31
32 it("returns null when env is not 32 bytes", () => {
33 process.env.SERVER_TARGETS_KEY = "abcd";
34 expect(getMasterKey()).toBeNull();
35 });
36
37 it("returns 32-byte buffer when env is valid hex", () => {
38 process.env.SERVER_TARGETS_KEY = TEST_KEY;
39 const key = getMasterKey();
40 expect(key).not.toBeNull();
41 expect(key!.length).toBe(32);
42 });
43
44 it("ignores non-hex input", () => {
45 process.env.SERVER_TARGETS_KEY = "zzzz".repeat(16);
46 expect(getMasterKey()).toBeNull();
47 });
48});
49
50describe("encrypt/decrypt round-trip", () => {
51 it("recovers the original plaintext", () => {
52 process.env.SERVER_TARGETS_KEY = TEST_KEY;
53 const enc = encryptValue("hello-server-target");
54 expect(enc.ok).toBe(true);
55 if (!enc.ok) return;
56 const dec = decryptValue(enc.ciphertext);
57 expect(dec.ok).toBe(true);
58 if (!dec.ok) return;
59 expect(dec.plaintext).toBe("hello-server-target");
60 });
61
62 it("uses a fresh IV per encryption so identical plaintexts diverge", () => {
63 process.env.SERVER_TARGETS_KEY = TEST_KEY;
64 const a = encryptValue("same");
65 const b = encryptValue("same");
66 expect(a.ok && b.ok).toBe(true);
67 if (!a.ok || !b.ok) return;
68 expect(a.ciphertext).not.toBe(b.ciphertext);
69 });
70
71 it("rejects encrypt when key missing", () => {
72 delete process.env.SERVER_TARGETS_KEY;
73 const enc = encryptValue("anything");
74 expect(enc.ok).toBe(false);
75 });
76
77 it("detects tampered ciphertext via GCM auth tag", () => {
78 process.env.SERVER_TARGETS_KEY = TEST_KEY;
79 const enc = encryptValue("payload");
80 if (!enc.ok) throw new Error("setup failed");
81 // Flip a byte inside the ciphertext portion (after IV+tag) — base64
82 // decode → mutate → re-encode.
83 const buf = Buffer.from(enc.ciphertext, "base64");
84 buf[buf.length - 1] ^= 0xff;
85 const tampered = buf.toString("base64");
86 const dec = decryptValue(tampered);
87 expect(dec.ok).toBe(false);
88 });
89
90 it("rejects too-short blob", () => {
91 process.env.SERVER_TARGETS_KEY = TEST_KEY;
92 const dec = decryptValue(Buffer.from("short").toString("base64"));
93 expect(dec.ok).toBe(false);
94 });
95});
96
97describe("isValidEnvName", () => {
98 it("accepts conventional env names", () => {
99 expect(isValidEnvName("FOO")).toBe(true);
100 expect(isValidEnvName("FOO_BAR")).toBe(true);
101 expect(isValidEnvName("_PRIVATE")).toBe(true);
102 expect(isValidEnvName("A1_B2")).toBe(true);
103 });
104
105 it("rejects lowercase, dashes, leading digits, empty", () => {
106 expect(isValidEnvName("foo")).toBe(false);
107 expect(isValidEnvName("FOO-BAR")).toBe(false);
108 expect(isValidEnvName("1FOO")).toBe(false);
109 expect(isValidEnvName("")).toBe(false);
110 expect(isValidEnvName(undefined)).toBe(false);
111 expect(isValidEnvName(null)).toBe(false);
112 });
113});
114
115describe("renderDotenv", () => {
116 it("renders alphabetised KEY='value' lines", () => {
117 const out = renderDotenv({ BAR: "two", FOO: "one" });
118 expect(out).toBe("BAR='two'\nFOO='one'\n");
119 });
120
121 it("escapes embedded single quotes", () => {
122 const out = renderDotenv({ FOO: "a'b" });
123 // a'b → 'a'\''b' — POSIX single-quote escape.
124 expect(out).toBe("FOO='a'\\''b'\n");
125 });
126
127 it("returns empty string for empty map", () => {
128 expect(renderDotenv({})).toBe("");
129 });
130
131 it("never leaves a value unquoted", () => {
132 const out = renderDotenv({ KEY: "has spaces and $vars" });
133 expect(out).toBe("KEY='has spaces and $vars'\n");
134 });
135});