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