import { beforeEach, describe, expect, it, mock } from "bun:test"; // ── Mock the auth module ──────────────────────────────────────────────────── const mockResolveToken = mock(); const mockResolveAllIdentities = mock(); mock.module("../../../src/providers/gcp/auth", () => ({ resolveToken: mockResolveToken, resolveAllIdentities: mockResolveAllIdentities, })); // ── Mock the client module ────────────────────────────────────────────────── const mockListSecrets = mock(); const mockAccessSecretVersion = mock(); const mockListProjects = mock(); const mockExtractSecretShortName = mock(); const mockBuildSecretName = mock(); mock.module("../../../src/providers/gcp/client", () => ({ listSecrets: mockListSecrets, accessSecretVersion: mockAccessSecretVersion, listProjects: mockListProjects, extractSecretShortName: mockExtractSecretShortName, buildSecretName: mockBuildSecretName, })); import { GcpSecretsService } from "../../../src/providers/gcp/secrets"; // ── Helpers ───────────────────────────────────────────────────────────────── function setupAuth() { mockResolveToken.mockResolvedValue({ token: "fake-gcp-token", expiresOn: Date.now() / 1000 + 3600, projectId: "test-project", }); } function makeSecretItem(name: string) { return { name, createTime: "2024-01-01T00:00:00Z" }; } // ── Tests ─────────────────────────────────────────────────────────────────── describe("GcpSecretsService", () => { let service: GcpSecretsService; beforeEach(() => { mockResolveToken.mockReset(); mockResolveAllIdentities.mockReset(); mockListSecrets.mockReset(); mockAccessSecretVersion.mockReset(); mockListProjects.mockReset(); mockExtractSecretShortName.mockReset(); mockBuildSecretName.mockReset(); // Default: short name extraction passes through the name mockExtractSecretShortName.mockImplementation( (name: string) => name.split("/").pop() ?? name, ); delete process.env.GCP_PROJECT; delete process.env.GOOGLE_CLOUD_PROJECT; service = new GcpSecretsService(); }); // ── Auth failure ────────────────────────────────────────────────────── it("returns failure when authentication fails", async () => { mockResolveToken.mockRejectedValue(new Error("No GCP credentials")); const result = await service.execute(); expect(result.success).toBe(true); const data = result.data as any; expect(data.ok).toBe(false); expect(data.authError).toBeArray(); expect(data.authError[0]).toContain("GCP auth failed"); }); // ── No projects found ───────────────────────────────────────────────── it("returns failure when no projects found", async () => { setupAuth(); mockListProjects.mockResolvedValue([]); const result = await service.execute(); expect(result.success).toBe(true); const data = result.data as any; expect(data.ok).toBe(false); expect(data.projectsFound).toBe(0); expect(data.dumped).toBe(0); }); // ── Env var project discovery ───────────────────────────────────────── it("discovers project from GCP_PROJECT env var", async () => { setupAuth(); process.env.GCP_PROJECT = "my-project"; mockListSecrets.mockResolvedValue([ makeSecretItem("projects/my-project/secrets/db-pass"), ]); mockAccessSecretVersion.mockResolvedValue("s3cr3t"); const result = await service.execute(); expect(result.success).toBe(true); const data = result.data as any; expect(data.projects).toHaveLength(1); expect(data.projects[0].projectId).toBe("my-project"); expect(data.projects[0].secrets["db-pass"]).toBe("s3cr3t"); }); // ── Single project, single secret ───────────────────────────────────── it("returns success with single project and secret", async () => { setupAuth(); process.env.GCP_PROJECT = "proj-1"; mockListSecrets.mockResolvedValue([ makeSecretItem("projects/proj-1/secrets/api-key"), ]); mockAccessSecretVersion.mockResolvedValue("sk-abc"); const result = await service.execute(); expect(result.success).toBe(true); const data = result.data as any; expect(data.projects).toHaveLength(1); expect(data.projects[0].secrets["api-key"]).toBe("sk-abc"); }); // ── Multiple secrets in one project ─────────────────────────────────── it("returns success with multiple secrets", async () => { setupAuth(); process.env.GCP_PROJECT = "proj-1"; mockListSecrets.mockResolvedValue([ makeSecretItem("projects/proj-1/secrets/s1"), makeSecretItem("projects/proj-1/secrets/s2"), makeSecretItem("projects/proj-1/secrets/s3"), ]); mockAccessSecretVersion.mockImplementation((name: string) => { if (name.includes("/s1")) return Promise.resolve("val1"); if (name.includes("/s2")) return Promise.resolve("val2"); if (name.includes("/s3")) return Promise.resolve("val3"); return Promise.resolve(undefined); }); const result = await service.execute(); expect(result.success).toBe(true); const secrets = (result.data as any).projects[0].secrets; expect(Object.keys(secrets)).toHaveLength(3); }); // ── Empty project (no secrets) ──────────────────────────────────────── it("handles project with no secrets", async () => { setupAuth(); process.env.GCP_PROJECT = "empty-proj"; mockListSecrets.mockResolvedValue([]); const result = await service.execute(); expect(result.success).toBe(true); const data = result.data as any; expect(data.projects).toHaveLength(1); expect(data.projects[0].secrets).toEqual({}); }); // ── All projects fail ───────────────────────────────────────────────── it("returns failure when all projects fail", async () => { setupAuth(); process.env.GCP_PROJECT = "bad-project"; mockListSecrets.mockRejectedValue(new Error("PermissionDenied")); const result = await service.execute(); expect(result.success).toBe(true); const data = result.data as any; expect(data.ok).toBe(false); expect(data.dumped).toBe(0); expect(data.errored).toBe(1); }); // ── Multiple projects via ARM ───────────────────────────────────────── it("discovers projects via Cloud Resource Manager", async () => { setupAuth(); mockListProjects.mockResolvedValue([ { projectId: "p1", name: "Project One", lifecycleState: "ACTIVE" }, { projectId: "p2", name: "Project Two", lifecycleState: "ACTIVE" }, ]); mockListSecrets.mockResolvedValue([ makeSecretItem("projects/p1/secrets/s1"), ]); mockAccessSecretVersion.mockResolvedValue("v1"); const result = await service.execute(); expect(result.success).toBe(true); const data = result.data as any; expect(data.projects).toHaveLength(2); }); });