From 3b96c833e3dff078a7f29c80fdf496db364f2663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patr=C3=ADcia=20Dias?= <116013814+patricia-mdias@users.noreply.github.com> Date: Thu, 20 Apr 2023 16:47:12 +0100 Subject: [PATCH] test: user use cases (#1407) --- backend/src/libs/guards/deleteUser.guard.ts | 15 ++++ .../applications/reset-password.use-case.ts | 2 +- .../applications/delete-user.use-case.spec.ts | 79 +++++++++++++++++ .../applications/delete-user.use-case.ts | 35 ++++---- .../get-all-users-with-teams.use-case.spec.ts | 87 +++++++++++++++++++ .../get-all-users-with-teams.use-case.ts | 17 ++-- .../get-all-users.use-case.spec.ts | 34 ++++++++ .../applications/get-all-users.use-case.ts | 5 +- .../applications/get-user.use-case.spec.ts | 34 ++++++++ .../users/applications/get-user.use-case.ts | 4 +- .../update-sadmin.use-case.spec.ts | 85 ++++++++++++++++++ .../applications/update-sadmin.use-case.ts | 10 +-- .../users/controller/users.controller.ts | 32 ++++--- .../dto/useCase/delete-user.use-case.dto.ts | 14 +++ .../get-all-users-with-teams.use-case.dto.ts | 19 ++++ .../dto/useCase/update-sadmin.use-case.dto.ts | 11 +++ .../delete-user.use-case.interface.ts | 5 -- ...all-users-with-teams.use-case.interface.ts | 14 --- .../get-all-users.use-case.interface.ts | 5 -- .../get-user.use-case.interface.ts | 5 -- .../update-sadmin.use-case.interface.ts | 7 -- .../update.user.service.interface.ts | 21 ----- .../validate-email.use-case.interface.ts | 3 - .../services/update.user.service.interface.ts | 13 +-- .../get-all-users-with-teams.presenter.ts | 8 ++ 25 files changed, 450 insertions(+), 114 deletions(-) create mode 100644 backend/src/libs/guards/deleteUser.guard.ts create mode 100644 backend/src/modules/users/applications/delete-user.use-case.spec.ts create mode 100644 backend/src/modules/users/applications/get-all-users-with-teams.use-case.spec.ts create mode 100644 backend/src/modules/users/applications/get-all-users.use-case.spec.ts create mode 100644 backend/src/modules/users/applications/get-user.use-case.spec.ts create mode 100644 backend/src/modules/users/applications/update-sadmin.use-case.spec.ts create mode 100644 backend/src/modules/users/dto/useCase/delete-user.use-case.dto.ts create mode 100644 backend/src/modules/users/dto/useCase/get-all-users-with-teams.use-case.dto.ts create mode 100644 backend/src/modules/users/dto/useCase/update-sadmin.use-case.dto.ts delete mode 100644 backend/src/modules/users/interfaces/applications/delete-user.use-case.interface.ts delete mode 100644 backend/src/modules/users/interfaces/applications/get-all-users-with-teams.use-case.interface.ts delete mode 100644 backend/src/modules/users/interfaces/applications/get-all-users.use-case.interface.ts delete mode 100644 backend/src/modules/users/interfaces/applications/get-user.use-case.interface.ts delete mode 100644 backend/src/modules/users/interfaces/applications/update-sadmin.use-case.interface.ts delete mode 100644 backend/src/modules/users/interfaces/applications/update.user.service.interface.ts delete mode 100644 backend/src/modules/users/interfaces/applications/validate-email.use-case.interface.ts create mode 100644 backend/src/modules/users/presenter/get-all-users-with-teams.presenter.ts diff --git a/backend/src/libs/guards/deleteUser.guard.ts b/backend/src/libs/guards/deleteUser.guard.ts new file mode 100644 index 000000000..092dc069d --- /dev/null +++ b/backend/src/libs/guards/deleteUser.guard.ts @@ -0,0 +1,15 @@ +import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'; +import User from 'src/modules/users/entities/user.schema'; + +@Injectable() +export class DeleteUserGuard implements CanActivate { + async canActivate(context: ExecutionContext) { + const request = context.switchToHttp().getRequest(); + + const user: User = request.user; + const userToDelete: string = request.params.userId; + + // user requesting can't delete itself + return user._id !== userToDelete; + } +} diff --git a/backend/src/modules/auth/applications/reset-password.use-case.ts b/backend/src/modules/auth/applications/reset-password.use-case.ts index 317add04f..431ae3c72 100644 --- a/backend/src/modules/auth/applications/reset-password.use-case.ts +++ b/backend/src/modules/auth/applications/reset-password.use-case.ts @@ -1,7 +1,7 @@ +import { UpdateUserServiceInterface } from 'src/modules/users/interfaces/services/update.user.service.interface'; import { Inject, Injectable } from '@nestjs/common'; import { TYPES } from 'src/modules/users/interfaces/types'; import { ResetPasswordUseCaseInterface } from '../interfaces/applications/reset-password.use-case.interface'; -import { UpdateUserServiceInterface } from 'src/modules/users/interfaces/services/update.user.service.interface'; import { UPDATE_FAILED } from 'src/libs/exceptions/messages'; import { InvalidTokenException } from '../exceptions/invalidTokenException'; import { UpdateFailedException } from 'src/libs/exceptions/updateFailedBadRequestException'; diff --git a/backend/src/modules/users/applications/delete-user.use-case.spec.ts b/backend/src/modules/users/applications/delete-user.use-case.spec.ts new file mode 100644 index 000000000..3f514cc7b --- /dev/null +++ b/backend/src/modules/users/applications/delete-user.use-case.spec.ts @@ -0,0 +1,79 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; +import { UserRepositoryInterface } from '../repository/user.repository.interface'; +import { deleteUserUseCase } from '../users.providers'; +import * as Users from 'src/modules/users/interfaces/types'; +import { Test, TestingModule } from '@nestjs/testing'; +import { DeleteTeamUserServiceInterface } from 'src/modules/teamUsers/interfaces/services/delete.team.user.service.interface'; +import { GetTeamUserServiceInterface } from 'src/modules/teamUsers/interfaces/services/get.team.user.service.interface'; +import { UserFactory } from 'src/libs/test-utils/mocks/factories/user-factory'; +import faker from '@faker-js/faker'; +import { DeleteFailedException } from 'src/libs/exceptions/deleteFailedBadRequestException'; +import { DELETE_TEAM_USER_SERVICE, GET_TEAM_USER_SERVICE } from 'src/modules/teamUsers/constants'; + +const userId = faker.datatype.uuid(); +const userDeleted = UserFactory.create({ _id: userId }); +const teamsOfUser = faker.datatype.number(); + +describe('DeleteUserUseCase', () => { + let deleteUser: UseCase; + let userRepositoryMock: DeepMocked; + let deleteTeamUserServiceMock: DeepMocked; + let getTeamUserServiceMock: DeepMocked; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + deleteUserUseCase, + { + provide: Users.TYPES.repository, + useValue: createMock() + }, + { + provide: DELETE_TEAM_USER_SERVICE, + useValue: createMock() + }, + { + provide: GET_TEAM_USER_SERVICE, + useValue: createMock() + } + ] + }).compile(); + + deleteUser = module.get>(deleteUserUseCase.provide); + + userRepositoryMock = module.get(Users.TYPES.repository); + deleteTeamUserServiceMock = module.get(DELETE_TEAM_USER_SERVICE); + getTeamUserServiceMock = module.get(GET_TEAM_USER_SERVICE); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + it('should be defined', () => { + expect(deleteUser).toBeDefined(); + }); + + describe('execute', () => { + it('should return true', async () => { + userRepositoryMock.deleteUser.mockResolvedValue(userDeleted); + getTeamUserServiceMock.countTeamsOfUser.mockResolvedValue(teamsOfUser); + deleteTeamUserServiceMock.deleteTeamUsersOfUser.mockResolvedValue(teamsOfUser); + await expect(deleteUser.execute(userId)).resolves.toEqual(true); + }); + + it('should throw error when user is not deleted', async () => { + await expect(deleteUser.execute(userId)).rejects.toThrowError(DeleteFailedException); + }); + + it('should throw error when commitTransaction fails', async () => { + userRepositoryMock.deleteUser.mockResolvedValue(userDeleted); + getTeamUserServiceMock.countTeamsOfUser.mockResolvedValue(teamsOfUser); + deleteTeamUserServiceMock.deleteTeamUsersOfUser.mockResolvedValue(teamsOfUser); + userRepositoryMock.commitTransaction.mockRejectedValue(new Error()); + await expect(deleteUser.execute(userId)).rejects.toThrowError(DeleteFailedException); + }); + }); +}); diff --git a/backend/src/modules/users/applications/delete-user.use-case.ts b/backend/src/modules/users/applications/delete-user.use-case.ts index 3b5da4e8f..e45324975 100644 --- a/backend/src/modules/users/applications/delete-user.use-case.ts +++ b/backend/src/modules/users/applications/delete-user.use-case.ts @@ -1,15 +1,14 @@ -import { DeleteUserUseCaseInterface } from '../interfaces/applications/delete-user.use-case.interface'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; import { Inject, Injectable } from '@nestjs/common'; import { TYPES } from '../interfaces/types'; import { DeleteTeamUserServiceInterface } from 'src/modules/teamUsers/interfaces/services/delete.team.user.service.interface'; import { GetTeamUserServiceInterface } from '../../teamUsers/interfaces/services/get.team.user.service.interface'; -import UserDto from '../dto/user.dto'; import { UserRepositoryInterface } from '../repository/user.repository.interface'; import { DeleteFailedException } from 'src/libs/exceptions/deleteFailedBadRequestException'; import { DELETE_TEAM_USER_SERVICE, GET_TEAM_USER_SERVICE } from 'src/modules/teamUsers/constants'; @Injectable() -export class DeleteUserUseCase implements DeleteUserUseCaseInterface { +export class DeleteUserUseCase implements UseCase { constructor( @Inject(TYPES.repository) private readonly userRepository: UserRepositoryInterface, @Inject(DELETE_TEAM_USER_SERVICE) @@ -18,23 +17,12 @@ export class DeleteUserUseCase implements DeleteUserUseCaseInterface { private readonly getTeamUserService: GetTeamUserServiceInterface ) {} - async execute(user: UserDto, userId: string) { - if (user._id == userId) { - throw new DeleteFailedException(); - } - + async execute(userId: string) { await this.userRepository.startTransaction(); await this.deleteTeamUserService.startTransaction(); try { - await this.deleteUser(userId, true); - const teamsOfUser = await this.getTeamUserService.countTeamsOfUser(userId); - - if (teamsOfUser > 0) { - await this.deleteTeamUserService.deleteTeamUsersOfUser(userId, false); - } - await this.userRepository.commitTransaction(); - await this.deleteTeamUserService.commitTransaction(); + await this.deleteUserAndTeamUsers(userId); return true; } catch (e) { @@ -47,6 +35,21 @@ export class DeleteUserUseCase implements DeleteUserUseCaseInterface { throw new DeleteFailedException(); } + private async deleteUserAndTeamUsers(userId: string) { + try { + await this.deleteUser(userId, true); + const teamsOfUser = await this.getTeamUserService.countTeamsOfUser(userId); + + if (teamsOfUser > 0) { + await this.deleteTeamUserService.deleteTeamUsersOfUser(userId, true); + } + await this.userRepository.commitTransaction(); + await this.deleteTeamUserService.commitTransaction(); + } catch (error) { + throw new DeleteFailedException(); + } + } + private async deleteUser(userId: string, withSession: boolean) { const result = await this.userRepository.deleteUser(userId, withSession); diff --git a/backend/src/modules/users/applications/get-all-users-with-teams.use-case.spec.ts b/backend/src/modules/users/applications/get-all-users-with-teams.use-case.spec.ts new file mode 100644 index 000000000..4e4e472b8 --- /dev/null +++ b/backend/src/modules/users/applications/get-all-users-with-teams.use-case.spec.ts @@ -0,0 +1,87 @@ +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; +import { UserRepositoryInterface } from '../repository/user.repository.interface'; +import { getAllUsersWithTeamsUseCase } from '../users.providers'; +import * as Users from 'src/modules/users/interfaces/types'; +import { Test, TestingModule } from '@nestjs/testing'; +import { GetUserServiceInterface } from '../interfaces/services/get.user.service.interface'; +import { GetTeamUserServiceInterface } from 'src/modules/teamUsers/interfaces/services/get.team.user.service.interface'; +import { UserFactory } from 'src/libs/test-utils/mocks/factories/user-factory'; +import { TeamFactory } from 'src/libs/test-utils/mocks/factories/team-factory.mock'; +import { UserWithTeams } from '../interfaces/type-user-with-teams'; +import GetAllUsersWithTeamsUseCaseDto from '../dto/useCase/get-all-users-with-teams.use-case.dto'; +import { GetAllUsersWithTeamsPresenter } from 'src/modules/users/presenter/get-all-users-with-teams.presenter'; +import { sortTeamUserListAlphabetically } from '../utils/sortings'; +import { GET_TEAM_USER_SERVICE } from 'src/modules/teamUsers/constants'; + +const users = UserFactory.createMany(10); +const teams = TeamFactory.createMany(5); + +const usersWithTeams: UserWithTeams[] = users.map((user) => ({ + user, + teams: [...teams.map((team) => team._id)] +})); + +const getAllUsersWithTeamsProps: GetAllUsersWithTeamsUseCaseDto = {}; + +const getAllUsersWithTeamsResult: GetAllUsersWithTeamsPresenter = { + userWithTeams: sortTeamUserListAlphabetically(usersWithTeams), + userAmount: users.length, + hasNextPage: 0 + 1 < Math.ceil(users.length / 15), + page: 0 +}; + +describe('GetAllUsersWithTeamsUseCase', () => { + let getAllUsersWithTeams: UseCase; + let userRepositoryMock: DeepMocked; + let getUserServiceMock: DeepMocked; + let getTeamUserServiceMock: DeepMocked; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + getAllUsersWithTeamsUseCase, + { + provide: Users.TYPES.repository, + useValue: createMock() + }, + { + provide: Users.TYPES.services.GetUserService, + useValue: createMock() + }, + { + provide: GET_TEAM_USER_SERVICE, + useValue: createMock() + } + ] + }).compile(); + + getAllUsersWithTeams = module.get< + UseCase + >(getAllUsersWithTeamsUseCase.provide); + + userRepositoryMock = module.get(Users.TYPES.repository); + getUserServiceMock = module.get(Users.TYPES.services.GetUserService); + getTeamUserServiceMock = module.get(GET_TEAM_USER_SERVICE); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + it('should be defined', () => { + expect(getAllUsersWithTeams).toBeDefined(); + }); + + describe('execute', () => { + it('should return paginated users with teams', async () => { + userRepositoryMock.getAllWithPagination.mockResolvedValue(users); + getUserServiceMock.countUsers.mockResolvedValue(users.length); + getTeamUserServiceMock.getUsersOnlyWithTeams.mockResolvedValue(usersWithTeams); + + await expect(getAllUsersWithTeams.execute(getAllUsersWithTeamsProps)).resolves.toEqual( + getAllUsersWithTeamsResult + ); + }); + }); +}); diff --git a/backend/src/modules/users/applications/get-all-users-with-teams.use-case.ts b/backend/src/modules/users/applications/get-all-users-with-teams.use-case.ts index 7b693aa6c..7e50a6bc8 100644 --- a/backend/src/modules/users/applications/get-all-users-with-teams.use-case.ts +++ b/backend/src/modules/users/applications/get-all-users-with-teams.use-case.ts @@ -1,15 +1,19 @@ +import { GetAllUsersWithTeamsPresenter } from '../presenter/get-all-users-with-teams.presenter'; import { GetTeamUserServiceInterface } from 'src/modules/teamUsers/interfaces/services/get.team.user.service.interface'; import { Inject, Injectable } from '@nestjs/common'; -import { GetAllUsersWithTeamsUseCaseInterface } from '../interfaces/applications/get-all-users-with-teams.use-case.interface'; import { UserWithTeams } from '../interfaces/type-user-with-teams'; import { TYPES } from '../interfaces/types'; import { UserRepositoryInterface } from '../repository/user.repository.interface'; -import { sortAlphabetically } from '../utils/sortings'; +import { sortTeamUserListAlphabetically } from '../utils/sortings'; import { GetUserServiceInterface } from '../interfaces/services/get.user.service.interface'; +import GetAllUsersWithTeamsUseCaseDto from '../dto/useCase/get-all-users-with-teams.use-case.dto'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; import { GET_TEAM_USER_SERVICE } from 'src/modules/teamUsers/constants'; @Injectable() -export default class GetAllUsersWithTeamsUseCase implements GetAllUsersWithTeamsUseCaseInterface { +export default class GetAllUsersWithTeamsUseCase + implements UseCase +{ constructor( @Inject(TYPES.repository) private readonly userRepository: UserRepositoryInterface, @@ -19,7 +23,10 @@ export default class GetAllUsersWithTeamsUseCase implements GetAllUsersWithTeams private readonly getTeamUserService: GetTeamUserServiceInterface ) {} - async execute(page = 0, size = 15, searchUser?: string) { + async execute({ page, size, searchUser }: GetAllUsersWithTeamsUseCaseDto) { + page = page ?? 0; + size = size ?? 15; + const [users, count] = await Promise.all([ this.getAllUsersWithPagination(page, size, searchUser), this.getUserService.countUsers() @@ -48,7 +55,7 @@ export default class GetAllUsersWithTeamsUseCase implements GetAllUsersWithTeams page }; - results.userWithTeams.sort((a, b) => sortAlphabetically(a.user, b.user)); + results.userWithTeams = sortTeamUserListAlphabetically(results.userWithTeams); return results; } diff --git a/backend/src/modules/users/applications/get-all-users.use-case.spec.ts b/backend/src/modules/users/applications/get-all-users.use-case.spec.ts new file mode 100644 index 000000000..174d61f3c --- /dev/null +++ b/backend/src/modules/users/applications/get-all-users.use-case.spec.ts @@ -0,0 +1,34 @@ +import { createMock } from '@golevelup/ts-jest'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; +import { UserRepositoryInterface } from '../repository/user.repository.interface'; +import { getAllUsersUseCase } from '../users.providers'; +import * as Users from 'src/modules/users/interfaces/types'; +import { Test, TestingModule } from '@nestjs/testing'; +import User from '../entities/user.schema'; + +describe('GetAllUsersUseCase', () => { + let getAllUsers: UseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + getAllUsersUseCase, + { + provide: Users.TYPES.repository, + useValue: createMock() + } + ] + }).compile(); + + getAllUsers = module.get>(getAllUsersUseCase.provide); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + it('should be defined', () => { + expect(getAllUsers).toBeDefined(); + }); +}); diff --git a/backend/src/modules/users/applications/get-all-users.use-case.ts b/backend/src/modules/users/applications/get-all-users.use-case.ts index f1717941a..cc0233f1e 100644 --- a/backend/src/modules/users/applications/get-all-users.use-case.ts +++ b/backend/src/modules/users/applications/get-all-users.use-case.ts @@ -1,10 +1,11 @@ import { Inject, Injectable } from '@nestjs/common'; -import { GetAllUsersUseCaseInterface } from '../interfaces/applications/get-all-users.use-case.interface'; import { TYPES } from '../interfaces/types'; import { UserRepositoryInterface } from '../repository/user.repository.interface'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; +import User from '../entities/user.schema'; @Injectable() -export default class GetAllUsersUseCase implements GetAllUsersUseCaseInterface { +export default class GetAllUsersUseCase implements UseCase { constructor( @Inject(TYPES.repository) private readonly userRepository: UserRepositoryInterface diff --git a/backend/src/modules/users/applications/get-user.use-case.spec.ts b/backend/src/modules/users/applications/get-user.use-case.spec.ts new file mode 100644 index 000000000..8092069c4 --- /dev/null +++ b/backend/src/modules/users/applications/get-user.use-case.spec.ts @@ -0,0 +1,34 @@ +import { createMock } from '@golevelup/ts-jest'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; +import { getUserUseCase } from '../users.providers'; +import * as Users from 'src/modules/users/interfaces/types'; +import { Test, TestingModule } from '@nestjs/testing'; +import { GetUserServiceInterface } from '../interfaces/services/get.user.service.interface'; +import User from '../entities/user.schema'; + +describe('GetUserUseCase', () => { + let getUser: UseCase; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + getUserUseCase, + { + provide: Users.TYPES.services.GetUserService, + useValue: createMock() + } + ] + }).compile(); + + getUser = module.get>(getUserUseCase.provide); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + it('should be defined', () => { + expect(getUser).toBeDefined(); + }); +}); diff --git a/backend/src/modules/users/applications/get-user.use-case.ts b/backend/src/modules/users/applications/get-user.use-case.ts index fdada1dc4..131ef75f8 100644 --- a/backend/src/modules/users/applications/get-user.use-case.ts +++ b/backend/src/modules/users/applications/get-user.use-case.ts @@ -1,11 +1,11 @@ import { Inject, Injectable } from '@nestjs/common'; import User from '../entities/user.schema'; -import { GetUserUseCaseInterface } from '../interfaces/applications/get-user.use-case.interface'; import { GetUserServiceInterface } from '../interfaces/services/get.user.service.interface'; import { TYPES } from '../interfaces/types'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; @Injectable() -export class GetUserUseCase implements GetUserUseCaseInterface { +export class GetUserUseCase implements UseCase { constructor( @Inject(TYPES.services.GetUserService) private readonly getUserService: GetUserServiceInterface diff --git a/backend/src/modules/users/applications/update-sadmin.use-case.spec.ts b/backend/src/modules/users/applications/update-sadmin.use-case.spec.ts new file mode 100644 index 000000000..e31f9402a --- /dev/null +++ b/backend/src/modules/users/applications/update-sadmin.use-case.spec.ts @@ -0,0 +1,85 @@ +import { updateSAdminUseCase } from './../users.providers'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { UserRepositoryInterface } from '../repository/user.repository.interface'; +import * as Users from 'src/modules/users/interfaces/types'; +import { Test, TestingModule } from '@nestjs/testing'; +import { UserFactory } from 'src/libs/test-utils/mocks/factories/user-factory'; +import faker from '@faker-js/faker'; +import UpdateUserDto from '../dto/update.user.dto'; +import { UserDtoFactory } from 'src/libs/test-utils/mocks/factories/dto/userDto-factory.mock'; +import { UpdateFailedException } from 'src/libs/exceptions/updateFailedBadRequestException'; +import UpdateSAdminUseCaseDto from '../dto/useCase/update-sadmin.use-case.dto'; +import User from '../entities/user.schema'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; + +const user = UserFactory.create({ isSAdmin: true }); +const userDto = UserDtoFactory.create(); + +const updateUserDto: UpdateUserDto = { + _id: user._id, + firstName: faker.name.firstName(), + lastName: faker.name.lastName(), + email: user.email, + isSAdmin: user.isSAdmin +}; + +const updatedUser = UserFactory.create({ + _id: user._id, + firstName: updateUserDto.firstName, + lastName: updateUserDto.lastName, + email: user.email, + isSAdmin: user.isSAdmin +}); + +describe('UpdateSAdminUseCase', () => { + let updateSAdmin: UseCase; + let userRepositoryMock: DeepMocked; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + updateSAdminUseCase, + { + provide: Users.TYPES.repository, + useValue: createMock() + } + ] + }).compile(); + + updateSAdmin = module.get>(updateSAdminUseCase.provide); + + userRepositoryMock = module.get(Users.TYPES.repository); + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + it('should be defined', () => { + expect(updateSAdmin).toBeDefined(); + }); + + describe('execute', () => { + it('should return updated super admin', async () => { + userRepositoryMock.updateSuperAdmin.mockResolvedValue(updatedUser); + await expect( + updateSAdmin.execute({ user: updateUserDto, requestUser: userDto }) + ).resolves.toEqual(updatedUser); + }); + + it('should throw update failed exception when request user is the user to update', async () => { + const userDto2 = { ...userDto, _id: updateUserDto._id }; + await expect( + updateSAdmin.execute({ user: updateUserDto, requestUser: userDto2 }) + ).rejects.toThrowError(UpdateFailedException); + }); + + it('should throw update failed exception when user is not updated', async () => { + userRepositoryMock.updateSuperAdmin.mockResolvedValue(null); + await expect( + updateSAdmin.execute({ user: updateUserDto, requestUser: userDto }) + ).rejects.toThrowError(UpdateFailedException); + }); + }); +}); diff --git a/backend/src/modules/users/applications/update-sadmin.use-case.ts b/backend/src/modules/users/applications/update-sadmin.use-case.ts index 011f1026e..ad7912ddf 100644 --- a/backend/src/modules/users/applications/update-sadmin.use-case.ts +++ b/backend/src/modules/users/applications/update-sadmin.use-case.ts @@ -1,19 +1,19 @@ import { Inject, Injectable } from '@nestjs/common'; -import UpdateUserDto from '../dto/update.user.dto'; import { TYPES } from '../interfaces/types'; import { UserRepositoryInterface } from '../repository/user.repository.interface'; -import UserDto from '../dto/user.dto'; -import { UpdateSAdminUseCaseInterface } from '../interfaces/applications/update-sadmin.use-case.interface'; import { UpdateFailedException } from 'src/libs/exceptions/updateFailedBadRequestException'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; +import User from '../entities/user.schema'; +import UpdateSAdminUseCaseDto from '../dto/useCase/update-sadmin.use-case.dto'; @Injectable() -export default class UpdateSAdminUseCase implements UpdateSAdminUseCaseInterface { +export default class UpdateSAdminUseCase implements UseCase { constructor( @Inject(TYPES.repository) private readonly userRepository: UserRepositoryInterface ) {} - async execute(user: UpdateUserDto, requestUser: UserDto) { + async execute({ user, requestUser }: UpdateSAdminUseCaseDto) { if (requestUser._id.toString() === user._id) { throw new UpdateFailedException(); } diff --git a/backend/src/modules/users/controller/users.controller.ts b/backend/src/modules/users/controller/users.controller.ts index f1fa975ec..eb60532c6 100644 --- a/backend/src/modules/users/controller/users.controller.ts +++ b/backend/src/modules/users/controller/users.controller.ts @@ -38,11 +38,12 @@ import { NotFoundResponse } from '../../../libs/swagger/errors/not-found.swagger import { UpdateSuperAdminSwagger } from '../swagger/update.superadmin.swagger'; import RequestWithUser from 'src/libs/interfaces/requestWithUser.interface'; import { PaginationParams } from 'src/libs/dto/param/pagination.params'; -import { GetAllUsersUseCaseInterface } from '../interfaces/applications/get-all-users.use-case.interface'; -import { GetAllUsersWithTeamsUseCaseInterface } from '../interfaces/applications/get-all-users-with-teams.use-case.interface'; -import { GetUserUseCaseInterface } from '../interfaces/applications/get-user.use-case.interface'; -import { UpdateSAdminUseCaseInterface } from '../interfaces/applications/update-sadmin.use-case.interface'; -import { DeleteUserUseCaseInterface } from '../interfaces/applications/delete-user.use-case.interface'; +import { DeleteUserGuard } from 'src/libs/guards/deleteUser.guard'; +import { UseCase } from 'src/libs/interfaces/use-case.interface'; +import UpdateSAdminUseCaseDto from '../dto/useCase/update-sadmin.use-case.dto'; +import User from '../entities/user.schema'; +import { GetAllUsersWithTeamsPresenter } from 'src/modules/users/presenter/get-all-users-with-teams.presenter'; +import GetAllUsersWithTeamsUseCaseDto from '../dto/useCase/get-all-users-with-teams.use-case.dto'; @ApiBearerAuth('access-token') @ApiTags('Users') @@ -51,15 +52,18 @@ import { DeleteUserUseCaseInterface } from '../interfaces/applications/delete-us export default class UsersController { constructor( @Inject(TYPES.applications.GetUserUseCase) - private getUserUseCase: GetUserUseCaseInterface, + private getUserUseCase: UseCase, @Inject(TYPES.applications.GetAllUsersUseCase) - private getAllUsersUseCase: GetAllUsersUseCaseInterface, + private getAllUsersUseCase: UseCase, @Inject(TYPES.applications.GetAllUsersWithTeamsUseCase) - private getAllUsersWithTeamsUseCase: GetAllUsersWithTeamsUseCaseInterface, + private getAllUsersWithTeamsUseCase: UseCase< + GetAllUsersWithTeamsUseCaseDto, + GetAllUsersWithTeamsPresenter + >, @Inject(TYPES.applications.UpdateSAdminUseCase) - private updateSAdminUseCase: UpdateSAdminUseCaseInterface, + private updateSAdminUseCase: UseCase, @Inject(TYPES.applications.DeleteUserUseCase) - private deleteUserApp: DeleteUserUseCaseInterface + private deleteUserApp: UseCase ) {} @ApiOperation({ summary: 'Retrieve a list of existing users' }) @@ -101,7 +105,7 @@ export default class UsersController { }) @Get('teams') getAllUsersWithTeams(@Query() { page, size, searchUser }: PaginationParams) { - return this.getAllUsersWithTeamsUseCase.execute(page, size, searchUser); + return this.getAllUsersWithTeamsUseCase.execute({ page, size, searchUser }); } @ApiOperation({ summary: 'Retrieve user' }) @@ -160,7 +164,7 @@ export default class UsersController { @UseGuards(SuperAdminGuard) @Put('/sadmin') updateUserSuperAdmin(@Req() request: RequestWithUser, @Body() userData: UpdateUserDto) { - return this.updateSAdminUseCase.execute(userData, request.user); + return this.updateSAdminUseCase.execute({ user: userData, requestUser: request.user }); } @ApiOperation({ summary: 'Delete user' }) @@ -193,9 +197,9 @@ export default class UsersController { description: 'Internal Server Error', type: InternalServerErrorResponse }) - @UseGuards(SuperAdminGuard) + @UseGuards(SuperAdminGuard, DeleteUserGuard) @Delete(':userId') deleteUser(@Req() request: RequestWithUser, @Param() { userId }: UserParams) { - return this.deleteUserApp.execute(request.user, userId); + return this.deleteUserApp.execute(userId); } } diff --git a/backend/src/modules/users/dto/useCase/delete-user.use-case.dto.ts b/backend/src/modules/users/dto/useCase/delete-user.use-case.dto.ts new file mode 100644 index 000000000..b658d8ce7 --- /dev/null +++ b/backend/src/modules/users/dto/useCase/delete-user.use-case.dto.ts @@ -0,0 +1,14 @@ +import UserDto from 'src/modules/users/dto/user.dto'; +import { ApiProperty } from '@nestjs/swagger'; +import { IsNotEmpty, IsString } from 'class-validator'; + +export default class DeleteUserUseCaseDto { + @ApiProperty() + @IsString() + @IsNotEmpty() + userId: string; + + @ApiProperty({ type: UserDto }) + @IsNotEmpty() + user: UserDto; +} diff --git a/backend/src/modules/users/dto/useCase/get-all-users-with-teams.use-case.dto.ts b/backend/src/modules/users/dto/useCase/get-all-users-with-teams.use-case.dto.ts new file mode 100644 index 000000000..307ba8ab9 --- /dev/null +++ b/backend/src/modules/users/dto/useCase/get-all-users-with-teams.use-case.dto.ts @@ -0,0 +1,19 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsNumber, IsOptional, IsString } from 'class-validator'; + +export default class GetAllUsersWithTeamsUseCaseDto { + @ApiProperty() + @IsNumber() + @IsOptional() + page?: number; + + @ApiProperty() + @IsNumber() + @IsOptional() + size?: number; + + @ApiProperty() + @IsString() + @IsOptional() + searchUser?: string; +} diff --git a/backend/src/modules/users/dto/useCase/update-sadmin.use-case.dto.ts b/backend/src/modules/users/dto/useCase/update-sadmin.use-case.dto.ts new file mode 100644 index 000000000..ba8d5a4ac --- /dev/null +++ b/backend/src/modules/users/dto/useCase/update-sadmin.use-case.dto.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; +import UpdateUserDto from '../update.user.dto'; +import UserDto from '../user.dto'; + +export default class UpdateSAdminUseCaseDto { + @ApiProperty() + user: UpdateUserDto; + + @ApiProperty() + requestUser: UserDto; +} diff --git a/backend/src/modules/users/interfaces/applications/delete-user.use-case.interface.ts b/backend/src/modules/users/interfaces/applications/delete-user.use-case.interface.ts deleted file mode 100644 index f47caf980..000000000 --- a/backend/src/modules/users/interfaces/applications/delete-user.use-case.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import UserDto from '../../dto/user.dto'; - -export interface DeleteUserUseCaseInterface { - execute(user: UserDto, userId: string): Promise; -} diff --git a/backend/src/modules/users/interfaces/applications/get-all-users-with-teams.use-case.interface.ts b/backend/src/modules/users/interfaces/applications/get-all-users-with-teams.use-case.interface.ts deleted file mode 100644 index 8e2fbc6d8..000000000 --- a/backend/src/modules/users/interfaces/applications/get-all-users-with-teams.use-case.interface.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { UserWithTeams } from '../type-user-with-teams'; - -export interface GetAllUsersWithTeamsUseCaseInterface { - execute( - page: number, - size: number, - searchUser?: string - ): Promise<{ - userWithTeams: UserWithTeams[]; - userAmount: number; - hasNextPage: boolean; - page: number; - }>; -} diff --git a/backend/src/modules/users/interfaces/applications/get-all-users.use-case.interface.ts b/backend/src/modules/users/interfaces/applications/get-all-users.use-case.interface.ts deleted file mode 100644 index aae3734b9..000000000 --- a/backend/src/modules/users/interfaces/applications/get-all-users.use-case.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import User from '../../entities/user.schema'; - -export interface GetAllUsersUseCaseInterface { - execute(): Promise; -} diff --git a/backend/src/modules/users/interfaces/applications/get-user.use-case.interface.ts b/backend/src/modules/users/interfaces/applications/get-user.use-case.interface.ts deleted file mode 100644 index fbd8513c8..000000000 --- a/backend/src/modules/users/interfaces/applications/get-user.use-case.interface.ts +++ /dev/null @@ -1,5 +0,0 @@ -import User from '../../entities/user.schema'; - -export interface GetUserUseCaseInterface { - execute(userId: string): Promise; -} diff --git a/backend/src/modules/users/interfaces/applications/update-sadmin.use-case.interface.ts b/backend/src/modules/users/interfaces/applications/update-sadmin.use-case.interface.ts deleted file mode 100644 index d02596bf5..000000000 --- a/backend/src/modules/users/interfaces/applications/update-sadmin.use-case.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -import UpdateUserDto from '../../dto/update.user.dto'; -import User from '../../entities/user.schema'; -import UserDto from '../../dto/user.dto'; - -export interface UpdateSAdminUseCaseInterface { - execute(user: UpdateUserDto, requestUser: UserDto): Promise; -} diff --git a/backend/src/modules/users/interfaces/applications/update.user.service.interface.ts b/backend/src/modules/users/interfaces/applications/update.user.service.interface.ts deleted file mode 100644 index 2bb1ec29a..000000000 --- a/backend/src/modules/users/interfaces/applications/update.user.service.interface.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { LeanDocument } from 'mongoose'; -import UpdateUserDto from '../../dto/update.user.dto'; -import User, { UserDocument } from '../../entities/user.schema'; -import UserDto from '../../dto/user.dto'; - -export interface UpdateUserApplicationInterface { - setCurrentRefreshToken( - refreshToken: string, - userId: string - ): Promise | null>; - - setPassword( - userEmail: string, - newPassword: string, - newPasswordConf: string - ): Promise; - - checkEmail(token: string): Promise; - - updateSuperAdmin(user: UpdateUserDto, requestUser: UserDto): Promise>; -} diff --git a/backend/src/modules/users/interfaces/applications/validate-email.use-case.interface.ts b/backend/src/modules/users/interfaces/applications/validate-email.use-case.interface.ts deleted file mode 100644 index ada97e019..000000000 --- a/backend/src/modules/users/interfaces/applications/validate-email.use-case.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface ValidateUserEmailUseCaseInterface { - execute(email: string): Promise; -} diff --git a/backend/src/modules/users/interfaces/services/update.user.service.interface.ts b/backend/src/modules/users/interfaces/services/update.user.service.interface.ts index c25531cb1..cd92866a6 100644 --- a/backend/src/modules/users/interfaces/services/update.user.service.interface.ts +++ b/backend/src/modules/users/interfaces/services/update.user.service.interface.ts @@ -1,18 +1,13 @@ -import { LeanDocument } from 'mongoose'; -import User, { UserDocument } from '../../entities/user.schema'; +import User from '../../entities/user.schema'; export interface UpdateUserServiceInterface { - setCurrentRefreshToken(refreshToken: string, userId: string): Promise; + setCurrentRefreshToken(refreshToken: string, userId: string): Promise; - setPassword( - userEmail: string, - newPassword: string, - newPasswordConf: string - ): Promise; + setPassword(userEmail: string, newPassword: string, newPasswordConf: string): Promise; checkEmailOfToken(token: string): Promise; - updateUserAvatar(avatar: string, userId: string): Promise>; + updateUserAvatar(avatar: string, userId: string): Promise; updateUserUpdatedAtField(user: string): Promise; } diff --git a/backend/src/modules/users/presenter/get-all-users-with-teams.presenter.ts b/backend/src/modules/users/presenter/get-all-users-with-teams.presenter.ts new file mode 100644 index 000000000..cd9464fba --- /dev/null +++ b/backend/src/modules/users/presenter/get-all-users-with-teams.presenter.ts @@ -0,0 +1,8 @@ +import { UserWithTeams } from 'src/modules/users/interfaces/type-user-with-teams'; + +export class GetAllUsersWithTeamsPresenter { + userWithTeams: UserWithTeams[]; + userAmount: number; + hasNextPage: boolean; + page: number; +}