Compare commits

..

2 Commits

Author SHA1 Message Date
7eae25d5de test: add comprehensive unit tests for groups and auth modules
Added unit tests for `GroupsService`, `GroupsController`, `AuthService`, `AuthController`, and `JwtAuthGuard` to ensure functionality and coverage. Includes tests for entity operations, exception handling, and integration scenarios.
2025-05-15 19:29:12 +02:00
04aba190ed docs: update project status with authentication progress
Marked authentication module, GitHub OAuth, JWT strategies, and guards as completed in `PROJECT_STATUS.md`. Updated progress percentages, adjusted task priorities, and revised
2025-05-15 19:28:45 +02:00
6 changed files with 936 additions and 18 deletions

View File

@ -0,0 +1,117 @@
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { UnauthorizedException } from '@nestjs/common';
import { AuthController } from './auth.controller';
import { AuthService } from '../services/auth.service';
describe('AuthController', () => {
let controller: AuthController;
let authService: AuthService;
let configService: ConfigService;
const mockAuthService = {
validateGithubUser: jest.fn(),
generateTokens: jest.fn(),
refreshTokens: jest.fn(),
};
const mockConfigService = {
get: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
providers: [
{ provide: AuthService, useValue: mockAuthService },
{ provide: ConfigService, useValue: mockConfigService },
],
}).compile();
controller = module.get<AuthController>(AuthController);
authService = module.get<AuthService>(AuthService);
configService = module.get<ConfigService>(ConfigService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('githubAuth', () => {
it('should be defined', () => {
expect(controller.githubAuth).toBeDefined();
});
});
describe('githubAuthCallback', () => {
it('should redirect to frontend with tokens', async () => {
const req = {
user: { id: 'user1', name: 'Test User' },
};
const res = {
redirect: jest.fn(),
};
const tokens = {
accessToken: 'access-token',
refreshToken: 'refresh-token',
};
const frontendUrl = 'http://localhost:3001';
const expectedRedirectUrl = `${frontendUrl}/auth/callback?accessToken=${tokens.accessToken}&refreshToken=${tokens.refreshToken}`;
mockAuthService.generateTokens.mockResolvedValue(tokens);
mockConfigService.get.mockReturnValue(frontendUrl);
await controller.githubAuthCallback(req as any, res as any);
expect(mockAuthService.generateTokens).toHaveBeenCalledWith('user1');
expect(mockConfigService.get).toHaveBeenCalledWith('FRONTEND_URL');
expect(res.redirect).toHaveBeenCalledWith(expectedRedirectUrl);
});
it('should throw UnauthorizedException if user is not provided', async () => {
const req = {};
const res = {
redirect: jest.fn(),
};
await expect(controller.githubAuthCallback(req as any, res as any)).rejects.toThrow(
UnauthorizedException,
);
expect(res.redirect).not.toHaveBeenCalled();
});
});
describe('refreshTokens', () => {
it('should refresh tokens', async () => {
const user = {
id: 'user1',
refreshToken: 'refresh-token',
};
const tokens = {
accessToken: 'new-access-token',
refreshToken: 'new-refresh-token',
};
mockAuthService.refreshTokens.mockResolvedValue(tokens);
const result = await controller.refreshTokens(user);
expect(mockAuthService.refreshTokens).toHaveBeenCalledWith('user1', 'refresh-token');
expect(result).toEqual(tokens);
});
});
describe('getProfile', () => {
it('should return user profile', () => {
const user = { id: 'user1', name: 'Test User' };
const result = controller.getProfile(user);
expect(result).toEqual(user);
});
});
});

View File

