mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 16:35:42 +00:00
139 lines
4.8 KiB
TypeScript
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();
|
|
});
|
|
});
|