Compare commits

...

9 Commits

Author SHA1 Message Date
cf292de428 docs: update documentation to reflect module completion and testing progress
Updated `PROJECT_STATUS.md` with completed modules (`auth`, `groups`, `tags`) and unit testing progress, including marked tests for controllers and services as done. Added logical and conceptual database models (`DATABASE_SCHEMA_PLAN.md`) and revised implementation statuses in `IMPLEMENTATION_GUIDE.md`.
2025-05-15 20:57:59 +02:00
2de57e6e6f feat: export projectCollaborators in schema index 2025-05-15 20:57:45 +02:00
92c44bce6f feat: add project collaborators relations
Defined `projectCollaborators` relations in the database schema. Updated `usersRelations` and `projectsRelations` to include `projectCollaborations` and `collaborators` respectively. Added `projectCollaboratorsRelations` to establish relationships with `projects` and `users`.
2025-05-15 20:57:32 +02:00
0154f9c0aa feat: add project collaborators schema
Introduced `projectCollaborators` schema to define project-user relationships. Includes indices and a unique constraint for `projectId` and `userId`. Added corresponding type definitions for select and insert operations.
2025-05-15 20:57:13 +02:00
c16c8d51d2 feat: add collaborator management to projects module
Added endpoints to manage collaborators in `ProjectsController`:
- Add collaborator
- Remove collaborator
- Get project collaborators

Updated `ProjectsService` with corresponding methods and enhanced `checkUserAccess` to validate user access as owner or collaborator. Included unit tests for new functionality in controllers and services.
2025-05-15 20:56:43 +02:00
576d063e52 test: add unit tests for users module
Added comprehensive unit tests for `UsersController` and `UsersService`, covering CRUD operations, GDPR consent updates, data export, and exception handling. Mocked `JwtAuthGuard` and database operations for all tests.
2025-05-15 20:48:42 +02:00
269ba622f8 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.
2025-05-15 20:31:07 +02:00
0f3c55f947 docs: update project status to reflect completed modules and testing progress
Updated `PROJECT_STATUS.md` to mark the `groups` and `tags` modules as completed, along with unit tests for services and controllers. Adjusted backend progress percentages and revised remaining priorities, focusing on module relations and real-time communication features.
2025-05-15 19:49:00 +02:00
50583f9ccc test: add unit tests for tags module and global auth guard
Added comprehensive unit tests for `TagsService` and `TagsController`, covering CRUD operations, exception handling, and associations with projects and persons. Mocked `JwtAuthGuard` globally for testing purposes.
2025-05-15 19:48:44 +02:00
17 changed files with 2292 additions and 43 deletions

View File

@ -18,6 +18,7 @@ export * from './tags';
export * from './personToGroup'; export * from './personToGroup';
export * from './personToTag'; export * from './personToTag';
export * from './projectToTag'; export * from './projectToTag';
export * from './projectCollaborators';
// Export relations // Export relations
export * from './relations'; export * from './relations';

View File

@ -0,0 +1,25 @@
import { pgTable, uuid, timestamp, index, uniqueIndex } from 'drizzle-orm/pg-core';
import { projects } from './projects';
import { users } from './users';
/**
* Project Collaborators relation table schema
*/
export const projectCollaborators = pgTable('project_collaborators', {
id: uuid('id').primaryKey().defaultRandom(),
projectId: uuid('projectId').notNull().references(() => projects.id, { onDelete: 'cascade' }),
userId: uuid('userId').notNull().references(() => users.id, { onDelete: 'cascade' }),
createdAt: timestamp('createdAt', { withTimezone: true }).defaultNow().notNull()
}, (table) => {
return {
projectIdIdx: index('pc_projectId_idx').on(table.projectId),
userIdIdx: index('pc_userId_idx').on(table.userId),
projectUserUniqueIdx: uniqueIndex('pc_project_user_unique_idx').on(table.projectId, table.userId)
};
});
/**
* ProjectCollaborators type definitions
*/
export type ProjectCollaborator = typeof projectCollaborators.$inferSelect;
export type NewProjectCollaborator = typeof projectCollaborators.$inferInsert;

View File

@ -7,12 +7,14 @@ import { tags } from './tags';
import { personToGroup } from './personToGroup'; import { personToGroup } from './personToGroup';
import { personToTag } from './personToTag'; import { personToTag } from './personToTag';
import { projectToTag } from './projectToTag'; import { projectToTag } from './projectToTag';
import { projectCollaborators } from './projectCollaborators';
/** /**
* Define relations for users table * Define relations for users table
*/ */
export const usersRelations = relations(users, ({ many }) => ({ export const usersRelations = relations(users, ({ many }) => ({
projects: many(projects), projects: many(projects),
projectCollaborations: many(projectCollaborators),
})); }));
/** /**
@ -26,6 +28,7 @@ export const projectsRelations = relations(projects, ({ one, many }) => ({
persons: many(persons), persons: many(persons),
groups: many(groups), groups: many(groups),
projectToTags: many(projectToTag), projectToTags: many(projectToTag),
collaborators: many(projectCollaborators),
})); }));
/** /**
@ -99,4 +102,18 @@ export const projectToTagRelations = relations(projectToTag, ({ one }) => ({
fields: [projectToTag.tagId], fields: [projectToTag.tagId],
references: [tags.id], references: [tags.id],
}), }),
})); }));
/**
* Define relations for projectCollaborators table
*/
export const projectCollaboratorsRelations = relations(projectCollaborators, ({ one }) => ({
project: one(projects, {
fields: [projectCollaborators.projectId],
references: [projects.id],
}),
user: one(users, {
fields: [projectCollaborators.userId],
references: [users.id],
}),
}));

View File