@ -0,0 +1,82 @@
import { ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtAuthGuard } from './jwt-auth.guard';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
describe('JwtAuthGuard', () => {
let guard: JwtAuthGuard;
let reflector: Reflector;
beforeEach(() => {
reflector = new Reflector();
guard = new JwtAuthGuard(reflector);
});
describe('canActivate', () => {
it('should return true if the route is public', () => {
const context = {
getHandler: jest.fn(),
getClass: jest.fn(),
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({}),
getResponse: jest.fn().mockReturnValue({}),
}),
} as unknown as ExecutionContext;
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(true);
expect(guard.canActivate(context)).toBe(true);
expect(reflector.getAllAndOverride).toHaveBeenCalledWith(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
});
it('should call super.canActivate if the route is not public', () => {
const context = {
getHandler: jest.fn(),
getClass: jest.fn(),
switchToHttp: jest.fn().mockReturnValue({
getRequest: jest.fn().mockReturnValue({}),
getResponse: jest.fn().mockReturnValue({}),
}),
} as unknown as ExecutionContext;
jest.spyOn(reflector, 'getAllAndOverride').mockReturnValue(false);
// Mock the AuthGuard's canActivate method
const canActivateSpy = jest.spyOn(guard, 'canActivate');
// We can't easily test the super.canActivate call directly,
// so we'll just verify our method was called with the right context
guard.canActivate(context);
expect(reflector.getAllAndOverride).toHaveBeenCalledWith(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
expect(canActivateSpy).toHaveBeenCalledWith(context);
});
});
describe('handleRequest', () => {
it('should return the user if no error and user exists', () => {
const user = { id: 'user1', name: 'Test User' };
const result = guard.handleRequest(null, user, null);
expect(result).toBe(user);
});
it('should throw the error if an error exists', () => {
const error = new Error('Test error');
expect(() => guard.handleRequest(error, null, null)).toThrow(error);
});
it('should throw UnauthorizedException if no error but user does not exist', () => {
expect(() => guard.handleRequest(null, null, null)).toThrow(UnauthorizedException);
expect(() => guard.handleRequest(null, null, null)).toThrow('Authentication required');
});
});
});

View File

