Skip to content

Commit c20e723

Browse files
fix: resolve credential mismatch in round robin reschedule causing 404 errors (#22993)
* fix: flaky e2e * fix: resolve credential mismatch * test: add comprehensive credential mismatch test for round robin reschedule - Verify original host credentials used for deletion operations - Verify new host credentials used for creation operations - Test dual EventManager pattern implementation - Fix timeFormat property access issue Co-Authored-By: [email protected] <[email protected]> * Update handleNewBooking.ts * Update booking.ts * Update handleNewBooking.ts * update * Update booking.ts --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 7b7478c commit c20e723

File tree

4 files changed

+184
-24
lines changed

4 files changed

+184
-24
lines changed

packages/features/bookings/lib/handleNewBooking.ts

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,13 @@ import {
8383
} from "@calcom/prisma/zod-utils";
8484
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
8585
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
86-
import type { AdditionalInformation, AppsStatus, CalendarEvent, Person } from "@calcom/types/Calendar";
86+
import type {
87+
AdditionalInformation,
88+
AppsStatus,
89+
CalendarEvent,
90+
CalEventResponses,
91+
Person,
92+
} from "@calcom/types/Calendar";
8793
import type { CredentialForCalendarService } from "@calcom/types/Credential";
8894
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
8995

@@ -1580,6 +1586,47 @@ async function handler(
15801586
evt.iCalUID = undefined;
15811587
}
15821588

1589+
if (changedOrganizer && originalRescheduledBooking?.user) {
1590+
const originalHostCredentials = await getAllCredentialsIncludeServiceAccountKey(
1591+
originalRescheduledBooking.user,
1592+
eventType
1593+
);
1594+
const refreshedOriginalHostCredentials = await refreshCredentials(originalHostCredentials);
1595+
1596+
// Create EventManager with original host's credentials for deletion operations
1597+
const originalHostEventManager = new EventManager(
1598+
{ ...originalRescheduledBooking.user, credentials: refreshedOriginalHostCredentials },
1599+
apps
1600+
);
1601+
log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking");
1602+
// Create deletion event with original host's organizer info and original booking properties
1603+
const deletionEvent = {
1604+
...evt,
1605+
organizer: {
1606+
id: originalRescheduledBooking.user.id,
1607+
name: originalRescheduledBooking.user.name || "",
1608+
email: originalRescheduledBooking.user.email,
1609+
username: originalRescheduledBooking.user.username || undefined,
1610+
timeZone: originalRescheduledBooking.user.timeZone,
1611+
language: { translate: tOrganizer, locale: originalRescheduledBooking.user.locale ?? "en" },
1612+
timeFormat: getTimeFormatStringFromUserTimeFormat(originalRescheduledBooking.user.timeFormat),
1613+
},
1614+
destinationCalendar: previousHostDestinationCalendar,
1615+
// Override with original booking properties used by deletion operations
1616+
startTime: originalRescheduledBooking.startTime.toISOString(),
1617+
endTime: originalRescheduledBooking.endTime.toISOString(),
1618+
uid: originalRescheduledBooking.uid,
1619+
location: originalRescheduledBooking.location,
1620+
responses: originalRescheduledBooking.responses
1621+
? (originalRescheduledBooking.responses as CalEventResponses)
1622+
: evt.responses,
1623+
};
1624+
1625+
await originalHostEventManager.deleteEventsAndMeetings({
1626+
event: deletionEvent,
1627+
bookingReferences: originalRescheduledBooking.references,
1628+
});
1629+
}
15831630
const updateManager = await eventManager.reschedule(
15841631
evt,
15851632
originalRescheduledBooking.uid,
@@ -1737,6 +1784,8 @@ async function handler(
17371784

17381785
originalBookingMemberEmails.push({
17391786
...originalRescheduledBooking.user,
1787+
username: originalRescheduledBooking.user.username ?? undefined,
1788+
timeFormat: getTimeFormatStringFromUserTimeFormat(originalRescheduledBooking.user.timeFormat),
17401789
name: originalRescheduledBooking.user.name || "",
17411790
language: { translate, locale: originalRescheduledBooking.user.locale ?? "en" },
17421791
});

packages/features/bookings/lib/handleNewBooking/test/reschedule.test.ts

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2827,5 +2827,133 @@ describe("handleNewBooking", () => {
28272827
timeout
28282828
);
28292829
});
2830+
2831+
test(
2832+
"should use correct credentials when round robin reschedule changes host - original host credentials for deletion, new host for creation",
2833+
async ({ emails }) => {
2834+
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
2835+
const booker = getBooker({
2836+
2837+
name: "Booker",
2838+
});
2839+
2840+
const originalHost = getOrganizer({
2841+
name: "Original Host",
2842+
2843+
id: 101,
2844+
schedules: [TestData.schedules.IstMorningShift],
2845+
credentials: [getGoogleCalendarCredential()],
2846+
selectedCalendars: [TestData.selectedCalendars.google],
2847+
});
2848+
2849+
const newHost = getOrganizer({
2850+
name: "New Host",
2851+
2852+
id: 102,
2853+
schedules: [TestData.schedules.IstEveningShift],
2854+
credentials: [getGoogleCalendarCredential()],
2855+
selectedCalendars: [TestData.selectedCalendars.google],
2856+
});
2857+
2858+
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
2859+
const uidOfBookingToBeRescheduled = "credential-test-booking-uid";
2860+
2861+
await createBookingScenario(
2862+
getScenarioData({
2863+
eventTypes: [
2864+
{
2865+
id: 1,
2866+
slotInterval: 15,
2867+
length: 15,
2868+
users: [{ id: 101 }, { id: 102 }],
2869+
schedulingType: SchedulingType.ROUND_ROBIN,
2870+
},
2871+
],
2872+
bookings: [
2873+
{
2874+
uid: uidOfBookingToBeRescheduled,
2875+
eventTypeId: 1,
2876+
userId: 101,
2877+
status: BookingStatus.ACCEPTED,
2878+
startTime: `${plus1DateString}T05:00:00.000Z`,
2879+
endTime: `${plus1DateString}T05:15:00.000Z`,
2880+
references: [
2881+
{
2882+
type: appStoreMetadata.dailyvideo.type,
2883+
uid: "MOCK_ID",
2884+
meetingId: "MOCK_ID",
2885+
meetingPassword: "MOCK_PASS",
2886+
meetingUrl: "http://mock-dailyvideo.example.com",
2887+
credentialId: null,
2888+
},
2889+
{
2890+
type: appStoreMetadata.googlecalendar.type,
2891+
uid: "MOCK_ID",
2892+
meetingId: "MOCK_ID",
2893+
meetingPassword: "MOCK_PASSWORD",
2894+
meetingUrl: "https://UNUSED_URL",
2895+
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
2896+
credentialId: undefined,
2897+
},
2898+
],
2899+
},
2900+
],
2901+
organizer: originalHost,
2902+
usersApartFromOrganizer: [newHost],
2903+
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
2904+
})
2905+
);
2906+
2907+
const videoMock = mockSuccessfulVideoMeetingCreation({
2908+
metadataLookupKey: "dailyvideo",
2909+
});
2910+
2911+
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
2912+
create: { uid: "NEW_EVENT_ID" },
2913+
update: { uid: "UPDATED_EVENT_ID" },
2914+
});
2915+
2916+
const mockBookingData = getMockRequestDataForBooking({
2917+
data: {
2918+
eventTypeId: 1,
2919+
rescheduleUid: uidOfBookingToBeRescheduled,
2920+
start: `${plus1DateString}T14:00:00.000Z`,
2921+
end: `${plus1DateString}T14:15:00.000Z`,
2922+
responses: {
2923+
email: booker.email,
2924+
name: booker.name,
2925+
location: { optionValue: "", value: BookingLocations.CalVideo },
2926+
},
2927+
},
2928+
});
2929+
2930+
const createdBooking = await handleNewBooking({
2931+
bookingData: mockBookingData,
2932+
});
2933+
2934+
expectSuccessfulCalendarEventDeletionInCalendar(calendarMock, {
2935+
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
2936+
calEvent: {
2937+
organizer: expect.objectContaining({
2938+
email: originalHost.email,
2939+
}),
2940+
startTime: `${plus1DateString}T05:00:00.000Z`,
2941+
endTime: `${plus1DateString}T05:15:00.000Z`,
2942+
uid: uidOfBookingToBeRescheduled,
2943+
},
2944+
uid: "MOCK_ID",
2945+
});
2946+
2947+
// Verify that creation occurred with new host credentials
2948+
expect(calendarMock.createEventCalls.length).toBe(1);
2949+
const createCall = calendarMock.createEventCalls[0];
2950+
expect(createCall.args.calEvent.organizer.email).toBe(newHost.email);
2951+
2952+
expect(createdBooking.userId).toBe(newHost.id);
2953+
expect(createdBooking.startTime?.toISOString()).toBe(`${plus1DateString}T14:00:00.000Z`);
2954+
expect(createdBooking.endTime?.toISOString()).toBe(`${plus1DateString}T14:15:00.000Z`);
2955+
},
2956+
timeout
2957+
);
28302958
});
28312959
});

