Compare commits

..

11 Commits

Author SHA1 Message Date
Mathis HERRIOT
aff21cb7ff
docs: update project status priorities and timeline adjustments 2025-05-17 00:23:43 +02:00
Mathis HERRIOT
e5121c4e7a
test: update and refactor person and group service tests 2025-05-17 00:23:34 +02:00
Mathis HERRIOT
fd783681ba
test(persons.service): enhance test coverage and improve mock logic readability
- Refactored test cases to use more precise assertions and enhanced expected data validation.
- Added mock implementations for database operations and service dependencies to improve clarity.
- Improved error handling test scenarios (e.g., NotFoundException cases).
- Increased test consistency with additional checks on method call counts.
2025-05-17 00:19:07 +02:00
Mathis HERRIOT
93acd7e452
test(groups): improve mocks and assertions in group service tests 2025-05-17 00:14:26 +02:00
Mathis HERRIOT
2a47417b47
test(groups): add test case for database error handling in findById method 2025-05-17 00:13:18 +02:00
Mathis HERRIOT
b5c0e2e98d
feat(docs): update project status with completed e2e tests and API documentation 2025-05-17 00:12:57 +02:00
Mathis HERRIOT
3fe47795d9
feat(projects): add Swagger decorators for API documentation in ProjectsController 2025-05-17 00:12:49 +02:00
Mathis HERRIOT
1308e9c599
fix(projects): handle non-array collaborators in service to prevent errors 2025-05-17 00:12:39 +02:00
Mathis HERRIOT
b7d899e66e
feat(users): add Swagger decorators for API documentation in UsersController 2025-05-17 00:12:31 +02:00
Mathis HERRIOT
818a92f18c
test(users): update unit tests for GDPR consent and export functionality 2025-05-17 00:12:23 +02:00
Mathis HERRIOT
ea6684b7fa
refactor(persons): simplify Person model by consolidating fields and updating related tests 2025-05-17 00:12:01 +02:00
9 changed files with 279 additions and 115 deletions

View File

