Files
stack/apps/api/src/teams/teams.service.spec.ts
Jason Woltje 68c1947b32
All checks were successful
ci/woodpecker/push/api Pipeline was successful
feat(api): add team management module with CRUD endpoints
- TeamsModule: controller, service, DTOs, specs
- POST/GET /api/workspaces/:id/teams
- POST/DELETE /api/workspaces/:id/teams/:teamId/members
- DELETE /api/workspaces/:id/teams/:teamId
- 19 tests

Refs: MS21-API-004
2026-02-28 12:23:48 -06:00

287 lines
8.6 KiB
TypeScript

import { BadRequestException, ConflictException, NotFoundException } from "@nestjs/common";
import { Test, TestingModule } from "@nestjs/testing";
import { TeamMemberRole } from "@prisma/client";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { PrismaService } from "../prisma/prisma.service";
import { TeamsService } from "./teams.service";
describe("TeamsService", () => {
let service: TeamsService;
let prisma: PrismaService;
const mockPrismaService = {
team: {
create: vi.fn(),
findMany: vi.fn(),
findFirst: vi.fn(),
deleteMany: vi.fn(),
},
workspaceMember: {
findUnique: vi.fn(),
},
teamMember: {
findUnique: vi.fn(),
create: vi.fn(),
deleteMany: vi.fn(),
},
};
const mockWorkspaceId = "550e8400-e29b-41d4-a716-446655440001";
const mockTeamId = "550e8400-e29b-41d4-a716-446655440002";
const mockUserId = "550e8400-e29b-41d4-a716-446655440003";
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
TeamsService,
{
provide: PrismaService,
useValue: mockPrismaService,
},
],
}).compile();
service = module.get<TeamsService>(TeamsService);
prisma = module.get<PrismaService>(PrismaService);
vi.clearAllMocks();
});
it("should be defined", () => {
expect(service).toBeDefined();
});
describe("create", () => {
it("should create a team", async () => {
const createDto = {
name: "Platform Team",
description: "Owns platform services",
};
const createdTeam = {
id: mockTeamId,
workspaceId: mockWorkspaceId,
name: createDto.name,
description: createDto.description,
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
mockPrismaService.team.create.mockResolvedValue(createdTeam);
const result = await service.create(mockWorkspaceId, createDto);
expect(result).toEqual(createdTeam);
expect(prisma.team.create).toHaveBeenCalledWith({
data: {
workspaceId: mockWorkspaceId,
name: createDto.name,
description: createDto.description,
},
});
});
});
describe("findAll", () => {
it("should list teams for a workspace", async () => {
const teams = [
{
id: mockTeamId,
workspaceId: mockWorkspaceId,
name: "Platform Team",
description: "Owns platform services",
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
_count: { members: 1 },
},
];
mockPrismaService.team.findMany.mockResolvedValue(teams);
const result = await service.findAll(mockWorkspaceId);
expect(result).toEqual(teams);
expect(prisma.team.findMany).toHaveBeenCalledWith({
where: { workspaceId: mockWorkspaceId },
include: {
_count: {
select: { members: true },
},
},
orderBy: { createdAt: "asc" },
});
});
});
describe("addMember", () => {
it("should add a workspace member to a team", async () => {
const dto = {
userId: mockUserId,
role: TeamMemberRole.ADMIN,
};
const createdTeamMember = {
teamId: mockTeamId,
userId: mockUserId,
role: TeamMemberRole.ADMIN,
joinedAt: new Date(),
user: {
id: mockUserId,
name: "Test User",
email: "test@example.com",
},
};
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ userId: mockUserId });
mockPrismaService.teamMember.findUnique.mockResolvedValue(null);
mockPrismaService.teamMember.create.mockResolvedValue(createdTeamMember);
const result = await service.addMember(mockWorkspaceId, mockTeamId, dto);
expect(result).toEqual(createdTeamMember);
expect(prisma.team.findFirst).toHaveBeenCalledWith({
where: {
id: mockTeamId,
workspaceId: mockWorkspaceId,
},
select: { id: true },
});
expect(prisma.workspaceMember.findUnique).toHaveBeenCalledWith({
where: {
workspaceId_userId: {
workspaceId: mockWorkspaceId,
userId: mockUserId,
},
},
select: { userId: true },
});
expect(prisma.teamMember.create).toHaveBeenCalledWith({
data: {
teamId: mockTeamId,
userId: mockUserId,
role: TeamMemberRole.ADMIN,
},
include: {
user: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
});
it("should use MEMBER role when role is omitted", async () => {
const dto = { userId: mockUserId };
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ userId: mockUserId });
mockPrismaService.teamMember.findUnique.mockResolvedValue(null);
mockPrismaService.teamMember.create.mockResolvedValue({
teamId: mockTeamId,
userId: mockUserId,
role: TeamMemberRole.MEMBER,
joinedAt: new Date(),
});
await service.addMember(mockWorkspaceId, mockTeamId, dto);
expect(prisma.teamMember.create).toHaveBeenCalledWith(
expect.objectContaining({
data: expect.objectContaining({
role: TeamMemberRole.MEMBER,
}),
})
);
});
it("should throw when team does not belong to workspace", async () => {
mockPrismaService.team.findFirst.mockResolvedValue(null);
await expect(
service.addMember(mockWorkspaceId, mockTeamId, { userId: mockUserId })
).rejects.toThrow(NotFoundException);
expect(prisma.workspaceMember.findUnique).not.toHaveBeenCalled();
});
it("should throw when user is not a workspace member", async () => {
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
mockPrismaService.workspaceMember.findUnique.mockResolvedValue(null);
await expect(
service.addMember(mockWorkspaceId, mockTeamId, { userId: mockUserId })
).rejects.toThrow(BadRequestException);
});
it("should throw when user is already in the team", async () => {
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
mockPrismaService.workspaceMember.findUnique.mockResolvedValue({ userId: mockUserId });
mockPrismaService.teamMember.findUnique.mockResolvedValue({ userId: mockUserId });
await expect(
service.addMember(mockWorkspaceId, mockTeamId, { userId: mockUserId })
).rejects.toThrow(ConflictException);
});
});
describe("removeMember", () => {
it("should remove a member from a team", async () => {
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
mockPrismaService.teamMember.deleteMany.mockResolvedValue({ count: 1 });
await service.removeMember(mockWorkspaceId, mockTeamId, mockUserId);
expect(prisma.teamMember.deleteMany).toHaveBeenCalledWith({
where: {
teamId: mockTeamId,
userId: mockUserId,
},
});
});
it("should throw when team does not belong to workspace", async () => {
mockPrismaService.team.findFirst.mockResolvedValue(null);
await expect(service.removeMember(mockWorkspaceId, mockTeamId, mockUserId)).rejects.toThrow(
NotFoundException
);
expect(prisma.teamMember.deleteMany).not.toHaveBeenCalled();
});
it("should throw when user is not in the team", async () => {
mockPrismaService.team.findFirst.mockResolvedValue({ id: mockTeamId });
mockPrismaService.teamMember.deleteMany.mockResolvedValue({ count: 0 });
await expect(service.removeMember(mockWorkspaceId, mockTeamId, mockUserId)).rejects.toThrow(
NotFoundException
);
});
});
describe("remove", () => {
it("should delete a team", async () => {
mockPrismaService.team.deleteMany.mockResolvedValue({ count: 1 });
await service.remove(mockWorkspaceId, mockTeamId);
expect(prisma.team.deleteMany).toHaveBeenCalledWith({
where: {
id: mockTeamId,
workspaceId: mockWorkspaceId,
},
});
});
it("should throw when team is not found", async () => {
mockPrismaService.team.deleteMany.mockResolvedValue({ count: 0 });
await expect(service.remove(mockWorkspaceId, mockTeamId)).rejects.toThrow(NotFoundException);
});
});
});