From 50583f9ccc28e1f3948bf3c4101be40fb51a65db Mon Sep 17 00:00:00 2001 From: Avnyr Date: Thu, 15 May 2025 19:48:44 +0200 Subject: [PATCH] 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. --- .../auth/guards/jwt-auth.guard.spec.ts | 13 + .../tags/controllers/tags.controller.spec.ts | 179 +++++++++ .../tags/services/tags.service.spec.ts | 339 ++++++++++++++++++ 3 files changed, 531 insertions(+) create mode 100644 backend/src/modules/tags/controllers/tags.controller.spec.ts create mode 100644 backend/src/modules/tags/services/tags.service.spec.ts diff --git a/backend/src/modules/auth/guards/jwt-auth.guard.spec.ts b/backend/src/modules/auth/guards/jwt-auth.guard.spec.ts index 7d8328e..c0f5893 100644 --- a/backend/src/modules/auth/guards/jwt-auth.guard.spec.ts +++ b/backend/src/modules/auth/guards/jwt-auth.guard.spec.ts @@ -3,6 +3,19 @@ import { Reflector } from '@nestjs/core'; import { JwtAuthGuard } from './jwt-auth.guard'; 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', () => { let guard: JwtAuthGuard; let reflector: Reflector; diff --git a/backend/src/modules/tags/controllers/tags.controller.spec.ts b/backend/src/modules/tags/controllers/tags.controller.spec.ts new file mode 100644 index 0000000..7552add --- /dev/null +++ b/backend/src/modules/tags/controllers/tags.controller.spec.ts @@ -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); + service = module.get(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); + }); + }); +}); diff --git a/backend/src/modules/tags/services/tags.service.spec.ts b/backend/src/modules/tags/services/tags.service.spec.ts new file mode 100644 index 0000000..c20d881 --- /dev/null +++ b/backend/src/modules/tags/services/tags.service.spec.ts @@ -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); + }); + + 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 }]); + }); + }); +});