@ -3,6 +3,19 @@ import { Reflector } from '@nestjs/core';
import { JwtAuthGuard } from './jwt-auth.guard'; import { JwtAuthGuard } from './jwt-auth.guard';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
// Mock the AuthGuard
jest.mock('@nestjs/passport', () => {
return {
AuthGuard: jest.fn().mockImplementation(() => {
return class {
canActivate() {
return true;
}
};
}),
};
});
describe('JwtAuthGuard', () => { describe('JwtAuthGuard', () => {
let guard: JwtAuthGuard; let guard: JwtAuthGuard;
let reflector: Reflector; let reflector: Reflector;

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,164 @@
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(),
};
const mockUser = {
id: 'user2',
name: 'Test User',
githubId: '12345',
createdAt: new Date(),
updatedAt: new Date(),
};
const mockCollaboration = {
id: 'collab1',
projectId: 'project1',
userId: 'user2',
createdAt: 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),
addCollaborator: jest.fn().mockResolvedValue(mockCollaboration),
removeCollaborator: jest.fn().mockResolvedValue(mockCollaboration),
getCollaborators: jest.fn().mockResolvedValue([{ user: mockUser }]),
},
},
],
})
.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);
});
});
describe('addCollaborator', () => {
it('should add a collaborator to a project', async () => {
const projectId = 'project1';
const userId = 'user2';
expect(await controller.addCollaborator(projectId, userId)).toBe(mockCollaboration);
expect(service.addCollaborator).toHaveBeenCalledWith(projectId, userId);
});
});
describe('removeCollaborator', () => {
it('should remove a collaborator from a project', async () => {
const projectId = 'project1';
const userId = 'user2';
expect(await controller.removeCollaborator(projectId, userId)).toBe(mockCollaboration);
expect(service.removeCollaborator).toHaveBeenCalledWith(projectId, userId);
});
});
describe('getCollaborators', () => {
it('should get all collaborators for a project', async () => {
const projectId = 'project1';
const mockCollaborators = [{ user: mockUser }];
expect(await controller.getCollaborators(projectId)).toEqual(mockCollaborators);
expect(service.getCollaborators).toHaveBeenCalledWith(projectId);
});
});
});

View File

@ -70,4 +70,30 @@ export class ProjectsController {
checkUserAccess(@Param('id') id: string, @Param('userId') userId: string) { checkUserAccess(@Param('id') id: string, @Param('userId') userId: string) {
return this.projectsService.checkUserAccess(id, userId); return this.projectsService.checkUserAccess(id, userId);
} }
}
/**
* Add a collaborator to a project
*/
@Post(':id/collaborators/:userId')
@HttpCode(HttpStatus.CREATED)
addCollaborator(@Param('id') id: string, @Param('userId') userId: string) {
return this.projectsService.addCollaborator(id, userId);
}
/**
* Remove a collaborator from a project
*/
@Delete(':id/collaborators/:userId')
@HttpCode(HttpStatus.NO_CONTENT)
removeCollaborator(@Param('id') id: string, @Param('userId') userId: string) {
return this.projectsService.removeCollaborator(id, userId);
}
/**
* Get all collaborators for a project
*/
@Get(':id/collaborators')
getCollaborators(@Param('id') id: string) {
return this.projectsService.getCollaborators(id);
}
}

View File

@ -0,0 +1,395 @@
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(),
};
const mockUser = {
id: 'user2',
name: 'Test User',
githubId: '12345',
createdAt: new Date(),
updatedAt: new Date(),
};
const mockCollaboration = {
id: 'collab1',
projectId: 'project1',
userId: 'user2',
createdAt: 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(),
innerJoin: 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 is the owner of the project', async () => {
const projectId = 'project1';
const userId = 'user1';
// Mock owner check
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 true if user is a collaborator on the project', async () => {
const projectId = 'project1';
const userId = 'user2';
// Mock owner check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
// Mock collaborator check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockCollaboration]);
const result = await service.checkUserAccess(projectId, userId);
expect(mockDb.select).toHaveBeenCalledTimes(2);
expect(mockDb.from).toHaveBeenCalledTimes(2);
expect(mockDb.where).toHaveBeenCalledTimes(2);
expect(result).toBe(true);
});
it('should return false if user does not have access to project', async () => {
const projectId = 'project1';
const userId = 'user3';
// Mock owner check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
// Mock collaborator check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
const result = await service.checkUserAccess(projectId, userId);
expect(mockDb.select).toHaveBeenCalledTimes(2);
expect(mockDb.from).toHaveBeenCalledTimes(2);
expect(mockDb.where).toHaveBeenCalledTimes(2);
expect(result).toBe(false);
});
});
describe('addCollaborator', () => {
it('should add a collaborator to a project', async () => {
const projectId = 'project1';
const userId = 'user2';
// Mock findById
jest.spyOn(service, 'findById').mockResolvedValueOnce(mockProject);
// Mock user check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockUser]);
// Mock relation check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
// Mock insert
mockDb.insert.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.values.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockCollaboration]);
const result = await service.addCollaborator(projectId, userId);
expect(service.findById).toHaveBeenCalledWith(projectId);
expect(mockDb.select).toHaveBeenCalledTimes(2);
expect(mockDb.from).toHaveBeenCalledTimes(2);
expect(mockDb.where).toHaveBeenCalledTimes(2);
expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith({
projectId,
userId,
});
expect(result).toEqual(mockCollaboration);
});
it('should return existing collaboration if user is already a collaborator', async () => {
const projectId = 'project1';
const userId = 'user2';
// Mock findById
jest.spyOn(service, 'findById').mockResolvedValueOnce(mockProject);
// Mock user check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockUser]);
// Mock relation check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockCollaboration]);
const result = await service.addCollaborator(projectId, userId);
expect(service.findById).toHaveBeenCalledWith(projectId);
expect(mockDb.select).toHaveBeenCalledTimes(2);
expect(mockDb.from).toHaveBeenCalledTimes(2);
expect(mockDb.where).toHaveBeenCalledTimes(2);
expect(mockDb.insert).not.toHaveBeenCalled();
expect(result).toEqual(mockCollaboration);
});
it('should throw NotFoundException if user not found', async () => {
const projectId = 'project1';
const userId = 'nonexistent';
// Mock findById
jest.spyOn(service, 'findById').mockResolvedValueOnce(mockProject);
// Mock user check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
await expect(service.addCollaborator(projectId, userId)).rejects.toThrow(NotFoundException);
});
});
describe('removeCollaborator', () => {
it('should remove a collaborator from a project', async () => {
const projectId = 'project1';
const userId = 'user2';
mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockCollaboration]);
const result = await service.removeCollaborator(projectId, userId);
expect(mockDb.delete).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockCollaboration);
});
it('should throw NotFoundException if collaboration not found', async () => {
const projectId = 'project1';
const userId = 'nonexistent';
mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => []);
await expect(service.removeCollaborator(projectId, userId)).rejects.toThrow(NotFoundException);
});
});
describe('getCollaborators', () => {
it('should get all collaborators for a project', async () => {
const projectId = 'project1';
const mockCollaborators = [{ user: mockUser }];
// Mock findById
jest.spyOn(service, 'findById').mockResolvedValueOnce(mockProject);
// Mock get collaborators
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.innerJoin.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockCollaborators);
const result = await service.getCollaborators(projectId);
expect(service.findById).toHaveBeenCalledWith(projectId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.innerJoin).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
});
});
});

