Files
edr-platform/apps/edr-hr-api/src/modules/employees/employees.service.spec.ts
2026-08-26 23:57:26 +03:00

139 lines
4.8 KiB
TypeScript

import { ConflictException, ForbiddenException } from "@nestjs/common";
import { ActorContext, EmployeesService } from "./employees.service";
import { EEmploymentState, EEmploymentType, EmployeeProfile } from "./entities/employee-profile.entity";
import { CreateEmployeeProfileDto } from "./dto/create-employee-profile.dto";
const actorFixture = (over: Partial<ActorContext> = {}): ActorContext => ({
employeeId: null,
userId: "user-1",
organizationId: "org-1",
isSuperAdmin: false,
...over,
});
const iamEmployeeFixture = (over: Record<string, unknown> = {}) => ({
id: "iam-emp-1",
organizationId: "org-1",
name: { am: "", en: "Jane Doe" },
status: "ACTIVE",
isCurrent: true,
unitId: null,
username: "jane.doe",
email: "jane@example.com",
phoneNumber: "0911000000",
createdAt: new Date("2026-01-01T00:00:00Z"),
...over,
});
const createDto = (over: Partial<CreateEmployeeProfileDto> = {}): CreateEmployeeProfileDto =>
({
employeeId: "iam-emp-1",
employmentType: EEmploymentType.PERMANENT,
hireDate: "2026-01-15",
...over,
}) as CreateEmployeeProfileDto;
describe("EmployeesService.create", () => {
let employeesRepository: {
findByEmployeeId: jest.Mock;
findByEmployeeNumber: jest.Mock;
findHighestEmployeeNumberSuffix: jest.Mock;
create: jest.Mock;
};
let jobTitlesRepository: { findById: jest.Mock };
let iamDirectory: {
requireEmployee: jest.Mock;
findEmployee: jest.Mock;
findLineManagerEmployeeId: jest.Mock;
};
let iamOperations: { activateEmployee: jest.Mock; deactivateEmployee: jest.Mock };
let service: EmployeesService;
beforeEach(() => {
employeesRepository = {
findByEmployeeId: jest.fn().mockResolvedValue(null),
findByEmployeeNumber: jest.fn().mockResolvedValue(null),
findHighestEmployeeNumberSuffix: jest.fn().mockResolvedValue(5),
create: jest.fn().mockImplementation(async (data: Partial<EmployeeProfile>) => ({
id: "profile-new",
managerEmployeeId: null,
...data,
})),
};
jobTitlesRepository = { findById: jest.fn() };
iamDirectory = {
requireEmployee: jest.fn().mockResolvedValue(iamEmployeeFixture()),
findEmployee: jest.fn().mockResolvedValue(iamEmployeeFixture()),
findLineManagerEmployeeId: jest.fn().mockResolvedValue(null),
};
iamOperations = {
activateEmployee: jest.fn().mockResolvedValue(undefined),
deactivateEmployee: jest.fn().mockResolvedValue(undefined),
};
service = new EmployeesService(
employeesRepository as never,
jobTitlesRepository as never,
iamDirectory as never,
iamOperations as never,
);
});
it("creates a profile for an IAM employee who does not have one yet", async () => {
const result = await service.create(createDto(), actorFixture());
expect(employeesRepository.create).toHaveBeenCalledWith(
expect.objectContaining({
employeeId: "iam-emp-1",
employeeNumber: "EMP-00006",
employmentState: EEmploymentState.ACTIVE,
isPensionEligible: true,
createdBy: "user-1",
updatedBy: "user-1",
}),
);
expect(result.employeeId).toBe("iam-emp-1");
expect(result.iam?.username).toBe("jane.doe");
});
it("throws ConflictException when a profile already exists for that IAM employee id — the re-hire guard", async () => {
employeesRepository.findByEmployeeId.mockResolvedValue({
id: "existing-profile-1",
employeeId: "iam-emp-1",
});
await expect(service.create(createDto(), actorFixture())).rejects.toBeInstanceOf(
ConflictException,
);
await expect(service.create(createDto(), actorFixture())).rejects.toThrow(
/already has HR profile existing-profile-1/,
);
expect(employeesRepository.create).not.toHaveBeenCalled();
});
it("is a SEPARATE guard from recruitment's hireFromOffer re-hire check — it fires purely off employeesRepository.findByEmployeeId, independent of any offer/application state", async () => {
employeesRepository.findByEmployeeId.mockResolvedValue({
id: "existing-profile-2",
employeeId: "iam-emp-1",
});
await expect(service.create(createDto(), actorFixture())).rejects.toBeInstanceOf(
ConflictException,
);
expect(employeesRepository.findByEmployeeId).toHaveBeenCalledWith("iam-emp-1");
});
it("refuses to create a profile for an IAM employee in another organization", async () => {
iamDirectory.requireEmployee.mockResolvedValue(
iamEmployeeFixture({ organizationId: "other-org" }),
);
await expect(service.create(createDto(), actorFixture())).rejects.toBeInstanceOf(
ForbiddenException,
);
expect(employeesRepository.findByEmployeeId).not.toHaveBeenCalled();
expect(employeesRepository.create).not.toHaveBeenCalled();
});
});