From 7eae25d5de5b9be4e7ff46361e772524e6c7f81b Mon Sep 17 00:00:00 2001 From: Avnyr Date: Thu, 15 May 2025 19:29:12 +0200 Subject: [PATCH] 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. --- .../auth/controllers/auth.controller.spec.ts | 117 +++++++ .../auth/guards/jwt-auth.guard.spec.ts | 82 +++++ .../auth/services/auth.service.spec.ts | 208 ++++++++++++ .../controllers/groups.controller.spec.ts | 194 +++++++++++ .../groups/services/groups.service.spec.ts | 317 ++++++++++++++++++ 5 files changed, 918 insertions(+) create mode 100644 backend/src/modules/auth/controllers/auth.controller.spec.ts create mode 100644 backend/src/modules/auth/guards/jwt-auth.guard.spec.ts create mode 100644 backend/src/modules/auth/services/auth.service.spec.ts create mode 100644 backend/src/modules/groups/controllers/groups.controller.spec.ts create mode 100644 backend/src/modules/groups/services/groups.service.spec.ts diff --git a/backend/src/modules/auth/controllers/auth.controller.spec.ts b/backend/src/modules/auth/controllers/auth.controller.spec.ts new file mode 100644 index 0000000..b66365e --- /dev/null +++ b/backend/src/modules/auth/controllers/auth.controller.spec.ts @@ -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); + authService = module.get(AuthService); + configService = module.get(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); + }); + }); +}); \ No newline at end of file diff --git a/backend/src/modules/auth/guards/jwt-auth.guard.spec.ts b/backend/src/modules/auth/guards/jwt-auth.guard.spec.ts new file mode 100644 index 0000000..7d8328e --- /dev/null +++ b/backend/src/modules/auth/guards/jwt-auth.guard.spec.ts @@ -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'); + }); + }); +}); diff --git a/backend/src/modules/auth/services/auth.service.spec.ts b/backend/src/modules/auth/services/auth.service.spec.ts new file mode 100644 index 0000000..fd5e42b --- /dev/null +++ b/backend/src/modules/auth/services/auth.service.spec.ts @@ -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); + usersService = module.get(UsersService); + jwtService = module.get(JwtService); + configService = module.get(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, + ); + }); + }); +}); \ No newline at end of file diff --git a/backend/src/modules/groups/controllers/groups.controller.spec.ts b/backend/src/modules/groups/controllers/groups.controller.spec.ts new file mode 100644 index 0000000..f6a25a1 --- /dev/null +++ b/backend/src/modules/groups/controllers/groups.controller.spec.ts @@ -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); + service = module.get(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); + }); + }); +}); \ No newline at end of file diff --git a/backend/src/modules/groups/services/groups.service.spec.ts b/backend/src/modules/groups/services/groups.service.spec.ts new file mode 100644 index 0000000..d61bb9b --- /dev/null +++ b/backend/src/modules/groups/services/groups.service.spec.ts @@ -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); + }); + + 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); + }); + }); +});