Files
trackgo-be/src/modules/users/users.service.spec.ts
T
shancheas 790725e227 Enhance employee and user management with linked user functionality
- Introduced `EmployeeUserWrite` type to manage user details associated with employees.
- Updated `EmployeesService` and `EmployeesRepository` to support user assignment and retrieval by user ID.
- Enhanced DTOs to include user information for employee creation and updates.
- Implemented validation to ensure proper handling of user data during employee operations.
- Added unit and e2e tests to validate the new functionality and ensure data integrity in user-employee relationships.
- Modified existing controllers to accommodate the new user linkage features in employee management.
2026-08-27 12:22:25 +07:00

364 lines
10 KiB
TypeScript

import {
BadRequestException,
ConflictException,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { DateTime } from '../../common/value-objects/date-time/date-time';
import { PhoneNumber } from '../../common/value-objects/phone-number/phone-number';
import { Status } from '../../common/value-objects/status/status';
import type { Employee } from '../configuration/employees/employee';
import { EmployeesRepository } from '../configuration/employees/employees.repository';
import { PrivilegesService } from '../privileges/privileges.service';
import type { User } from './user';
import { UsersRepository } from './users.repository';
import { UsersService } from './users.service';
describe('UsersService', () => {
let service: UsersService;
let repository: jest.Mocked<
Pick<
UsersRepository,
| 'findById'
| 'findByUsername'
| 'create'
| 'update'
| 'updatePrivilegeId'
| 'updateStatus'
| 'bulkUpdateStatus'
| 'delete'
| 'bulkDelete'
| 'list'
>
>;
let privilegesService: jest.Mocked<
Pick<PrivilegesService, 'findPrivilegeSummary'>
>;
let employeesRepository: jest.Mocked<
Pick<EmployeesRepository, 'findById' | 'findByUserId' | 'update'>
>;
const now = DateTime.fromUnixMs(1_700_000_000_000);
const sampleUser: User = {
id: 'user-1',
username: 'alice',
passwordHash: 'hashed',
privilegeId: null,
isSuperadmin: false,
status: Status.create('draft'),
createdAt: now,
updatedAt: now,
createdBy: 'user-1',
updatedBy: 'user-1',
privilege: null,
employee: null,
createdByUser: { id: 'user-1', username: 'alice' },
updatedByUser: { id: 'user-1', username: 'alice' },
};
const sampleEmployee: Employee = {
id: 'emp-1',
code: 'EMP_01',
name: 'Ada Lovelace',
phone: PhoneNumber.create('+6281234567890'),
position: 'sales',
status: Status.create('draft'),
createdAt: now,
updatedAt: now,
createdBy: 'actor-1',
updatedBy: 'actor-1',
userId: null,
user: null,
};
beforeEach(async () => {
repository = {
findById: jest.fn(),
findByUsername: jest.fn(),
create: jest.fn(),
update: jest.fn(),
updatePrivilegeId: jest.fn(),
updateStatus: jest.fn(),
bulkUpdateStatus: jest.fn(),
delete: jest.fn(),
bulkDelete: jest.fn(),
list: jest.fn(),
};
privilegesService = {
findPrivilegeSummary: jest.fn(),
};
employeesRepository = {
findById: jest.fn(),
findByUserId: jest.fn(),
update: jest.fn(),
};
const moduleRef: TestingModule = await Test.createTestingModule({
providers: [
UsersService,
{ provide: UsersRepository, useValue: repository },
{ provide: PrivilegesService, useValue: privilegesService },
{ provide: EmployeesRepository, useValue: employeesRepository },
{
provide: ConfigService,
useValue: { getOrThrow: () => 4 },
},
],
}).compile();
service = moduleRef.get(UsersService);
});
it('findById delegates to repository', async () => {
repository.findById.mockResolvedValue(sampleUser);
await expect(service.findById('user-1')).resolves.toEqual(sampleUser);
});
it('create stores lowercase username', async () => {
repository.findByUsername.mockResolvedValue(null);
repository.create.mockResolvedValue(sampleUser);
await service.create('Alice', 'hashed');
expect(repository.findByUsername).toHaveBeenCalledWith('alice');
expect(repository.create).toHaveBeenCalledWith({
username: 'alice',
passwordHash: 'hashed',
});
});
it('create throws ConflictException when username exists', async () => {
repository.findByUsername.mockResolvedValue(sampleUser);
await expect(service.create('alice', 'hashed')).rejects.toBeInstanceOf(
ConflictException,
);
expect(repository.create).not.toHaveBeenCalled();
});
it('findByUsername delegates to repository', async () => {
repository.findByUsername.mockResolvedValue(sampleUser);
await expect(service.findByUsername('Alice')).resolves.toEqual(sampleUser);
});
it('assignPrivilege validates privilege exists', async () => {
repository.findById.mockResolvedValue(sampleUser);
privilegesService.findPrivilegeSummary.mockResolvedValue({
id: 'priv-1',
name: 'Admin',
code: 'ADMIN',
status: 'active',
});
repository.updatePrivilegeId.mockResolvedValue({
...sampleUser,
privilegeId: 'priv-1',
privilege: { id: 'priv-1', code: 'ADMIN', name: 'Admin' },
});
const result = await service.assignPrivilege('user-1', 'priv-1');
expect(result.privilege).toEqual({
id: 'priv-1',
code: 'ADMIN',
name: 'Admin',
});
expect(result).not.toHaveProperty('passwordHash');
});
it('list maps nested relations and omits password', async () => {
repository.list.mockResolvedValue({ data: [sampleUser], total: 1 });
const result = await service.list({ page: 1, limit: 10 });
expect(result.total).toBe(1);
expect(result.data[0]).toMatchObject({
id: 'user-1',
username: 'alice',
status: 'draft',
privilege: null,
employee: null,
createdBy: { id: 'user-1', username: 'alice' },
});
expect(result.data[0]).not.toHaveProperty('passwordHash');
});
it('update rejects status field', async () => {
await expect(
service.update('user-1', { status: 'active', actorUserId: 'user-1' }),
).rejects.toBeInstanceOf(BadRequestException);
});
it('assertCanAuthenticate allows active users without employee', () => {
expect(() =>
service.assertCanAuthenticate({
...sampleUser,
status: Status.create('active'),
}),
).not.toThrow();
});
it('assertCanAuthenticate rejects draft users and inactive employees', () => {
expect(() => service.assertCanAuthenticate(sampleUser)).toThrow(
UnauthorizedException,
);
expect(() =>
service.assertCanAuthenticate({
...sampleUser,
status: Status.create('active'),
employee: {
id: 'emp-1',
code: 'EMP_01',
name: 'Ada',
status: Status.create('draft'),
},
}),
).toThrow(UnauthorizedException);
});
it('importCsv requires username and password headers', async () => {
await expect(
service.importCsv('name\nalice', 'user-1'),
).rejects.toBeInstanceOf(BadRequestException);
});
it('createManaged links an existing employee by id', async () => {
repository.create.mockResolvedValue(sampleUser);
employeesRepository.findById.mockResolvedValue(sampleEmployee);
employeesRepository.update.mockResolvedValue({
...sampleEmployee,
userId: 'user-1',
});
repository.findById.mockResolvedValue({
...sampleUser,
employee: {
id: 'emp-1',
code: 'EMP_01',
name: 'Ada Lovelace',
status: Status.create('draft'),
},
});
const result = await service.createManaged({
username: 'alice',
password: 'password123',
actorUserId: 'actor-1',
employeeId: 'emp-1',
});
expect(employeesRepository.update).toHaveBeenCalledWith(
'emp-1',
expect.objectContaining({
userId: 'actor-1',
assignedUserId: 'user-1',
}),
);
expect(result.employee).toEqual({
id: 'emp-1',
code: 'EMP_01',
name: 'Ada Lovelace',
});
});
it('createManaged rejects linking an employee assigned to another user', async () => {
repository.create.mockResolvedValue(sampleUser);
employeesRepository.findById.mockResolvedValue({
...sampleEmployee,
userId: 'other-user',
});
repository.delete.mockResolvedValue(undefined);
await expect(
service.createManaged({
username: 'alice',
password: 'password123',
actorUserId: 'actor-1',
employeeId: 'emp-1',
}),
).rejects.toBeInstanceOf(ConflictException);
expect(repository.delete).toHaveBeenCalledWith('user-1');
});
it('update with employeeId null unlinks the current employee', async () => {
repository.update.mockResolvedValue({
...sampleUser,
employee: {
id: 'emp-1',
code: 'EMP_01',
name: 'Ada Lovelace',
status: Status.create('draft'),
},
});
employeesRepository.findByUserId.mockResolvedValue({
...sampleEmployee,
userId: 'user-1',
});
employeesRepository.update.mockResolvedValue({
...sampleEmployee,
userId: null,
});
repository.findById.mockResolvedValue({ ...sampleUser, employee: null });
const result = await service.update('user-1', {
actorUserId: 'actor-1',
employeeId: null,
});
expect(employeesRepository.update).toHaveBeenCalledWith(
'emp-1',
expect.objectContaining({ assignedUserId: null, userId: 'actor-1' }),
);
expect(result.employee).toBeNull();
});
it('update with employeeId reassigns after unlinking the previous employee', async () => {
repository.update.mockResolvedValue(sampleUser);
employeesRepository.findById.mockResolvedValue({
...sampleEmployee,
id: 'emp-2',
code: 'EMP_02',
});
employeesRepository.findByUserId.mockResolvedValue({
...sampleEmployee,
userId: 'user-1',
});
employeesRepository.update.mockResolvedValue(sampleEmployee);
repository.findById.mockResolvedValue({
...sampleUser,
employee: {
id: 'emp-2',
code: 'EMP_02',
name: 'Ada Lovelace',
status: Status.create('draft'),
},
});
await service.update('user-1', {
actorUserId: 'actor-1',
employeeId: 'emp-2',
});
expect(employeesRepository.update).toHaveBeenNthCalledWith(
1,
'emp-1',
expect.objectContaining({ assignedUserId: null }),
);
expect(employeesRepository.update).toHaveBeenNthCalledWith(
2,
'emp-2',
expect.objectContaining({ assignedUserId: 'user-1' }),
);
});
it('update with unknown employeeId throws NotFoundException', async () => {
repository.findById.mockResolvedValue(sampleUser);
employeesRepository.findById.mockResolvedValue(null);
await expect(
service.update('user-1', {
actorUserId: 'actor-1',
employeeId: 'missing',
}),
).rejects.toBeInstanceOf(NotFoundException);
expect(repository.update).not.toHaveBeenCalled();
});
});