diff --git a/apps/dokploy/__test__/env/vault.test.ts b/apps/dokploy/__test__/env/vault.test.ts index ea57b6800..871fe5c57 100644 --- a/apps/dokploy/__test__/env/vault.test.ts +++ b/apps/dokploy/__test__/env/vault.test.ts @@ -20,6 +20,7 @@ import { import { azureClient } from "@dokploy/server/utils/vault/azure"; import { dopplerClient } from "@dokploy/server/utils/vault/doppler"; import { hashicorpClient } from "@dokploy/server/utils/vault/hashicorp"; +import { infisicalClient } from "@dokploy/server/utils/vault/infisical"; import { phaseClient } from "@dokploy/server/utils/vault/phase"; import { scalewayClient } from "@dokploy/server/utils/vault/scaleway"; @@ -431,6 +432,56 @@ describe("azure client", () => { }); }); +describe("infisical client", () => { + const config = { + providerType: "infisical" as const, + siteUrl: "https://app.infisical.com", + clientId: "client-1", + clientSecret: "client-secret", + projectId: "workspace-1", + environmentSlug: "prod", + secretPath: "/frontend", + }; + + const loginResponse = () => jsonResponse({ accessToken: "token-1" }); + + it("asks the list endpoint to expand secret references", async () => { + mockFetch.mockResolvedValueOnce(loginResponse()).mockResolvedValueOnce( + jsonResponse({ + secrets: [{ secretKey: "DB_URL", secretValue: "postgres://real" }], + }), + ); + + const result = await infisicalClient.getSecrets(config, ["DB_URL"]); + + expect(result).toEqual({ DB_URL: "postgres://real" }); + const [listUrl] = mockFetch.mock.calls[1] as [string]; + const params = new URL(listUrl).searchParams; + expect(params.get("expandSecretReferences")).toBe("true"); + expect(params.get("workspaceId")).toBe("workspace-1"); + expect(params.get("environment")).toBe("prod"); + expect(params.get("secretPath")).toBe("/frontend"); + }); + + it("throws a clear error for a missing secret", async () => { + mockFetch + .mockResolvedValueOnce(loginResponse()) + .mockResolvedValueOnce(jsonResponse({ secrets: [] })); + + await expect( + infisicalClient.getSecrets(config, ["ABSENT"]), + ).rejects.toThrow('secret "ABSENT" not found in environment "prod"'); + }); + + it("propagates authentication failures with the status code", async () => { + mockFetch.mockResolvedValueOnce(jsonResponse({}, false, 401)); + + await expect( + infisicalClient.getSecrets(config, ["DB_URL"]), + ).rejects.toThrow("authentication failed (status 401)"); + }); +}); + describe("doppler client", () => { it("propagates auth errors with the status code", async () => { mockFetch.mockResolvedValue(jsonResponse({}, false, 401)); diff --git a/packages/server/src/utils/vault/infisical.ts b/packages/server/src/utils/vault/infisical.ts index a32852c05..ad3339ff7 100644 --- a/packages/server/src/utils/vault/infisical.ts +++ b/packages/server/src/utils/vault/infisical.ts @@ -38,6 +38,12 @@ const fetchSecrets = async (config: InfisicalConfig) => { workspaceId: config.projectId, environment: config.environmentSlug, secretPath: config.secretPath, + // Infisical's list endpoint leaves secret references (`${env.folder.KEY}`) + // unexpanded unless asked, so without this a referencing secret arrives as + // the literal `${...}` string, lands in the generated .env and the deploy + // still reports success. Single secrets read via /raw/{name} expand by + // default, which makes the difference easy to miss in the UI. + expandSecretReferences: "true", }); const response = await vaultFetch( `${baseUrl(config)}/api/v3/secrets/raw?${params.toString()}`,