All checks were successful
ci/woodpecker/push/api Pipeline was successful
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
478 lines
15 KiB
TypeScript
478 lines
15 KiB
TypeScript
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>(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" },
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|