import { afterEach, describe, expect, it, vi } from "vitest"; import { ZodError } from "zod"; import { RecordingAuditLog } from "@repo/core-testing/instrumentation"; import { connectWorkspaceUseCase } from "@/application/use-cases/connect-workspace.use-case"; import { MockWorkspaceRepository } from "@/infrastructure/repositories/workspace.repository.mock"; const INPUT = { name: "Acme Web", gitUrl: "https://github.com/acme/web.git", pat: "ghp_super-secret-token", }; describe("connectWorkspaceUseCase", () => { it("creates a workspace with initial status 'created'", async () => { const repo = new MockWorkspaceRepository(new Map()); const useCase = connectWorkspaceUseCase(repo); const result = await useCase(INPUT); expect(result.id).not.toBe(""); expect(result.name).toBe("Acme Web"); expect(result.gitUrl).toBe("https://github.com/acme/web.git"); expect(result.status).toBe("created"); }); it("persists the workspace — it is readable back through the repository", async () => { const repo = new MockWorkspaceRepository(new Map()); const useCase = connectWorkspaceUseCase(repo); const created = await useCase(INPUT); const found = await repo.getWorkspace(created.id); expect(found).toEqual(created); }); it("never includes the credential in the output", async () => { const repo = new MockWorkspaceRepository(new Map()); const useCase = connectWorkspaceUseCase(repo); const result = await useCase(INPUT); expect(result).not.toHaveProperty("pat"); expect(result).not.toHaveProperty("credential"); expect(JSON.stringify(result)).not.toContain(INPUT.pat); }); it("strips a credential leaked by a misbehaving repository", async () => { const leakyRepo = { getWorkspace: async () => null, getDecryptedCredential: async () => null, listWorkspaces: async () => [], createWorkspace: async () => ({ id: "ws-1", name: "Acme Web", gitUrl: "https://github.com/acme/web.git", status: "created" as const, credential: "ghp_super-secret-token", }), }; const useCase = connectWorkspaceUseCase(leakyRepo); const result = await useCase(INPUT); expect(result).not.toHaveProperty("credential"); }); it("records the workspace-connected audit event", async () => { const repo = new MockWorkspaceRepository(new Map()); const auditLog = new RecordingAuditLog(); const useCase = connectWorkspaceUseCase(repo, auditLog); const created = await useCase(INPUT); expect(auditLog.recorded).toHaveLength(1); const entry = auditLog.recorded[0]!; expect(entry.action).toBe("CREATE"); expect(entry.reason).toBe("workspace-connected"); expect(entry.resource).toEqual({ type: "workspaces", id: created.id }); expect(entry.outcome).toBe("success"); expect(entry.containsPii).toBe(false); expect(entry.scope.feature).toBe("workspaces"); }); describe("audit scope environment", () => { afterEach(() => { vi.unstubAllEnvs(); }); it("falls back to 'development' when NODE_ENV is unset", async () => { vi.stubEnv("NODE_ENV", undefined); const repo = new MockWorkspaceRepository(new Map()); const auditLog = new RecordingAuditLog(); const useCase = connectWorkspaceUseCase(repo, auditLog); await useCase(INPUT); expect(auditLog.recorded[0]!.scope.environment).toBe("development"); }); }); it("does not audit when the repository write fails", async () => { const failingRepo = { getWorkspace: async () => null, getDecryptedCredential: async () => null, listWorkspaces: async () => [], createWorkspace: async () => { throw new Error("boom"); }, }; const auditLog = new RecordingAuditLog(); const useCase = connectWorkspaceUseCase(failingRepo, auditLog); await expect(useCase(INPUT)).rejects.toThrow("boom"); expect(auditLog.recorded).toHaveLength(0); }); it("works without an audit log (optional dependency)", async () => { const repo = new MockWorkspaceRepository(new Map()); const useCase = connectWorkspaceUseCase(repo, undefined); await expect(useCase(INPUT)).resolves.toMatchObject({ status: "created" }); }); it("throws ZodError when the repository returns malformed data", async () => { const malformedRepo = { getWorkspace: async () => null, getDecryptedCredential: async () => null, listWorkspaces: async () => [], createWorkspace: async () => ({ id: "", name: "x" }) as never, }; const useCase = connectWorkspaceUseCase(malformedRepo); await expect(useCase(INPUT)).rejects.toBeInstanceOf(ZodError); }); });