View File

@ -45,11 +45,11 @@ export class ProjectsService {
.select() .select()
.from(schema.projects) .from(schema.projects)
.where(eq(schema.projects.id, id)); .where(eq(schema.projects.id, id));
if (!project) { if (!project) {
throw new NotFoundException(`Project with ID ${id} not found`); throw new NotFoundException(`Project with ID ${id} not found`);
} }
return project; return project;
} }
@ -65,11 +65,11 @@ export class ProjectsService {
}) })
.where(eq(schema.projects.id, id)) .where(eq(schema.projects.id, id))
.returning(); .returning();
if (!project) { if (!project) {
throw new NotFoundException(`Project with ID ${id} not found`); throw new NotFoundException(`Project with ID ${id} not found`);
} }
return project; return project;
} }
@ -81,11 +81,11 @@ export class ProjectsService {
.delete(schema.projects) .delete(schema.projects)
.where(eq(schema.projects.id, id)) .where(eq(schema.projects.id, id))
.returning(); .returning();
if (!project) { if (!project) {
throw new NotFoundException(`Project with ID ${id} not found`); throw new NotFoundException(`Project with ID ${id} not found`);
} }
return project; return project;
} }
@ -93,6 +93,7 @@ export class ProjectsService {
* Check if a user has access to a project * Check if a user has access to a project
*/ */
async checkUserAccess(projectId: string, userId: string) { async checkUserAccess(projectId: string, userId: string) {
// Check if the user is the owner of the project
const [project] = await this.db const [project] = await this.db
.select() .select()
.from(schema.projects) .from(schema.projects)
@ -102,7 +103,104 @@ export class ProjectsService {
eq(schema.projects.ownerId, userId) eq(schema.projects.ownerId, userId)
) )
); );
return !!project; if (project) {
return true;
}
// Check if the user is a collaborator on the project
const [collaboration] = await this.db
.select()
.from(schema.projectCollaborators)
.where(
and(
eq(schema.projectCollaborators.projectId, projectId),
eq(schema.projectCollaborators.userId, userId)
)
);
return !!collaboration;
} }
}
/**
* Add a collaborator to a project
*/
async addCollaborator(projectId: string, userId: string) {
// Check if the project exists
await this.findById(projectId);
// Check if the user exists
const [user] = await this.db
.select()
.from(schema.users)
.where(eq(schema.users.id, userId));
if (!user) {
throw new NotFoundException(`User with ID ${userId} not found`);
}
// Check if the user is already a collaborator on the project
const [existingCollaboration] = await this.db
.select()
.from(schema.projectCollaborators)
.where(
and(
eq(schema.projectCollaborators.projectId, projectId),
eq(schema.projectCollaborators.userId, userId)
)
);
if (existingCollaboration) {
return existingCollaboration;
}
// Add the user as a collaborator on the project
const [collaboration] = await this.db
.insert(schema.projectCollaborators)
.values({
projectId,
userId,
})
.returning();
return collaboration;
}
/**
* Remove a collaborator from a project
*/
async removeCollaborator(projectId: string, userId: string) {
const [collaboration] = await this.db
.delete(schema.projectCollaborators)
.where(
and(
eq(schema.projectCollaborators.projectId, projectId),
eq(schema.projectCollaborators.userId, userId)
)
)
.returning();
if (!collaboration) {
throw new NotFoundException(`User with ID ${userId} is not a collaborator on project with ID ${projectId}`);
}
return collaboration;
}
/**
* Get all collaborators for a project
*/
async getCollaborators(projectId: string) {
// Check if the project exists
await this.findById(projectId);
// Get all collaborators for the project
return this.db
.select({
user: schema.users,
})
.from(schema.projectCollaborators)
.innerJoin(schema.users, eq(schema.projectCollaborators.userId, schema.users.id))
.where(eq(schema.projectCollaborators.projectId, projectId));
}
}

View File