@ -0,0 +1,208 @@
import { Test, TestingModule } from '@nestjs/testing';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { AuthService } from './auth.service';
import { UsersService } from '../../users/services/users.service';
import { UnauthorizedException } from '@nestjs/common';
describe('AuthService', () => {
let service: AuthService;
let usersService: UsersService;
let jwtService: JwtService;
let configService: ConfigService;
const mockUsersService = {
findByGithubId: jest.fn(),
findById: jest.fn(),
create: jest.fn(),
update: jest.fn(),
};
const mockJwtService = {
signAsync: jest.fn(),
verifyAsync: jest.fn(),
};
const mockConfigService = {
get: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{ provide: UsersService, useValue: mockUsersService },
{ provide: JwtService, useValue: mockJwtService },
{ provide: ConfigService, useValue: mockConfigService },
],
}).compile();
service = module.get<AuthService>(AuthService);
usersService = module.get<UsersService>(UsersService);
jwtService = module.get<JwtService>(JwtService);
configService = module.get<ConfigService>(ConfigService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('validateGithubUser', () => {
it('should create a new user if one does not exist', async () => {
const githubId = 'github123';
const email = 'test@example.com';
const name = 'Test User';
const avatarUrl = 'https://example.com/avatar.jpg';
const newUser = { id: 'user1', githubId, name, avatar: avatarUrl };
mockUsersService.findByGithubId.mockResolvedValue(null);
mockUsersService.create.mockResolvedValue(newUser);
const result = await service.validateGithubUser(githubId, email, name, avatarUrl);
expect(mockUsersService.findByGithubId).toHaveBeenCalledWith(githubId);
expect(mockUsersService.create).toHaveBeenCalledWith({
githubId,
name,
avatar: avatarUrl,
metadata: { email },
});
expect(result).toEqual(newUser);
});
it('should return an existing user if one exists', async () => {
const githubId = 'github123';
const email = 'test@example.com';
const name = 'Test User';
const avatarUrl = 'https://example.com/avatar.jpg';
const existingUser = { id: 'user1', githubId, name, avatar: avatarUrl };
mockUsersService.findByGithubId.mockResolvedValue(existingUser);
const result = await service.validateGithubUser(githubId, email, name, avatarUrl);
expect(mockUsersService.findByGithubId).toHaveBeenCalledWith(githubId);
expect(mockUsersService.create).not.toHaveBeenCalled();
expect(result).toEqual(existingUser);
});
});
describe('generateTokens', () => {
it('should generate access and refresh tokens', async () => {
const userId = 'user1';
const accessToken = 'access-token';
const refreshToken = 'refresh-token';
const refreshExpiration = '7d';
const refreshSecret = 'refresh-secret';
mockJwtService.signAsync.mockResolvedValueOnce(accessToken);
mockJwtService.signAsync.mockResolvedValueOnce(refreshToken);
mockConfigService.get.mockReturnValueOnce(refreshExpiration);
mockConfigService.get.mockReturnValueOnce(refreshSecret);
const result = await service.generateTokens(userId);
expect(mockJwtService.signAsync).toHaveBeenCalledWith({ sub: userId });
expect(mockJwtService.signAsync).toHaveBeenCalledWith(
{ sub: userId, isRefreshToken: true },
{
expiresIn: refreshExpiration,
secret: refreshSecret,
},
);
expect(result).toEqual({
accessToken,
refreshToken,
});
});
});
describe('refreshTokens', () => {
it('should refresh tokens if refresh token is valid', async () => {
const userId = 'user1';
const refreshToken = 'valid-refresh-token';
const newAccessToken = 'new-access-token';
const newRefreshToken = 'new-refresh-token';
const payload = { sub: userId, isRefreshToken: true };
mockJwtService.verifyAsync.mockResolvedValue(payload);
mockJwtService.signAsync.mockResolvedValueOnce(newAccessToken);
mockJwtService.signAsync.mockResolvedValueOnce(newRefreshToken);
const result = await service.refreshTokens(userId, refreshToken);
expect(mockJwtService.verifyAsync).toHaveBeenCalledWith(refreshToken, {
secret: undefined,
});
expect(result).toEqual({
accessToken: newAccessToken,
refreshToken: newRefreshToken,
});
});
it('should throw UnauthorizedException if refresh token is invalid', async () => {
const userId = 'user1';
const refreshToken = 'invalid-refresh-token';
mockJwtService.verifyAsync.mockRejectedValue(new Error('Invalid token'));
await expect(service.refreshTokens(userId, refreshToken)).rejects.toThrow(
UnauthorizedException,
);
});
it('should throw UnauthorizedException if token is not a refresh token', async () => {
const userId = 'user1';
const refreshToken = 'not-a-refresh-token';
const payload = { sub: userId }; // Missing isRefreshToken: true
mockJwtService.verifyAsync.mockResolvedValue(payload);
await expect(service.refreshTokens(userId, refreshToken)).rejects.toThrow(
UnauthorizedException,
);
});
it('should throw UnauthorizedException if user ID does not match', async () => {
const userId = 'user1';
const refreshToken = 'wrong-user-token';
const payload = { sub: 'user2', isRefreshToken: true }; // Different user ID
mockJwtService.verifyAsync.mockResolvedValue(payload);
await expect(service.refreshTokens(userId, refreshToken)).rejects.toThrow(
UnauthorizedException,
);
});
});
describe('validateJwtUser', () => {
it('should return user if user exists', async () => {
const userId = 'user1';
const user = { id: userId, name: 'Test User' };
const payload = { sub: userId };
mockUsersService.findById.mockResolvedValue(user);
const result = await service.validateJwtUser(payload);
expect(mockUsersService.findById).toHaveBeenCalledWith(userId);
expect(result).toEqual(user);
});
it('should throw UnauthorizedException if user does not exist', async () => {
const userId = 'nonexistent';
const payload = { sub: userId };
mockUsersService.findById.mockResolvedValue(null);
await expect(service.validateJwtUser(payload)).rejects.toThrow(
UnauthorizedException,
);
});
});
});

View File

