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: anik@cal.com <adhabal2002@gmail.com> * 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>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
7b7478c0a9
commit
c20e723e34
@@ -83,7 +83,13 @@ import {
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import type { AdditionalInformation, AppsStatus, CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
import type {
|
||||
AdditionalInformation,
|
||||
AppsStatus,
|
||||
CalendarEvent,
|
||||
CalEventResponses,
|
||||
Person,
|
||||
} from "@calcom/types/Calendar";
|
||||
import type { CredentialForCalendarService } from "@calcom/types/Credential";
|
||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
@@ -1580,6 +1586,47 @@ async function handler(
|
||||
evt.iCalUID = undefined;
|
||||
}
|
||||
|
||||
if (changedOrganizer && originalRescheduledBooking?.user) {
|
||||
const originalHostCredentials = await getAllCredentialsIncludeServiceAccountKey(
|
||||
originalRescheduledBooking.user,
|
||||
eventType
|
||||
);
|
||||
const refreshedOriginalHostCredentials = await refreshCredentials(originalHostCredentials);
|
||||
|
||||
// Create EventManager with original host's credentials for deletion operations
|
||||
const originalHostEventManager = new EventManager(
|
||||
{ ...originalRescheduledBooking.user, credentials: refreshedOriginalHostCredentials },
|
||||
apps
|
||||
);
|
||||
log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking");
|
||||
// Create deletion event with original host's organizer info and original booking properties
|
||||
const deletionEvent = {
|
||||
...evt,
|
||||
organizer: {
|
||||
id: originalRescheduledBooking.user.id,
|
||||
name: originalRescheduledBooking.user.name || "",
|
||||
email: originalRescheduledBooking.user.email,
|
||||
username: originalRescheduledBooking.user.username || undefined,
|
||||
timeZone: originalRescheduledBooking.user.timeZone,
|
||||
language: { translate: tOrganizer, locale: originalRescheduledBooking.user.locale ?? "en" },
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(originalRescheduledBooking.user.timeFormat),
|
||||
},
|
||||
destinationCalendar: previousHostDestinationCalendar,
|
||||
// Override with original booking properties used by deletion operations
|
||||
startTime: originalRescheduledBooking.startTime.toISOString(),
|
||||
endTime: originalRescheduledBooking.endTime.toISOString(),
|
||||
uid: originalRescheduledBooking.uid,
|
||||
location: originalRescheduledBooking.location,
|
||||
responses: originalRescheduledBooking.responses
|
||||
? (originalRescheduledBooking.responses as CalEventResponses)
|
||||
: evt.responses,
|
||||
};
|
||||
|
||||
await originalHostEventManager.deleteEventsAndMeetings({
|
||||
event: deletionEvent,
|
||||
bookingReferences: originalRescheduledBooking.references,
|
||||
});
|
||||
}
|
||||
const updateManager = await eventManager.reschedule(
|
||||
evt,
|
||||
originalRescheduledBooking.uid,
|
||||
@@ -1737,6 +1784,8 @@ async function handler(
|
||||
|
||||
originalBookingMemberEmails.push({
|
||||
...originalRescheduledBooking.user,
|
||||
username: originalRescheduledBooking.user.username ?? undefined,
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(originalRescheduledBooking.user.timeFormat),
|
||||
name: originalRescheduledBooking.user.name || "",
|
||||
language: { translate, locale: originalRescheduledBooking.user.locale ?? "en" },
|
||||
});
|
||||
|
||||
@@ -2827,5 +2827,133 @@ describe("handleNewBooking", () => {
|
||||
timeout
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
"should use correct credentials when round robin reschedule changes host - original host credentials for deletion, new host for creation",
|
||||
async ({ emails }) => {
|
||||
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const originalHost = getOrganizer({
|
||||
name: "Original Host",
|
||||
email: "originalhost@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstMorningShift],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const newHost = getOrganizer({
|
||||
name: "New Host",
|
||||
email: "newhost@example.com",
|
||||
id: 102,
|
||||
schedules: [TestData.schedules.IstEveningShift],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const uidOfBookingToBeRescheduled = "credential-test-booking-uid";
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 15,
|
||||
length: 15,
|
||||
users: [{ id: 101 }, { id: 102 }],
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
eventTypeId: 1,
|
||||
userId: 101,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
references: [
|
||||
{
|
||||
type: appStoreMetadata.dailyvideo.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASS",
|
||||
meetingUrl: "http://mock-dailyvideo.example.com",
|
||||
credentialId: null,
|
||||
},
|
||||
{
|
||||
type: appStoreMetadata.googlecalendar.type,
|
||||
uid: "MOCK_ID",
|
||||
meetingId: "MOCK_ID",
|
||||
meetingPassword: "MOCK_PASSWORD",
|
||||
meetingUrl: "https://UNUSED_URL",
|
||||
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
|
||||
credentialId: undefined,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer: originalHost,
|
||||
usersApartFromOrganizer: [newHost],
|
||||
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
const videoMock = mockSuccessfulVideoMeetingCreation({
|
||||
metadataLookupKey: "dailyvideo",
|
||||
});
|
||||
|
||||
const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", {
|
||||
create: { uid: "NEW_EVENT_ID" },
|
||||
update: { uid: "UPDATED_EVENT_ID" },
|
||||
});
|
||||
|
||||
const mockBookingData = getMockRequestDataForBooking({
|
||||
data: {
|
||||
eventTypeId: 1,
|
||||
rescheduleUid: uidOfBookingToBeRescheduled,
|
||||
start: `${plus1DateString}T14:00:00.000Z`,
|
||||
end: `${plus1DateString}T14:15:00.000Z`,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const createdBooking = await handleNewBooking({
|
||||
bookingData: mockBookingData,
|
||||
});
|
||||
|
||||
expectSuccessfulCalendarEventDeletionInCalendar(calendarMock, {
|
||||
externalCalendarId: "MOCK_EXTERNAL_CALENDAR_ID",
|
||||
calEvent: {
|
||||
organizer: expect.objectContaining({
|
||||
email: originalHost.email,
|
||||
}),
|
||||
startTime: `${plus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${plus1DateString}T05:15:00.000Z`,
|
||||
uid: uidOfBookingToBeRescheduled,
|
||||
},
|
||||
uid: "MOCK_ID",
|
||||
});
|
||||
|
||||
// Verify that creation occurred with new host credentials
|
||||
expect(calendarMock.createEventCalls.length).toBe(1);
|
||||
const createCall = calendarMock.createEventCalls[0];
|
||||
expect(createCall.args.calEvent.organizer.email).toBe(newHost.email);
|
||||
|
||||
expect(createdBooking.userId).toBe(newHost.id);
|
||||
expect(createdBooking.startTime?.toISOString()).toBe(`${plus1DateString}T14:00:00.000Z`);
|
||||
expect(createdBooking.endTime?.toISOString()).toBe(`${plus1DateString}T14:15:00.000Z`);
|
||||
},
|
||||
timeout
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -635,7 +635,7 @@ export default class EventManager {
|
||||
const shouldUpdateBookingReferences =
|
||||
!!changedOrganizer || isLocationChanged || !!isBookingRequestedReschedule || isDailyVideoRoomExpired;
|
||||
|
||||
if (evt.requiresConfirmation) {
|
||||
if (evt.requiresConfirmation && !changedOrganizer) {
|
||||
log.debug("RescheduleRequiresConfirmation: Deleting Event and Meeting for previous booking");
|
||||
// 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.
|
||||
await this.deleteEventsAndMeetings({
|
||||
@@ -644,14 +644,7 @@ export default class EventManager {
|
||||
});
|
||||
} else {
|
||||
if (changedOrganizer) {
|
||||
log.debug("RescheduleOrganizerChanged: Deleting Event and Meeting for previous booking");
|
||||
await this.deleteEventsAndMeetings({
|
||||
event: { ...event, destinationCalendar: previousHostDestinationCalendar },
|
||||
bookingReferences: booking.references,
|
||||
});
|
||||
|
||||
log.debug("RescheduleOrganizerChanged: Creating Event and Meeting for for new booking");
|
||||
|
||||
const createdEvent = await this.create(originalEvt);
|
||||
results.push(...createdEvent.results);
|
||||
updatedBookingReferences.push(...createdEvent.referencesToCreate);
|
||||
@@ -728,7 +721,7 @@ export default class EventManager {
|
||||
});
|
||||
}
|
||||
|
||||
private async deleteEventsAndMeetings({
|
||||
public async deleteEventsAndMeetings({
|
||||
event,
|
||||
bookingReferences,
|
||||
isBookingInRecurringSeries,
|
||||
|
||||
@@ -7,6 +7,7 @@ import { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { Booking } from "@calcom/prisma/client";
|
||||
import { RRTimestampBasis } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential";
|
||||
|
||||
import { UserRepository } from "./user";
|
||||
|
||||
@@ -669,25 +670,14 @@ export class BookingRepository {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
username: true,
|
||||
email: true,
|
||||
locale: true,
|
||||
timeZone: true,
|
||||
timeFormat: true,
|
||||
destinationCalendar: true,
|
||||
credentials: {
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
key: true,
|
||||
type: true,
|
||||
teamId: true,
|
||||
appId: true,
|
||||
invalid: true,
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user