test: add unit tests for persons and projects modules

Added comprehensive unit tests for `PersonsController`, `PersonsService`, `ProjectsController`, and `ProjectsService`. Covered CRUD operations, exception handling, group/project associations, and user access validation. Mocked `JwtAuthGuard` for all tests.
This commit is contained in:
Mathis H (Avnyr) 2025-05-15 20:31:07 +02:00
parent 0f3c55f947
commit 269ba622f8
4 changed files with 764 additions and 0 deletions

View File

@ -0,0 +1,154 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PersonsController } from './persons.controller';
import { PersonsService } from '../services/persons.service';
import { CreatePersonDto, Gender, OralEaseLevel } from '../dto/create-person.dto';
import { UpdatePersonDto } from '../dto/update-person.dto';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
describe('PersonsController', () => {
let controller: PersonsController;
let service: PersonsService;
// Mock data
const mockPerson = {
id: 'person1',
firstName: 'John',
lastName: 'Doe',
gender: Gender.MALE,
technicalLevel: 3,
hasTechnicalTraining: true,
frenchSpeakingLevel: 4,
oralEaseLevel: OralEaseLevel.COMFORTABLE,
age: 30,
projectId: 'project1',
attributes: {},
createdAt: new Date(),
updatedAt: new Date(),
};
const mockPersonToGroup = {
personId: 'person1',
groupId: 'group1',
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [PersonsController],
providers: [
{
provide: PersonsService,
useValue: {
create: jest.fn().mockResolvedValue(mockPerson),
findAll: jest.fn().mockResolvedValue([mockPerson]),
findByProjectId: jest.fn().mockResolvedValue([mockPerson]),
findById: jest.fn().mockResolvedValue(mockPerson),
update: jest.fn().mockResolvedValue(mockPerson),
remove: jest.fn().mockResolvedValue(mockPerson),
findByProjectIdAndGroupId: jest.fn().mockResolvedValue([{ person: mockPerson }]),
addToGroup: jest.fn().mockResolvedValue(mockPersonToGroup),
removeFromGroup: jest.fn().mockResolvedValue(mockPersonToGroup),
},
},
],
})
.overrideGuard(JwtAuthGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<PersonsController>(PersonsController);
service = module.get<PersonsService>(PersonsService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create', () => {
it('should create a new person', async () => {
const createPersonDto: CreatePersonDto = {
firstName: 'John',
lastName: 'Doe',
gender: Gender.MALE,
technicalLevel: 3,
hasTechnicalTraining: true,
frenchSpeakingLevel: 4,
oralEaseLevel: OralEaseLevel.COMFORTABLE,
projectId: 'project1',
};
expect(await controller.create(createPersonDto)).toBe(mockPerson);
expect(service.create).toHaveBeenCalledWith(createPersonDto);
});
});
describe('findAll', () => {
it('should return all persons when no projectId is provided', async () => {
expect(await controller.findAll()).toEqual([mockPerson]);
expect(service.findAll).toHaveBeenCalled();
});
it('should return persons filtered by projectId when projectId is provided', async () => {
const projectId = 'project1';
expect(await controller.findAll(projectId)).toEqual([mockPerson]);
expect(service.findByProjectId).toHaveBeenCalledWith(projectId);
});
});
describe('findOne', () => {
it('should return a person by ID', async () => {
const id = 'person1';
expect(await controller.findOne(id)).toBe(mockPerson);
expect(service.findById).toHaveBeenCalledWith(id);
});
});
describe('update', () => {
it('should update a person', async () => {
const id = 'person1';
const updatePersonDto: UpdatePersonDto = {
firstName: 'Jane',
};
expect(await controller.update(id, updatePersonDto)).toBe(mockPerson);
expect(service.update).toHaveBeenCalledWith(id, updatePersonDto);
});
});
describe('remove', () => {
it('should delete a person', async () => {
const id = 'person1';
expect(await controller.remove(id)).toBe(mockPerson);
expect(service.remove).toHaveBeenCalledWith(id);
});
});
describe('findByProjectIdAndGroupId', () => {
it('should return persons by project ID and group ID', async () => {
const projectId = 'project1';
const groupId = 'group1';
expect(await controller.findByProjectIdAndGroupId(projectId, groupId)).toEqual([{ person: mockPerson }]);
expect(service.findByProjectIdAndGroupId).toHaveBeenCalledWith(projectId, groupId);
});
});
describe('addToGroup', () => {
it('should add a person to a group', async () => {
const id = 'person1';
const groupId = 'group1';
expect(await controller.addToGroup(id, groupId)).toBe(mockPersonToGroup);
expect(service.addToGroup).toHaveBeenCalledWith(id, groupId);
});
});
describe('removeFromGroup', () => {
it('should remove a person from a group', async () => {
const id = 'person1';
const groupId = 'group1';
expect(await controller.removeFromGroup(id, groupId)).toBe(mockPersonToGroup);
expect(service.removeFromGroup).toHaveBeenCalledWith(id, groupId);
});
});
});

