Files
calendar/apps/web/components/booking/actions/bookingActions.test.ts
T
Joe Au-YeungGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
06bb994145 fix: Improve wrong assignment report (#27162)
* Pass already fetched booking

* Fetch the assignment reason

* Update tests

* Label as first assignment reason

* Pass single booking object

* Remove unused repository method

* Only pass bookingUid to handler

* Rename booking `assignmentReason` to `assignmentReasonSortedByCreatedAt`

* fix: use renamed assignmentReasonSortedByCreatedAt property

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* Remove take statement

* fix: address Udit's review comments

- Restore BookingAccessService for access control checks
- Rename repository method from findByUidForWrongAssignmentReport to findByUidIncludeUserAndEventTypeTeamAndAttendeesAndAssignmentReason

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* test: update tests to use BookingAccessService mock

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

* fix: use latest assignment reason in BookingDetailsSheet for consistency

Co-Authored-By: joe@cal.com <j.auyeung419@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-11 13:12:21 -05:00

763 lines
26 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { BookingStatus, SchedulingType } from "@calcom/prisma/enums";
import {
getPendingActions,
getCancelEventAction,
getVideoOptionsActions,
getEditEventActions,
getAfterEventActions,
shouldShowPendingActions,
shouldShowEditActions,
shouldShowRecurringCancelAction,
isActionDisabled,
getActionLabel,
type BookingActionContext,
} from "./bookingActions";
const mockT = (key: string) => key;
function createMockContext(overrides: Partial<BookingActionContext> = {}): BookingActionContext {
const now = new Date();
const startTime = new Date(now.getTime() + 24 * 60 * 60 * 1000); // Tomorrow
const endTime = new Date(startTime.getTime() + 60 * 60 * 1000); // 1 hour later
return {
booking: {
id: 1,
uid: "booking-123",
title: "Test Meeting",
description: "Test meeting description",
startTime: startTime.toISOString(),
endTime: endTime.toISOString(),
createdAt: now,
updatedAt: now,
status: BookingStatus.ACCEPTED,
paid: false,
fromReschedule: null,
recurringEventId: null,
rescheduled: false,
isRecorded: false,
rescheduler: null,
userPrimaryEmail: "organizer@example.com",
customInputs: {},
responses: {},
references: [],
attendees: [
{
id: 1,
name: "John Doe",
email: "john@example.com",
timeZone: "America/New_York",
phoneNumber: null,
locale: "en",
bookingId: 1,
noShow: false,
},
],
user: {
id: 1,
name: "Organizer",
email: "organizer@example.com",
},
eventType: {
id: 1,
title: "Test Event Type",
slug: "test-event",
length: 60,
schedulingType: SchedulingType.COLLECTIVE,
team: null,
eventTypeColor: {
lightEventTypeColor: "#000000",
darkEventTypeColor: "#ffffff",
},
disableCancelling: false,
disableRescheduling: false,
disableGuests: false,
allowReschedulingPastBookings: false,
recurringEvent: null,
price: 0,
currency: "usd",
metadata: {},
},
location: "integrations:daily",
payment: [],
seatsReferences: [],
assignmentReasonSortedByCreatedAt: [],
metadata: null,
routedFromRoutingFormReponse: null,
listingStatus: "upcoming",
recurringInfo: undefined,
loggedInUser: {
userId: 1,
userTimeZone: "America/New_York",
userTimeFormat: 12,
userEmail: "organizer@example.com",
},
isToday: false,
},
isUpcoming: true,
isOngoing: false,
isBookingInPast: false,
isCancelled: false,
isConfirmed: true,
isRejected: false,
isPending: false,
isRescheduled: false,
isRecurring: false,
isTabRecurring: false,
isTabUnconfirmed: false,
isBookingFromRoutingForm: false,
isDisabledCancelling: false,
isDisabledRescheduling: false,
isCalVideoLocation: true,
showPendingPayment: false,
cardCharged: false,
attendeeList: [
{
name: "John Doe",
email: "john@example.com",
id: 1,
noShow: false,
phoneNumber: null,
},
],
getSeatReferenceUid: () => undefined,
t: mockT,
...overrides,
} as BookingActionContext;
}
describe("Booking Actions", () => {
describe("getPendingActions", () => {
it("should return reject action for pending booking", () => {
const context = createMockContext({ isPending: true });
const actions = getPendingActions(context);
expect(actions).toHaveLength(2);
expect(actions[0]).toEqual({
id: "confirm",
bookingUid: "booking-123",
label: "confirm",
icon: "check",
disabled: false,
});
expect(actions[1]).toEqual({
id: "reject",
label: "reject",
icon: "ban",
disabled: false,
});
});
it("should return reject action only for non-pending booking", () => {
const context = createMockContext({ isPending: false });
const actions = getPendingActions(context);
expect(actions).toHaveLength(1);
expect(actions[0].id).toBe("reject");
});
it("should use correct labels for recurring bookings", () => {
const context = createMockContext({
isPending: true,
isRecurring: true,
isTabRecurring: true,
});
const actions = getPendingActions(context);
expect(actions[0].label).toBe("confirm_all");
expect(actions[1].label).toBe("reject_all");
});
});
describe("getCancelEventAction", () => {
it("should return cancel action with correct properties", () => {
const context = createMockContext();
const action = getCancelEventAction(context);
expect(action).toEqual({
id: "cancel",
label: "cancel_event",
icon: "circle-x",
color: "destructive",
disabled: false,
bookingUid: "booking-123",
});
});
it("should be disabled when cancellation is disabled", () => {
const context = createMockContext({ isDisabledCancelling: true });
const action = getCancelEventAction(context);
expect(action.disabled).toBe(true);
});
it("should be disabled for past pending bookings", () => {
const context = createMockContext({
isBookingInPast: true,
isPending: true,
isConfirmed: false,
});
const action = getCancelEventAction(context);
expect(action.disabled).toBe(true);
});
it("should include recurring parameters for recurring bookings", () => {
const context = createMockContext({
isRecurring: true,
isTabRecurring: true,
});
const action = getCancelEventAction(context);
expect(action.label).toBe("cancel_all_remaining");
});
});
describe("getVideoOptionsActions", () => {
it("should return video actions for past confirmed bookings", () => {
const context = createMockContext({
isBookingInPast: true,
isConfirmed: true,
isCalVideoLocation: true,
booking: {
...createMockContext().booking,
isRecorded: true,
},
});
const actions = getVideoOptionsActions(context);
expect(actions).toHaveLength(2);
expect(actions[0].id).toBe("view_recordings");
expect(actions[1].id).toBe("meeting_session_details");
expect(actions[0].disabled).toBe(false);
expect(actions[1].disabled).toBe(false);
});
it("should disable video actions for upcoming bookings", () => {
const context = createMockContext({ isBookingInPast: false });
const actions = getVideoOptionsActions(context);
expect(actions[0].disabled).toBe(true);
expect(actions[1].disabled).toBe(true);
});
it("should disable video actions for non-Cal video locations", () => {
const context = createMockContext({
isBookingInPast: true,
isConfirmed: true,
isCalVideoLocation: false,
});
const actions = getVideoOptionsActions(context);
expect(actions[0].disabled).toBe(true);
expect(actions[1].disabled).toBe(true);
});
});
describe("getEditEventActions", () => {
it("should return basic edit actions", () => {
const context = createMockContext();
const actions = getEditEventActions(context);
const actionIds = actions.map((a) => a.id);
expect(actionIds).toContain("reschedule");
expect(actionIds).toContain("reschedule_request");
expect(actionIds).toContain("change_location");
expect(actionIds).toContain("add_members");
});
it("should include reroute action for routing form bookings", () => {
const context = createMockContext({ isBookingFromRoutingForm: true });
const actions = getEditEventActions(context);
const rerouteAction = actions.find((a) => a.id === "reroute");
expect(rerouteAction).toBeDefined();
});
it("should include reassign action for round robin events with no host groups", () => {
const context = createMockContext({
booking: {
...createMockContext().booking,
eventType: {
...createMockContext().booking.eventType,
schedulingType: SchedulingType.ROUND_ROBIN,
hostGroups: [],
},
},
});
const actions = getEditEventActions(context);
const reassignAction = actions.find((a) => a.id === "reassign");
expect(reassignAction).toBeDefined();
});
it("should include reassign action for round robin events with one host group", () => {
const context = createMockContext({
booking: {
...createMockContext().booking,
eventType: {
...createMockContext().booking.eventType,
schedulingType: SchedulingType.ROUND_ROBIN,
hostGroups: [{ id: "group-1", name: "Group 1" }],
},
},
});
const actions = getEditEventActions(context);
const reassignAction = actions.find((a) => a.id === "reassign");
expect(reassignAction).toBeDefined();
});
it("should exclude reassign action for round robin events with more than one host groups", () => {
const context = createMockContext({
booking: {
...createMockContext().booking,
eventType: {
...createMockContext().booking.eventType,
schedulingType: SchedulingType.ROUND_ROBIN,
hostGroups: [
{ id: "group-1", name: "Group 1" },
{ id: "group-2", name: "Group 2" },
],
},
},
});
const actions = getEditEventActions(context);
const reassignAction = actions.find((a) => a.id === "reassign");
expect(reassignAction).toBeUndefined();
});
it("should exclude reassign action for non-round-robin events", () => {
const context = createMockContext({
booking: {
...createMockContext().booking,
eventType: {
...createMockContext().booking.eventType,
schedulingType: SchedulingType.COLLECTIVE,
hostGroups: [],
},
},
});
const actions = getEditEventActions(context);
const reassignAction = actions.find((a) => a.id === "reassign");
expect(reassignAction).toBeUndefined();
});
it("should exclude add_members when guests are disabled", () => {
const context = createMockContext({
booking: {
...createMockContext().booking,
eventType: {
...createMockContext().booking.eventType,
disableGuests: true,
},
},
});
const actions = getEditEventActions(context);
const addMembersAction = actions.find((a) => a.id === "add_members");
expect(addMembersAction).toBeUndefined();
});
it("should disable reschedule actions when rescheduling is disabled", () => {
const context = createMockContext({ isDisabledRescheduling: true });
const actions = getEditEventActions(context);
const rescheduleAction = actions.find((a) => a.id === "reschedule");
const rescheduleRequestAction = actions.find((a) => a.id === "reschedule_request");
expect(rescheduleAction?.disabled).toBe(true);
expect(rescheduleRequestAction?.disabled).toBe(true);
});
it("should disable change_location for past bookings", () => {
const context = createMockContext({ isBookingInPast: true });
const actions = getEditEventActions(context);
const changeLocationAction = actions.find((a) => a.id === "change_location");
expect(changeLocationAction?.disabled).toBe(true);
});
it("should disable change_location for cancelled bookings", () => {
const context = createMockContext({ isCancelled: true });
const actions = getEditEventActions(context);
const changeLocationAction = actions.find((a) => a.id === "change_location");
expect(changeLocationAction?.disabled).toBe(true);
});
it("should disable add_members for rejected bookings", () => {
const context = createMockContext({ isRejected: true });
const actions = getEditEventActions(context);
const addMembersAction = actions.find((a) => a.id === "add_members");
expect(addMembersAction?.disabled).toBe(true);
});
it("should disable reroute for past bookings from routing form", () => {
const context = createMockContext({
isBookingFromRoutingForm: true,
isBookingInPast: true,
});
const actions = getEditEventActions(context);
const rerouteAction = actions.find((a) => a.id === "reroute");
expect(rerouteAction?.disabled).toBe(true);
});
it("should disable reassign for cancelled round robin bookings", () => {
const context = createMockContext({
isCancelled: true,
booking: {
...createMockContext().booking,
eventType: {
...createMockContext().booking.eventType,
schedulingType: SchedulingType.ROUND_ROBIN,
hostGroups: [],
},
},
});
const actions = getEditEventActions(context);
const reassignAction = actions.find((a) => a.id === "reassign");
expect(reassignAction?.disabled).toBe(true);
});
it("should enable edit actions for upcoming active bookings", () => {
const context = createMockContext({
isBookingInPast: false,
isCancelled: false,
isRejected: false,
isBookingFromRoutingForm: true,
booking: {
...createMockContext().booking,
eventType: {
...createMockContext().booking.eventType,
schedulingType: SchedulingType.ROUND_ROBIN,
hostGroups: [],
},
},
});
const actions = getEditEventActions(context);
const changeLocationAction = actions.find((a) => a.id === "change_location");
const addMembersAction = actions.find((a) => a.id === "add_members");
const rerouteAction = actions.find((a) => a.id === "reroute");
const reassignAction = actions.find((a) => a.id === "reassign");
expect(changeLocationAction?.disabled).toBe(false);
expect(addMembersAction?.disabled).toBe(false);
expect(rerouteAction?.disabled).toBe(false);
expect(reassignAction?.disabled).toBe(false);
});
});
describe("getAfterEventActions", () => {
it("should include video actions and no-show action", () => {
const context = createMockContext({ isBookingInPast: true, isConfirmed: true });
const actions = getAfterEventActions(context);
const actionIds = actions.map((a) => a.id);
expect(actionIds).toContain("view_recordings");
expect(actionIds).toContain("meeting_session_details");
expect(actionIds).toContain("no_show");
});
it("should include charge card action for held payments", () => {
const context = createMockContext({
booking: {
...createMockContext().booking,
status: BookingStatus.ACCEPTED,
paid: true,
payment: [
{
paymentOption: "HOLD",
amount: 1000,
currency: "usd",
success: true,
appId: null,
refunded: false,
},
],
},
});
const actions = getAfterEventActions(context);
const chargeCardAction = actions.find((a) => a.id === "charge_card");
expect(chargeCardAction).toBeDefined();
});
it("should show correct no-show label for single attendee", () => {
const context = createMockContext({
attendeeList: [{ name: "John", email: "john@example.com", id: 1, noShow: true, phoneNumber: null }],
});
const actions = getAfterEventActions(context);
const noShowAction = actions.find((a) => a.id === "no_show");
expect(noShowAction?.label).toBe("unmark_as_no_show");
expect(noShowAction?.icon).toBe("eye");
});
});
describe("shouldShowPendingActions", () => {
it("should return true for pending upcoming bookings", () => {
const context = createMockContext({ isPending: true, isUpcoming: true, isCancelled: false });
expect(shouldShowPendingActions(context)).toBe(true);
});
it("should return false for cancelled bookings", () => {
const context = createMockContext({ isPending: true, isUpcoming: true, isCancelled: true });
expect(shouldShowPendingActions(context)).toBe(false);
});
it("should return false for past bookings", () => {
const context = createMockContext({ isPending: true, isUpcoming: false, isCancelled: false });
expect(shouldShowPendingActions(context)).toBe(false);
});
});
describe("shouldShowEditActions", () => {
it("should return true for confirmed upcoming bookings", () => {
const context = createMockContext({ isPending: false, isCancelled: false });
expect(shouldShowEditActions(context)).toBe(true);
});
it("should return false for pending bookings", () => {
const context = createMockContext({ isPending: true });
expect(shouldShowEditActions(context)).toBe(false);
});
it("should return false for cancelled bookings", () => {
const context = createMockContext({ isCancelled: true });
expect(shouldShowEditActions(context)).toBe(false);
});
});
describe("shouldShowRecurringCancelAction", () => {
it("should return true for recurring bookings in recurring tab", () => {
const context = createMockContext({ isTabRecurring: true, isRecurring: true });
expect(shouldShowRecurringCancelAction(context)).toBe(true);
});
it("should return false for non-recurring bookings", () => {
const context = createMockContext({ isTabRecurring: true, isRecurring: false });
expect(shouldShowRecurringCancelAction(context)).toBe(false);
});
});
describe("isActionDisabled", () => {
it("should disable reschedule actions when rescheduling is disabled", () => {
const context = createMockContext({ isDisabledRescheduling: true });
expect(isActionDisabled("reschedule", context)).toBe(true);
expect(isActionDisabled("reschedule_request", context)).toBe(true);
});
it("should disable cancel action when cancellation is disabled", () => {
const context = createMockContext({ isDisabledCancelling: true });
expect(isActionDisabled("cancel", context)).toBe(true);
});
it("should disable cancelling all past bookings", () => {
const pastConfirmedContext = createMockContext({
isBookingInPast: true,
isPending: false,
isConfirmed: true,
});
const pastPendingContext = createMockContext({
isBookingInPast: true,
isPending: true,
isConfirmed: false,
});
// Current implementation blocks ALL past bookings
expect(isActionDisabled("cancel", pastConfirmedContext)).toBe(true);
expect(isActionDisabled("cancel", pastPendingContext)).toBe(true);
});
it("should allow cancelling future bookings when cancelling is not disabled", () => {
const futureContext = createMockContext({
isBookingInPast: false,
isPending: true,
isConfirmed: false,
});
expect(isActionDisabled("cancel", futureContext)).toBe(false);
});
it("should disable video actions for non-past bookings", () => {
const context = createMockContext({ isBookingInPast: false });
expect(isActionDisabled("view_recordings", context)).toBe(true);
expect(isActionDisabled("meeting_session_details", context)).toBe(true);
});
it("should disable charge card action when already charged", () => {
const context = createMockContext({ cardCharged: true });
expect(isActionDisabled("charge_card", context)).toBe(true);
});
describe("change_location action", () => {
it("should be disabled for past bookings", () => {
const context = createMockContext({ isBookingInPast: true });
expect(isActionDisabled("change_location", context)).toBe(true);
});
it("should be disabled for cancelled bookings", () => {
const context = createMockContext({ isCancelled: true });
expect(isActionDisabled("change_location", context)).toBe(true);
});
it("should be disabled for rejected bookings", () => {
const context = createMockContext({ isRejected: true });
expect(isActionDisabled("change_location", context)).toBe(true);
});
it("should be enabled for upcoming active bookings", () => {
const context = createMockContext({
isBookingInPast: false,
isCancelled: false,
isRejected: false,
});
expect(isActionDisabled("change_location", context)).toBe(false);
});
});
describe("add_members action", () => {
it("should be disabled for past bookings", () => {
const context = createMockContext({ isBookingInPast: true });
expect(isActionDisabled("add_members", context)).toBe(true);
});
it("should be disabled for cancelled bookings", () => {
const context = createMockContext({ isCancelled: true });
expect(isActionDisabled("add_members", context)).toBe(true);
});
it("should be disabled for rejected bookings", () => {
const context = createMockContext({ isRejected: true });
expect(isActionDisabled("add_members", context)).toBe(true);
});
it("should be enabled for upcoming active bookings", () => {
const context = createMockContext({
isBookingInPast: false,
isCancelled: false,
isRejected: false,
});
expect(isActionDisabled("add_members", context)).toBe(false);
});
});
describe("reroute action", () => {
it("should be disabled for past bookings", () => {
const context = createMockContext({ isBookingInPast: true });
expect(isActionDisabled("reroute", context)).toBe(true);
});
it("should be disabled for cancelled bookings", () => {
const context = createMockContext({ isCancelled: true });
expect(isActionDisabled("reroute", context)).toBe(true);
});
it("should be disabled for rejected bookings", () => {
const context = createMockContext({ isRejected: true });
expect(isActionDisabled("reroute", context)).toBe(true);
});
it("should be enabled for upcoming active bookings", () => {
const context = createMockContext({
isBookingInPast: false,
isCancelled: false,
isRejected: false,
});
expect(isActionDisabled("reroute", context)).toBe(false);
});
});
describe("reassign action", () => {
it("should be disabled for past bookings", () => {
const context = createMockContext({ isBookingInPast: true });
expect(isActionDisabled("reassign", context)).toBe(true);
});
it("should be disabled for cancelled bookings", () => {
const context = createMockContext({ isCancelled: true });
expect(isActionDisabled("reassign", context)).toBe(true);
});
it("should be disabled for rejected bookings", () => {
const context = createMockContext({ isRejected: true });
expect(isActionDisabled("reassign", context)).toBe(true);
});
it("should be enabled for upcoming active bookings", () => {
const context = createMockContext({
isBookingInPast: false,
isCancelled: false,
isRejected: false,
});
expect(isActionDisabled("reassign", context)).toBe(false);
});
});
describe("cancel action with cancelled/rejected states", () => {
it("should be disabled for already cancelled bookings", () => {
const context = createMockContext({ isCancelled: true });
expect(isActionDisabled("cancel", context)).toBe(true);
});
it("should be disabled for rejected bookings", () => {
const context = createMockContext({ isRejected: true });
expect(isActionDisabled("cancel", context)).toBe(true);
});
});
describe("reschedule_request action with cancelled/rejected states", () => {
it("should be disabled for cancelled bookings", () => {
const context = createMockContext({ isCancelled: true });
expect(isActionDisabled("reschedule_request", context)).toBe(true);
});
it("should be disabled for rejected bookings", () => {
const context = createMockContext({ isRejected: true });
expect(isActionDisabled("reschedule_request", context)).toBe(true);
});
});
});
describe("getActionLabel", () => {
it("should return correct labels for different actions", () => {
const context = createMockContext();
expect(getActionLabel("reject", context)).toBe("reject");
expect(getActionLabel("confirm", context)).toBe("confirm");
expect(getActionLabel("cancel", context)).toBe("cancel_event");
});
it("should return correct labels for recurring bookings", () => {
const context = createMockContext({ isRecurring: true, isTabRecurring: true });
expect(getActionLabel("reject", context)).toBe("reject_all");
expect(getActionLabel("confirm", context)).toBe("confirm_all");
expect(getActionLabel("cancel", context)).toBe("cancel_all_remaining");
});
it("should return correct no-show label based on attendee state", () => {
const contextWithNoShow = createMockContext({
attendeeList: [{ name: "John", email: "john@example.com", id: 1, noShow: true, phoneNumber: null }],
});
expect(getActionLabel("no_show", contextWithNoShow)).toBe("unmark_as_no_show");
});
});
});