import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { ExecutionContext, ForbiddenException, BadRequestException, InternalServerErrorException, } from "@nestjs/common"; import { Prisma } from "@prisma/client"; import { WorkspaceGuard } from "./workspace.guard"; import { PrismaService } from "../../prisma/prisma.service"; describe("WorkspaceGuard", () => { let guard: WorkspaceGuard; let prismaService: PrismaService; const mockPrismaService = { workspaceMember: { findUnique: vi.fn(), }, $executeRaw: vi.fn(), }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ WorkspaceGuard, { provide: PrismaService, useValue: mockPrismaService, }, ], }).compile(); guard = module.get(WorkspaceGuard); prismaService = module.get(PrismaService); // Clear all mocks vi.clearAllMocks(); }); const createMockExecutionContext = ( user: any, headers: Record = {}, params: Record = {}, body: Record = {}, query: Record = {} ): ExecutionContext => { const mockRequest = { user, headers, params, body, query, }; return { switchToHttp: () => ({ getRequest: () => mockRequest, }), } as ExecutionContext; }; describe("canActivate", () => { const userId = "user-123"; const workspaceId = "workspace-456"; it("should allow access when user is a workspace member (via header)", async () => { const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId }); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ workspaceId, userId, role: "MEMBER", }); const result = await guard.canActivate(context); expect(result).toBe(true); expect(mockPrismaService.workspaceMember.findUnique).toHaveBeenCalledWith({ where: { workspaceId_userId: { workspaceId, userId, }, }, }); const request = context.switchToHttp().getRequest(); expect(request.workspace).toEqual({ id: workspaceId }); expect(request.user.workspaceId).toBe(workspaceId); }); it("should allow access when user is a workspace member (via URL param)", async () => { const context = createMockExecutionContext({ id: userId }, {}, { workspaceId }); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ workspaceId, userId, role: "ADMIN", }); const result = await guard.canActivate(context); expect(result).toBe(true); }); it("should allow access when user is a workspace member (via body)", async () => { const context = createMockExecutionContext({ id: userId }, {}, {}, { workspaceId }); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ workspaceId, userId, role: "OWNER", }); const result = await guard.canActivate(context); expect(result).toBe(true); }); it("should allow access when user is a workspace member (via query string)", async () => { const context = createMockExecutionContext({ id: userId }, {}, {}, {}, { workspaceId }); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ workspaceId, userId, role: "MEMBER", }); const result = await guard.canActivate(context); expect(result).toBe(true); expect(mockPrismaService.workspaceMember.findUnique).toHaveBeenCalledWith({ where: { workspaceId_userId: { workspaceId, userId, }, }, }); }); it("should prioritize header over param, body, and query", async () => { const headerWorkspaceId = "workspace-header"; const paramWorkspaceId = "workspace-param"; const bodyWorkspaceId = "workspace-body"; const queryWorkspaceId = "workspace-query"; const context = createMockExecutionContext( { id: userId }, { "x-workspace-id": headerWorkspaceId }, { workspaceId: paramWorkspaceId }, { workspaceId: bodyWorkspaceId }, { workspaceId: queryWorkspaceId } ); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ workspaceId: headerWorkspaceId, userId, role: "MEMBER", }); await guard.canActivate(context); expect(mockPrismaService.workspaceMember.findUnique).toHaveBeenCalledWith({ where: { workspaceId_userId: { workspaceId: headerWorkspaceId, userId, }, }, }); }); it("should prioritize param over body and query when header missing", async () => { const paramWorkspaceId = "workspace-param"; const bodyWorkspaceId = "workspace-body"; const queryWorkspaceId = "workspace-query"; const context = createMockExecutionContext( { id: userId }, {}, { workspaceId: paramWorkspaceId }, { workspaceId: bodyWorkspaceId }, { workspaceId: queryWorkspaceId } ); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ workspaceId: paramWorkspaceId, userId, role: "MEMBER", }); await guard.canActivate(context); expect(mockPrismaService.workspaceMember.findUnique).toHaveBeenCalledWith({ where: { workspaceId_userId: { workspaceId: paramWorkspaceId, userId, }, }, }); }); it("should prioritize body over query when header and param missing", async () => { const bodyWorkspaceId = "workspace-body"; const queryWorkspaceId = "workspace-query"; const context = createMockExecutionContext( { id: userId }, {}, {}, { workspaceId: bodyWorkspaceId }, { workspaceId: queryWorkspaceId } ); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ workspaceId: bodyWorkspaceId, userId, role: "MEMBER", }); await guard.canActivate(context); expect(mockPrismaService.workspaceMember.findUnique).toHaveBeenCalledWith({ where: { workspaceId_userId: { workspaceId: bodyWorkspaceId, userId, }, }, }); }); it("should throw ForbiddenException when user is not authenticated", async () => { const context = createMockExecutionContext(null, { "x-workspace-id": workspaceId }); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); await expect(guard.canActivate(context)).rejects.toThrow("User not authenticated"); }); it("should throw BadRequestException when workspace ID is missing", async () => { const context = createMockExecutionContext({ id: userId }); await expect(guard.canActivate(context)).rejects.toThrow(BadRequestException); await expect(guard.canActivate(context)).rejects.toThrow("Workspace ID is required"); }); it("should throw ForbiddenException when user is not a workspace member", async () => { const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId }); mockPrismaService.workspaceMember.findUnique.mockResolvedValue(null); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); await expect(guard.canActivate(context)).rejects.toThrow( "You do not have access to this workspace" ); }); it("should throw InternalServerErrorException on database connection errors", async () => { const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId }); mockPrismaService.workspaceMember.findUnique.mockRejectedValue( new Error("Database connection failed") ); await expect(guard.canActivate(context)).rejects.toThrow(InternalServerErrorException); await expect(guard.canActivate(context)).rejects.toThrow("Failed to verify workspace access"); }); it("should throw InternalServerErrorException on Prisma connection timeout", async () => { const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId }); const prismaError = new Prisma.PrismaClientKnownRequestError("Connection timed out", { code: "P1001", // Authentication failed (connection error) clientVersion: "5.0.0", }); mockPrismaService.workspaceMember.findUnique.mockRejectedValue(prismaError); await expect(guard.canActivate(context)).rejects.toThrow(InternalServerErrorException); }); it("should return false for Prisma not found error (P2025)", async () => { const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId }); const prismaError = new Prisma.PrismaClientKnownRequestError("Record not found", { code: "P2025", // Record not found clientVersion: "5.0.0", }); mockPrismaService.workspaceMember.findUnique.mockRejectedValue(prismaError); // P2025 should be treated as "not a member" -> ForbiddenException await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); await expect(guard.canActivate(context)).rejects.toThrow( "You do not have access to this workspace" ); }); it("should NOT mask database pool exhaustion as access denied", async () => { const context = createMockExecutionContext({ id: userId }, { "x-workspace-id": workspaceId }); const prismaError = new Prisma.PrismaClientKnownRequestError("Connection pool exhausted", { code: "P2024", // Connection pool timeout clientVersion: "5.0.0", }); mockPrismaService.workspaceMember.findUnique.mockRejectedValue(prismaError); // Should NOT throw ForbiddenException for DB errors await expect(guard.canActivate(context)).rejects.toThrow(InternalServerErrorException); await expect(guard.canActivate(context)).rejects.not.toThrow(ForbiddenException); }); }); });