@ -0,0 +1,179 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TagsController } from './tags.controller';
import { TagsService } from '../services/tags.service';
import { CreateTagDto } from '../dto/create-tag.dto';
import { UpdateTagDto } from '../dto/update-tag.dto';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
describe('TagsController', () => {
let controller: TagsController;
let service: TagsService;
// Mock data
const mockTag = {
id: 'tag1',
name: 'Test Tag',
description: 'Test Description',
color: '#FF0000',
type: 'PERSON',
createdAt: new Date(),
updatedAt: new Date(),
};
const mockPersonToTag = {
personId: 'person1',
tagId: 'tag1',
};
const mockProjectToTag = {
projectId: 'project1',
tagId: 'tag1',
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [TagsController],
providers: [
{
provide: TagsService,
useValue: {
create: jest.fn().mockResolvedValue(mockTag),
findAll: jest.fn().mockResolvedValue([mockTag]),
findByType: jest.fn().mockResolvedValue([mockTag]),
findById: jest.fn().mockResolvedValue(mockTag),
update: jest.fn().mockResolvedValue(mockTag),
remove: jest.fn().mockResolvedValue(mockTag),
addTagToPerson: jest.fn().mockResolvedValue(mockPersonToTag),
removeTagFromPerson: jest.fn().mockResolvedValue(mockPersonToTag),
getTagsForPerson: jest.fn().mockResolvedValue([{ tag: mockTag }]),
addTagToProject: jest.fn().mockResolvedValue(mockProjectToTag),
removeTagFromProject: jest.fn().mockResolvedValue(mockProjectToTag),
getTagsForProject: jest.fn().mockResolvedValue([{ tag: mockTag }]),
},
},
],
})
.overrideGuard(JwtAuthGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<TagsController>(TagsController);
service = module.get<TagsService>(TagsService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create', () => {
it('should create a new tag', async () => {
const createTagDto: CreateTagDto = {
name: 'Test Tag',
color: '#FF0000',
type: 'PERSON',
};
expect(await controller.create(createTagDto)).toBe(mockTag);
expect(service.create).toHaveBeenCalledWith(createTagDto);
});
});
describe('findAll', () => {
it('should return all tags when no type is provided', async () => {
expect(await controller.findAll()).toEqual([mockTag]);
expect(service.findAll).toHaveBeenCalled();
});
it('should return tags filtered by type when type is provided', async () => {
const type = 'PERSON';
expect(await controller.findAll(type)).toEqual([mockTag]);
expect(service.findByType).toHaveBeenCalledWith(type);
});
});
describe('findOne', () => {
it('should return a tag by ID', async () => {
const id = 'tag1';
expect(await controller.findOne(id)).toBe(mockTag);
expect(service.findById).toHaveBeenCalledWith(id);
});
});
describe('update', () => {
it('should update a tag', async () => {
const id = 'tag1';
const updateTagDto: UpdateTagDto = {
name: 'Updated Tag',
};
expect(await controller.update(id, updateTagDto)).toBe(mockTag);
expect(service.update).toHaveBeenCalledWith(id, updateTagDto);
});
});
describe('remove', () => {
it('should delete a tag', async () => {
const id = 'tag1';
expect(await controller.remove(id)).toBe(mockTag);
expect(service.remove).toHaveBeenCalledWith(id);
});
});
describe('addTagToPerson', () => {
it('should add a tag to a person', async () => {
const personId = 'person1';
const tagId = 'tag1';
expect(await controller.addTagToPerson(personId, tagId)).toBe(mockPersonToTag);
expect(service.addTagToPerson).toHaveBeenCalledWith(tagId, personId);
});
});
describe('removeTagFromPerson', () => {
it('should remove a tag from a person', async () => {
const personId = 'person1';
const tagId = 'tag1';
expect(await controller.removeTagFromPerson(personId, tagId)).toBe(mockPersonToTag);
expect(service.removeTagFromPerson).toHaveBeenCalledWith(tagId, personId);
});
});
describe('getTagsForPerson', () => {
it('should get all tags for a person', async () => {
const personId = 'person1';
expect(await controller.getTagsForPerson(personId)).toEqual([{ tag: mockTag }]);
expect(service.getTagsForPerson).toHaveBeenCalledWith(personId);
});
});
describe('addTagToProject', () => {
it('should add a tag to a project', async () => {
const projectId = 'project1';
const tagId = 'tag1';
expect(await controller.addTagToProject(projectId, tagId)).toBe(mockProjectToTag);
expect(service.addTagToProject).toHaveBeenCalledWith(tagId, projectId);
});
});
describe('removeTagFromProject', () => {
it('should remove a tag from a project', async () => {
const projectId = 'project1';
const tagId = 'tag1';
expect(await controller.removeTagFromProject(projectId, tagId)).toBe(mockProjectToTag);
expect(service.removeTagFromProject).toHaveBeenCalledWith(tagId, projectId);
});
});
describe('getTagsForProject', () => {
it('should get all tags for a project', async () => {
const projectId = 'project1';
expect(await controller.getTagsForProject(projectId)).toEqual([{ tag: mockTag }]);
expect(service.getTagsForProject).toHaveBeenCalledWith(projectId);
});
});
});

View File

