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.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.test.tsBlame176 lines · 1 contributor
783dd46Claude1/**
2 * Driver-level tests for src/lib/server-targets.ts.
3 *
4 * We never actually shell out — `__setSpawnForTests` lets us inspect the
5 * command line the driver builds and feed back canned exit codes. Covers:
6 * - testConnection happy path (returns SHA256 fingerprint)
7 * - deployToTarget happy path (env file is materialised + sourced)
8 * - host-key fingerprint mismatch aborts the deploy
9 * - missing SERVER_TARGETS_KEY collapses to ok:false (no throw)
10 */
11
12import { describe, it, expect, afterEach, beforeEach } from "bun:test";
13import {
14 __setSpawnForTests,
15 deployToTarget,
16 testConnection,
17 type SpawnResult,
18} from "../lib/server-targets";
19import { encryptValue } from "../lib/server-targets-crypto";
20import type { ServerTarget } from "../db/schema";
21
22const TEST_KEY =
23 "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
24const HOST_FP = "SHA256:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
25
26const originalKey = process.env.SERVER_TARGETS_KEY;
27
28beforeEach(() => {
29 process.env.SERVER_TARGETS_KEY = TEST_KEY;
30});
31
32afterEach(() => {
33 __setSpawnForTests(null);
34 if (originalKey === undefined) delete process.env.SERVER_TARGETS_KEY;
35 else process.env.SERVER_TARGETS_KEY = originalKey;
36});
37
38function buildTarget(overrides: Partial<ServerTarget> = {}): ServerTarget {
39 const enc = encryptValue("-----BEGIN OPENSSH PRIVATE KEY-----\nfake\n-----END OPENSSH PRIVATE KEY-----\n");
40 if (!enc.ok) throw new Error("encrypt failed in test setup: " + enc.error);
41 return {
42 id: "00000000-0000-0000-0000-000000000001",
43 name: "test-box",
44 host: "192.0.2.10",
45 port: 22,
46 sshUser: "deploy",
47 encryptedPrivateKey: enc.ciphertext,
48 hostFingerprint: null,
49 deployPath: "/var/www/app",
50 deployScript: "echo deployed",
51 watchedRepositoryId: null,
52 watchedBranch: null,
53 status: "unverified",
54 lastSeenAt: null,
55 createdBy: null,
56 createdAt: new Date(),
57 updatedAt: new Date(),
58 ...overrides,
59 } as ServerTarget;
60}
61
62describe("testConnection", () => {
63 it("returns the fingerprint and ok=true on a clean run", async () => {
64 const calls: Array<string[]> = [];
65 __setSpawnForTests(async (cmd) => {
66 calls.push(cmd);
67 const [bin] = cmd;
68 if (bin === "ssh-keyscan") {
69 return ok("192.0.2.10 ssh-ed25519 AAAA...\n");
70 }
71 if (bin === "ssh-keygen") {
72 return ok(`256 ${HOST_FP} root@box (ED25519)\n`);
73 }
74 if (bin === "ssh") {
75 return ok("gluecron-ok\n");
76 }
77 return fail("unexpected cmd: " + cmd.join(" "));
78 });
79
80 const result = await testConnection(buildTarget());
81 expect(result.ok).toBe(true);
82 if (!result.ok) return;
83 expect(result.fingerprint).toBe(HOST_FP);
84 expect(calls.length).toBe(3);
85 expect(calls[0][0]).toBe("ssh-keyscan");
86 expect(calls[2][0]).toBe("ssh");
87 });
88
89 it("returns ok=false at the scan stage when ssh-keyscan fails", async () => {
90 __setSpawnForTests(async (cmd) => {
91 if (cmd[0] === "ssh-keyscan") return fail("Connection refused");
92 return ok("");
93 });
94 const result = await testConnection(buildTarget());
95 expect(result.ok).toBe(false);
96 if (result.ok) return;
97 expect(result.stage).toBe("scan");
98 });
99
100 it("returns ok=false at auth stage when ssh probe fails", async () => {
101 __setSpawnForTests(async (cmd) => {
102 if (cmd[0] === "ssh-keyscan") return ok("192.0.2.10 ssh-ed25519 AAAA\n");
103 if (cmd[0] === "ssh-keygen") return ok(`256 ${HOST_FP} (ED25519)\n`);
104 if (cmd[0] === "ssh") return fail("Permission denied (publickey)");
105 return fail("?");
106 });
107 const result = await testConnection(buildTarget());
108 expect(result.ok).toBe(false);
109 if (result.ok) return;
110 expect(result.stage).toBe("auth");
111 });
112});
113
114describe("deployToTarget", () => {
115 it("scp's the env file then ssh-runs the deploy script", async () => {
116 const calls: Array<string[]> = [];
117 __setSpawnForTests(async (cmd) => {
118 calls.push(cmd);
119 if (cmd[0] === "scp") return ok("");
120 if (cmd[0] === "ssh") return ok("done\n");
121 return fail("?");
122 });
123 const result = await deployToTarget(buildTarget(), {
124 env: { FOO: "1", BAR: "two" },
125 commitSha: "abc1234",
126 ref: "refs/heads/main",
127 });
128 expect(result.ok).toBe(true);
129 expect(result.exitCode).toBe(0);
130
131 // Two calls: scp, then ssh.
132 expect(calls.length).toBe(2);
133 expect(calls[0][0]).toBe("scp");
134 // scp target ends at deploy_path/.env.gluecron
135 const scpDst = calls[0][calls[0].length - 1];
136 expect(scpDst).toBe("deploy@192.0.2.10:/var/www/app/.env.gluecron");
137
138 expect(calls[1][0]).toBe("ssh");
139 const remoteCmd = calls[1][calls[1].length - 1];
140 expect(remoteCmd).toContain("cd '/var/www/app'");
141 expect(remoteCmd).toContain(". ./.env.gluecron");
142 expect(remoteCmd).toContain("export GLUECRON_COMMIT_SHA='abc1234'");
143 expect(remoteCmd).toContain("echo deployed");
144 });
145
146 it("aborts deploy when pinned fingerprint doesn't match live", async () => {
147 __setSpawnForTests(async (cmd) => {
148 if (cmd[0] === "ssh-keyscan") return ok("192.0.2.10 ssh-ed25519 AAAA\n");
149 if (cmd[0] === "ssh-keygen") return ok("256 SHA256:DIFFERENT (ED25519)\n");
150 return fail("should not get here");
151 });
152 const result = await deployToTarget(
153 buildTarget({ hostFingerprint: HOST_FP }),
154 { env: {} }
155 );
156 expect(result.ok).toBe(false);
157 expect(result.stderr).toContain("fingerprint mismatch");
158 });
159
160 it("collapses to ok=false when SERVER_TARGETS_KEY is missing", async () => {
161 const target = buildTarget(); // build while key is present so encrypt works
162 delete process.env.SERVER_TARGETS_KEY; // now drop it — decrypt must fail cleanly
163 const result = await deployToTarget(target, { env: {} });
164 expect(result.ok).toBe(false);
165 expect(result.exitCode).toBe(-1);
166 });
167});
168
169// ─── helpers ────────────────────────────────────────────────────────────────
170
171function ok(stdout: string): SpawnResult {
172 return { exitCode: 0, stdout, stderr: "" };
173}
174function fail(stderr: string): SpawnResult {
175 return { exitCode: 1, stdout: "", stderr };
176}