@ -161,7 +161,16 @@ describe('GroupsService', () => {
const id = 'nonexistent'; const id = 'nonexistent';
mockDb.select.mockImplementationOnce(() => mockDbOperations); mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations); mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [undefined]); mockDbOperations.where.mockImplementationOnce(() => []);
await expect(service.findById(id)).rejects.toThrow(NotFoundException);
});
it('should throw NotFoundException if there is a database error', async () => {
const id = 'invalid-id';
mockDb.select.mockImplementationOnce(() => {
throw new Error('Database error');
});
await expect(service.findById(id)).rejects.toThrow(NotFoundException); await expect(service.findById(id)).rejects.toThrow(NotFoundException);
}); });
@ -174,8 +183,12 @@ describe('GroupsService', () => {
name: 'Updated Group', name: 'Updated Group',
}; };
// Mock findById to return the group
jest.spyOn(service, 'findById').mockResolvedValueOnce(mockGroup);
const result = await service.update(id, updateGroupDto); const result = await service.update(id, updateGroupDto);
expect(service.findById).toHaveBeenCalledWith(id);
expect(mockDb.update).toHaveBeenCalled(); expect(mockDb.update).toHaveBeenCalled();
expect(mockDb.set).toHaveBeenCalled(); expect(mockDb.set).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled(); expect(mockDb.where).toHaveBeenCalled();
@ -197,10 +210,8 @@ describe('GroupsService', () => {
name: 'Updated Group', name: 'Updated Group',
}; };
mockDb.update.mockImplementationOnce(() => mockDbOperations); // Mock findById to throw NotFoundException
mockDbOperations.set.mockImplementationOnce(() => mockDbOperations); jest.spyOn(service, 'findById').mockRejectedValueOnce(new NotFoundException(`Group with ID ${id} not found`));
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [undefined]);
await expect(service.update(id, updateGroupDto)).rejects.toThrow(NotFoundException); await expect(service.update(id, updateGroupDto)).rejects.toThrow(NotFoundException);
}); });
@ -251,19 +262,22 @@ describe('GroupsService', () => {
// Mock person lookup // Mock person lookup
mockDb.select.mockImplementationOnce(() => mockDbOperations); mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations); mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [[mockPerson]]); mockDbOperations.where.mockImplementationOnce(() => [mockPerson]);
// Mock relation lookup // Mock relation lookup
mockDb.select.mockImplementationOnce(() => mockDbOperations); mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations); mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations); mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [undefined]); mockDbOperations.where.mockImplementationOnce(() => []);
// Mock relation creation // Mock relation creation
mockDb.insert.mockImplementationOnce(() => mockDbOperations); mockDb.insert.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.values.mockImplementationOnce(() => mockDbOperations); mockDbOperations.values.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]); mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]);
// Mock getPersonsInGroup
jest.spyOn(service, 'getPersonsInGroup').mockResolvedValueOnce([mockPerson]);
const result = await service.addPersonToGroup(groupId, personId); const result = await service.addPersonToGroup(groupId, personId);
expect(service.findById).toHaveBeenCalledWith(groupId); expect(service.findById).toHaveBeenCalledWith(groupId);
@ -274,14 +288,14 @@ describe('GroupsService', () => {
personId, personId,
groupId, groupId,
}); });
expect(result).toEqual(mockPersonToGroup); expect(result).toEqual({ ...mockGroup, persons: [mockPerson] });
// Check if WebSocketsService.emitPersonAddedToGroup was called with correct parameters // Check if WebSocketsService.emitPersonAddedToGroup was called with correct parameters
expect(mockWebSocketsService.emitPersonAddedToGroup).toHaveBeenCalledWith( expect(mockWebSocketsService.emitPersonAddedToGroup).toHaveBeenCalledWith(
mockGroup.projectId, mockGroup.projectId,
{ {
group: mockGroup, group: mockGroup,
person: [mockPerson], person: mockPerson,
relation: mockPersonToGroup, relation: mockPersonToGroup,
} }
); );
@ -300,7 +314,12 @@ describe('GroupsService', () => {
// Mock person lookup to return no person // Mock person lookup to return no person
mockDb.select.mockImplementationOnce(() => mockDbOperations); mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations); mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [undefined]); mockDbOperations.where.mockImplementationOnce(() => []);
// Mock user lookup to return no user
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
await expect(service.addPersonToGroup(groupId, personId)).rejects.toThrow(NotFoundException); await expect(service.addPersonToGroup(groupId, personId)).rejects.toThrow(NotFoundException);
}); });
@ -328,6 +347,9 @@ describe('GroupsService', () => {
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations); mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]); mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]);
// Mock getPersonsInGroup
jest.spyOn(service, 'getPersonsInGroup').mockResolvedValueOnce([mockPerson]);
const result = await service.removePersonFromGroup(groupId, personId); const result = await service.removePersonFromGroup(groupId, personId);
expect(service.findById).toHaveBeenCalledWith(groupId); expect(service.findById).toHaveBeenCalledWith(groupId);
@ -335,7 +357,7 @@ describe('GroupsService', () => {
expect(mockDb.from).toHaveBeenCalled(); expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.delete).toHaveBeenCalled(); expect(mockDb.delete).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled(); expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockPersonToGroup); expect(result).toEqual({ ...mockGroup, persons: [mockPerson] });
// Check if WebSocketsService.emitPersonRemovedFromGroup was called with correct parameters // Check if WebSocketsService.emitPersonRemovedFromGroup was called with correct parameters
expect(mockWebSocketsService.emitPersonRemovedFromGroup).toHaveBeenCalledWith( expect(mockWebSocketsService.emitPersonRemovedFromGroup).toHaveBeenCalledWith(
@ -376,7 +398,7 @@ describe('GroupsService', () => {
describe('getPersonsInGroup', () => { describe('getPersonsInGroup', () => {
it('should get all persons in a group', async () => { it('should get all persons in a group', async () => {
const groupId = 'group1'; const groupId = 'group1';
const mockPersons = [{ person: mockPerson }]; const personIds = [{ id: 'person1' }];
// Mock findById to return the group // Mock findById to return the group
jest.spyOn(service, 'findById').mockResolvedValueOnce(mockGroup); jest.spyOn(service, 'findById').mockResolvedValueOnce(mockGroup);
@ -384,22 +406,25 @@ describe('GroupsService', () => {
// Reset and setup mocks for this test // Reset and setup mocks for this test
jest.clearAllMocks(); jest.clearAllMocks();
// Mock the select chain to return the expected result // Mock the select chain to return person IDs
mockDb.select.mockImplementationOnce(() => mockDbOperations); mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations); mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.innerJoin.mockImplementationOnce(() => mockDbOperations); mockDbOperations.where.mockImplementationOnce(() => personIds);
mockDbOperations.where.mockImplementationOnce(() => mockPersons);
// Mock the person lookup
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockPerson]);
const result = await service.getPersonsInGroup(groupId); const result = await service.getPersonsInGroup(groupId);
expect(service.findById).toHaveBeenCalledWith(groupId); expect(service.findById).toHaveBeenCalledWith(groupId);
expect(mockDb.select).toHaveBeenCalled(); expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled(); expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.innerJoin).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled(); expect(mockDb.where).toHaveBeenCalled();
// Just verify the result is defined, since the mock implementation is complex // Verify the result is the expected array of persons
expect(result).toBeDefined(); expect(result).toEqual([mockPerson]);
}); });
}); });
}); });

View File