View File

@ -0,0 +1,277 @@
import { Test, TestingModule } from '@nestjs/testing';
import { PersonsService } from './persons.service';
import { NotFoundException } from '@nestjs/common';
import { DRIZZLE } from '../../../database/database.module';
import { Gender, OralEaseLevel } from '../dto/create-person.dto';
describe('PersonsService', () => {
let service: PersonsService;
let mockDb: any;
// Mock data
const mockPerson = {
id: 'person1',
firstName: 'John',
lastName: 'Doe',
gender: Gender.MALE,
technicalLevel: 3,
hasTechnicalTraining: true,
frenchSpeakingLevel: 4,
oralEaseLevel: OralEaseLevel.COMFORTABLE,
age: 30,
projectId: 'project1',
attributes: {},
createdAt: new Date(),
updatedAt: new Date(),
};
const mockGroup = {
id: 'group1',
name: 'Test Group',
projectId: 'project1',
createdAt: new Date(),
updatedAt: new Date(),
};
const mockPersonToGroup = {
personId: 'person1',
groupId: 'group1',
};
// Mock database operations
const mockDbOperations = {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
delete: jest.fn().mockReturnThis(),
innerJoin: jest.fn().mockReturnThis(),
returning: jest.fn().mockImplementation(() => {
return [mockPerson];
}),
};
beforeEach(async () => {
mockDb = {
...mockDbOperations,
};
const module: TestingModule = await Test.createTestingModule({
providers: [
PersonsService,
{
provide: DRIZZLE,
useValue: mockDb,
},
],
}).compile();
service = module.get<PersonsService>(PersonsService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a new person', async () => {
const createPersonDto = {
firstName: 'John',
lastName: 'Doe',
gender: Gender.MALE,
technicalLevel: 3,
hasTechnicalTraining: true,
frenchSpeakingLevel: 4,
oralEaseLevel: OralEaseLevel.COMFORTABLE,
projectId: 'project1',
};
const result = await service.create(createPersonDto);
expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith(createPersonDto);
expect(result).toEqual(mockPerson);
});
});
describe('findAll', () => {
it('should return all persons', async () => {
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => [mockPerson]);
const result = await service.findAll();
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(result).toEqual([mockPerson]);
});
});
describe('findByProjectId', () => {
it('should return persons for a specific project', async () => {
const projectId = 'project1';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockPerson]);
const result = await service.findByProjectId(projectId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual([mockPerson]);
});
});
describe('findById', () => {
it('should return a person by ID', async () => {
const id = 'person1';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockPerson]);
const result = await service.findById(id);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockPerson);
});
it('should throw NotFoundException if person not found', async () => {
const id = 'nonexistent';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
await expect(service.findById(id)).rejects.toThrow(NotFoundException);
});
});
describe('update', () => {
it('should update a person', async () => {
const id = 'person1';
const updatePersonDto = {
firstName: 'Jane',
};
const result = await service.update(id, updatePersonDto);
expect(mockDb.update).toHaveBeenCalled();
expect(mockDb.set).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockPerson);
});
it('should throw NotFoundException if person not found', async () => {
const id = 'nonexistent';
const updatePersonDto = {
firstName: 'Jane',
};
mockDb.update.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.set.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => []);
await expect(service.update(id, updatePersonDto)).rejects.toThrow(NotFoundException);
});
});
describe('remove', () => {
it('should delete a person', async () => {
const id = 'person1';
const result = await service.remove(id);
expect(mockDb.delete).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockPerson);
});
it('should throw NotFoundException if person not found', async () => {
const id = 'nonexistent';
mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => []);
await expect(service.remove(id)).rejects.toThrow(NotFoundException);
});
});
describe('findByProjectIdAndGroupId', () => {
it('should return persons by project ID and group ID', async () => {
const projectId = 'project1';
const groupId = 'group1';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.innerJoin.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [{ person: mockPerson }]);
const result = await service.findByProjectIdAndGroupId(projectId, groupId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.innerJoin).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual([{ person: mockPerson }]);
});
});
describe('addToGroup', () => {
it('should add a person to a group', async () => {
const personId = 'person1';
const groupId = 'group1';
mockDb.insert.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.values.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]);
const result = await service.addToGroup(personId, groupId);
expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith({
personId,
groupId,
});
expect(result).toEqual(mockPersonToGroup);
});
});
describe('removeFromGroup', () => {
it('should remove a person from a group', async () => {
const personId = 'person1';
const groupId = 'group1';
mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]);
const result = await service.removeFromGroup(personId, groupId);
expect(mockDb.delete).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockPersonToGroup);
});
it('should throw NotFoundException if relation not found', async () => {
const personId = 'nonexistent';
const groupId = 'group1';
mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => []);
await expect(service.removeFromGroup(personId, groupId)).rejects.toThrow(NotFoundException);
});
});
});