packages/lib/EventManager.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -635,7 +635,7 @@ export default class EventManager {
635635
const shouldUpdateBookingReferences =
636636
!!changedOrganizer || isLocationChanged || !!isBookingRequestedReschedule || isDailyVideoRoomExpired;
637637

638-
if (evt.requiresConfirmation) {
638+
if (evt.requiresConfirmation && !changedOrganizer) {
639639
log.debug("RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking");
640640
// As the reschedule requires confirmation, we can't update the events and meetings to new time yet. So, just delete them and let it be handled when organizer confirms the booking.
641641
await this.deleteEventsAndMeetings({
@@ -644,14 +644,7 @@ export default class EventManager {
644644
});
645645
} else {
646646
if (changedOrganizer) {
647-
log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking");
648-
await this.deleteEventsAndMeetings({
649-
event: { ...event, destinationCalendar: previousHostDestinationCalendar },
650-
bookingReferences: booking.references,
651-
});
652-
653647
log.debug("RescheduleOrganizerChanged: Creating Event and Meeting for for new booking");
654-
655648
const createdEvent = await this.create(originalEvt);
656649
results.push(...createdEvent.results);
657650
updatedBookingReferences.push(...createdEvent.referencesToCreate);
@@ -728,7 +721,7 @@ export default class EventManager {
728721
});
729722
}
730723

731-
private async deleteEventsAndMeetings({
724+
public async deleteEventsAndMeetings({
732725
event,
733726
bookingReferences,
734727
isBookingInRecurringSeries,

packages/lib/server/repository/booking.ts

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { bookingMinimalSelect } from "@calcom/prisma";
77
import type { Booking } from "@calcom/prisma/client";
88
import { RRTimestampBasis } from "@calcom/prisma/enums";
99
import { BookingStatus } from "@calcom/prisma/enums";
10+
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
1011

1112
import { UserRepository } from "./user";
1213

@@ -669,25 +670,14 @@ export class BookingRepository {
669670
select: {
670671
id: true,
671672
name: true,
673+
username: true,
672674
email: true,
673675
locale: true,
674676
timeZone: true,
677+
timeFormat: true,
675678
destinationCalendar: true,
676679
credentials: {
677-
select: {
678-
id: true,
679-
userId: true,
680-
key: true,
681-
type: true,
682-
teamId: true,
683-
appId: true,
684-
invalid: true,
685-
user: {
686-
select: {
687-
email: true,
688-
},
689-
},
690-
},
680+
select: credentialForCalendarServiceSelect,
691681
},
692682
},
693683
},

0 commit comments

Comments
 (0)