import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { ExecutionContext, ForbiddenException, InternalServerErrorException } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { Prisma, WorkspaceMemberRole } from "@prisma/client"; import { PermissionGuard } from "./permission.guard"; import { PrismaService } from "../../prisma/prisma.service"; import { Permission } from "../decorators/permissions.decorator"; describe("PermissionGuard", () => { let guard: PermissionGuard; let reflector: Reflector; let prismaService: PrismaService; const mockReflector = { getAllAndOverride: vi.fn(), }; const mockPrismaService = { workspaceMember: { findUnique: vi.fn(), }, }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ PermissionGuard, { provide: Reflector, useValue: mockReflector, }, { provide: PrismaService, useValue: mockPrismaService, }, ], }).compile(); guard = module.get(PermissionGuard); reflector = module.get(Reflector); prismaService = module.get(PrismaService); vi.clearAllMocks(); }); const createMockExecutionContext = (user: any, workspace: any): ExecutionContext => { const mockRequest = { user, workspace, }; return { switchToHttp: () => ({ getRequest: () => mockRequest, }), getHandler: vi.fn(), getClass: vi.fn(), } as any; }; describe("canActivate", () => { const userId = "user-123"; const workspaceId = "workspace-456"; it("should allow access when no permission is required", async () => { const context = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(undefined); const result = await guard.canActivate(context); expect(result).toBe(true); }); it("should allow OWNER to access WORKSPACE_OWNER permission", async () => { const context = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_OWNER); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ role: WorkspaceMemberRole.OWNER, }); const result = await guard.canActivate(context); expect(result).toBe(true); expect(context.switchToHttp().getRequest().user.workspaceRole).toBe( WorkspaceMemberRole.OWNER ); }); it("should deny ADMIN access to WORKSPACE_OWNER permission", async () => { const context = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_OWNER); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ role: WorkspaceMemberRole.ADMIN, }); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); }); it("should allow OWNER and ADMIN to access WORKSPACE_ADMIN permission", async () => { const context1 = createMockExecutionContext({ id: userId }, { id: workspaceId }); const context2 = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_ADMIN); // Test OWNER mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ role: WorkspaceMemberRole.OWNER, }); expect(await guard.canActivate(context1)).toBe(true); // Test ADMIN mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ role: WorkspaceMemberRole.ADMIN, }); expect(await guard.canActivate(context2)).toBe(true); }); it("should deny MEMBER access to WORKSPACE_ADMIN permission", async () => { const context = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_ADMIN); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ role: WorkspaceMemberRole.MEMBER, }); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); }); it("should allow OWNER, ADMIN, and MEMBER to access WORKSPACE_MEMBER permission", async () => { const context1 = createMockExecutionContext({ id: userId }, { id: workspaceId }); const context2 = createMockExecutionContext({ id: userId }, { id: workspaceId }); const context3 = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_MEMBER); // Test OWNER mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ role: WorkspaceMemberRole.OWNER, }); expect(await guard.canActivate(context1)).toBe(true); // Test ADMIN mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ role: WorkspaceMemberRole.ADMIN, }); expect(await guard.canActivate(context2)).toBe(true); // Test MEMBER mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ role: WorkspaceMemberRole.MEMBER, }); expect(await guard.canActivate(context3)).toBe(true); }); it("should deny GUEST access to WORKSPACE_MEMBER permission", async () => { const context = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_MEMBER); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ role: WorkspaceMemberRole.GUEST, }); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); }); it("should allow any role (including GUEST) to access WORKSPACE_ANY permission", async () => { const context = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_ANY); mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ role: WorkspaceMemberRole.GUEST, }); const result = await guard.canActivate(context); expect(result).toBe(true); }); it("should throw ForbiddenException when user context is missing", async () => { const context = createMockExecutionContext(null, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_MEMBER); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); }); it("should throw ForbiddenException when workspace context is missing", async () => { const context = createMockExecutionContext({ id: userId }, null); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_MEMBER); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); }); it("should throw ForbiddenException when user is not a workspace member", async () => { const context = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_MEMBER); mockPrismaService.workspaceMember.findUnique.mockResolvedValue(null); await expect(guard.canActivate(context)).rejects.toThrow(ForbiddenException); await expect(guard.canActivate(context)).rejects.toThrow( "You are not a member of this workspace" ); }); it("should throw InternalServerErrorException on database connection errors", async () => { const context = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_MEMBER); 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 permissions"); }); it("should throw InternalServerErrorException on Prisma connection timeout", async () => { const context = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_MEMBER); 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 null role for Prisma not found error (P2025)", async () => { const context = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_MEMBER); 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 are not a member of this workspace" ); }); it("should NOT mask database pool exhaustion as permission denied", async () => { const context = createMockExecutionContext({ id: userId }, { id: workspaceId }); mockReflector.getAllAndOverride.mockReturnValue(Permission.WORKSPACE_MEMBER); 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); }); }); });