View File

@ -0,0 +1,116 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProjectsController } from './projects.controller';
import { ProjectsService } from '../services/projects.service';
import { CreateProjectDto } from '../dto/create-project.dto';
import { UpdateProjectDto } from '../dto/update-project.dto';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
describe('ProjectsController', () => {
let controller: ProjectsController;
let service: ProjectsService;
// Mock data
const mockProject = {
id: 'project1',
name: 'Test Project',
description: 'Test Description',
ownerId: 'user1',
settings: {},
createdAt: new Date(),
updatedAt: new Date(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [ProjectsController],
providers: [
{
provide: ProjectsService,
useValue: {
create: jest.fn().mockResolvedValue(mockProject),
findAll: jest.fn().mockResolvedValue([mockProject]),
findByOwnerId: jest.fn().mockResolvedValue([mockProject]),
findById: jest.fn().mockResolvedValue(mockProject),
update: jest.fn().mockResolvedValue(mockProject),
remove: jest.fn().mockResolvedValue(mockProject),
checkUserAccess: jest.fn().mockResolvedValue(true),
},
},
],
})
.overrideGuard(JwtAuthGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<ProjectsController>(ProjectsController);
service = module.get<ProjectsService>(ProjectsService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create', () => {
it('should create a new project', async () => {
const createProjectDto: CreateProjectDto = {
name: 'Test Project',
description: 'Test Description',
ownerId: 'user1',
};
expect(await controller.create(createProjectDto)).toBe(mockProject);
expect(service.create).toHaveBeenCalledWith(createProjectDto);
});
});
describe('findAll', () => {
it('should return all projects when no ownerId is provided', async () => {
expect(await controller.findAll()).toEqual([mockProject]);
expect(service.findAll).toHaveBeenCalled();
});
it('should return projects filtered by ownerId when ownerId is provided', async () => {
const ownerId = 'user1';
expect(await controller.findAll(ownerId)).toEqual([mockProject]);
expect(service.findByOwnerId).toHaveBeenCalledWith(ownerId);
});
});
describe('findOne', () => {
it('should return a project by ID', async () => {
const id = 'project1';
expect(await controller.findOne(id)).toBe(mockProject);
expect(service.findById).toHaveBeenCalledWith(id);
});
});
describe('update', () => {
it('should update a project', async () => {
const id = 'project1';
const updateProjectDto: UpdateProjectDto = {
name: 'Updated Project',
};
expect(await controller.update(id, updateProjectDto)).toBe(mockProject);
expect(service.update).toHaveBeenCalledWith(id, updateProjectDto);
});
});
describe('remove', () => {
it('should delete a project', async () => {
const id = 'project1';
expect(await controller.remove(id)).toBe(mockProject);
expect(service.remove).toHaveBeenCalledWith(id);
});
});
describe('checkUserAccess', () => {
it('should check if a user has access to a project', async () => {
const projectId = 'project1';
const userId = 'user1';
expect(await controller.checkUserAccess(projectId, userId)).toBe(true);
expect(service.checkUserAccess).toHaveBeenCalledWith(projectId, userId);
});
});
});