@ -0,0 +1,339 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TagsService } from './tags.service';
import { NotFoundException } from '@nestjs/common';
import { DRIZZLE } from '../../../database/database.module';
describe('TagsService', () => {
let service: TagsService;
let mockDb: any;
// Mock data
const mockTag = {
id: 'tag1',
name: 'Test Tag',
description: 'Test Description',
color: '#FF0000',
type: 'PERSON',
createdAt: new Date(),
updatedAt: new Date(),
};
const mockPerson = {
id: 'person1',
name: 'Test Person',
projectId: 'project1',
createdAt: new Date(),
updatedAt: new Date(),
};
const mockProject = {
id: 'project1',
name: 'Test Project',
userId: 'user1',
createdAt: new Date(),
updatedAt: new Date(),
};
const mockPersonToTag = {
personId: 'person1',
tagId: 'tag1',
};
const mockProjectToTag = {
projectId: 'project1',
tagId: 'tag1',
};
// 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 [mockTag];
}),
};
beforeEach(async () => {
mockDb = {
...mockDbOperations,
};
const module: TestingModule = await Test.createTestingModule({
providers: [
TagsService,
{
provide: DRIZZLE,
useValue: mockDb,
},
],
}).compile();
service = module.get<TagsService>(TagsService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a new tag', async () => {
const createTagDto = {
name: 'Test Tag',
color: '#FF0000',
type: 'PERSON' as 'PERSON',
};
const result = await service.create(createTagDto);
expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith({
...createTagDto,
});
expect(result).toEqual(mockTag);
});
});
describe('findAll', () => {
it('should return all tags', async () => {
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => [mockTag]);
const result = await service.findAll();
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(result).toEqual([mockTag]);
});
});
describe('findByType', () => {
it('should return tags for a specific type', async () => {
const type = 'PERSON';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockTag]);
const result = await service.findByType(type);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual([mockTag]);
});
});
describe('findById', () => {
it('should return a tag by ID', async () => {
const id = 'tag1';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockTag]);
const result = await service.findById(id);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockTag);
});
it('should throw NotFoundException if tag 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 tag', async () => {
const id = 'tag1';
const updateTagDto = {
name: 'Updated Tag',
};
const result = await service.update(id, updateTagDto);
expect(mockDb.update).toHaveBeenCalled();
expect(mockDb.set).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockTag);
});
it('should throw NotFoundException if tag not found', async () => {
const id = 'nonexistent';
const updateTagDto = {
name: 'Updated Tag',
};
mockDb.update.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.set.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => []);
await expect(service.update(id, updateTagDto)).rejects.toThrow(NotFoundException);
});
});
describe('remove', () => {
it('should delete a tag', async () => {
const id = 'tag1';
const result = await service.remove(id);
expect(mockDb.delete).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockTag);
});
it('should throw NotFoundException if tag 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('addTagToPerson', () => {
it('should add a tag to a person', async () => {
const tagId = 'tag1';
const personId = 'person1';
// Mock findById to return a PERSON tag
jest.spyOn(service, 'findById').mockResolvedValueOnce(mockTag);
// Mock person check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockPerson]);
// Mock relation check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
// Mock insert
mockDb.insert.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.values.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToTag]);
const result = await service.addTagToPerson(tagId, personId);
expect(service.findById).toHaveBeenCalledWith(tagId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith({
personId,
tagId,
});
expect(result).toEqual(mockPersonToTag);
});
});
describe('getTagsForPerson', () => {
it('should get all tags for a person', async () => {
const personId = 'person1';
// Mock person check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockPerson]);
// Mock get tags
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.innerJoin.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [{ tag: mockTag }]);
const result = await service.getTagsForPerson(personId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.innerJoin).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual([{ tag: mockTag }]);
});
});
describe('addTagToProject', () => {
it('should add a tag to a project', async () => {
const tagId = 'tag1';
const projectId = 'project1';
// Mock findById to return a PROJECT tag
const projectTag = { ...mockTag, type: 'PROJECT' };
jest.spyOn(service, 'findById').mockResolvedValueOnce(projectTag);
// Mock project check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockProject]);
// Mock relation check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
// Mock insert
mockDb.insert.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.values.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockProjectToTag]);
const result = await service.addTagToProject(tagId, projectId);
expect(service.findById).toHaveBeenCalledWith(tagId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith({
projectId,
tagId,
});
expect(result).toEqual(mockProjectToTag);
});
});
describe('getTagsForProject', () => {
it('should get all tags for a project', async () => {
const projectId = 'project1';
// Mock project check
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockProject]);
// Mock get tags
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.innerJoin.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [{ tag: mockTag }]);
const result = await service.getTagsForProject(projectId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.innerJoin).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual([{ tag: mockTag }]);
});
});
});

View File

@ -0,0 +1,127 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersController } from './users.controller';
import { UsersService } from '../services/users.service';
import { CreateUserDto } from '../dto/create-user.dto';
import { UpdateUserDto } from '../dto/update-user.dto';
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
describe('UsersController', () => {
let controller: UsersController;
let service: UsersService;
// Mock data
const mockUser = {
id: 'user1',
name: 'Test User',
avatar: 'https://example.com/avatar.jpg',
githubId: '12345',
metadata: {},
gdprTimestamp: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
const mockUserData = {
user: mockUser,
projects: [
{
id: 'project1',
name: 'Test Project',
ownerId: 'user1',
},
],
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
providers: [
{
provide: UsersService,
useValue: {
create: jest.fn().mockResolvedValue(mockUser),
findAll: jest.fn().mockResolvedValue([mockUser]),
findById: jest.fn().mockResolvedValue(mockUser),
update: jest.fn().mockResolvedValue(mockUser),
remove: jest.fn().mockResolvedValue(mockUser),
updateGdprConsent: jest.fn().mockResolvedValue(mockUser),
exportUserData: jest.fn().mockResolvedValue(mockUserData),
},
},
],
})
.overrideGuard(JwtAuthGuard)
.useValue({ canActivate: () => true })
.compile();
controller = module.get<UsersController>(UsersController);
service = module.get<UsersService>(UsersService);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create', () => {
it('should create a new user', async () => {
const createUserDto: CreateUserDto = {
name: 'Test User',
githubId: '12345',
};
expect(await controller.create(createUserDto)).toBe(mockUser);
expect(service.create).toHaveBeenCalledWith(createUserDto);
});
});
describe('findAll', () => {
it('should return all users', async () => {
expect(await controller.findAll()).toEqual([mockUser]);
expect(service.findAll).toHaveBeenCalled();
});
});
describe('findOne', () => {
it('should return a user by ID', async () => {
const id = 'user1';
expect(await controller.findOne(id)).toBe(mockUser);
expect(service.findById).toHaveBeenCalledWith(id);
});
});
describe('update', () => {
it('should update a user', async () => {
const id = 'user1';
const updateUserDto: UpdateUserDto = {
name: 'Updated User',
};
expect(await controller.update(id, updateUserDto)).toBe(mockUser);
expect(service.update).toHaveBeenCalledWith(id, updateUserDto);
});
});
describe('remove', () => {
it('should delete a user', async () => {
const id = 'user1';
expect(await controller.remove(id)).toBe(mockUser);
expect(service.remove).toHaveBeenCalledWith(id);
});
});
describe('updateGdprConsent', () => {
it('should update GDPR consent timestamp', async () => {
const id = 'user1';
expect(await controller.updateGdprConsent(id)).toBe(mockUser);
expect(service.updateGdprConsent).toHaveBeenCalledWith(id);
});
});
describe('exportUserData', () => {
it('should export user data', async () => {
const id = 'user1';
expect(await controller.exportUserData(id)).toBe(mockUserData);
expect(service.exportUserData).toHaveBeenCalledWith(id);
});
});
});

