206 lines
7.8 KiB
TypeScript
206 lines
7.8 KiB
TypeScript
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);
|
|
});
|
|
});
|