Skip to content

Commit

Permalink
test: user use cases (#1407)
Browse files Browse the repository at this point in the history
  • Loading branch information
patricia-mdias authored Apr 20, 2023
1 parent e9e09c0 commit 3b96c83
Show file tree
Hide file tree
Showing 25 changed files with 450 additions and 114 deletions.
15 changes: 15 additions & 0 deletions backend/src/libs/guards/deleteUser.guard.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string, boolean>;
let userRepositoryMock: DeepMocked<UserRepositoryInterface>;
let deleteTeamUserServiceMock: DeepMocked<DeleteTeamUserServiceInterface>;
let getTeamUserServiceMock: DeepMocked<GetTeamUserServiceInterface>;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
deleteUserUseCase,
{
provide: Users.TYPES.repository,
useValue: createMock<UserRepositoryInterface>()
},
{
provide: DELETE_TEAM_USER_SERVICE,
useValue: createMock<DeleteTeamUserServiceInterface>()
},
{
provide: GET_TEAM_USER_SERVICE,
useValue: createMock<GetTeamUserServiceInterface>()
}
]
}).compile();

deleteUser = module.get<UseCase<string, boolean>>(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);
});
});
});
35 changes: 19 additions & 16 deletions backend/src/modules/users/applications/delete-user.use-case.ts
Original file line number Diff line number Diff line change
@@ -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<string, boolean> {
constructor(
@Inject(TYPES.repository) private readonly userRepository: UserRepositoryInterface,
@Inject(DELETE_TEAM_USER_SERVICE)
Expand All @@ -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) {
Expand All @@ -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);

Expand Down
Original file line number Diff line number Diff line change
@@ -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<GetAllUsersWithTeamsUseCaseDto, GetAllUsersWithTeamsPresenter>;
let userRepositoryMock: DeepMocked<UserRepositoryInterface>;
let getUserServiceMock: DeepMocked<GetUserServiceInterface>;
let getTeamUserServiceMock: DeepMocked<GetTeamUserServiceInterface>;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
getAllUsersWithTeamsUseCase,
{
provide: Users.TYPES.repository,
useValue: createMock<UserRepositoryInterface>()
},
{
provide: Users.TYPES.services.GetUserService,
useValue: createMock<GetUserServiceInterface>()
},
{
provide: GET_TEAM_USER_SERVICE,
useValue: createMock<GetTeamUserServiceInterface>()
}
]
}).compile();

getAllUsersWithTeams = module.get<
UseCase<GetAllUsersWithTeamsUseCaseDto, GetAllUsersWithTeamsPresenter>
>(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
);
});
});
});
Original file line number Diff line number Diff line change
@@ -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<GetAllUsersWithTeamsUseCaseDto, GetAllUsersWithTeamsPresenter>
{
constructor(
@Inject(TYPES.repository)
private readonly userRepository: UserRepositoryInterface,
Expand All @@ -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()
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void, User[]>;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
getAllUsersUseCase,
{
provide: Users.TYPES.repository,
useValue: createMock<UserRepositoryInterface>()
}
]
}).compile();

getAllUsers = module.get<UseCase<void, User[]>>(getAllUsersUseCase.provide);
});

beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
});

it('should be defined', () => {
expect(getAllUsers).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -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<void, User[]> {
constructor(
@Inject(TYPES.repository)
private readonly userRepository: UserRepositoryInterface
Expand Down
34 changes: 34 additions & 0 deletions backend/src/modules/users/applications/get-user.use-case.spec.ts
Original file line number Diff line number Diff line change
@@ -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<string, User>;

beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
getUserUseCase,
{
provide: Users.TYPES.services.GetUserService,
useValue: createMock<GetUserServiceInterface>()
}
]
}).compile();

getUser = module.get<UseCase<string, User>>(getUserUseCase.provide);
});

beforeEach(() => {
jest.clearAllMocks();
jest.resetAllMocks();
});

it('should be defined', () => {
expect(getUser).toBeDefined();
});
});
Loading

0 comments on commit 3b96c83

Please sign in to comment.