View File

@ -0,0 +1,250 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersService } from './users.service';
import { NotFoundException } from '@nestjs/common';
import { DRIZZLE } from '../../../database/database.module';
describe('UsersService', () => {
let service: UsersService;
let mockDb: any;
// Mock data
const mockUser = {
id: 'user1',
name: 'Test User',
avatar: 'https://example.com/avatar.jpg',
githubId: '12345',
metadata: {},
gdprTimestamp: new Date(),
createdAt: new Date(),
updatedAt: new Date(),
};
const mockProject = {
id: 'project1',
name: 'Test Project',
ownerId: 'user1',
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 [mockUser];
}),
};
beforeEach(async () => {
mockDb = {
...mockDbOperations,
};
const module: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{
provide: DRIZZLE,
useValue: mockDb,
},
],
}).compile();
service = module.get<UsersService>(UsersService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a new user', async () => {
const createUserDto = {
name: 'Test User',
githubId: '12345',
};
const result = await service.create(createUserDto);
expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith({
...createUserDto,
gdprTimestamp: expect.any(Date),
});
expect(result).toEqual(mockUser);
});
});
describe('findAll', () => {
it('should return all users', async () => {
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => [mockUser]);
const result = await service.findAll();
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(result).toEqual([mockUser]);
});
});
describe('findById', () => {
it('should return a user by ID', async () => {
const id = 'user1';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockUser]);
const result = await service.findById(id);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockUser);
});
it('should throw NotFoundException if user 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('findByGithubId', () => {
it('should return a user by GitHub ID', async () => {
const githubId = '12345';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockUser]);
const result = await service.findByGithubId(githubId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockUser);
});
it('should return undefined if user not found', async () => {
const githubId = 'nonexistent';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => []);
const result = await service.findByGithubId(githubId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toBeUndefined();
});
});
describe('update', () => {
it('should update a user', async () => {
const id = 'user1';
const updateUserDto = {
name: 'Updated User',
};
const result = await service.update(id, updateUserDto);
expect(mockDb.update).toHaveBeenCalled();
expect(mockDb.set).toHaveBeenCalledWith({
...updateUserDto,
updatedAt: expect.any(Date),
});
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockUser);
});
it('should throw NotFoundException if user not found', async () => {
const id = 'nonexistent';
const updateUserDto = {
name: 'Updated User',
};
mockDb.update.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.set.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => []);
await expect(service.update(id, updateUserDto)).rejects.toThrow(NotFoundException);
});
});
describe('remove', () => {
it('should delete a user', async () => {
const id = 'user1';
const result = await service.remove(id);
expect(mockDb.delete).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockUser);
});
it('should throw NotFoundException if user 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('updateGdprConsent', () => {
it('should update GDPR consent timestamp', async () => {
const id = 'user1';
// Mock the update method
jest.spyOn(service, 'update').mockResolvedValueOnce(mockUser);
const result = await service.updateGdprConsent(id);
expect(service.update).toHaveBeenCalledWith(id, { gdprTimestamp: expect.any(Date) });
expect(result).toEqual(mockUser);
});
});
describe('exportUserData', () => {
it('should export user data', async () => {
const id = 'user1';
// Mock the findById method
jest.spyOn(service, 'findById').mockResolvedValueOnce(mockUser);
// Mock the database query for projects
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockProject]);
const result = await service.exportUserData(id);
expect(service.findById).toHaveBeenCalledWith(id);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual({
user: mockUser,
projects: [mockProject],
});
});
});
});

View File

@ -32,11 +32,15 @@ Pour une implémentation efficace, nous recommandons de suivre l'ordre suivant :
4. Configurer les guards et décorateurs pour la protection des routes 4. Configurer les guards et décorateurs pour la protection des routes
### Phase 3 : Modules Principaux ### Phase 3 : Modules Principaux
1. Implémenter le module projets 1. Implémenter le module projets ✅
2. Implémenter le module personnes 2. Implémenter le module personnes ✅
3. Implémenter le module groupes 3. Implémenter le module groupes ✅
4. Implémenter le module tags 4. Implémenter le module tags ✅
5. Établir les relations entre les modules 5. Établir les relations entre les modules ✅
- Relations PersonToGroup ✅
- Relations PersonToTag ✅
- Relations ProjectToTag ✅
- Relations ProjectCollaborators ✅
### Phase 4 : Communication en Temps Réel ### Phase 4 : Communication en Temps Réel
1. Configurer Socket.IO avec NestJS 1. Configurer Socket.IO avec NestJS
@ -101,4 +105,4 @@ Pour une implémentation efficace, nous recommandons de suivre l'ordre suivant :
Ce guide d'implémentation fournit une feuille de route complète pour le développement de l'application. En suivant les plans détaillés et les bonnes pratiques recommandées, vous pourrez construire une application robuste, sécurisée et performante. Ce guide d'implémentation fournit une feuille de route complète pour le développement de l'application. En suivant les plans détaillés et les bonnes pratiques recommandées, vous pourrez construire une application robuste, sécurisée et performante.
Pour plus de détails sur l'état actuel du projet et les tâches restantes, consultez le document [État d'Avancement du Projet](PROJECT_STATUS.md). Pour plus de détails sur l'état actuel du projet et les tâches restantes, consultez le document [État d'Avancement du Projet](PROJECT_STATUS.md).

