feat: implement collaborator reactivation logic, add access denied handling, and improve metric filtering
This commit is contained in:
@@ -0,0 +1,387 @@
|
||||
/**
|
||||
* Tests for Appointments.getAppointmentEvent snapshot fallback
|
||||
* when a removed employee's User account has been deleted.
|
||||
*
|
||||
* The fallback uses Employee.profileSnapshot fields instead of crashing.
|
||||
*/
|
||||
|
||||
// Mock all external dependencies
|
||||
jest.mock("../../Users/Users", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
users: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
getUserFullName: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Clients/Clients", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
clients: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
getClientFullName: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Companies/Companies", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
companies: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Employees/Employee", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
employees: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Services/Service", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
services: {
|
||||
find: jest.fn(),
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../HeatMap/HeatMap", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
getHeatMap: jest.fn(),
|
||||
addHeatMap: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Schedules/Schedules", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
find: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../SchedulesEnabled/SchedulesEnabled", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../SchedulesDisabled/SchedulesDisabled", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
find: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../SchedulesOverrides/SchedulesOverrides", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
find: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Metrics/Metrics", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
addAppointment: jest.fn(),
|
||||
canAddAppointment: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Notifications/Notifications", () => ({
|
||||
__esModule: true,
|
||||
NotificationsManager: {
|
||||
sendSystemNotification: jest.fn(),
|
||||
sendPushNotification: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../SystemNotifications/SystemNotification", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
create: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../PlanSubscriptions/PlanSubscriptons", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Repeats/Repeats", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
repeats: {
|
||||
find: jest.fn().mockResolvedValue([]),
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
existDisabledDate: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../Discounts/Discounts", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
getDiscountData: jest.fn().mockResolvedValue({
|
||||
name: "",
|
||||
value: 0,
|
||||
type: "PERCENTAGE",
|
||||
code: "",
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../../helpers/check", () => ({
|
||||
validatePermissionsByCompany: jest.fn(),
|
||||
validateSessionUser: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../Templates/Templates", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
sendEmail: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../ClientAccounts/ClientAccount", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
getAvailableAmount: jest.fn().mockResolvedValue(0),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../DiscountsEmail/DiscountsEmail", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
findOne: jest.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock("../../../index", () => ({
|
||||
io: {
|
||||
to: jest.fn(() => ({ emit: jest.fn() })),
|
||||
},
|
||||
}));
|
||||
|
||||
import AppointmentsList from "../Appointments";
|
||||
import UsersManager from "../../Users/Users";
|
||||
import EmployeesList from "../../Employees/Employee";
|
||||
import ClientsManager from "../../Clients/Clients";
|
||||
import ServiceList from "../../Services/Service";
|
||||
|
||||
describe("getAppointmentEvent — historical read snapshot fallback", () => {
|
||||
let originalAdapter: any;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Save and replace adapter
|
||||
originalAdapter = AppointmentsList.Appointments;
|
||||
const mockAppointmentFindOne = jest.fn();
|
||||
(AppointmentsList as any).Appointments = {
|
||||
find: jest.fn(),
|
||||
findOne: mockAppointmentFindOne,
|
||||
AppointmentList: {
|
||||
findOne: mockAppointmentFindOne,
|
||||
find: jest.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
(AppointmentsList as any).Appointments = originalAdapter;
|
||||
});
|
||||
|
||||
it("falls back to Employee snapshot when User account is deleted", async () => {
|
||||
const mockAppointment = {
|
||||
_id: "appt-001",
|
||||
id: "appt-001",
|
||||
companyId: "company-001",
|
||||
clientId: "client-001",
|
||||
employeeId: "emp-removed",
|
||||
serviceId: "svc-001",
|
||||
start: new Date("2026-07-20T10:00:00Z"),
|
||||
startHour: 10,
|
||||
endHour: 11,
|
||||
dateDay: new Date("2026-07-20"),
|
||||
price: 100,
|
||||
length: 60,
|
||||
payment: false,
|
||||
comments: "",
|
||||
discountId: undefined,
|
||||
};
|
||||
|
||||
const mockClient = {
|
||||
_id: "client-001",
|
||||
id: "client-001",
|
||||
userId: "client-user-001",
|
||||
firstName: "Client",
|
||||
lastName: "Smith",
|
||||
};
|
||||
|
||||
const mockClientUser = {
|
||||
_id: "client-user-001",
|
||||
id: "client-user-001",
|
||||
firstName: "Client",
|
||||
lastName: "Smith",
|
||||
avatar: "client-avatar",
|
||||
};
|
||||
|
||||
// Employee exists (removed, with snapshot)
|
||||
const mockRemovedEmployee = {
|
||||
_id: "emp-removed",
|
||||
id: "emp-removed",
|
||||
companyId: "company-001",
|
||||
userId: "deleted-user-id",
|
||||
calendarColor: "#ff0000",
|
||||
removed: true,
|
||||
profileSnapshot: {
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
email: "john@example.com",
|
||||
avatar: "old-avatar-url",
|
||||
},
|
||||
};
|
||||
|
||||
// Mock adapter.findOne for the appointment
|
||||
const mockAppointmentFindOne = (AppointmentsList as any).Appointments.AppointmentList.findOne;
|
||||
mockAppointmentFindOne.mockResolvedValue(mockAppointment);
|
||||
|
||||
// Mock lookups
|
||||
(ClientsManager.clients.findOne as jest.Mock).mockResolvedValue(mockClient);
|
||||
(ServiceList.services.findOne as jest.Mock).mockResolvedValue({
|
||||
_id: "svc-001",
|
||||
name: "Haircut",
|
||||
description: "Professional haircut",
|
||||
});
|
||||
|
||||
// Call order for UsersManager.users.findOne (same mock as UserList):
|
||||
// 1. Line 1217: UserList.users.findOne (NOT awaited, but consumes a value)
|
||||
// 2. Line 1241: UsersManager.users.findOne for userClient
|
||||
// 3. Line 1253: UsersManager.users.findOne for userEmployee
|
||||
(UsersManager.users.findOne as jest.Mock)
|
||||
.mockResolvedValueOnce({ _id: "session-user" }) // line 1217: not awaited, just needs truthy
|
||||
.mockResolvedValueOnce(mockClientUser) // line 1241: client user
|
||||
.mockResolvedValueOnce(null); // line 1253: employee user DELETED → triggers fallback
|
||||
(EmployeesList.employees.findOne as jest.Mock).mockResolvedValue(mockRemovedEmployee);
|
||||
(EmployeesList as any).findById = jest.fn().mockResolvedValue({
|
||||
id: "emp-removed",
|
||||
fullName: "John Doe",
|
||||
avatar: "old-avatar-url",
|
||||
});
|
||||
|
||||
const result = await AppointmentsList.getAppointmentEvent({
|
||||
appointmentId: "appt-001",
|
||||
sessionUser: "session-user",
|
||||
});
|
||||
|
||||
// Verify snapshot fields are used for collaboratorName
|
||||
expect(result.collaboratorName).toBe("John Doe");
|
||||
|
||||
// Verify snapshot avatar is used
|
||||
expect(result.collaboratorAvatar).toBe("old-avatar-url");
|
||||
|
||||
// Verify collaboratorId comes from employee, not user
|
||||
expect(result.collaboratorId).toBe("emp-removed");
|
||||
});
|
||||
|
||||
it("uses live User data when User account still exists", async () => {
|
||||
const mockAppointment = {
|
||||
_id: "appt-002",
|
||||
id: "appt-002",
|
||||
companyId: "company-001",
|
||||
clientId: "client-001",
|
||||
employeeId: "emp-active",
|
||||
serviceId: "svc-001",
|
||||
start: new Date("2026-07-20T10:00:00Z"),
|
||||
startHour: 10,
|
||||
endHour: 11,
|
||||
dateDay: new Date("2026-07-20"),
|
||||
price: 100,
|
||||
length: 60,
|
||||
payment: false,
|
||||
comments: "",
|
||||
discountId: undefined,
|
||||
};
|
||||
|
||||
const mockClient = {
|
||||
_id: "client-001",
|
||||
id: "client-001",
|
||||
userId: "client-user-001",
|
||||
firstName: "Client",
|
||||
lastName: "Smith",
|
||||
};
|
||||
|
||||
const mockClientUser = {
|
||||
_id: "client-user-001",
|
||||
id: "client-user-001",
|
||||
firstName: "Client",
|
||||
lastName: "Smith",
|
||||
avatar: "client-avatar",
|
||||
};
|
||||
|
||||
const mockActiveEmployee = {
|
||||
_id: "emp-active",
|
||||
id: "emp-active",
|
||||
companyId: "company-001",
|
||||
userId: "active-user-id",
|
||||
calendarColor: "#00ff00",
|
||||
removed: false,
|
||||
profileSnapshot: undefined,
|
||||
};
|
||||
|
||||
const mockActiveUser = {
|
||||
_id: "active-user-id",
|
||||
id: "active-user-id",
|
||||
firstName: "Jane",
|
||||
lastName: "Roe",
|
||||
avatar: "live-avatar",
|
||||
email: "jane@example.com",
|
||||
};
|
||||
|
||||
const mockAppointmentFindOne = (AppointmentsList as any).Appointments.AppointmentList.findOne;
|
||||
mockAppointmentFindOne.mockResolvedValue(mockAppointment);
|
||||
|
||||
(ClientsManager.clients.findOne as jest.Mock).mockResolvedValue(mockClient);
|
||||
(ServiceList.services.findOne as jest.Mock).mockResolvedValue({
|
||||
_id: "svc-001",
|
||||
name: "Haircut",
|
||||
description: "Professional haircut",
|
||||
});
|
||||
|
||||
// Call order for UsersManager.users.findOne (same mock as UserList):
|
||||
// 1. Line 1217: UserList.users.findOne (NOT awaited, but consumes a value)
|
||||
// 2. Line 1241: UsersManager.users.findOne for userClient
|
||||
// 3. Line 1253: UsersManager.users.findOne for userEmployee
|
||||
(UsersManager.users.findOne as jest.Mock)
|
||||
.mockResolvedValueOnce({ _id: "session-user" }) // line 1217: not awaited
|
||||
.mockResolvedValueOnce(mockClientUser) // line 1241: client user
|
||||
.mockResolvedValueOnce(mockActiveUser); // line 1253: live user exists
|
||||
(EmployeesList.employees.findOne as jest.Mock).mockResolvedValue(mockActiveEmployee);
|
||||
(EmployeesList as any).findById = jest.fn().mockResolvedValue({
|
||||
id: "emp-active",
|
||||
fullName: "Jane Roe",
|
||||
avatar: "live-avatar",
|
||||
});
|
||||
|
||||
const result = await AppointmentsList.getAppointmentEvent({
|
||||
appointmentId: "appt-002",
|
||||
sessionUser: "session-user",
|
||||
});
|
||||
|
||||
// Live user data is used, not snapshot
|
||||
expect(result.collaboratorName).toBe("Jane Roe");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user