Skip to content

Commit 12e4884

Browse files
author
Continue Agent
committed
test: add comprehensive tests for CLIPlatformClient secret resolution
Add tests covering: - Fallback to local sources when Hub API returns not found - Fallback to local sources when Hub API call fails - Proper handling of API results with fewer elements than requested - Correct SecretType.ProcessEnv usage for process.env secrets - Empty string handling for environment variables - Edge cases with null/undefined API results Co-authored-by: nate <[email protected]> Generated with Continue (https://continue.dev)
1 parent a699f29 commit 12e4884

File tree

1 file changed

+302
-0
lines changed

1 file changed

+302
-0
lines changed
Lines changed: 302 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
import * as fs from "node:fs";
2+
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
3+
4+
import {
5+
FQSN,
6+
SecretResult,
7+
SecretType,
8+
} from "@continuedev/config-yaml";
9+
import { DefaultApiInterface } from "@continuedev/sdk/dist/api";
10+
11+
import { CLIPlatformClient } from "./CLIPlatformClient.js";
12+
13+
// Mock dependencies
14+
vi.mock("node:fs");
15+
vi.mock("./env.js", () => ({
16+
env: {
17+
continueHome: "/home/user/.continue",
18+
},
19+
}));
20+
21+
describe("CLIPlatformClient", () => {
22+
let mockApiClient: DefaultApiInterface;
23+
let platformClient: CLIPlatformClient;
24+
const originalEnv = process.env;
25+
26+
beforeEach(() => {
27+
vi.clearAllMocks();
28+
process.env = { ...originalEnv };
29+
30+
mockApiClient = {
31+
syncSecrets: vi.fn(),
32+
} as unknown as DefaultApiInterface;
33+
34+
platformClient = new CLIPlatformClient("test-org-id", mockApiClient);
35+
});
36+
37+
afterEach(() => {
38+
process.env = originalEnv;
39+
vi.restoreAllMocks();
40+
});
41+
42+
describe("resolveFQSNs - local fallback behavior", () => {
43+
test("should fallback to process.env when API returns not found", async () => {
44+
const fqsns: FQSN[] = [
45+
{ secretName: "ANTHROPIC_API_KEY", ownerSlug: null, packageSlug: null },
46+
];
47+
48+
// Set the secret in process.env
49+
process.env.ANTHROPIC_API_KEY = "sk-ant-test-key";
50+
51+
// Mock API returning found: false
52+
vi.mocked(mockApiClient.syncSecrets).mockResolvedValue([
53+
{ found: false, fqsn: fqsns[0] },
54+
]);
55+
56+
const results = await platformClient.resolveFQSNs(fqsns);
57+
58+
expect(results).toHaveLength(1);
59+
expect(results[0]).toEqual({
60+
found: true,
61+
fqsn: fqsns[0],
62+
value: "sk-ant-test-key",
63+
secretLocation: {
64+
secretName: "ANTHROPIC_API_KEY",
65+
secretType: SecretType.ProcessEnv,
66+
},
67+
});
68+
});
69+
70+
test("should fallback to local sources when API call fails", async () => {
71+
const fqsns: FQSN[] = [
72+
{ secretName: "OPENAI_API_KEY", ownerSlug: null, packageSlug: null },
73+
];
74+
75+
process.env.OPENAI_API_KEY = "sk-test-key";
76+
77+
// Mock API throwing an error
78+
vi.mocked(mockApiClient.syncSecrets).mockRejectedValue(
79+
new Error("API connection failed")
80+
);
81+
82+
const consoleWarnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
83+
84+
const results = await platformClient.resolveFQSNs(fqsns);
85+
86+
expect(results).toHaveLength(1);
87+
expect(results[0]).toEqual({
88+
found: true,
89+
fqsn: fqsns[0],
90+
value: "sk-test-key",
91+
secretLocation: {
92+
secretName: "OPENAI_API_KEY",
93+
secretType: SecretType.ProcessEnv,
94+
},
95+
});
96+
97+
expect(consoleWarnSpy).toHaveBeenCalledWith(
98+
expect.stringContaining("Error resolving FQSNs through API")
99+
);
100+
101+
consoleWarnSpy.mockRestore();
102+
});
103+
104+
test("should use API result when found is true", async () => {
105+
const fqsns: FQSN[] = [
106+
{ secretName: "HUB_SECRET", ownerSlug: "test", packageSlug: "pkg" },
107+
];
108+
109+
// Set a different value in process.env
110+
process.env.HUB_SECRET = "local-value";
111+
112+
// Mock API returning found: true
113+
vi.mocked(mockApiClient.syncSecrets).mockResolvedValue([
114+
{
115+
found: true,
116+
fqsn: fqsns[0],
117+
value: "hub-value",
118+
secretLocation: {
119+
secretName: "HUB_SECRET",
120+
secretType: SecretType.Organization,
121+
},
122+
},
123+
]);
124+
125+
const results = await platformClient.resolveFQSNs(fqsns);
126+
127+
expect(results).toHaveLength(1);
128+
expect(results[0]?.value).toBe("hub-value");
129+
expect(results[0]?.secretLocation.secretType).toBe(SecretType.Organization);
130+
});
131+
132+
test("should handle API returning fewer results than requested", async () => {
133+
const fqsns: FQSN[] = [
134+
{ secretName: "SECRET_1", ownerSlug: null, packageSlug: null },
135+
{ secretName: "SECRET_2", ownerSlug: null, packageSlug: null },
136+
];
137+
138+
process.env.SECRET_1 = "value-1";
139+
process.env.SECRET_2 = "value-2";
140+
141+
// API only returns result for first secret
142+
vi.mocked(mockApiClient.syncSecrets).mockResolvedValue([
143+
{ found: false, fqsn: fqsns[0] },
144+
]);
145+
146+
const results = await platformClient.resolveFQSNs(fqsns);
147+
148+
expect(results).toHaveLength(2);
149+
expect(results[0]?.value).toBe("value-1");
150+
expect(results[1]?.value).toBe("value-2");
151+
});
152+
153+
test("should handle empty API results array", async () => {
154+
const fqsns: FQSN[] = [
155+
{ secretName: "MY_SECRET", ownerSlug: null, packageSlug: null },
156+
];
157+
158+
process.env.MY_SECRET = "local-value";
159+
160+
// API returns empty array
161+
vi.mocked(mockApiClient.syncSecrets).mockResolvedValue([]);
162+
163+
const results = await platformClient.resolveFQSNs(fqsns);
164+
165+
expect(results).toHaveLength(1);
166+
expect(results[0]?.value).toBe("local-value");
167+
});
168+
169+
test("should fallback to .env files when process.env doesn't have the secret", async () => {
170+
const fqsns: FQSN[] = [
171+
{ secretName: "FILE_SECRET", ownerSlug: null, packageSlug: null },
172+
];
173+
174+
// Mock API returning not found
175+
vi.mocked(mockApiClient.syncSecrets).mockResolvedValue([
176+
{ found: false, fqsn: fqsns[0] },
177+
]);
178+
179+
// Mock fs operations for .env file
180+
vi.mocked(fs.existsSync).mockReturnValue(true);
181+
vi.mocked(fs.readFileSync).mockReturnValue("FILE_SECRET=from-env-file\n");
182+
183+
const results = await platformClient.resolveFQSNs(fqsns);
184+
185+
expect(results).toHaveLength(1);
186+
expect(results[0]?.found).toBe(true);
187+
expect(results[0]?.value).toBe("from-env-file");
188+
expect(results[0]?.secretLocation.secretType).toBe(SecretType.LocalEnv);
189+
});
190+
191+
test("should handle mix of API found and local fallback", async () => {
192+
const fqsns: FQSN[] = [
193+
{ secretName: "HUB_SECRET", ownerSlug: "test", packageSlug: "pkg" },
194+
{ secretName: "LOCAL_SECRET", ownerSlug: null, packageSlug: null },
195+
];
196+
197+
process.env.LOCAL_SECRET = "local-value";
198+
199+
// API finds first but not second
200+
vi.mocked(mockApiClient.syncSecrets).mockResolvedValue([
201+
{
202+
found: true,
203+
fqsn: fqsns[0],
204+
value: "hub-value",
205+
secretLocation: {
206+
secretName: "HUB_SECRET",
207+
secretType: SecretType.Organization,
208+
},
209+
},
210+
{ found: false, fqsn: fqsns[1] },
211+
]);
212+
213+
const results = await platformClient.resolveFQSNs(fqsns);
214+
215+
expect(results).toHaveLength(2);
216+
expect(results[0]?.value).toBe("hub-value");
217+
expect(results[0]?.secretLocation.secretType).toBe(SecretType.Organization);
218+
expect(results[1]?.value).toBe("local-value");
219+
expect(results[1]?.secretLocation.secretType).toBe(SecretType.ProcessEnv);
220+
});
221+
});
222+
223+
describe("findSecretInProcessEnv - correct secret type", () => {
224+
test("should use SecretType.ProcessEnv for process.env secrets", async () => {
225+
const fqsn: FQSN = {
226+
secretName: "TEST_KEY",
227+
ownerSlug: null,
228+
packageSlug: null,
229+
};
230+
231+
process.env.TEST_KEY = "test-value";
232+
233+
vi.mocked(mockApiClient.syncSecrets).mockResolvedValue([
234+
{ found: false, fqsn },
235+
]);
236+
237+
const results = await platformClient.resolveFQSNs([fqsn]);
238+
239+
expect(results[0]?.secretLocation.secretType).toBe(SecretType.ProcessEnv);
240+
});
241+
242+
test("should handle empty string values from process.env", async () => {
243+
const fqsn: FQSN = {
244+
secretName: "EMPTY_KEY",
245+
ownerSlug: null,
246+
packageSlug: null,
247+
};
248+
249+
process.env.EMPTY_KEY = "";
250+
251+
vi.mocked(mockApiClient.syncSecrets).mockResolvedValue([
252+
{ found: false, fqsn },
253+
]);
254+
255+
const results = await platformClient.resolveFQSNs([fqsn]);
256+
257+
expect(results[0]?.found).toBe(true);
258+
expect(results[0]?.value).toBe("");
259+
});
260+
});
261+
262+
describe("edge cases", () => {
263+
test("should return empty array for empty input", async () => {
264+
const results = await platformClient.resolveFQSNs([]);
265+
266+
expect(results).toEqual([]);
267+
expect(mockApiClient.syncSecrets).not.toHaveBeenCalled();
268+
});
269+
270+
test("should handle undefined secret that doesn't exist anywhere", async () => {
271+
const fqsns: FQSN[] = [
272+
{ secretName: "NONEXISTENT", ownerSlug: null, packageSlug: null },
273+
];
274+
275+
vi.mocked(mockApiClient.syncSecrets).mockResolvedValue([
276+
{ found: false, fqsn: fqsns[0] },
277+
]);
278+
279+
vi.mocked(fs.existsSync).mockReturnValue(false);
280+
281+
const results = await platformClient.resolveFQSNs(fqsns);
282+
283+
expect(results).toHaveLength(1);
284+
expect(results[0]).toBeUndefined();
285+
});
286+
287+
test("should handle null API results", async () => {
288+
const fqsns: FQSN[] = [
289+
{ secretName: "TEST", ownerSlug: null, packageSlug: null },
290+
];
291+
292+
process.env.TEST = "fallback-value";
293+
294+
// API returns array with null element
295+
vi.mocked(mockApiClient.syncSecrets).mockResolvedValue([null]);
296+
297+
const results = await platformClient.resolveFQSNs(fqsns);
298+
299+
expect(results[0]?.value).toBe("fallback-value");
300+
});
301+
});
302+
});

0 commit comments

Comments
 (0)