View File

@ -22,19 +22,21 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
#### Composants En Cours #### Composants En Cours
- ⏳ Relations entre les modules existants - ⏳ Relations entre les modules existants
- ⏳ Tests unitaires et e2e
#### Composants Récemment Implémentés #### Composants Récemment Implémentés
- ✅ Système de migrations de base de données avec DrizzleORM - ✅ Système de migrations de base de données avec DrizzleORM
- ✅ Tests unitaires pour les modules auth, groups et tags
#### Composants Non Implémentés #### Composants Non Implémentés
- Module d'authentification avec GitHub OAuth - Module d'authentification avec GitHub OAuth
- Stratégies JWT pour la gestion des sessions - Stratégies JWT pour la gestion des sessions
- ✅ Guards et décorateurs pour la protection des routes - ✅ Guards et décorateurs pour la protection des routes
- Module groupes - Module groupes
- Module tags - Module tags
- ❌ Communication en temps réel avec Socket.IO - ❌ Communication en temps réel avec Socket.IO
- ❌ Fonctionnalités de conformité RGPD - ❌ Fonctionnalités de conformité RGPD
- ⏳ Tests unitaires et e2e - ❌ Tests e2e complets
- ❌ Documentation API avec Swagger - ❌ Documentation API avec Swagger
### Frontend ### Frontend
@ -72,9 +74,17 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
- [x] Implémenter le refresh token - [x] Implémenter le refresh token
##### Modules Manquants ##### Modules Manquants
- [ ] Implémenter le module groupes (contrôleurs, services, DTOs) - [x] Implémenter le module groupes (contrôleurs, services, DTOs)
- [ ] Implémenter le module tags (contrôleurs, services, DTOs) - [x] Implémenter le module tags (contrôleurs, services, DTOs)
- [ ] Compléter les relations entre les modules existants - [x] Compléter les relations entre les modules existants
##### Tests Unitaires
- [x] Écrire des tests unitaires pour le module auth
- [x] Écrire des tests unitaires pour le module groups
- [x] Écrire des tests unitaires pour le module tags
- [x] Écrire des tests unitaires pour le module persons
- [x] Écrire des tests unitaires pour le module projects
- [x] Écrire des tests unitaires pour le module users
#### Priorité Moyenne #### Priorité Moyenne
@ -95,8 +105,9 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
#### Priorité Basse #### Priorité Basse
##### Tests et Documentation ##### Tests et Documentation
- [ ] Écrire des tests unitaires pour les services - [x] Écrire des tests unitaires pour les services (tous les modules)
- [ ] Écrire des tests unitaires pour les contrôleurs - [x] Écrire des tests unitaires pour les contrôleurs (tous les modules)
- [x] Écrire des tests unitaires pour tous les modules
- [ ] Développer des tests e2e pour les API - [ ] Développer des tests e2e pour les API
- [ ] Configurer Swagger pour la documentation API - [ ] Configurer Swagger pour la documentation API
- [ ] Documenter les endpoints API - [ ] Documenter les endpoints API
@ -169,10 +180,10 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
- Configurer les stratégies JWT pour la gestion des sessions ✅ - Configurer les stratégies JWT pour la gestion des sessions ✅
- Créer les guards et décorateurs pour la protection des routes ✅ - Créer les guards et décorateurs pour la protection des routes ✅
2. **Modules Manquants** 2. **Modules et Relations**
- Implémenter le module groupes - Implémenter le module groupes
- Implémenter le module tags - Implémenter le module tags
- Compléter les relations entre les modules existants - Compléter les relations entre les modules existants
### Frontend (Priorité Haute) ### Frontend (Priorité Haute)
1. **Authentification** 1. **Authentification**
@ -191,10 +202,10 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
|-----------|-------------| |-----------|-------------|
| Backend - Structure de Base | 90% | | Backend - Structure de Base | 90% |
| Backend - Base de Données | 100% | | Backend - Base de Données | 100% |
| Backend - Modules Fonctionnels | 60% | | Backend - Modules Fonctionnels | 80% |
| Backend - Authentification | 90% | | Backend - Authentification | 90% |
| Backend - WebSockets | 0% | | Backend - WebSockets | 0% |
| Backend - Tests et Documentation | 20% | | Backend - Tests et Documentation | 70% |
| Frontend - Structure de Base | 70% | | Frontend - Structure de Base | 70% |
| Frontend - Pages et Composants | 10% | | Frontend - Pages et Composants | 10% |
| Frontend - Authentification | 0% | | Frontend - Authentification | 0% |
@ -205,11 +216,12 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
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**: ~3-4 semaines - **Backend**: ~2-3 semaines
- Authentification: ✅ Terminé - Authentification: ✅ Terminé
- Modules manquants: 1-2 semaines - Modules manquants: ✅ Terminé
- Relations entre modules: 1 semaine
- WebSockets: 1 semaine - WebSockets: 1 semaine
- Tests et documentation: 1 semaine - Tests e2e et documentation: 1 semaine
- **Frontend**: ~5-6 semaines - **Frontend**: ~5-6 semaines
- Authentification: 1 semaine - Authentification: 1 semaine
@ -235,4 +247,6 @@ Basé sur l'état d'avancement actuel et les tâches restantes, l'estimation du
## Conclusion ## Conclusion
Le projet a bien avancé sur la structure de base et la définition du schéma de données, mais il reste encore un travail significatif à réaliser. Les prochaines étapes prioritaires devraient se concentrer sur l'authentification et les fonctionnalités de base pour avoir rapidement une version minimale fonctionnelle. Le projet a considérablement progressé avec l'implémentation complète de la structure de base, du schéma de données, de l'authentification, et des modules principaux (utilisateurs, projets, personnes, groupes, tags). Les tests unitaires pour les services et contrôleurs ont également été mis en place.
Les prochaines étapes prioritaires devraient se concentrer sur la complétion des relations entre les modules existants et le développement des fonctionnalités de communication en temps réel avec Socket.IO. Côté frontend, il est maintenant temps de commencer l'implémentation des pages d'authentification et des fonctionnalités de base.

