diff --git a/.github/workflows/e2e-app-store.yml b/.github/workflows/e2e-app-store.yml index c652a81c16..02ddd49fe4 100644 --- a/.github/workflows/e2e-app-store.yml +++ b/.github/workflows/e2e-app-store.yml @@ -62,6 +62,7 @@ jobs: env: ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }} CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }} + DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }} DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }} DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }} diff --git a/.github/workflows/e2e-embed-react.yml b/.github/workflows/e2e-embed-react.yml index 6a1c9f359a..dab5df8ed0 100644 --- a/.github/workflows/e2e-embed-react.yml +++ b/.github/workflows/e2e-embed-react.yml @@ -49,6 +49,7 @@ jobs: env: ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }} CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }} + DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }} DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }} DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }} diff --git a/.github/workflows/e2e-embed.yml b/.github/workflows/e2e-embed.yml index 7c757aec19..677626c303 100644 --- a/.github/workflows/e2e-embed.yml +++ b/.github/workflows/e2e-embed.yml @@ -55,6 +55,7 @@ jobs: env: ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }} CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }} + DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }} DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }} DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }} diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index c4057f8ad0..e6afe6c691 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -53,6 +53,7 @@ jobs: env: ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }} CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }} + DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }} DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }} DEPLOYSENTINEL_API_KEY: ${{ secrets.DEPLOYSENTINEL_API_KEY }} diff --git a/.github/workflows/production-build-without-database.yml b/.github/workflows/production-build-without-database.yml index 2542c1c680..a459da750d 100644 --- a/.github/workflows/production-build-without-database.yml +++ b/.github/workflows/production-build-without-database.yml @@ -6,6 +6,7 @@ on: env: ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }} CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }} + DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }} DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }} E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }} diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml index 061ebcf8ca..f9828cc958 100644 --- a/.github/workflows/production-build.yml +++ b/.github/workflows/production-build.yml @@ -6,6 +6,7 @@ on: env: ALLOWED_HOSTNAMES: ${{ vars.CI_ALLOWED_HOSTNAMES }} CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }} + DAILY_API_KEY: ${{ secrets.CI_DAILY_API_KEY }} DATABASE_URL: ${{ secrets.CI_DATABASE_URL }} DATABASE_DIRECT_URL: ${{ secrets.CI_DATABASE_URL }} E2E_TEST_APPLE_CALENDAR_EMAIL: ${{ secrets.E2E_TEST_APPLE_CALENDAR_EMAIL }} diff --git a/apps/web/pages/auth/verify-email-change.tsx b/apps/web/pages/auth/verify-email-change.tsx index e5b9d49df7..378c9be03b 100644 --- a/apps/web/pages/auth/verify-email-change.tsx +++ b/apps/web/pages/auth/verify-email-change.tsx @@ -25,15 +25,15 @@ function VerifyEmailChange(props: PageProps) { const router = useRouter(); useEffect(() => { - async function updateSession() { + async function updateSessionAndDisplayToast() { await update({ email: props.updatedEmail }); - router.push("/event-types"); - } - if (props.updateSession) { - updateSession(); if (isLocaleReady) { showToast(t("verify_email_change_success_toast", { email: props.updatedEmail }), "success"); } + router.push("/event-types"); + } + if (props.updateSession) { + updateSessionAndDisplayToast(); } else { if (isLocaleReady) { showToast(t("verify_email_change_failure_toast"), "error"); diff --git a/apps/web/playwright/booking-pages.e2e.ts b/apps/web/playwright/booking-pages.e2e.ts index e86d8f6863..3a45dcb017 100644 --- a/apps/web/playwright/booking-pages.e2e.ts +++ b/apps/web/playwright/booking-pages.e2e.ts @@ -11,7 +11,6 @@ import { bookFirstEvent, bookOptinEvent, bookTimeSlot, - expectEmailsToHaveSubject, selectFirstAvailableTimeSlotNextMonth, testEmail, testName, @@ -62,6 +61,7 @@ testBothFutureAndLegacyRoutes.describe("free user", () => { test("cannot book same slot multiple times", async ({ page, users, emails }) => { const [user] = users.get(); + const bookerObj = { email: users.trackEmail({ username: "testEmail", domain: "example.com" }), name: "testBooker", @@ -80,12 +80,6 @@ testBothFutureAndLegacyRoutes.describe("free user", () => { await expect(page.locator("[data-testid=success-page]")).toBeVisible(); const { title: eventTitle } = await user.getFirstEventAsOwner(); - await expectEmailsToHaveSubject({ - emails, - organizer: user, - booker: bookerObj, - eventTitle, - }); await page.goto(bookingUrl); // book same time spot again diff --git a/packages/app-store/locations.ts b/packages/app-store/locations.ts index 6412478008..54415d3c69 100644 --- a/packages/app-store/locations.ts +++ b/packages/app-store/locations.ts @@ -346,6 +346,7 @@ export const getLocationValueForDB = ( ) => { let bookingLocation = bookingLocationTypeOrValue; let conferenceCredentialId = undefined; + eventLocations.forEach((location) => { if (location.type === bookingLocationTypeOrValue) { const eventLocationType = getEventLocationType(bookingLocationTypeOrValue); @@ -362,6 +363,11 @@ export const getLocationValueForDB = ( bookingLocation = location[eventLocationType.defaultValueVariable] || bookingLocation; } }); + + if (bookingLocation.trim().length === 0) { + bookingLocation = DailyLocationType; + } + return { bookingLocation, conferenceCredentialId }; }; diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index d911e62f9b..faba12aa50 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -2416,6 +2416,150 @@ describe("handleNewBooking", () => { }, timeout ); + test( + `cannot book same slot multiple times `, + async ({ emails }) => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const organizerOtherEmail = "organizer2@example.com"; + const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com"; + + const organizer = getOrganizer({ + name: "Organizer", + email: "organizer@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: "google_calendar", + externalId: "organizer@google-calendar.com", + primaryEmail: organizerOtherEmail, + }, + }); + + await createBookingScenario( + getScenarioData({ + eventTypes: [ + { + id: 1, + slotInterval: 30, + length: 30, + useEventTypeDestinationCalendarEmail: true, + users: [ + { + id: 101, + }, + ], + destinationCalendar: { + integration: "google_calendar", + externalId: "event-type-1@google-calendar.com", + primaryEmail: organizerDestinationCalendarEmailOnEventType, + }, + }, + ], + organizer, + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], + }) + ); + + mockSuccessfulVideoMeetingCreation({ + metadataLookupKey: "dailyvideo", + videoMeetingData: { + id: "MOCK_ID", + password: "MOCK_PASS", + url: `http://mock-dailyvideo.example.com/meeting-1`, + }, + }); + + const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + }, + }); + + const mockBookingData = getMockRequestDataForBooking({ + data: { + user: organizer.username, + eventTypeId: 1, + responses: { + email: booker.email, + name: booker.name, + location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + const createdBooking = await handleNewBooking(req); + + expect(createdBooking.responses).toContain({ + email: booker.email, + name: booker.name, + }); + + expect(createdBooking).toContain({ + location: BookingLocations.CalVideo, + }); + + await expectBookingToBeInDatabase({ + description: "", + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: mockBookingData.eventTypeId, + status: BookingStatus.ACCEPTED, + references: [ + { + type: appStoreMetadata.dailyvideo.type, + uid: "MOCK_ID", + meetingId: "MOCK_ID", + meetingPassword: "MOCK_PASS", + meetingUrl: "http://mock-dailyvideo.example.com/meeting-1", + }, + { + type: appStoreMetadata.googlecalendar.type, + uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + meetingPassword: "MOCK_PASSWORD", + meetingUrl: "https://UNUSED_URL", + }, + ], + iCalUID: createdBooking.iCalUID, + }); + + expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { + calendarId: "event-type-1@google-calendar.com", + videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", + }); + + const iCalUID = expectICalUIDAsString(createdBooking.iCalUID); + + expectSuccessfulBookingCreationEmails({ + booking: { + uid: createdBooking.uid!, + urlOrigin: WEBSITE_URL, + }, + booker, + organizer, + emails, + iCalUID, + destinationEmail: organizerDestinationCalendarEmailOnEventType, + }); + + await expect(async () => await handleNewBooking(req)).rejects.toThrowError( + ErrorCode.NoAvailableUsersFound + ); + }, + timeout + ); }); });