@ -0,0 +1,194 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GroupsController } from './groups.controller';
import { GroupsService } from '../services/groups.service';
import { CreateGroupDto } from '../dto/create-group.dto';
import { UpdateGroupDto } from '../dto/update-group.dto';
describe('GroupsController', () => {
let controller: GroupsController;
let service: GroupsService;
// Mock data
const mockGroup = {
id: 'group1',
name: 'Test Group',
projectId: 'project1',
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
const mockPerson = {
id: 'person1',
name: 'Test Person',
projectId: 'project1',
createdAt: new Date(),
updatedAt: new Date(),
};
// Mock service
const mockGroupsService = {
create: jest.fn(),
findAll: jest.fn(),
findByProjectId: jest.fn(),
findById: jest.fn(),
update: jest.fn(),
remove: jest.fn(),
addPersonToGroup: jest.fn(),
removePersonFromGroup: jest.fn(),
getPersonsInGroup: jest.fn(),
};
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GroupsController],
providers: [
{
provide: GroupsService,
useValue: mockGroupsService,
},
],
}).compile();
controller = module.get<GroupsController>(GroupsController);
service = module.get<GroupsService>(GroupsService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
describe('create', () => {
it('should create a new group', async () => {
const createGroupDto: CreateGroupDto = {
name: 'Test Group',
projectId: 'project1',
metadata: {},
};
mockGroupsService.create.mockResolvedValue(mockGroup);
const result = await controller.create(createGroupDto);
expect(mockGroupsService.create).toHaveBeenCalledWith(createGroupDto);
expect(result).toEqual(mockGroup);
});
});
describe('findAll', () => {
it('should return all groups when no projectId is provided', async () => {
mockGroupsService.findAll.mockResolvedValue([mockGroup]);
const result = await controller.findAll();
expect(mockGroupsService.findAll).toHaveBeenCalled();
expect(mockGroupsService.findByProjectId).not.toHaveBeenCalled();
expect(result).toEqual([mockGroup]);
});
it('should return groups for a specific project when projectId is provided', async () => {
const projectId = 'project1';
mockGroupsService.findByProjectId.mockResolvedValue([mockGroup]);
const result = await controller.findAll(projectId);
expect(mockGroupsService.findByProjectId).toHaveBeenCalledWith(projectId);
expect(mockGroupsService.findAll).not.toHaveBeenCalled();
expect(result).toEqual([mockGroup]);
});
});
describe('findOne', () => {
it('should return a group by id', async () => {
const id = 'group1';
mockGroupsService.findById.mockResolvedValue(mockGroup);
const result = await controller.findOne(id);
expect(mockGroupsService.findById).toHaveBeenCalledWith(id);
expect(result).toEqual(mockGroup);
});
});
describe('update', () => {
it('should update a group', async () => {
const id = 'group1';
const updateGroupDto: UpdateGroupDto = {
name: 'Updated Group',
};
mockGroupsService.update.mockResolvedValue({
...mockGroup,
name: 'Updated Group',
});
const result = await controller.update(id, updateGroupDto);
expect(mockGroupsService.update).toHaveBeenCalledWith(id, updateGroupDto);
expect(result).toEqual({
...mockGroup,
name: 'Updated Group',
});
});
});
describe('remove', () => {
it('should remove a group', async () => {
const id = 'group1';
mockGroupsService.remove.mockResolvedValue(mockGroup);
const result = await controller.remove(id);
expect(mockGroupsService.remove).toHaveBeenCalledWith(id);
expect(result).toEqual(mockGroup);
});
});
describe('addPersonToGroup', () => {
it('should add a person to a group', async () => {
const groupId = 'group1';
const personId = 'person1';
const mockRelation = { groupId, personId };
mockGroupsService.addPersonToGroup.mockResolvedValue(mockRelation);
const result = await controller.addPersonToGroup(groupId, personId);
expect(mockGroupsService.addPersonToGroup).toHaveBeenCalledWith(groupId, personId);
expect(result).toEqual(mockRelation);
});
});
describe('removePersonFromGroup', () => {
it('should remove a person from a group', async () => {
const groupId = 'group1';
const personId = 'person1';
const mockRelation = { groupId, personId };
mockGroupsService.removePersonFromGroup.mockResolvedValue(mockRelation);
const result = await controller.removePersonFromGroup(groupId, personId);
expect(mockGroupsService.removePersonFromGroup).toHaveBeenCalledWith(groupId, personId);
expect(result).toEqual(mockRelation);
});
});
describe('getPersonsInGroup', () => {
it('should get all persons in a group', async () => {
const groupId = 'group1';
const mockPersons = [{ person: mockPerson }];
mockGroupsService.getPersonsInGroup.mockResolvedValue(mockPersons);
const result = await controller.getPersonsInGroup(groupId);
expect(mockGroupsService.getPersonsInGroup).toHaveBeenCalledWith(groupId);
expect(result).toEqual(mockPersons);
});
});
});

View File