View File

@ -11,6 +11,172 @@ Le schéma de base de données est conçu pour supporter les fonctionnalités su
- Création et gestion de groupes - Création et gestion de groupes
- Système de tags pour catégoriser les personnes et les projets - Système de tags pour catégoriser les personnes et les projets
### 1.1 Modèle Conceptuel de Données (MCD)
Le MCD représente les entités principales et leurs relations à un niveau conceptuel.
```mermaid
erDiagram
USER ||--o{ PROJECT : "possède"
USER ||--o{ PROJECT_COLLABORATOR : "collabore sur"
PROJECT ||--o{ PERSON : "contient"
PROJECT ||--o{ GROUP : "organise"
PROJECT ||--o{ PROJECT_COLLABORATOR : "a des collaborateurs"
PROJECT }o--o{ TAG : "est catégorisé par"
PERSON }o--o{ GROUP : "appartient à"
PERSON }o--o{ TAG : "est catégorisé par"
USER {
uuid id PK
string githubId
string name
string avatar
string role
datetime gdprTimestamp
}
PROJECT {
uuid id PK
string name
string description
json settings
uuid ownerId FK
boolean isPublic
}
PERSON {
uuid id PK
string name
string email
int technicalLevel
string gender
json attributes
uuid projectId FK
}
GROUP {
uuid id PK
string name
string description
json settings
uuid projectId FK
}
TAG {
uuid id PK
string name
string description
string color
enum type
}
PROJECT_COLLABORATOR {
uuid projectId FK
uuid userId FK
}
```
### 1.2 Modèle Logique de Données (MLD)
Le MLD représente la structure de la base de données avec toutes les tables, y compris les tables de jonction pour les relations many-to-many.
```mermaid
erDiagram
users ||--o{ projects : "owns"
users ||--o{ project_collaborators : "collaborates on"
projects ||--o{ persons : "contains"
projects ||--o{ groups : "organizes"
projects ||--o{ project_collaborators : "has collaborators"
projects ||--o{ project_to_tag : "is categorized by"
persons ||--o{ person_to_group : "belongs to"
persons ||--o{ person_to_tag : "is categorized by"
groups ||--o{ person_to_group : "contains"
tags ||--o{ person_to_tag : "categorizes"
tags ||--o{ project_to_tag : "categorizes"
users {
uuid id PK
string github_id
string name
string avatar
string role
datetime gdpr_timestamp
datetime created_at
datetime updated_at
}
projects {
uuid id PK
string name
string description
json settings
uuid owner_id FK
boolean is_public
datetime created_at
datetime updated_at
}
persons {
uuid id PK
string name
string email
int technical_level
string gender
json attributes
uuid project_id FK
datetime created_at
datetime updated_at
}
groups {
uuid id PK
string name
string description
json settings
uuid project_id FK
datetime created_at
datetime updated_at
}
tags {
uuid id PK
string name
string description
string color
enum type
datetime created_at
datetime updated_at
}
person_to_group {
uuid id PK
uuid person_id FK
uuid group_id FK
datetime created_at
}
person_to_tag {
uuid id PK
uuid person_id FK
uuid tag_id FK
datetime created_at
}
project_to_tag {
uuid id PK
uuid project_id FK
uuid tag_id FK
datetime created_at
}
project_collaborators {
uuid id PK
uuid project_id FK
uuid user_id FK
datetime created_at
}
```
## 2. Tables Principales ## 2. Tables Principales
### 2.1 Table `users` ### 2.1 Table `users`
@ -295,11 +461,11 @@ async function main() {
const db = drizzle(pool); const db = drizzle(pool);
console.log('Running migrations...'); console.log('Running migrations...');
await migrate(db, { migrationsFolder: './src/database/migrations' }); await migrate(db, { migrationsFolder: './src/database/migrations' });
console.log('Migrations completed successfully!'); console.log('Migrations completed successfully!');
await pool.end(); await pool.end();
} }
@ -372,7 +538,7 @@ const getProjectWithPersonsAndGroups = async (db, projectId) => {
}, },
}, },
}); });
return project; return project;
}; };
``` ```
@ -393,7 +559,7 @@ const getPersonsWithTags = async (db, projectId) => {
}, },
}, },
}); });
return persons; return persons;
}; };
``` ```
@ -402,4 +568,4 @@ const getPersonsWithTags = async (db, projectId) => {
Ce schéma de base de données fournit une structure solide pour l'application de création de groupes, avec une conception qui prend en compte les performances, la flexibilité et l'intégrité des données. Les relations entre les entités sont clairement définies, et les types de données sont optimisés pour les besoins de l'application. Ce schéma de base de données fournit une structure solide pour l'application de création de groupes, avec une conception qui prend en compte les performances, la flexibilité et l'intégrité des données. Les relations entre les entités sont clairement définies, et les types de données sont optimisés pour les besoins de l'application.
L'utilisation de DrizzleORM permet une intégration transparente avec NestJS et offre une expérience de développement type-safe, facilitant la maintenance et l'évolution du schéma au fil du temps. L'utilisation de DrizzleORM permet une intégration transparente avec NestJS et offre une expérience de développement type-safe, facilitant la maintenance et l'évolution du schéma au fil du temps.