From 576d063e52f925b0de0e5bbbcd8e8a2c2f20c1e8 Mon Sep 17 00:00:00 2001 From: Avnyr Date: Thu, 15 May 2025 20:48:42 +0200 Subject: [PATCH] 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. --- .../controllers/users.controller.spec.ts | 127 +++++++++ .../users/services/users.service.spec.ts | 250 ++++++++++++++++++ 2 files changed, 377 insertions(+) create mode 100644 backend/src/modules/users/controllers/users.controller.spec.ts create mode 100644 backend/src/modules/users/services/users.service.spec.ts diff --git a/backend/src/modules/users/controllers/users.controller.spec.ts b/backend/src/modules/users/controllers/users.controller.spec.ts new file mode 100644 index 0000000..1488c5e --- /dev/null +++ b/backend/src/modules/users/controllers/users.controller.spec.ts @@ -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); + service = module.get(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); + }); + }); +}); \ No newline at end of file diff --git a/backend/src/modules/users/services/users.service.spec.ts b/backend/src/modules/users/services/users.service.spec.ts new file mode 100644 index 0000000..129efb4 --- /dev/null +++ b/backend/src/modules/users/services/users.service.spec.ts @@ -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); + }); + + 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], + }); + }); + }); +}); \ No newline at end of file