Files
agentic-dev/packages/workspaces/src/application/use-cases/connect-workspace.use-case.test.ts
Danijel Martinek eec402d49d feat(workspaces): status and list use cases
getWorkspaceStatus returns persisted { id, status } only (runner-driven
transitions arrive in story 07); listWorkspaces returns the
credential-free workspace array with a strict void input (optional on
the tRPC procedure so clients can call without args). Repository gains
listWorkspaces on interface, mock, and Payload impl; the contract suite
covers listing incl. the never-returns-credentials guarantee.
Controller span+capture wrapping is extracted into a shared
bindWorkspacesController helper used by both binders, trimming the
bind-production/bind-dev-seed clone family fallow was flagging.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
2026-07-12 22:36:18 +02:00

136 lines
4.6 KiB
TypeScript

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);
});
});