import { describe, it, expect, beforeEach, vi } from "vitest"; import { Test, TestingModule } from "@nestjs/testing"; import { AdminService } from "./admin.service"; import { PrismaService } from "../prisma/prisma.service"; import { BadRequestException, ConflictException, NotFoundException } from "@nestjs/common"; import { WorkspaceMemberRole } from "@prisma/client"; describe("AdminService", () => { let service: AdminService; const mockPrismaService = { user: { findMany: vi.fn(), findUnique: vi.fn(), count: vi.fn(), create: vi.fn(), update: vi.fn(), }, workspace: { findUnique: vi.fn(), create: vi.fn(), update: vi.fn(), }, workspaceMember: { create: vi.fn(), }, session: { deleteMany: vi.fn(), }, $transaction: vi.fn(async (ops) => { if (typeof ops === "function") { return ops(mockPrismaService); } return Promise.all(ops); }), }; const mockAdminId = "550e8400-e29b-41d4-a716-446655440001"; const mockUserId = "550e8400-e29b-41d4-a716-446655440002"; const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440003"; const mockUser = { id: mockUserId, name: "Test User", email: "test@example.com", emailVerified: false, image: null, createdAt: new Date("2026-01-01"), updatedAt: new Date("2026-01-01"), deactivatedAt: null, isLocalAuth: false, passwordHash: null, invitedBy: null, invitationToken: null, invitedAt: null, authProviderId: null, preferences: {}, workspaceMemberships: [ { workspaceId: mockWorkspaceId, userId: mockUserId, role: WorkspaceMemberRole.MEMBER, joinedAt: new Date("2026-01-01"), workspace: { id: mockWorkspaceId, name: "Test Workspace" }, }, ], }; const mockWorkspace = { id: mockWorkspaceId, name: "Test Workspace", ownerId: mockAdminId, settings: {}, createdAt: new Date("2026-01-01"), updatedAt: new Date("2026-01-01"), matrixRoomId: null, }; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ AdminService, { provide: PrismaService, useValue: mockPrismaService, }, ], }).compile(); service = module.get(AdminService); vi.clearAllMocks(); }); it("should be defined", () => { expect(service).toBeDefined(); }); describe("listUsers", () => { it("should return paginated users with memberships", async () => { mockPrismaService.user.findMany.mockResolvedValue([mockUser]); mockPrismaService.user.count.mockResolvedValue(1); const result = await service.listUsers(1, 50); expect(result.data).toHaveLength(1); expect(result.data[0]?.id).toBe(mockUserId); expect(result.data[0]?.workspaceMemberships).toHaveLength(1); expect(result.meta).toEqual({ total: 1, page: 1, limit: 50, totalPages: 1, }); }); it("should use default pagination when not provided", async () => { mockPrismaService.user.findMany.mockResolvedValue([]); mockPrismaService.user.count.mockResolvedValue(0); await service.listUsers(); expect(mockPrismaService.user.findMany).toHaveBeenCalledWith( expect.objectContaining({ skip: 0, take: 50, }) ); }); it("should calculate pagination correctly", async () => { mockPrismaService.user.findMany.mockResolvedValue([]); mockPrismaService.user.count.mockResolvedValue(150); const result = await service.listUsers(3, 25); expect(mockPrismaService.user.findMany).toHaveBeenCalledWith( expect.objectContaining({ skip: 50, take: 25, }) ); expect(result.meta.totalPages).toBe(6); }); }); describe("inviteUser", () => { it("should create a user with invitation token", async () => { mockPrismaService.user.findUnique.mockResolvedValue(null); const createdUser = { id: "new-user-id", email: "new@example.com", name: "new", invitationToken: "some-token", }; mockPrismaService.user.create.mockResolvedValue(createdUser); const result = await service.inviteUser({ email: "new@example.com" }, mockAdminId); expect(result.email).toBe("new@example.com"); expect(result.invitationToken).toBeDefined(); expect(result.userId).toBe("new-user-id"); expect(mockPrismaService.user.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ email: "new@example.com", invitedBy: mockAdminId, invitationToken: expect.any(String), }), }) ); }); it("should add user to workspace when workspaceId provided", async () => { mockPrismaService.user.findUnique.mockResolvedValue(null); mockPrismaService.workspace.findUnique.mockResolvedValue(mockWorkspace); const createdUser = { id: "new-user-id", email: "new@example.com", name: "new" }; mockPrismaService.user.create.mockResolvedValue(createdUser); await service.inviteUser( { email: "new@example.com", workspaceId: mockWorkspaceId, role: WorkspaceMemberRole.ADMIN, }, mockAdminId ); expect(mockPrismaService.workspaceMember.create).toHaveBeenCalledWith({ data: { workspaceId: mockWorkspaceId, userId: "new-user-id", role: WorkspaceMemberRole.ADMIN, }, }); }); it("should throw ConflictException if email already exists", async () => { mockPrismaService.user.findUnique.mockResolvedValue(mockUser); await expect(service.inviteUser({ email: "test@example.com" }, mockAdminId)).rejects.toThrow( ConflictException ); }); it("should throw NotFoundException if workspace does not exist", async () => { mockPrismaService.user.findUnique.mockResolvedValue(null); mockPrismaService.workspace.findUnique.mockResolvedValue(null); await expect( service.inviteUser({ email: "new@example.com", workspaceId: "non-existent" }, mockAdminId) ).rejects.toThrow(NotFoundException); }); it("should use email prefix as default name", async () => { mockPrismaService.user.findUnique.mockResolvedValue(null); const createdUser = { id: "new-user-id", email: "jane.doe@example.com", name: "jane.doe" }; mockPrismaService.user.create.mockResolvedValue(createdUser); await service.inviteUser({ email: "jane.doe@example.com" }, mockAdminId); expect(mockPrismaService.user.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ name: "jane.doe", }), }) ); }); it("should use provided name when given", async () => { mockPrismaService.user.findUnique.mockResolvedValue(null); const createdUser = { id: "new-user-id", email: "j@example.com", name: "Jane Doe" }; mockPrismaService.user.create.mockResolvedValue(createdUser); await service.inviteUser({ email: "j@example.com", name: "Jane Doe" }, mockAdminId); expect(mockPrismaService.user.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ name: "Jane Doe", }), }) ); }); }); describe("updateUser", () => { it("should update user fields", async () => { mockPrismaService.user.findUnique.mockResolvedValue(mockUser); mockPrismaService.user.update.mockResolvedValue({ ...mockUser, name: "Updated Name", }); const result = await service.updateUser(mockUserId, { name: "Updated Name" }); expect(result.name).toBe("Updated Name"); expect(mockPrismaService.user.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: mockUserId }, data: { name: "Updated Name" }, }) ); }); it("should set deactivatedAt when provided", async () => { const deactivatedAt = "2026-02-28T00:00:00.000Z"; mockPrismaService.user.findUnique.mockResolvedValue(mockUser); mockPrismaService.user.update.mockResolvedValue({ ...mockUser, deactivatedAt: new Date(deactivatedAt), }); const result = await service.updateUser(mockUserId, { deactivatedAt }); expect(result.deactivatedAt).toEqual(new Date(deactivatedAt)); }); it("should clear deactivatedAt when set to null", async () => { const deactivatedUser = { ...mockUser, deactivatedAt: new Date() }; mockPrismaService.user.findUnique.mockResolvedValue(deactivatedUser); mockPrismaService.user.update.mockResolvedValue({ ...deactivatedUser, deactivatedAt: null, }); const result = await service.updateUser(mockUserId, { deactivatedAt: null }); expect(result.deactivatedAt).toBeNull(); }); it("should throw NotFoundException if user does not exist", async () => { mockPrismaService.user.findUnique.mockResolvedValue(null); await expect(service.updateUser("non-existent", { name: "Test" })).rejects.toThrow( NotFoundException ); }); it("should update emailVerified", async () => { mockPrismaService.user.findUnique.mockResolvedValue(mockUser); mockPrismaService.user.update.mockResolvedValue({ ...mockUser, emailVerified: true, }); const result = await service.updateUser(mockUserId, { emailVerified: true }); expect(result.emailVerified).toBe(true); }); it("should update preferences", async () => { const prefs = { theme: "dark" }; mockPrismaService.user.findUnique.mockResolvedValue(mockUser); mockPrismaService.user.update.mockResolvedValue({ ...mockUser, preferences: prefs, }); await service.updateUser(mockUserId, { preferences: prefs }); expect(mockPrismaService.user.update).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ preferences: prefs }), }) ); }); }); describe("deactivateUser", () => { it("should set deactivatedAt and invalidate sessions", async () => { mockPrismaService.user.findUnique.mockResolvedValue(mockUser); mockPrismaService.user.update.mockResolvedValue({ ...mockUser, deactivatedAt: new Date(), }); mockPrismaService.session.deleteMany.mockResolvedValue({ count: 3 }); const result = await service.deactivateUser(mockUserId); expect(result.deactivatedAt).toBeDefined(); expect(mockPrismaService.user.update).toHaveBeenCalledWith( expect.objectContaining({ where: { id: mockUserId }, data: { deactivatedAt: expect.any(Date) }, }) ); expect(mockPrismaService.session.deleteMany).toHaveBeenCalledWith({ where: { userId: mockUserId } }); }); it("should throw NotFoundException if user does not exist", async () => { mockPrismaService.user.findUnique.mockResolvedValue(null); await expect(service.deactivateUser("non-existent")).rejects.toThrow(NotFoundException); }); it("should throw BadRequestException if user is already deactivated", async () => { mockPrismaService.user.findUnique.mockResolvedValue({ ...mockUser, deactivatedAt: new Date(), }); await expect(service.deactivateUser(mockUserId)).rejects.toThrow(BadRequestException); }); }); describe("createWorkspace", () => { it("should create a workspace with owner membership", async () => { mockPrismaService.user.findUnique.mockResolvedValue(mockUser); mockPrismaService.workspace.create.mockResolvedValue(mockWorkspace); const result = await service.createWorkspace({ name: "New Workspace", ownerId: mockAdminId, }); expect(result.name).toBe("Test Workspace"); expect(result.memberCount).toBe(1); expect(mockPrismaService.workspace.create).toHaveBeenCalled(); expect(mockPrismaService.workspaceMember.create).toHaveBeenCalledWith({ data: { workspaceId: mockWorkspace.id, userId: mockAdminId, role: WorkspaceMemberRole.OWNER, }, }); }); it("should throw NotFoundException if owner does not exist", async () => { mockPrismaService.user.findUnique.mockResolvedValue(null); await expect( service.createWorkspace({ name: "New Workspace", ownerId: "non-existent" }) ).rejects.toThrow(NotFoundException); }); it("should pass settings when provided", async () => { const settings = { theme: "dark", features: ["chat"] }; mockPrismaService.user.findUnique.mockResolvedValue(mockUser); mockPrismaService.workspace.create.mockResolvedValue({ ...mockWorkspace, settings, }); await service.createWorkspace({ name: "New Workspace", ownerId: mockAdminId, settings, }); expect(mockPrismaService.workspace.create).toHaveBeenCalledWith( expect.objectContaining({ data: expect.objectContaining({ settings }), }) ); }); }); describe("updateWorkspace", () => { it("should update workspace name", async () => { mockPrismaService.workspace.findUnique.mockResolvedValue(mockWorkspace); mockPrismaService.workspace.update.mockResolvedValue({ ...mockWorkspace, name: "Updated Workspace", _count: { members: 3 }, }); const result = await service.updateWorkspace(mockWorkspaceId, { name: "Updated Workspace", }); expect(result.name).toBe("Updated Workspace"); expect(result.memberCount).toBe(3); }); it("should update workspace settings", async () => { const newSettings = { notifications: true }; mockPrismaService.workspace.findUnique.mockResolvedValue(mockWorkspace); mockPrismaService.workspace.update.mockResolvedValue({ ...mockWorkspace, settings: newSettings, _count: { members: 1 }, }); const result = await service.updateWorkspace(mockWorkspaceId, { settings: newSettings, }); expect(result.settings).toEqual(newSettings); }); it("should throw NotFoundException if workspace does not exist", async () => { mockPrismaService.workspace.findUnique.mockResolvedValue(null); await expect(service.updateWorkspace("non-existent", { name: "Test" })).rejects.toThrow( NotFoundException ); }); it("should only update provided fields", async () => { mockPrismaService.workspace.findUnique.mockResolvedValue(mockWorkspace); mockPrismaService.workspace.update.mockResolvedValue({ ...mockWorkspace, _count: { members: 1 }, }); await service.updateWorkspace(mockWorkspaceId, { name: "Only Name" }); expect(mockPrismaService.workspace.update).toHaveBeenCalledWith( expect.objectContaining({ data: { name: "Only Name" }, }) ); }); }); });