@ -1,7 +1,7 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { PersonsController } from './persons.controller'; import { PersonsController } from './persons.controller';
import { PersonsService } from '../services/persons.service'; import { PersonsService } from '../services/persons.service';
import { CreatePersonDto, Gender, OralEaseLevel } from '../dto/create-person.dto'; import { CreatePersonDto } from '../dto/create-person.dto';
import { UpdatePersonDto } from '../dto/update-person.dto'; import { UpdatePersonDto } from '../dto/update-person.dto';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard'; import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
@ -12,16 +12,10 @@ describe('PersonsController', () => {
// Mock data // Mock data
const mockPerson = { const mockPerson = {
id: 'person1', id: 'person1',
firstName: 'John', name: 'John Doe',
lastName: 'Doe',
gender: Gender.MALE,
technicalLevel: 3,
hasTechnicalTraining: true,
frenchSpeakingLevel: 4,
oralEaseLevel: OralEaseLevel.COMFORTABLE,
age: 30,
projectId: 'project1', projectId: 'project1',
attributes: {}, skills: ['JavaScript', 'TypeScript'],
metadata: {},
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; };
@ -66,14 +60,10 @@ describe('PersonsController', () => {
describe('create', () => { describe('create', () => {
it('should create a new person', async () => { it('should create a new person', async () => {
const createPersonDto: CreatePersonDto = { const createPersonDto: CreatePersonDto = {
firstName: 'John', name: 'John Doe',
lastName: 'Doe',
gender: Gender.MALE,
technicalLevel: 3,
hasTechnicalTraining: true,
frenchSpeakingLevel: 4,
oralEaseLevel: OralEaseLevel.COMFORTABLE,
projectId: 'project1', projectId: 'project1',
skills: ['JavaScript', 'TypeScript'],
metadata: {},
}; };
expect(await controller.create(createPersonDto)).toBe(mockPerson); expect(await controller.create(createPersonDto)).toBe(mockPerson);
@ -106,7 +96,7 @@ describe('PersonsController', () => {
it('should update a person', async () => { it('should update a person', async () => {
const id = 'person1'; const id = 'person1';
const updatePersonDto: UpdatePersonDto = { const updatePersonDto: UpdatePersonDto = {
firstName: 'Jane', name: 'Jane Doe',
}; };
expect(await controller.update(id, updatePersonDto)).toBe(mockPerson); expect(await controller.update(id, updatePersonDto)).toBe(mockPerson);

View File

@ -2,7 +2,6 @@ import { Test, TestingModule } from '@nestjs/testing';
import { PersonsService } from './persons.service'; import { PersonsService } from './persons.service';
import { NotFoundException } from '@nestjs/common'; import { NotFoundException } from '@nestjs/common';
import { DRIZZLE } from '../../../database/database.module'; import { DRIZZLE } from '../../../database/database.module';
import { Gender, OralEaseLevel } from '../dto/create-person.dto';
describe('PersonsService', () => { describe('PersonsService', () => {
let service: PersonsService; let service: PersonsService;
@ -11,16 +10,21 @@ describe('PersonsService', () => {
// Mock data // Mock data
const mockPerson = { const mockPerson = {
id: 'person1', id: 'person1',
firstName: 'John', name: 'John Doe',
lastName: 'Doe',
gender: Gender.MALE,
technicalLevel: 3,
hasTechnicalTraining: true,
frenchSpeakingLevel: 4,
oralEaseLevel: OralEaseLevel.COMFORTABLE,
age: 30,
projectId: 'project1', projectId: 'project1',
attributes: {}, skills: ['JavaScript', 'TypeScript'],
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
// Updated mock person for update test
const updatedMockPerson = {
id: 'person1',
name: 'Jane Doe',
projectId: 'project1',
skills: [],
metadata: {},
createdAt: new Date(), createdAt: new Date(),
updatedAt: new Date(), updatedAt: new Date(),
}; };
@ -83,20 +87,29 @@ describe('PersonsService', () => {
describe('create', () => { describe('create', () => {
it('should create a new person', async () => { it('should create a new person', async () => {
const createPersonDto = { const createPersonDto = {
name: 'John Doe',
projectId: 'project1',
skills: ['JavaScript', 'TypeScript'],
metadata: {},
};
// Expected values that will be passed to the database
const expectedPersonData = {
firstName: 'John', firstName: 'John',
lastName: 'Doe', lastName: 'Doe',
gender: Gender.MALE, gender: 'MALE',
technicalLevel: 3, technicalLevel: 3,
hasTechnicalTraining: true, hasTechnicalTraining: true,
frenchSpeakingLevel: 4, frenchSpeakingLevel: 5,
oralEaseLevel: OralEaseLevel.COMFORTABLE, oralEaseLevel: 'COMFORTABLE',
projectId: 'project1', projectId: 'project1',
attributes: {},
}; };
const result = await service.create(createPersonDto); const result = await service.create(createPersonDto);
expect(mockDb.insert).toHaveBeenCalled(); expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith(createPersonDto); expect(mockDb.values).toHaveBeenCalledWith(expectedPersonData);
expect(result).toEqual(mockPerson); expect(result).toEqual(mockPerson);
}); });
}); });
@ -159,27 +172,45 @@ describe('PersonsService', () => {
it('should update a person', async () => { it('should update a person', async () => {
const id = 'person1'; const id = 'person1';
const updatePersonDto = { const updatePersonDto = {
name: 'Jane Doe',
};
// Mock the findById method to return a person
const existingPerson = {
id: 'person1',
firstName: 'John',
lastName: 'Doe',
projectId: 'project1',
attributes: {},
createdAt: new Date(),
updatedAt: new Date(),
};
jest.spyOn(service, 'findById').mockResolvedValueOnce(existingPerson);
// Expected values that will be passed to the database
const expectedUpdateData = {
firstName: 'Jane', firstName: 'Jane',
lastName: 'Doe',
updatedAt: expect.any(Date),
}; };
const result = await service.update(id, updatePersonDto); const result = await service.update(id, updatePersonDto);
expect(service.findById).toHaveBeenCalledWith(id);
expect(mockDb.update).toHaveBeenCalled(); expect(mockDb.update).toHaveBeenCalled();
expect(mockDb.set).toHaveBeenCalled(); expect(mockDb.set).toHaveBeenCalledWith(expectedUpdateData);
expect(mockDb.where).toHaveBeenCalled(); expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockPerson); expect(result).toEqual(updatedMockPerson);
}); });
it('should throw NotFoundException if person not found', async () => { it('should throw NotFoundException if person not found', async () => {
const id = 'nonexistent'; const id = 'nonexistent';
const updatePersonDto = { const updatePersonDto = {
firstName: 'Jane', name: 'Jane Doe',
}; };
mockDb.update.mockImplementationOnce(() => mockDbOperations); // Mock the findById method to throw a NotFoundException
mockDbOperations.set.mockImplementationOnce(() => mockDbOperations); jest.spyOn(service, 'findById').mockRejectedValueOnce(new NotFoundException(`Person with ID ${id} not found`));
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => []);
await expect(service.update(id, updatePersonDto)).rejects.toThrow(NotFoundException); await expect(service.update(id, updatePersonDto)).rejects.toThrow(NotFoundException);
}); });
@ -189,6 +220,11 @@ describe('PersonsService', () => {
it('should delete a person', async () => { it('should delete a person', async () => {
const id = 'person1'; const id = 'person1';
// Mock the database to return a person
mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockPerson]);
const result = await service.remove(id); const result = await service.remove(id);
expect(mockDb.delete).toHaveBeenCalled(); expect(mockDb.delete).toHaveBeenCalled();
@ -199,6 +235,7 @@ describe('PersonsService', () => {
it('should throw NotFoundException if person not found', async () => { it('should throw NotFoundException if person not found', async () => {
const id = 'nonexistent'; const id = 'nonexistent';
// Mock the database to return no person
mockDb.delete.mockImplementationOnce(() => mockDbOperations); mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations); mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => []); mockDbOperations.returning.mockImplementationOnce(() => []);
@ -212,6 +249,17 @@ describe('PersonsService', () => {
const projectId = 'project1'; const projectId = 'project1';
const groupId = 'group1'; const groupId = 'group1';
// Mock project check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [{ id: projectId }]);
// Mock group check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [{ id: groupId }]);
// Mock persons query
mockDb.select.mockImplementationOnce(() => mockDbOperations); mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations); mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.innerJoin.mockImplementationOnce(() => mockDbOperations); mockDbOperations.innerJoin.mockImplementationOnce(() => mockDbOperations);
@ -219,11 +267,11 @@ describe('PersonsService', () => {
const result = await service.findByProjectIdAndGroupId(projectId, groupId); const result = await service.findByProjectIdAndGroupId(projectId, groupId);
expect(mockDb.select).toHaveBeenCalled(); expect(mockDb.select).toHaveBeenCalledTimes(3);
expect(mockDb.from).toHaveBeenCalled(); expect(mockDb.from).toHaveBeenCalledTimes(3);
expect(mockDb.innerJoin).toHaveBeenCalled(); expect(mockDb.innerJoin).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled(); expect(mockDb.where).toHaveBeenCalledTimes(3);
expect(result).toEqual([{ person: mockPerson }]); expect(result).toEqual([mockPerson]);
}); });
}); });
@ -232,12 +280,31 @@ describe('PersonsService', () => {
const personId = 'person1'; const personId = 'person1';
const groupId = 'group1'; const groupId = 'group1';
// Mock person check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockPerson]);
// Mock group check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockGroup]);
// Mock relation check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
// Mock relation creation
mockDb.insert.mockImplementationOnce(() => mockDbOperations); mockDb.insert.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.values.mockImplementationOnce(() => mockDbOperations); mockDbOperations.values.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]); mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]);
const result = await service.addToGroup(personId, groupId); const result = await service.addToGroup(personId, groupId);
expect(mockDb.select).toHaveBeenCalledTimes(3);
expect(mockDb.from).toHaveBeenCalledTimes(3);
expect(mockDb.where).toHaveBeenCalledTimes(3);
expect(mockDb.insert).toHaveBeenCalled(); expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith({ expect(mockDb.values).toHaveBeenCalledWith({
personId, personId,
@ -252,14 +319,16 @@ describe('PersonsService', () => {
const personId = 'person1'; const personId = 'person1';
const groupId = 'group1'; const groupId = 'group1';
// Mock delete operation
mockDb.delete.mockImplementationOnce(() => mockDbOperations); mockDb.delete.mockImplementationOnce(() => mockDbOperations);
// The where call with the and() condition
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations); mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]); mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]);
const result = await service.removeFromGroup(personId, groupId); const result = await service.removeFromGroup(personId, groupId);
expect(mockDb.delete).toHaveBeenCalled(); expect(mockDb.delete).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled(); expect(mockDb.where).toHaveBeenCalledTimes(1);
expect(result).toEqual(mockPersonToGroup); expect(result).toEqual(mockPersonToGroup);
}); });
@ -267,8 +336,10 @@ describe('PersonsService', () => {
const personId = 'nonexistent'; const personId = 'nonexistent';
const groupId = 'group1'; const groupId = 'group1';
// Mock delete operation to return no relation
mockDb.delete.mockImplementationOnce(() => mockDbOperations); mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations); mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => []); mockDbOperations.returning.mockImplementationOnce(() => []);
await expect(service.removeFromGroup(personId, groupId)).rejects.toThrow(NotFoundException); await expect(service.removeFromGroup(personId, groupId)).rejects.toThrow(NotFoundException);

View File

@ -126,9 +126,13 @@ describe('ProjectsController', () => {
it('should check if a user has access to a project', async () => { it('should check if a user has access to a project', async () => {
const projectId = 'project1'; const projectId = 'project1';
const userId = 'user1'; const userId = 'user1';
const mockRes = {
json: jest.fn().mockReturnValue(true)
};
expect(await controller.checkUserAccess(projectId, userId)).toBe(true); await controller.checkUserAccess(projectId, userId, mockRes);
expect(service.checkUserAccess).toHaveBeenCalledWith(projectId, userId); expect(service.checkUserAccess).toHaveBeenCalledWith(projectId, userId);
expect(mockRes.json).toHaveBeenCalledWith(true);
}); });
}); });

View File

@ -11,10 +11,12 @@ import {
Query, Query,
Res, Res,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam, ApiQuery } from '@nestjs/swagger';
import { ProjectsService } from '../services/projects.service'; import { ProjectsService } from '../services/projects.service';
import { CreateProjectDto } from '../dto/create-project.dto'; import { CreateProjectDto } from '../dto/create-project.dto';
import { UpdateProjectDto } from '../dto/update-project.dto'; import { UpdateProjectDto } from '../dto/update-project.dto';
@ApiTags('projects')
@Controller('projects') @Controller('projects')
export class ProjectsController { export class ProjectsController {
constructor(private readonly projectsService: ProjectsService) {} constructor(private readonly projectsService: ProjectsService) {}
@ -22,6 +24,9 @@ export class ProjectsController {
/** /**
* Create a new project * Create a new project
*/ */
@ApiOperation({ summary: 'Create a new project' })
@ApiResponse({ status: 201, description: 'The project has been successfully created.' })
@ApiResponse({ status: 400, description: 'Bad request.' })
@Post() @Post()
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
create(@Body() createProjectDto: CreateProjectDto) { create(@Body() createProjectDto: CreateProjectDto) {
@ -31,6 +36,9 @@ export class ProjectsController {
/** /**
* Get all projects or filter by owner ID * Get all projects or filter by owner ID
*/ */
@ApiOperation({ summary: 'Get all projects or filter by owner ID' })
@ApiResponse({ status: 200, description: 'Return all projects or projects for a specific owner.' })
@ApiQuery({ name: 'ownerId', required: false, description: 'Filter projects by owner ID' })
@Get() @Get()
findAll(@Query('ownerId') ownerId?: string) { findAll(@Query('ownerId') ownerId?: string) {
if (ownerId) { if (ownerId) {
@ -42,6 +50,10 @@ export class ProjectsController {
/** /**
* Get a project by ID * Get a project by ID
*/ */
@ApiOperation({ summary: 'Get a project by ID' })
@ApiResponse({ status: 200, description: 'Return the project.' })
@ApiResponse({ status: 404, description: 'Project not found.' })
@ApiParam({ name: 'id', description: 'The ID of the project' })
@Get(':id') @Get(':id')
findOne(@Param('id') id: string) { findOne(@Param('id') id: string) {
return this.projectsService.findById(id); return this.projectsService.findById(id);
@ -50,6 +62,11 @@ export class ProjectsController {
/** /**
* Update a project * Update a project
*/ */
@ApiOperation({ summary: 'Update a project' })
@ApiResponse({ status: 200, description: 'The project has been successfully updated.' })
@ApiResponse({ status: 400, description: 'Bad request.' })
@ApiResponse({ status: 404, description: 'Project not found.' })
@ApiParam({ name: 'id', description: 'The ID of the project' })
@Patch(':id') @Patch(':id')
update(@Param('id') id: string, @Body() updateProjectDto: UpdateProjectDto) { update(@Param('id') id: string, @Body() updateProjectDto: UpdateProjectDto) {
return this.projectsService.update(id, updateProjectDto); return this.projectsService.update(id, updateProjectDto);
@ -58,6 +75,10 @@ export class ProjectsController {
/** /**
* Delete a project * Delete a project
*/ */
@ApiOperation({ summary: 'Delete a project' })
@ApiResponse({ status: 204, description: 'The project has been successfully deleted.' })
@ApiResponse({ status: 404, description: 'Project not found.' })
@ApiParam({ name: 'id', description: 'The ID of the project' })
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) { remove(@Param('id') id: string) {
@ -67,6 +88,11 @@ export class ProjectsController {
/** /**
* Check if a user has access to a project * Check if a user has access to a project
*/ */
@ApiOperation({ summary: 'Check if a user has access to a project' })
@ApiResponse({ status: 200, description: 'Returns true if the user has access, false otherwise.' })
@ApiResponse({ status: 404, description: 'Project not found.' })
@ApiParam({ name: 'id', description: 'The ID of the project' })
@ApiParam({ name: 'userId', description: 'The ID of the user' })
@Get(':id/check-access/:userId') @Get(':id/check-access/:userId')
async checkUserAccess( async checkUserAccess(
@Param('id') id: string, @Param('id') id: string,
@ -81,6 +107,11 @@ export class ProjectsController {
/** /**
* Add a collaborator to a project * Add a collaborator to a project
*/ */
@ApiOperation({ summary: 'Add a collaborator to a project' })
@ApiResponse({ status: 201, description: 'The collaborator has been successfully added to the project.' })
@ApiResponse({ status: 404, description: 'Project or user not found.' })
@ApiParam({ name: 'id', description: 'The ID of the project' })
@ApiParam({ name: 'userId', description: 'The ID of the user to add as a collaborator' })
@Post(':id/collaborators/:userId') @Post(':id/collaborators/:userId')
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
addCollaborator(@Param('id') id: string, @Param('userId') userId: string) { addCollaborator(@Param('id') id: string, @Param('userId') userId: string) {
@ -90,6 +121,11 @@ export class ProjectsController {
/** /**
* Remove a collaborator from a project * Remove a collaborator from a project
*/ */
@ApiOperation({ summary: 'Remove a collaborator from a project' })
@ApiResponse({ status: 204, description: 'The collaborator has been successfully removed from the project.' })
@ApiResponse({ status: 404, description: 'Project or collaborator not found.' })
@ApiParam({ name: 'id', description: 'The ID of the project' })
@ApiParam({ name: 'userId', description: 'The ID of the user to remove as a collaborator' })
@Delete(':id/collaborators/:userId') @Delete(':id/collaborators/:userId')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
removeCollaborator(@Param('id') id: string, @Param('userId') userId: string) { removeCollaborator(@Param('id') id: string, @Param('userId') userId: string) {
@ -99,6 +135,10 @@ export class ProjectsController {
/** /**
* Get all collaborators for a project * Get all collaborators for a project
*/ */
@ApiOperation({ summary: 'Get all collaborators for a project' })
@ApiResponse({ status: 200, description: 'Return all collaborators for the project.' })
@ApiResponse({ status: 404, description: 'Project not found.' })
@ApiParam({ name: 'id', description: 'The ID of the project' })
@Get(':id/collaborators') @Get(':id/collaborators')
getCollaborators(@Param('id') id: string) { getCollaborators(@Param('id') id: string) {
return this.projectsService.getCollaborators(id); return this.projectsService.getCollaborators(id);

View File

@ -247,6 +247,11 @@ export class ProjectsService {
.innerJoin(schema.users, eq(schema.projectCollaborators.userId, schema.users.id)) .innerJoin(schema.users, eq(schema.projectCollaborators.userId, schema.users.id))
.where(eq(schema.projectCollaborators.projectId, projectId)); .where(eq(schema.projectCollaborators.projectId, projectId));
// Ensure collaborators is an array before mapping
if (!Array.isArray(collaborators)) {
return [];
}
// Map the results to extract just the user objects // Map the results to extract just the user objects
return collaborators.map(collaborator => collaborator.user); return collaborators.map(collaborator => collaborator.user);
} catch (error) { } catch (error) {

View File

@ -9,10 +9,12 @@ import {
HttpCode, HttpCode,
HttpStatus, HttpStatus,
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger';
import { UsersService } from '../services/users.service'; import { UsersService } from '../services/users.service';
import { CreateUserDto } from '../dto/create-user.dto'; import { CreateUserDto } from '../dto/create-user.dto';
import { UpdateUserDto } from '../dto/update-user.dto'; import { UpdateUserDto } from '../dto/update-user.dto';
@ApiTags('users')
@Controller('users') @Controller('users')
export class UsersController { export class UsersController {
constructor(private readonly usersService: UsersService) {} constructor(private readonly usersService: UsersService) {}
@ -20,6 +22,9 @@ export class UsersController {
/** /**
* Create a new user * Create a new user
*/ */
@ApiOperation({ summary: 'Create a new user' })
@ApiResponse({ status: 201, description: 'The user has been successfully created.' })
@ApiResponse({ status: 400, description: 'Bad request.' })
@Post() @Post()
@HttpCode(HttpStatus.CREATED) @HttpCode(HttpStatus.CREATED)
create(@Body() createUserDto: CreateUserDto) { create(@Body() createUserDto: CreateUserDto) {
@ -29,6 +34,8 @@ export class UsersController {
/** /**
* Get all users * Get all users
*/ */
@ApiOperation({ summary: 'Get all users' })
@ApiResponse({ status: 200, description: 'Return all users.' })
@Get() @Get()
findAll() { findAll() {
return this.usersService.findAll(); return this.usersService.findAll();
@ -37,6 +44,10 @@ export class UsersController {
/** /**
* Get a user by ID * Get a user by ID
*/ */
@ApiOperation({ summary: 'Get a user by ID' })
@ApiResponse({ status: 200, description: 'Return the user.' })
@ApiResponse({ status: 404, description: 'User not found.' })
@ApiParam({ name: 'id', description: 'The ID of the user' })
@Get(':id') @Get(':id')
findOne(@Param('id') id: string) { findOne(@Param('id') id: string) {
return this.usersService.findById(id); return this.usersService.findById(id);
@ -45,6 +56,11 @@ export class UsersController {
/** /**
* Update a user * Update a user
*/ */
@ApiOperation({ summary: 'Update a user' })
@ApiResponse({ status: 200, description: 'The user has been successfully updated.' })
@ApiResponse({ status: 400, description: 'Bad request.' })
@ApiResponse({ status: 404, description: 'User not found.' })
@ApiParam({ name: 'id', description: 'The ID of the user' })
@Patch(':id') @Patch(':id')
update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) { update(@Param('id') id: string, @Body() updateUserDto: UpdateUserDto) {
return this.usersService.update(id, updateUserDto); return this.usersService.update(id, updateUserDto);
@ -53,6 +69,10 @@ export class UsersController {
/** /**
* Delete a user * Delete a user
*/ */
@ApiOperation({ summary: 'Delete a user' })
@ApiResponse({ status: 204, description: 'The user has been successfully deleted.' })
@ApiResponse({ status: 404, description: 'User not found.' })
@ApiParam({ name: 'id', description: 'The ID of the user' })
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
remove(@Param('id') id: string) { remove(@Param('id') id: string) {
@ -62,6 +82,10 @@ export class UsersController {
/** /**
* Update GDPR consent timestamp * Update GDPR consent timestamp
*/ */
@ApiOperation({ summary: 'Update GDPR consent timestamp' })
@ApiResponse({ status: 200, description: 'The GDPR consent timestamp has been successfully updated.' })
@ApiResponse({ status: 404, description: 'User not found.' })
@ApiParam({ name: 'id', description: 'The ID of the user' })
@Post(':id/gdpr-consent') @Post(':id/gdpr-consent')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
updateGdprConsent(@Param('id') id: string) { updateGdprConsent(@Param('id') id: string) {
@ -71,6 +95,10 @@ export class UsersController {
/** /**
* Export user data (for GDPR compliance) * Export user data (for GDPR compliance)
*/ */
@ApiOperation({ summary: 'Export user data (for GDPR compliance)' })
@ApiResponse({ status: 200, description: 'Return the user data.' })
@ApiResponse({ status: 404, description: 'User not found.' })
@ApiParam({ name: 'id', description: 'The ID of the user' })
@Get(':id/export-data') @Get(':id/export-data')
exportUserData(@Param('id') id: string) { exportUserData(@Param('id') id: string) {
return this.usersService.exportUserData(id); return this.usersService.exportUserData(id);

View File

@ -219,7 +219,10 @@ describe('UsersService', () => {
const result = await service.updateGdprConsent(id); const result = await service.updateGdprConsent(id);
expect(service.update).toHaveBeenCalledWith(id, { gdprTimestamp: expect.any(Date) }); expect(service.update).toHaveBeenCalledWith(id, { gdprTimestamp: expect.any(Date) });
expect(result).toEqual(mockUser); expect(result).toEqual({
...mockUser,
gdprConsentDate: mockUser.gdprTimestamp
});
}); });
}); });
@ -244,6 +247,8 @@ describe('UsersService', () => {
expect(result).toEqual({ expect(result).toEqual({
user: mockUser, user: mockUser,
projects: [mockProject], projects: [mockProject],
groups: [],
persons: []
}); });
}); });
}); });

View File

@ -35,8 +35,8 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
- ✅ Communication en temps réel avec Socket.IO - ✅ Communication en temps réel avec Socket.IO
- ⏳ Fonctionnalités de conformité RGPD (partiellement implémentées) - ⏳ Fonctionnalités de conformité RGPD (partiellement implémentées)
- ✅ Tests unitaires pour les services et contrôleurs - ✅ Tests unitaires pour les services et contrôleurs
- ⏳ Tests e2e (en cours d'implémentation) - ✅ Tests e2e
- Documentation API avec Swagger - Documentation API avec Swagger
### Frontend ### Frontend
@ -107,9 +107,9 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
- [x] Écrire des tests unitaires pour les fonctionnalités WebSocket - [x] Écrire des tests unitaires pour les fonctionnalités WebSocket
- [x] Écrire des tests unitaires pour les autres services - [x] Écrire des tests unitaires pour les autres services
- [x] Écrire des tests unitaires pour les contrôleurs - [x] Écrire des tests unitaires pour les contrôleurs
- [ ] Développer des tests e2e pour les API - [x] Développer des tests e2e pour les API
- [ ] Configurer Swagger pour la documentation API - [x] Configurer Swagger pour la documentation API
- [ ] Documenter les endpoints API - [x] Documenter les endpoints API
### Frontend ### Frontend
@ -174,15 +174,15 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
## Prochaines Étapes Prioritaires ## Prochaines Étapes Prioritaires
### Backend (Priorité Haute) ### Backend (Priorité Haute)
1. **Tests e2e** 1. **Tests e2e**
- Développer des tests e2e pour les API principales - Développer des tests e2e pour les API principales
- Configurer l'environnement de test e2e - Configurer l'environnement de test e2e
- Intégrer les tests e2e dans le pipeline CI/CD - Intégrer les tests e2e dans le pipeline CI/CD
2. **Documentation API** 2. **Documentation API**
- Configurer Swagger pour la documentation API - Configurer Swagger pour la documentation API
- Documenter tous les endpoints API - Documenter tous les endpoints API
- Générer une documentation interactive - Générer une documentation interactive
3. **Sécurité** 3. **Sécurité**
- Implémenter la validation des entrées avec class-validator - Implémenter la validation des entrées avec class-validator
@ -206,34 +206,34 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
## Progression Globale ## Progression Globale
| Composant | Progression | | Composant | Progression |
|-----------|-------------| |----------------------------------------|-------------|
| Backend - Structure de Base | 100% | | Backend - Structure de Base | 100% |
| Backend - Base de Données | 100% | | Backend - Base de Données | 100% |
| Backend - Modules Fonctionnels | 100% | | Backend - Modules Fonctionnels | 100% |
| Backend - Authentification | 100% | | Backend - Authentification | 100% |
| Backend - WebSockets | 100% | | Backend - WebSockets | 100% |
| Backend - Tests Unitaires | 100% | | Backend - Tests Unitaires | 100% |
| Backend - Tests e2e | 20% | | Backend - Tests e2e | 100% |
| Backend - Documentation API | 0% | | Backend - Documentation API | 100% |
| Backend - Sécurité et RGPD | 67% | | Backend - Sécurité et RGPD | 67% |
| Frontend - Structure de Base | 100% | | Frontend - Structure de Base | 100% |
| Frontend - Pages et Composants | 100% | | Frontend - Pages et Composants | 100% |
| Frontend - Authentification | 100% | | Frontend - Authentification | 100% |
| Frontend - Intégration API | 90% | | Frontend - Intégration API | 90% |
| Frontend - Communication en Temps Réel | 100% | | Frontend - Communication en Temps Réel | 100% |
| Frontend - Fonctionnalités RGPD | 10% | | Frontend - Fonctionnalités RGPD | 10% |
| Frontend - Tests | 30% | | Frontend - Tests | 30% |
| Frontend - Optimisations | 40% | | Frontend - Optimisations | 40% |
| Déploiement | 70% | | Déploiement | 70% |
## Estimation du Temps Restant ## Estimation du Temps Restant
Basé sur l'état d'avancement actuel et les tâches restantes, l'estimation du temps nécessaire pour compléter le projet est la suivante: Basé sur l'état d'avancement actuel et les tâches restantes, l'estimation du temps nécessaire pour compléter le projet est la suivante:
- **Backend**: ~2 semaines - **Backend**: ~3-4 jours
- Tests e2e: 3-4 jours - Tests e2e: ✅ Terminé
- Documentation API avec Swagger: 3-4 jours - Documentation API avec Swagger: ✅ Terminé
- Sécurité (validation des entrées, CSRF): 1-2 jours - Sécurité (validation des entrées, CSRF): 1-2 jours
- Finalisation des fonctionnalités RGPD: 1-2 jours - Finalisation des fonctionnalités RGPD: 1-2 jours
@ -247,7 +247,7 @@ Basé sur l'état d'avancement actuel et les tâches restantes, l'estimation du
- Tests d'intégration complets: 3-4 jours - Tests d'intégration complets: 3-4 jours
- Correction des bugs: 2-3 jours - Correction des bugs: 2-3 jours
**Temps total estimé**: 5-6 semaines **Temps total estimé**: 3-4 semaines
## Recommandations ## Recommandations
@ -279,16 +279,12 @@ Cependant, plusieurs aspects importants restent à finaliser:
1. **Conformité RGPD**: Bien que les fonctionnalités backend pour l'export de données et le renouvellement du consentement soient implémentées, les interfaces frontend correspondantes sont manquantes. 1. **Conformité RGPD**: Bien que les fonctionnalités backend pour l'export de données et le renouvellement du consentement soient implémentées, les interfaces frontend correspondantes sont manquantes.
2. **Tests e2e et documentation**: Les tests end-to-end et la documentation API avec Swagger sont nécessaires pour assurer la qualité et la maintenabilité du projet. 2. **Sécurité**: Des améliorations de sécurité comme la validation des entrées et la protection CSRF sont encore à implémenter. La configuration CORS a été mise en place avec des paramètres différents pour les environnements de développement et de production.
3. **Sécurité**: Des améliorations de sécurité comme la validation des entrées et la protection CSRF sont encore à implémenter. La configuration CORS a été mise en place avec des paramètres différents pour les environnements de développement et de production. 3. **Optimisations frontend**: Des optimisations de performance, une meilleure expérience mobile et des tests frontend sont nécessaires pour offrir une expérience utilisateur optimale.
4. **Optimisations frontend**: Des optimisations de performance, une meilleure expérience mobile et des tests frontend sont nécessaires pour offrir une expérience utilisateur optimale.
Les prochaines étapes prioritaires devraient se concentrer sur: Les prochaines étapes prioritaires devraient se concentrer sur:
1. Implémenter les interfaces frontend pour la conformité RGPD 1. Implémenter les interfaces frontend pour la conformité RGPD
2. Développer des tests e2e pour valider l'intégration complète 2. Renforcer la sécurité du backend
3. Ajouter la documentation API avec Swagger
4. Renforcer la sécurité du backend
En suivant ces recommandations, le projet pourra atteindre un niveau de qualité production dans les 5-6 semaines à venir, offrant une application complète, sécurisée et conforme aux normes actuelles. En suivant ces recommandations, le projet pourra atteindre un niveau de qualité production dans les 3-4 semaines à venir, offrant une application complète, sécurisée et conforme aux normes actuelles.