@ -0,0 +1,317 @@
import { Test, TestingModule } from '@nestjs/testing';
import { GroupsService } from './groups.service';
import { NotFoundException } from '@nestjs/common';
import { DRIZZLE } from '../../../database/database.module';
describe('GroupsService', () => {
let service: GroupsService;
let mockDb: any;
// Mock data
const mockGroup = {
id: 'group1',
name: 'Test Group',
projectId: 'project1',
metadata: {},
createdAt: new Date(),
updatedAt: new Date(),
};
const mockPerson = {
id: 'person1',
name: 'Test Person',
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 [mockGroup];
}),
};
beforeEach(async () => {
mockDb = {
...mockDbOperations,
};
const module: TestingModule = await Test.createTestingModule({
providers: [
GroupsService,
{
provide: DRIZZLE,
useValue: mockDb,
},
],
}).compile();
service = module.get<GroupsService>(GroupsService);
});
afterEach(() => {
jest.clearAllMocks();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('create', () => {
it('should create a new group', async () => {
const createGroupDto = {
name: 'Test Group',
projectId: 'project1',
metadata: {},
};
const result = await service.create(createGroupDto);
expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith({
...createGroupDto,
});
expect(result).toEqual(mockGroup);
});
});
describe('findAll', () => {
it('should return all groups', async () => {
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => [mockGroup]);
const result = await service.findAll();
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(result).toEqual([mockGroup]);
});
});
describe('findByProjectId', () => {
it('should return groups for a specific project', async () => {
const projectId = 'project1';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockGroup]);
const result = await service.findByProjectId(projectId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual([mockGroup]);
});
});
describe('findById', () => {
it('should return a group by id', async () => {
const id = 'group1';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [mockGroup]);
const result = await service.findById(id);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockGroup);
});
it('should throw NotFoundException if group not found', async () => {
const id = 'nonexistent';
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [undefined]);
await expect(service.findById(id)).rejects.toThrow(NotFoundException);
});
});
describe('update', () => {
it('should update a group', async () => {
const id = 'group1';
const updateGroupDto = {
name: 'Updated Group',
};
const result = await service.update(id, updateGroupDto);
expect(mockDb.update).toHaveBeenCalled();
expect(mockDb.set).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockGroup);
});
it('should throw NotFoundException if group not found', async () => {
const id = 'nonexistent';
const updateGroupDto = {
name: 'Updated Group',
};
mockDb.update.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.set.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [undefined]);
await expect(service.update(id, updateGroupDto)).rejects.toThrow(NotFoundException);
});
});
describe('remove', () => {
it('should remove a group', async () => {
const id = 'group1';
const result = await service.remove(id);
expect(mockDb.delete).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockGroup);
});
it('should throw NotFoundException if group not found', async () => {
const id = 'nonexistent';
mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [undefined]);
await expect(service.remove(id)).rejects.toThrow(NotFoundException);
});
});
describe('addPersonToGroup', () => {
it('should add a person to a group', async () => {
const groupId = 'group1';
const personId = 'person1';
// Mock findById to return the group
jest.spyOn(service, 'findById').mockResolvedValueOnce(mockGroup);
// Reset and setup mocks for this test
jest.clearAllMocks();
// Mock person lookup
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [[mockPerson]]);
// Mock relation lookup
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [undefined]);
// Mock relation creation
mockDb.insert.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.values.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]);
const result = await service.addPersonToGroup(groupId, personId);
expect(service.findById).toHaveBeenCalledWith(groupId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.insert).toHaveBeenCalled();
expect(mockDb.values).toHaveBeenCalledWith({
personId,
groupId,
});
expect(result).toEqual(mockPersonToGroup);
});
it('should throw NotFoundException if person not found', async () => {
const groupId = 'group1';
const personId = 'nonexistent';
// Mock findById to return the group
jest.spyOn(service, 'findById').mockResolvedValueOnce(mockGroup);
// Reset and setup mocks for this test
jest.clearAllMocks();
// Mock person lookup to return no person
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => [undefined]);
await expect(service.addPersonToGroup(groupId, personId)).rejects.toThrow(NotFoundException);
});
});
describe('removePersonFromGroup', () => {
it('should remove a person from a group', async () => {
const groupId = 'group1';
const personId = 'person1';
// Reset and setup mocks for this test
jest.clearAllMocks();
mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [mockPersonToGroup]);
const result = await service.removePersonFromGroup(groupId, personId);
expect(mockDb.delete).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockPersonToGroup);
});
it('should throw NotFoundException if relation not found', async () => {
const groupId = 'group1';
const personId = 'nonexistent';
// Reset and setup mocks for this test
jest.clearAllMocks();
mockDb.delete.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.returning.mockImplementationOnce(() => [undefined]);
await expect(service.removePersonFromGroup(groupId, personId)).rejects.toThrow(NotFoundException);
});
});
describe('getPersonsInGroup', () => {
it('should get all persons in a group', async () => {
const groupId = 'group1';
const mockPersons = [{ person: mockPerson }];
// Mock findById to return the group
jest.spyOn(service, 'findById').mockResolvedValueOnce(mockGroup);
mockDb.select.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.from.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.innerJoin.mockImplementationOnce(() => mockDbOperations);
mockDbOperations.where.mockImplementationOnce(() => mockPersons);
const result = await service.getPersonsInGroup(groupId);
expect(service.findById).toHaveBeenCalledWith(groupId);
expect(mockDb.select).toHaveBeenCalled();
expect(mockDb.from).toHaveBeenCalled();
expect(mockDb.innerJoin).toHaveBeenCalled();
expect(mockDb.where).toHaveBeenCalled();
expect(result).toEqual(mockPersons);
});
});
});

