feat(workspaces): connectWorkspace use case with audit
Manifest-first: connectWorkspace declared mutates:true with the workspace-connected audit event, requiredCores gains audit. Workspace entity gains gitUrl + persisted status enum (created/connecting/ready/ error). Input takes name + git URL + PAT; the output schema is the credential-free workspace entity, so the PAT can never round-trip. Audit emission asserted with RecordingAuditLog; binders wire the use case through wireUseCase with the __audited brand, and web-next bindAll now binds core-audit (payload+stdout sinks in production, stdout in dev-seed) so boot conformance passes in both modes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
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,
|
||||
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,
|
||||
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,
|
||||
createWorkspace: async () => ({ id: "", name: "x" }) as never,
|
||||
};
|
||||
const useCase = connectWorkspaceUseCase(malformedRepo);
|
||||
|
||||
await expect(useCase(INPUT)).rejects.toBeInstanceOf(ZodError);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user