View File

@ -0,0 +1,217 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ProjectsService } from './projects.service';
import { NotFoundException } from '@nestjs/common';
import { DRIZZLE } from '../../../database/database.module';
describe('ProjectsService', () => {
let service: ProjectsService;
let mockDb: any;
// Mock data
const mockProject = {
id: 'project1',
name: 'Test Project',
description: 'Test Description',
ownerId: 'user1',
settings: {},
createdAt: new Date(),
updatedAt: new Date(),
};
// Mock database operations
const mockDbOperations = {
select: jest.fn().mockReturnThis(),
from: jest.fn().mockReturnThis(),
where: jest.fn().mockReturnThis(),
insert: jest.fn().mockReturnThis(),
values: jest.fn().mockReturnThis(),
update: jest.fn().mockReturnThis(),
set: jest.fn().mockReturnThis(),
delete: jest.fn().mockReturnThis(),
returning: jest.fn().mockImplementation(() => {
return [mockProject];
}),
};
beforeEach(async () => {
mockDb = {
...mockDbOperations,
};
const module: TestingModule = await Test.createTestingModule({
providers: [
ProjectsService,
{
provide: DRIZZLE,
useValue: mockDb,
},
],
}).compile();
service = module.get<ProjectsService>(ProjectsService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a new project', async () => {
const createProjectDto = {
name: 'Test Project',
description: 'Test Description',
ownerId: 'user1',
};
const result = await service.create(createProjectDto);
expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith(createProjectDto);
expect(result).toEqual(mockProject);
});
});
describe('findAll', () => {
it('should return all projects', async () => {
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => [mockProject]);
const result = await service.findAll();
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(result).toEqual([mockProject]);
});
});
describe('findByOwnerId', () => {
it('should return projects for a specific owner', async () => {
const ownerId = 'user1';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockProject]);
const result = await service.findByOwnerId(ownerId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual([mockProject]);
});
});
describe('findById', () => {
it('should return a project by ID', async () => {
const id = 'project1';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockProject]);
const result = await service.findById(id);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockProject);
});
it('should throw NotFoundException if project not found', async () => {
const id = 'nonexistent';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
await expect(service.findById(id)).rejects.toThrow(NotFoundException);
});
});
describe('update', () => {
it('should update a project', async () => {
const id = 'project1';
const updateProjectDto = {
name: 'Updated Project',
};
const result = await service.update(id, updateProjectDto);
expect(mockDb.update).toHaveBeenCalled();
expect(mockDb.set).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockProject);
});
it('should throw NotFoundException if project not found', async () => {
const id = 'nonexistent';
const updateProjectDto = {
name: 'Updated Project',
};
mockDb.update.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.set.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => []);
await expect(service.update(id, updateProjectDto)).rejects.toThrow(NotFoundException);
});
});
describe('remove', () => {
it('should delete a project', async () => {
const id = 'project1';
const result = await service.remove(id);
expect(mockDb.delete).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockProject);
});
it('should throw NotFoundException if project not found', async () => {
const id = 'nonexistent';
mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => []);
await expect(service.remove(id)).rejects.toThrow(NotFoundException);
});
});
describe('checkUserAccess', () => {
it('should return true if user has access to project', async () => {
const projectId = 'project1';
const userId = 'user1';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockProject]);
const result = await service.checkUserAccess(projectId, userId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toBe(true);
});
it('should return false if user does not have access to project', async () => {
const projectId = 'project1';
const userId = 'user2';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
const result = await service.checkUserAccess(projectId, userId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toBe(false);
});
});
});