View File

@ -27,14 +27,14 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
- ✅ Système de migrations de base de données avec DrizzleORM - ✅ Système de migrations de base de données avec DrizzleORM
#### 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 unitaires et e2e
- ❌ Documentation API avec Swagger - ❌ Documentation API avec Swagger
### Frontend ### Frontend
@ -65,11 +65,11 @@ Nous avons élaboré un plan de bataille complet pour l'implémentation du backe
- [x] Créer un script pour exécuter les migrations automatiquement au démarrage - [x] Créer un script pour exécuter les migrations automatiquement au démarrage
##### Authentification ##### Authentification
- [ ] Implémenter le module d'authentification - [x] Implémenter le module d'authentification
- [ ] Configurer l'authentification OAuth avec GitHub - [x] Configurer l'authentification OAuth avec GitHub
- [ ] Implémenter les stratégies JWT pour la gestion des sessions - [x] Implémenter les stratégies JWT pour la gestion des sessions
- [ ] Créer les guards et décorateurs pour la protection des routes - [x] Créer les guards et décorateurs pour la protection des routes
- [ ] Implémenter le refresh token - [x] Implémenter le refresh token
##### Modules Manquants ##### Modules Manquants
- [ ] Implémenter le module groupes (contrôleurs, services, DTOs) - [ ] Implémenter le module groupes (contrôleurs, services, DTOs)
@ -164,10 +164,10 @@ 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. **Authentification** 1. **Authentification**
- Implémenter le module d'authentification avec GitHub OAuth - Implémenter le module d'authentification avec GitHub OAuth
- 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 Manquants**
- Implémenter le module groupes - Implémenter le module groupes
@ -192,9 +192,9 @@ 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 | 60% |
| Backend - Authentification | 0% | | Backend - Authentification | 90% |
| Backend - WebSockets | 0% | | Backend - WebSockets | 0% |
| Backend - Tests et Documentation | 0% | | Backend - Tests et Documentation | 20% |
| 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,8 +205,8 @@ 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**: ~4-5 semaines - **Backend**: ~3-4 semaines
- Authentification: 1 semaine - Authentification: ✅ Terminé
- Modules manquants: 1-2 semaines - Modules manquants: 1-2 semaines
- WebSockets: 1 semaine - WebSockets: 1 semaine
- Tests et documentation: 1 semaine - Tests et documentation: 1 semaine
@ -219,7 +219,7 @@ Basé sur l'état d'avancement actuel et les tâches restantes, l'estimation du
- **Intégration et Tests**: ~1-2 semaines - **Intégration et Tests**: ~1-2 semaines
**Temps total estimé**: 10-13 semaines **Temps total estimé**: 9-12 semaines
## Recommandations ## Recommandations