From 56efa864082c6e3051f7b9d1741f972ad17efbfb Mon Sep 17 00:00:00 2001 From: Marius Friess <34072851+mariusfriess@users.noreply.github.com> Date: Sun, 13 Aug 2023 17:52:40 +0200 Subject: [PATCH] Add unit tests for CategoryModule (#45) * implement tests * update test for dates * add test to category controller * refactor category module mocks --- src/category/category.controller.spec.ts | 146 ++++++++++++++++++ src/category/category.dto.spec.ts | 34 ++++ src/category/category.entity.spec.ts | 39 +++++ src/category/category.service.spec.ts | 141 +++++++++++++++++ src/category/mock/category.repository.mock.ts | 20 +++ src/category/mock/category.service.mock.ts | 32 ++++ 6 files changed, 412 insertions(+) create mode 100644 src/category/category.controller.spec.ts create mode 100644 src/category/category.dto.spec.ts create mode 100644 src/category/category.entity.spec.ts create mode 100644 src/category/category.service.spec.ts create mode 100644 src/category/mock/category.repository.mock.ts diff --git a/src/category/category.controller.spec.ts b/src/category/category.controller.spec.ts new file mode 100644 index 0000000..24fe440 --- /dev/null +++ b/src/category/category.controller.spec.ts @@ -0,0 +1,146 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CategoryController } from './category.controller'; +import { JwtService } from '@nestjs/jwt'; +import { CategoryService } from './category.service'; +import { AuthService } from '../auth/auth.service'; +import { isGuarded } from '../../test/utils'; +import { AuthGuard } from '../auth/auth.guard'; +import { Category } from './category.entity'; +import { Room } from '../room/room.entity'; +import { User } from '../user/user.entity'; +import { MockCategoryService } from './mock/category.service.mock'; +import { MockAuthService } from '../auth/mock/auth.service.mock'; + +const CATEGORIES = []; +const TEST_USER = { + id: 1, + name: 'Test User', + email: 'test@example.com', + organization: 'Test Organization', + categories: { + loadItems: jest.fn().mockResolvedValue(CATEGORIES), + }, + password: 'password', + createdAt: new Date(), + updatedAt: new Date(), + role: 'user', +} as unknown as User; + +for (let i = 0; i < 5; i++) { + CATEGORIES.push(new Category(`Category ${i}`, TEST_USER)); + CATEGORIES[i].id = i + 1; + CATEGORIES[i].rooms = [new Room(`Room ${i}`, CATEGORIES[i])]; +} + +describe('CategoryController', () => { + let controller: CategoryController; + let authService: AuthService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [CategoryController], + providers: [ + { + provide: CategoryService, + useValue: new MockCategoryService(CATEGORIES), + }, + { + provide: AuthService, + useValue: new MockAuthService(TEST_USER), + }, + { + provide: JwtService, + useValue: {}, + }, + ], + }).compile(); + + controller = module.get<CategoryController>(CategoryController); + authService = module.get<AuthService>(AuthService); + }); + + /** + * Test case to ensure that the CategoryController is defined. + * It checks whether the controller instance is created successfully. + */ + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should be protected by AuthGuard', () => { + expect(isGuarded(CategoryController, AuthGuard)).toBe(true); + }); + + describe('index', () => { + it('should return all categories of the current user', async () => { + expect(await controller.index()).toEqual(CATEGORIES); + }); + }); + + describe('create', () => { + it('should create a new category', async () => { + const category = await controller.create({ + name: 'Test Category', + }); + + expect(category).toBeDefined(); + expect(category.name).toBe('Test Category'); + expect(category.owner).toBe(TEST_USER); + }); + }); + + describe('update', () => { + it('should update a category', async () => { + const category = await controller.update(1, { + name: 'Updated Category', + }); + + expect(category).toBeDefined(); + expect(category.name).toBe('Updated Category'); + }); + + it('should throw an error if the id does not exist', async () => { + await expect( + controller.update(6, { + name: 'Updated Category', + }), + ).rejects.toThrow(); + }); + + it('should throw an error if the category does not belong to the user', async () => { + jest.spyOn(authService, 'user').mockResolvedValueOnce({ + id: 2, + name: 'Test User 2', + email: 'test2@example.com', + } as unknown as User); + + await expect( + controller.update(1, { + name: 'Updated Category', + }), + ).rejects.toThrow(); + }); + }); + + describe('delete', () => { + it('should delete a category', async () => { + await controller.delete(1); + + expect(CATEGORIES.length).toBe(4); + }); + + it('should throw an error if the id does not exist', async () => { + await expect(controller.delete(6)).rejects.toThrow(); + }); + + it('should throw an error if the category does not belong to the user', async () => { + jest.spyOn(authService, 'user').mockResolvedValueOnce({ + id: 2, + name: 'Test User 2', + email: 'test2@example.com', + } as unknown as User); + + await expect(controller.delete(1)).rejects.toThrow(); + }); + }); +}); diff --git a/src/category/category.dto.spec.ts b/src/category/category.dto.spec.ts new file mode 100644 index 0000000..16aff15 --- /dev/null +++ b/src/category/category.dto.spec.ts @@ -0,0 +1,34 @@ +import { validate } from 'class-validator'; +import { CreateCategory, UpdateCategory } from './category.dto'; + +describe('CreateCategory DTO', () => { + it('should validate a valid name', async () => { + const dto = new CreateCategory(); + dto.name = 'TestCategory'; + + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); + + it('should not validate an empty name', async () => { + const dto = new CreateCategory(); + dto.name = ''; + + const errors = await validate(dto); + expect(errors).toHaveLength(1); + expect(errors[0].constraints).toBeDefined(); + expect(errors[0].constraints['isNotEmpty']).toBe( + 'Name darf nicht leer sein', + ); + }); +}); + +describe('UpdateCategory DTO', () => { + it('should validate a valid name', async () => { + const dto = new UpdateCategory(); + dto.name = 'TestUpdateCategory'; + + const errors = await validate(dto); + expect(errors).toHaveLength(0); + }); +}); diff --git a/src/category/category.entity.spec.ts b/src/category/category.entity.spec.ts new file mode 100644 index 0000000..edf30ce --- /dev/null +++ b/src/category/category.entity.spec.ts @@ -0,0 +1,39 @@ +import { Category } from './category.entity'; +import { User } from '../user/user.entity'; + +describe('Category Entity', () => { + let user: User; + + beforeEach(() => { + user = new User( + 'Test User', + 'test@example.com', + 'Test Organization', + 'password', + ); + }); + + it('should be able to create a Category instance', () => { + const category = new Category('TestCategory', user); + expect(category).toBeInstanceOf(Category); + expect(category.name).toBe('TestCategory'); + expect(category.owner).toBe(user); + }); + + it('should initialize with an empty rooms collection', () => { + const category = new Category('TestCategory', user); + expect(category.rooms).toHaveLength(0); + }); + + it('should set default dates on creation', () => { + const category = new Category('TestCategory', user); + const currentDate = new Date(); + + expect(category.createdAt.getTime()).toBeLessThanOrEqual( + currentDate.getTime(), + ); + expect(category.updatedAt.getTime()).toBeLessThanOrEqual( + currentDate.getTime(), + ); + }); +}); diff --git a/src/category/category.service.spec.ts b/src/category/category.service.spec.ts new file mode 100644 index 0000000..5738768 --- /dev/null +++ b/src/category/category.service.spec.ts @@ -0,0 +1,141 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CategoryService } from './category.service'; +import { getRepositoryToken } from '@mikro-orm/nestjs'; +import { Category } from './category.entity'; +import { EntityManager } from '@mikro-orm/core'; +import { ChannelService } from '../channel/channel.service'; +import { User } from '../user/user.entity'; +import { Room } from '../room/room.entity'; +import { MockCategoryRepository } from './mock/category.repository.mock'; + +const CATEGORIES = []; +for (let i = 0; i < 5; i++) { + CATEGORIES.push(new Category(`Category ${i}`, undefined)); + CATEGORIES[i].rooms = [new Room(`Room ${i}`, CATEGORIES[i])]; +} +const TEST_USER = { + id: 1, + name: 'Test User', + email: 'test@example.com', + organization: 'Test Organization', + categories: { + loadItems: jest.fn().mockResolvedValue(CATEGORIES), + }, + password: 'password', + createdAt: new Date(), + updatedAt: new Date(), + role: 'user', +} as unknown as User; + +/** + * Test suite for the CategoryService class. + */ +describe('CategoryService', () => { + let service: CategoryService; + let channels: ChannelService; + let entityManager: EntityManager; + + /** + * Setup before each test case by creating a testing module and + * obtaining an instance of the CategoryService. + */ + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + CategoryService, + { + provide: EntityManager, + useValue: { + persistAndFlush: jest.fn(), + removeAndFlush: jest.fn(), + }, + }, + { + provide: getRepositoryToken(Category), + useValue: new MockCategoryRepository(TEST_USER), + }, + { + provide: ChannelService, + useValue: { + getChannelFromRoom: jest.fn().mockReturnValue(null), + }, + }, + ], + }).compile(); + + service = module.get<CategoryService>(CategoryService); + channels = module.get<ChannelService>(ChannelService); + entityManager = module.get<EntityManager>(EntityManager); + }); + + /** + * Test case to check if the CategoryService instance is defined. + */ + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('allFromUser', () => { + it('should return all categories for a user', async () => { + const categories = await service.allFromUser(TEST_USER); + + expect(TEST_USER.categories.loadItems).toBeCalledWith({ + populate: ['rooms'], + }); + expect(channels.getChannelFromRoom).toHaveBeenCalledTimes(5); + expect(categories).toHaveLength(5); + }); + }); + + describe('get', () => { + it('should return a category', async () => { + const category = await service.get(1, TEST_USER); + + expect(category).toBeDefined(); + expect(category.id).toBe(1); + }); + + it('should throw an error if the category does not exist', async () => { + await expect(service.get(2, TEST_USER)).rejects.toThrow(); + }); + }); + + describe('create', () => { + it('should create a category', async () => { + const category = await service.create('Test Category', TEST_USER); + + expect(category).toBeDefined(); + expect(category.name).toBe('Test Category'); + expect(category.owner).toBe(TEST_USER); + expect(entityManager.persistAndFlush).toBeCalledTimes(1); + }); + }); + + describe('update', () => { + it('should update a category', async () => { + const category = await service.update(1, TEST_USER, 'Updated Name'); + + expect(category).toBeDefined(); + expect(category.name).toBe('Updated Name'); + expect(entityManager.persistAndFlush).toBeCalledTimes(1); + }); + + it('should throw an error if the category does not exist', async () => { + await expect( + service.update(2, TEST_USER, 'Updated Name'), + ).rejects.toThrow(); + }); + }); + + describe('delete', () => { + it('should delete a category', async () => { + await service.delete(1, TEST_USER); + + expect(entityManager.removeAndFlush).toBeCalledTimes(1); + }); + + it('should throw an error if the category does not exist', async () => { + await expect(service.delete(2, TEST_USER)).rejects.toThrow(); + }); + }); +}); diff --git a/src/category/mock/category.repository.mock.ts b/src/category/mock/category.repository.mock.ts new file mode 100644 index 0000000..4df9d7b --- /dev/null +++ b/src/category/mock/category.repository.mock.ts @@ -0,0 +1,20 @@ +import { User } from '../../user/user.entity'; + +export class MockCategoryRepository { + constructor(private readonly testUser: User) {} + + public async findOneOrFail(data) { + if (data.id === 1) { + return Promise.resolve({ + id: 1, + name: 'Test Category', + owner: this.testUser, + rooms: [], + createdAt: new Date(), + updatedAt: new Date(), + }); + } else { + throw new Error('Category not found'); + } + } +} diff --git a/src/category/mock/category.service.mock.ts b/src/category/mock/category.service.mock.ts index 0f47061..fda3eaf 100644 --- a/src/category/mock/category.service.mock.ts +++ b/src/category/mock/category.service.mock.ts @@ -12,4 +12,36 @@ export class MockCategoryService { throw new Error('Category not found'); } } + + public async allFromUser(user: User) { + if (user.id === 1) { + return Promise.resolve(this.testCategories); + } + return Promise.resolve([]); + } + + public async create(name: string, owner: User) { + const category = new Category(name, owner); + return Promise.resolve(category); + } + + public async update(id: number, owner: User, name: string) { + const category = this.testCategories.find((c) => c.id === id); + if (category && category.owner === owner) { + category.name = name; + return Promise.resolve(category); + } else { + throw new Error('Category not found'); + } + } + + public async delete(id: number, owner: User) { + const category = this.testCategories.find((c) => c.id === id); + if (category && category.owner === owner) { + this.testCategories.splice(this.testCategories.indexOf(category), 1); + return Promise.resolve(category); + } else { + throw new Error('Category not found'); + } + } } -- GitLab