import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { WorkspacesController } from "./workspaces.controller"; import { WorkspacesService } from "./workspaces.service"; import { AuthGuard } from "../auth/guards/auth.guard"; import { WorkspaceGuard, PermissionGuard } from "../common/guards"; import { WorkspaceMemberRole } from "@prisma/client"; import type { AuthUser } from "@mosaic/shared"; describe("WorkspacesController", () => { let controller: WorkspacesController; let service: WorkspacesService; const mockWorkspacesService = { getUserWorkspaces: vi.fn(), addMember: vi.fn(), updateMemberRole: vi.fn(), removeMember: vi.fn(), }; const mockUser: AuthUser = { id: "user-1", email: "test@example.com", name: "Test User", }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ controllers: [WorkspacesController], providers: [ { provide: WorkspacesService, useValue: mockWorkspacesService, }, ], }) .overrideGuard(AuthGuard) .useValue({ canActivate: () => true }) .overrideGuard(WorkspaceGuard) .useValue({ canActivate: () => true }) .overrideGuard(PermissionGuard) .useValue({ canActivate: () => true }) .compile(); controller = module.get(WorkspacesController); service = module.get(WorkspacesService); vi.clearAllMocks(); }); describe("GET /api/workspaces", () => { it("should call service with authenticated user id", async () => { mockWorkspacesService.getUserWorkspaces.mockResolvedValueOnce([]); await controller.getUserWorkspaces(mockUser); expect(service.getUserWorkspaces).toHaveBeenCalledWith("user-1"); }); it("should return workspace list from service", async () => { const mockWorkspaces = [ { id: "ws-1", name: "My Workspace", ownerId: "user-1", role: WorkspaceMemberRole.OWNER, createdAt: new Date("2026-01-01"), }, ]; mockWorkspacesService.getUserWorkspaces.mockResolvedValueOnce(mockWorkspaces); const result = await controller.getUserWorkspaces(mockUser); expect(result).toEqual(mockWorkspaces); }); it("should propagate service errors", async () => { mockWorkspacesService.getUserWorkspaces.mockRejectedValueOnce(new Error("Database error")); await expect(controller.getUserWorkspaces(mockUser)).rejects.toThrow("Database error"); }); }); describe("POST /api/workspaces/:id/members", () => { it("should call service with workspace id, actor id, and add member dto", async () => { const workspaceId = "ws-1"; const addMemberDto = { userId: "user-2", role: WorkspaceMemberRole.MEMBER, }; const mockMember = { workspaceId, userId: "user-2", role: WorkspaceMemberRole.MEMBER, joinedAt: new Date("2026-02-01"), }; mockWorkspacesService.addMember.mockResolvedValueOnce(mockMember); const result = await controller.addMember(workspaceId, addMemberDto, mockUser); expect(result).toEqual(mockMember); expect(service.addMember).toHaveBeenCalledWith(workspaceId, mockUser.id, addMemberDto); }); }); describe("PATCH /api/workspaces/:id/members/:userId", () => { it("should call service with workspace id, actor id, target user id, and role dto", async () => { const workspaceId = "ws-1"; const targetUserId = "user-2"; const updateRoleDto = { role: WorkspaceMemberRole.ADMIN, }; const mockMember = { workspaceId, userId: targetUserId, role: WorkspaceMemberRole.ADMIN, joinedAt: new Date("2026-02-01"), }; mockWorkspacesService.updateMemberRole.mockResolvedValueOnce(mockMember); const result = await controller.updateMemberRole( workspaceId, targetUserId, updateRoleDto, mockUser ); expect(result).toEqual(mockMember); expect(service.updateMemberRole).toHaveBeenCalledWith( workspaceId, mockUser.id, targetUserId, updateRoleDto ); }); }); describe("DELETE /api/workspaces/:id/members/:userId", () => { it("should call service with workspace id, actor id, and target user id", async () => { const workspaceId = "ws-1"; const targetUserId = "user-2"; mockWorkspacesService.removeMember.mockResolvedValueOnce(undefined); await controller.removeMember(workspaceId, targetUserId, mockUser); expect(service.removeMember).toHaveBeenCalledWith(workspaceId, mockUser.id, targetUserId); }); }); });