// Mock dependencies jest.mock("../../Users/Users", () => ({ __esModule: true, default: { users: { findOne: jest.fn(), }, getUserFullName: jest.fn(), }, })); jest.mock("../../Companies/Companies", () => ({ __esModule: true, default: { companies: { findOne: jest.fn(), }, }, })); jest.mock("../../Services/Service", () => ({ __esModule: true, default: { services: { find: jest.fn(), findOne: jest.fn(), }, }, })); jest.mock("../../Notifications/Notifications", () => ({ __esModule: true, NotificationsManager: { sendSystemNotification: jest.fn(), }, })); jest.mock("../../Employees/Employee", () => { const mockFindOne = jest.fn(); const mockFind = jest.fn(); const mockFindById = jest.fn(); return { __esModule: true, default: { employees: { findOne: mockFindOne, find: mockFind, employeeList: { findOne: mockFindOne, find: mockFind, }, }, findById: mockFindById, checkPermission: jest.fn().mockResolvedValue(true), _mocks: { findOne: mockFindOne, find: mockFind, findById: mockFindById }, }, }; }); import EmployeesServicesList from "../EmployeesServices"; import EmployeesList from "../../Employees/Employee"; describe("EmployeesServices.findByService — removed employee exclusion", () => { let mockEmployeesFindOne: jest.Mock; let mockEmployeesFindById: jest.Mock; let originalEmployeesServicesAdapter: any; beforeEach(() => { jest.clearAllMocks(); mockEmployeesFindOne = (EmployeesList as any)._mocks.findOne; mockEmployeesFindById = (EmployeesList as any)._mocks.findById; // Save and replace the employeesServices adapter originalEmployeesServicesAdapter = EmployeesServicesList.employeesServices; const mockESFindOne = jest.fn(); const mockESFind = jest.fn(); (EmployeesServicesList as any).employeesServices = { findOne: mockESFindOne, find: mockESFind, create: jest.fn(), delete: jest.fn(), update: jest.fn(), paginate: jest.fn(), employeesServicesList: { findOne: mockESFindOne, find: mockESFind, deleteMany: jest.fn(), }, }; }); afterEach(() => { (EmployeesServicesList as any).employeesServices = originalEmployeesServicesAdapter; }); it("excludes removed employees from findByService results", async () => { // Service assignments include one for an active employee and one for a removed employee const activeServiceAssignment = { _id: "es-001", employeeId: "emp-active", serviceId: "svc-001", companyId: "company-001", }; const removedServiceAssignment = { _id: "es-002", employeeId: "emp-removed", serviceId: "svc-001", companyId: "company-001", }; const activeEmployeeDoc = { _id: "emp-active", id: "emp-active", companyId: "company-001", userId: "user-active", removed: false, }; const removedEmployeeDoc = { _id: "emp-removed", id: "emp-removed", companyId: "company-001", userId: "user-removed", removed: true, }; // employeesServices.find returns both assignments const mockESFind = (EmployeesServicesList as any).employeesServices.find; mockESFind.mockResolvedValue([activeServiceAssignment, removedServiceAssignment]); // EmployeesList.employees.findOne returns the employee docs mockEmployeesFindOne .mockResolvedValueOnce(activeEmployeeDoc) .mockResolvedValueOnce(removedEmployeeDoc); // EmployeesList.findById for the active employee mockEmployeesFindById.mockResolvedValue({ id: "emp-active", fullName: "Active User", avatar: "", }); const result = await EmployeesServicesList.findByService({ serviceId: "svc-001", }); // Only the active employee should appear in results expect(result).toHaveLength(1); expect(result[0].id).toBe("emp-active"); expect(result[0].name).toBe("Active User"); }); it("returns empty when all assigned employees are removed", async () => { const removedServiceAssignment = { _id: "es-001", employeeId: "emp-removed", serviceId: "svc-001", companyId: "company-001", }; const removedEmployeeDoc = { _id: "emp-removed", id: "emp-removed", companyId: "company-001", userId: "user-removed", removed: true, }; const mockESFind = (EmployeesServicesList as any).employeesServices.find; mockESFind.mockResolvedValue([removedServiceAssignment]); mockEmployeesFindOne.mockResolvedValueOnce(removedEmployeeDoc); const result = await EmployeesServicesList.findByService({ serviceId: "svc-001", }); expect(result).toEqual([]); expect(result).toHaveLength(0); // Prove the guard stopped processing: employee was found but removed, // so findById should NEVER have been called for enrichment. expect((EmployeesList as any)._mocks.findById).not.toHaveBeenCalled(); }); it("excludes employees whose Employee document is null (hard-deleted User)", async () => { // Scenario: service assignment exists but the Employee record was hard-deleted // (e.g. cascade cleanup). findByService must skip gracefully. const activeServiceAssignment = { _id: "es-001", employeeId: "emp-active", serviceId: "svc-001", companyId: "company-001", }; const orphanServiceAssignment = { _id: "es-002", employeeId: "emp-orphan", serviceId: "svc-001", companyId: "company-001", }; const activeEmployeeDoc = { _id: "emp-active", id: "emp-active", companyId: "company-001", userId: "user-active", removed: false, }; const mockESFind = (EmployeesServicesList as any).employeesServices.find; mockESFind.mockResolvedValue([activeServiceAssignment, orphanServiceAssignment]); // First findOne: active employee → found; second: orphan → null (deleted) mockEmployeesFindOne .mockResolvedValueOnce(activeEmployeeDoc) .mockResolvedValueOnce(null); mockEmployeesFindById.mockResolvedValue({ id: "emp-active", fullName: "Active User", avatar: "", }); const result = await EmployeesServicesList.findByService({ serviceId: "svc-001", }); // Only the active employee should appear expect(result).toHaveLength(1); expect(result[0].id).toBe("emp-active"); // findById was called once for the active employee only expect(mockEmployeesFindById).toHaveBeenCalledTimes(1); expect(mockEmployeesFindById).toHaveBeenCalledWith({ id: "emp-active" }); }); it("does not call findById for removed employees, only for active ones", async () => { // Proves the continue guard prevents enrichment calls for removed employees. const activeAssignment = { _id: "es-001", employeeId: "emp-active", serviceId: "svc-001", companyId: "company-001", }; const removedAssignment = { _id: "es-002", employeeId: "emp-removed", serviceId: "svc-001", companyId: "company-001", }; const activeDoc = { _id: "emp-active", id: "emp-active", removed: false, }; const removedDoc = { _id: "emp-removed", id: "emp-removed", removed: true, }; const mockESFind = (EmployeesServicesList as any).employeesServices.find; mockESFind.mockResolvedValue([activeAssignment, removedAssignment]); mockEmployeesFindOne .mockResolvedValueOnce(activeDoc) .mockResolvedValueOnce(removedDoc); mockEmployeesFindById.mockResolvedValue({ id: "emp-active", fullName: "Active User", avatar: "", }); const result = await EmployeesServicesList.findByService({ serviceId: "svc-001", }); // findById called ONCE (active only), never for the removed employee expect(mockEmployeesFindById).toHaveBeenCalledTimes(1); expect(mockEmployeesFindById).toHaveBeenCalledWith({ id: "emp-active" }); expect(result).toHaveLength(1); }); }); // ───────────────────────────────────────────────────────────────────────────── // Public Booking Path — Limitation Documentation // ───────────────────────────────────────────────────────────────────────────── // // The actual public booking flow for collaborator resolution is: // // Frontend (Appointment.tsx) // → POST /employees/employees-by-service // → FindEmployeesByServiceController.findEmployeesByService() // → new EmployeesService().findByService(requestBody) [thin wrapper: connect() + delegate] // → EmployeesServicesList.findByService(data) [model — tested above] // // The model layer (EmployeesServicesManager.findByService) is the GATEKEEPER // that applies the `removed` guard. The controller and service classes are thin // pass-through wrappers with no additional filtering logic. // // Full-path integration testing (controller → service → model → DB) is not // practically unit-testable because: // 1. The service layer calls mongoose.connect() which requires a real DB // 2. The controller instantiates the service class internally // 3. 5+ collections would need seeding (Employees, EmployeesServices, Users, Companies) // // The model-level tests above provide the STRONGEST feasible coverage because: // - They exercise the exact `removed` guard logic (line 444 of EmployeesServices.ts) // - They prove findById enrichment is skipped for removed/null employees // - They prove the correct filter is applied to service assignment queries // - The controller/service layers add no filtering — they are pure delegation // ─────────────────────────────────────────────────────────────────────────────