Skip to content

Commit

Permalink
feature/user-soft-delete-unit-tests: added unit tests for soft delete…
Browse files Browse the repository at this point in the history
… user feature (#165)
  • Loading branch information
BEdev24 authored Jul 16, 2024
1 parent 3224b25 commit bf86bdd
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 97 deletions.
4 changes: 2 additions & 2 deletions backend/src/users/api/users.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,11 +238,11 @@ export class UsersController {
})
@ApiResponse({
status: 403,
description: 'Forbidden resource"',
description: 'Forbidden resource',
})
@ApiResponse({
status: 404,
description: 'User with {id} not found"',
description: 'User with {id} not found',
})
@ApiParam({
name: 'id',
Expand Down
168 changes: 122 additions & 46 deletions backend/src/users/facade/users.facade.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { Test, TestingModule } from '@nestjs/testing';
import { UsersFacade } from './users.facade';
import { UsersService } from '../services/users.service';
import { ConflictException, NotFoundException } from '@nestjs/common';
import {
ConflictException,
ForbiddenException,
NotFoundException,
} from '@nestjs/common';
import { UserStatusEnum } from '../enums/user-status.enum';
import { UserDto } from '../dto/user.dto';
import { Permission } from '../entities/permission.entity';
Expand All @@ -13,6 +17,9 @@ import { UserResponse } from '../api/response/user.response';
import { UserMapper } from '../mapper/userMapper.mapper';
import { PaginatedDto } from 'src/util/pagination/dto/paginated.dto';
import { PaginateQuery } from 'nestjs-paginate';
import { ToggleStatusRequest } from '../api/request/toggle-status.request';
import { PermissionEnum } from '../enums/permission.enum';
import { RoleFactory } from '../role/role.factory';

describe('UsersFacade', () => {
let facade: UsersFacade;
Expand Down Expand Up @@ -82,7 +89,7 @@ describe('UsersFacade', () => {
hotAddresses: ['sofija123', 'sofija234'],
role: mockRoles[0].code,
permissions: [mockPermissions[0].code],
isDeleted: false,
deactivatedAt: null,
createdAt: null,
updatedAt: null,
},
Expand All @@ -96,7 +103,7 @@ describe('UsersFacade', () => {
hotAddresses: ['ivan1', 'ivan2'],
role: mockRoles[2].code,
permissions: [],
isDeleted: false,
deactivatedAt: null,
createdAt: null,
updatedAt: null,
},
Expand All @@ -113,7 +120,7 @@ describe('UsersFacade', () => {
hotAddresses: ['sofija123', 'sofija234'],
role: mockRoles[0].code,
permissions: [mockPermissions[0].code],
isDeleted: false,
deactivatedAt: null,
createdAt: null,
updatedAt: null,
},
Expand All @@ -127,7 +134,7 @@ describe('UsersFacade', () => {
hotAddresses: ['ivan1', 'ivan2'],
role: mockRoles[2].code,
permissions: [],
isDeleted: false,
deactivatedAt: null,
createdAt: null,
updatedAt: null,
},
Expand All @@ -141,7 +148,7 @@ describe('UsersFacade', () => {
hotAddresses: ['zoran1', 'zoran2'],
role: mockRoles[2].code,
permissions: [],
isDeleted: false,
deactivatedAt: null,
createdAt: null,
updatedAt: null,
},
Expand Down Expand Up @@ -176,7 +183,7 @@ describe('UsersFacade', () => {
}
});
if (!foundUser) {
return new NotFoundException(`User with id ${id} not found`);
throw new NotFoundException(`User with id ${id} not found`);
}
return foundUser;
}),
Expand Down Expand Up @@ -233,29 +240,17 @@ describe('UsersFacade', () => {

return usersPaginatedDto;
}),
softDelete: jest.fn().mockImplementation((id) => {
let foundUser: UserDto;
mockUsers.forEach((item) => {
if (id === item.id) {
foundUser = item;
}
});
if (!foundUser) {
return new NotFoundException(`User with id ${id} not found`);
}
if (foundUser.isDeleted) {
return new ConflictException(`User already deleted`);
}
foundUser.isDeleted = true;
foundUser.status = UserStatusEnum.INACTIVE;
return UserMapper.mapUserDtoToResponse(foundUser);
}),
updateUserStatus: jest.fn(),
};

const mockS3Service = {
deleteFile: jest.fn(),
};

const mockRoleFactory = {
getInstance: jest.fn(),
};

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
Expand All @@ -265,6 +260,10 @@ describe('UsersFacade', () => {
provide: UsersService,
useValue: mockUserService,
},
{
provide: RoleFactory,
useValue: mockRoleFactory,
},
],
}).compile();

Expand Down Expand Up @@ -466,35 +465,112 @@ describe('UsersFacade', () => {
});
});

describe('Soft delete users', () => {
it('should delete a user by id', async () => {
const userId = mockUsers[0].id;
const result = await facade.softDelete(userId);
expect(result.isDeleted).toEqual(true);
expect(result.status).toEqual(UserStatusEnum.INACTIVE);
expect(mockUserService.softDelete).toHaveBeenCalledWith(userId);
describe(`Activate/Deactivate user's status`, () => {
it('should deactivate a user by id', async () => {
const user = mockUsers[1];
const request: ToggleStatusRequest = {
userId: user.id,
status: UserStatusEnum.INACTIVE,
};
const permissions: PermissionEnum[] = [PermissionEnum.MANAGE_CC_MEMBERS];
const updatedUser: UserDto = { ...user, status: UserStatusEnum.INACTIVE };
const userResponse: UserResponse =
UserMapper.mapUserDtoToResponse(updatedUser);
mockRoleFactory.getInstance.mockReturnValue({
managedBy: () => PermissionEnum.MANAGE_CC_MEMBERS,
});
mockUserService.updateUserStatus.mockResolvedValue(updatedUser);
const result = await facade.toggleStatus(request, permissions);
expect(mockUserService.findById).toHaveBeenCalledWith(request.userId);
expect(mockUserService.updateUserStatus).toHaveBeenCalledWith(
user.id,
request.status,
);
expect(result).toEqual(userResponse);
});
it(`shouldn't delete a user - user not found by id`, async () => {
const userId = 'notExistingUser';

it('should deactivate an admin', async () => {
const user = mockUsers[0];
const request: ToggleStatusRequest = {
userId: user.id,
status: UserStatusEnum.INACTIVE,
};
const permissions: PermissionEnum[] = [
PermissionEnum.MANAGE_CC_MEMBERS,
PermissionEnum.MANAGE_ADMINS,
];
const updatedUser: UserDto = { ...user, status: UserStatusEnum.INACTIVE };
const userResponse: UserResponse =
UserMapper.mapUserDtoToResponse(updatedUser);
mockRoleFactory.getInstance.mockReturnValue({
managedBy: () => PermissionEnum.MANAGE_ADMINS,
});
mockUserService.updateUserStatus.mockResolvedValue(updatedUser);
const result = await facade.toggleStatus(request, permissions);
expect(mockUserService.findById).toHaveBeenCalledWith(request.userId);
expect(mockUserService.updateUserStatus).toHaveBeenCalledWith(
user.id,
request.status,
);
expect(result).toEqual(userResponse);
});

it(`shouldn't deactivate an admin - no permission`, async () => {
const user = mockUsers[0];
const request: ToggleStatusRequest = {
userId: user.id,
status: UserStatusEnum.INACTIVE,
};
const permissions: PermissionEnum[] = [PermissionEnum.MANAGE_CC_MEMBERS];
mockRoleFactory.getInstance.mockReturnValue({
managedBy: () => PermissionEnum.MANAGE_ADMINS,
});
try {
await facade.softDelete(userId);
await facade.toggleStatus(request, permissions);
} catch (error) {
expect(error).toBeInstanceOf(NotFoundException);
expect(error.status).toEqual(404);
expect(error.message).toEqual(`User with id ${userId} not found`);
expect(mockUserService.softDelete).toHaveBeenCalledWith(userId);
expect(error).toBeInstanceOf(ForbiddenException);
expect(error.status).toEqual(403);
expect(error.message).toEqual(`You have no permission for this action`);
}
});
it(`shouldn't delete a user - user already deleted`, async () => {
const user = mockUsers[1];
user.isDeleted = true;

it(`shouldn't deactivate a super admin - no permission`, async () => {
const user = mockUsers[0];
const request: ToggleStatusRequest = {
userId: user.id,
status: UserStatusEnum.INACTIVE,
};
const permissions: PermissionEnum[] = [
PermissionEnum.MANAGE_CC_MEMBERS,
PermissionEnum.MANAGE_ADMINS,
];
mockRoleFactory.getInstance.mockReturnValue({
managedBy: () => null,
});
try {
await facade.softDelete(user.id);
await facade.toggleStatus(request, permissions);
} catch (error) {
expect(error).toBeInstanceOf(ConflictException);
expect(error.status).toEqual(409);
expect(error.message).toEqual(`User already deleted`);
expect(mockUserService.softDelete).toHaveBeenCalledWith(user.id);
expect(error).toBeInstanceOf(ForbiddenException);
expect(error.status).toEqual(403);
expect(error.message).toEqual(`You have no permission for this action`);
}
});

it(`shouldn't deactivate a user - user not found by id`, async () => {
const request: ToggleStatusRequest = {
userId: 'notExistingUser',
status: UserStatusEnum.INACTIVE,
};
const permissions: PermissionEnum[] = [PermissionEnum.MANAGE_CC_MEMBERS];
try {
await facade.toggleStatus(request, permissions);
} catch (error) {
expect(error).toBeInstanceOf(NotFoundException);
expect(error.status).toEqual(404);
expect(error.message).toEqual(
`User with id ${request.userId} not found`,
);
expect(mockUserService.findById).toHaveBeenCalledWith(request.userId);
}
});
});
Expand Down
54 changes: 5 additions & 49 deletions backend/src/users/services/users.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ const user: User = {
},
permissions: null,
hotAddresses: null,
isDeleted: false,
deactivatedAt: null,
createdAt: null,
updatedAt: null,
};
Expand Down Expand Up @@ -130,7 +130,7 @@ const mockUsers: User[] = [
status: UserStatusEnum.ACTIVE,
role: mockRoles[2],
permissions: [],
isDeleted: false,
deactivatedAt: null,
createdAt: null,
updatedAt: null,
},
Expand All @@ -144,7 +144,7 @@ const mockUsers: User[] = [
status: UserStatusEnum.ACTIVE,
role: mockRoles[1],
permissions: [mockPermissions[0], mockPermissions[1]],
isDeleted: false,
deactivatedAt: null,
createdAt: null,
updatedAt: null,
},
Expand Down Expand Up @@ -335,7 +335,7 @@ describe('UsersService', () => {
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('Create a CC member POST api/auth/register-user && api/auth/register-admin', () => {
describe('Create a CC member and admin', () => {
afterEach(() => {
jest.clearAllMocks();
});
Expand Down Expand Up @@ -499,7 +499,7 @@ describe('UsersService', () => {
});
});
//TODO This is a bad description - why would user service test know the api where it has been referenced on the upper layers?
describe('Fetch all roles GET /api/users/roles', () => {
describe('Fetch all roles', () => {
it('should return all roles', async () => {
const roles = await service.getAllRoles();
for (const value of roles) {
Expand Down Expand Up @@ -709,48 +709,4 @@ describe('UsersService', () => {
expect(mockPaginator.paginate).toHaveBeenCalled();
});
});

describe(`Soft delete users`, () => {
it(`should delete a user by id`, async () => {
const mockUser = mockUsers[0];
const mockFindEntityById = jest
.spyOn<any, any>(service, 'findEntityById')
.mockResolvedValueOnce(mockUser);
const deletedUser = await service.softDelete(mockUser.id);
expect(deletedUser.isDeleted).toEqual(true);
expect(deletedUser.status).toEqual(UserStatusEnum.INACTIVE);
expect(mockFindEntityById).toHaveBeenCalledWith(mockUser.id);
});
it(`shouldn't delete a user - already deleted`, async () => {
const mockUser = mockUsers[0];
mockUser.isDeleted = true;
jest
.spyOn<any, any>(service, 'findEntityById')
.mockResolvedValueOnce(mockUser);
try {
await service.softDelete(mockUser.id);
} catch (error) {
expect(error).toBeInstanceOf(ConflictException);
expect(error.status).toEqual(409);
expect(error.message).toEqual(`User already deleted`);
}
});
it(`shouldn't delete a user - user not found`, async () => {
const userId = 'notExistingUser';
const mockFindEntityById = jest.spyOn<any, any>(
service,
'findEntityById',
);
mockFindEntityById.mockImplementation(() => {
throw new NotFoundException(`User with id ${userId} not found`);
});
try {
await service.softDelete(userId);
} catch (error) {
expect(error).toBeInstanceOf(NotFoundException);
expect(error.status).toEqual(404);
expect(error.message).toEqual(`User with id ${userId} not found`);
}
});
});
});

0 comments on commit bf86bdd

Please sign in to comment.