test: Integration Test GCal Primary Calendar (#12011)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
parent
48dd4048f9
commit
87316c79c9
|
@ -250,6 +250,12 @@ AUTH_BEARER_TOKEN_VERCEL=
|
|||
E2E_TEST_APPLE_CALENDAR_EMAIL=""
|
||||
E2E_TEST_APPLE_CALENDAR_PASSWORD=""
|
||||
|
||||
# - CALCOM QA ACCOUNT
|
||||
# Used for E2E tests on Cal.com that require 3rd party integrations
|
||||
E2E_TEST_CALCOM_QA_EMAIL="qa@example.com"
|
||||
# Replace with your own password
|
||||
E2E_TEST_CALCOM_QA_PASSWORD="password"
|
||||
|
||||
# - APP CREDENTIAL SYNC ***********************************************************************************
|
||||
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
|
||||
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
|
||||
|
|
|
@ -396,6 +396,15 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
|
|||
await prisma.user.delete({ where: { id } });
|
||||
store.users = store.users.filter((b) => b.id !== id);
|
||||
},
|
||||
set: async (email: string) => {
|
||||
const user = await prisma.user.findUniqueOrThrow({
|
||||
where: { email },
|
||||
include: userIncludes,
|
||||
});
|
||||
const userFixture = createUserFixture(user, store.page);
|
||||
store.users.push(userFixture);
|
||||
return userFixture;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -420,7 +429,8 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
|||
eventTypes: user.eventTypes,
|
||||
routingForms: user.routingForms,
|
||||
self,
|
||||
apiLogin: async () => apiLogin({ ...(await self()), password: user.username }, store.page),
|
||||
apiLogin: async (password?: string) =>
|
||||
apiLogin({ ...(await self()), password: password || user.username }, store.page),
|
||||
/**
|
||||
* @deprecated use apiLogin instead
|
||||
*/
|
||||
|
|
|
@ -78,17 +78,15 @@ test("Calendar Cache is being called", async () => {
|
|||
|
||||
// prismaMock.calendarCache.create.mock.
|
||||
const calendarService = new CalendarService(testCredential);
|
||||
// @ts-expect-error authedCalendar is a private method, hence the TS error
|
||||
vi.spyOn(calendarService, "authedCalendar").mockReturnValue(
|
||||
// @ts-expect-error trust me bro
|
||||
{
|
||||
freebusy: {
|
||||
query: vi.fn().mockReturnValue({
|
||||
data: testFreeBusyResponse,
|
||||
}),
|
||||
},
|
||||
}
|
||||
);
|
||||
vi.spyOn(calendarService, "authedCalendar").mockReturnValue({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore - Mocking the authedCalendar so can't return the actual response
|
||||
freebusy: {
|
||||
query: vi.fn().mockReturnValue({
|
||||
data: testFreeBusyResponse,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
await calendarService.getAvailability(new Date().toISOString(), new Date().toISOString(), [
|
||||
testSelectedCalendar,
|
||||
|
|
|
@ -132,7 +132,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||
};
|
||||
};
|
||||
|
||||
private authedCalendar = async () => {
|
||||
public authedCalendar = async () => {
|
||||
const myGoogleAuth = await this.auth.getToken();
|
||||
const calendar = google.calendar({
|
||||
version: "v3",
|
||||
|
|
|
@ -0,0 +1,215 @@
|
|||
import { expect } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { test } from "@calcom/web/playwright/lib/fixtures";
|
||||
import { selectSecondAvailableTimeSlotNextMonth } from "@calcom/web/playwright/lib/testUtils";
|
||||
|
||||
import metadata from "../_metadata";
|
||||
import GoogleCalendarService from "../lib/CalendarService";
|
||||
import { createBookingAndFetchGCalEvent, deleteBookingAndEvent, assertValueExists } from "./testUtils";
|
||||
|
||||
test.describe("Google Calendar", async () => {
|
||||
test.describe("Test using the primary calendar", async () => {
|
||||
let qaUsername: string;
|
||||
let qaGCalCredential: Prisma.CredentialGetPayload<{ select: { id: true } }>;
|
||||
test.beforeAll(async () => {
|
||||
let runIntegrationTest = false;
|
||||
|
||||
test.skip(!!APP_CREDENTIAL_SHARING_ENABLED, "Credential sharing enabled");
|
||||
|
||||
if (process.env.E2E_TEST_CALCOM_QA_EMAIL && process.env.E2E_TEST_CALCOM_QA_PASSWORD) {
|
||||
qaGCalCredential = await prisma.credential.findFirstOrThrow({
|
||||
where: {
|
||||
user: {
|
||||
email: process.env.E2E_TEST_CALCOM_QA_EMAIL,
|
||||
},
|
||||
type: metadata.type,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
const qaUserQuery = await prisma.user.findFirstOrThrow({
|
||||
where: {
|
||||
email: process.env.E2E_TEST_CALCOM_QA_EMAIL,
|
||||
},
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
});
|
||||
|
||||
assertValueExists(qaUserQuery.username, "qaUsername");
|
||||
qaUsername = qaUserQuery.username;
|
||||
|
||||
if (qaGCalCredential && qaUsername) runIntegrationTest = true;
|
||||
}
|
||||
|
||||
test.skip(!runIntegrationTest, "QA user not found");
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
assertValueExists(process.env.E2E_TEST_CALCOM_QA_EMAIL, "qaEmail");
|
||||
|
||||
const qaUserStore = await users.set(process.env.E2E_TEST_CALCOM_QA_EMAIL);
|
||||
|
||||
await qaUserStore.apiLogin(process.env.E2E_TEST_CALCOM_QA_PASSWORD);
|
||||
|
||||
// Need to refresh keys from DB
|
||||
const refreshedCredential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
id: qaGCalCredential?.id,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
assertValueExists(refreshedCredential, "refreshedCredential");
|
||||
|
||||
const googleCalendarService = new GoogleCalendarService(refreshedCredential);
|
||||
|
||||
const calendars = await googleCalendarService.listCalendars();
|
||||
|
||||
const primaryCalendarName = calendars.find((calendar) => calendar.primary)?.name;
|
||||
assertValueExists(primaryCalendarName, "primaryCalendarName");
|
||||
|
||||
await page.goto("/apps/installed/calendar");
|
||||
|
||||
await page.waitForSelector('[title*="Create events on"]');
|
||||
await page.locator('[title*="Create events on"]').locator("svg").click();
|
||||
await page.locator("#react-select-2-option-0-0").getByText(primaryCalendarName).click();
|
||||
});
|
||||
|
||||
test("On new booking, event should be created on GCal", async ({ page }) => {
|
||||
const { gCalEvent, gCalReference, booking, authedCalendar } = await createBookingAndFetchGCalEvent(
|
||||
page as Page,
|
||||
qaGCalCredential,
|
||||
qaUsername
|
||||
);
|
||||
|
||||
assertValueExists(gCalEvent.start?.timeZone, "gCalEvent");
|
||||
assertValueExists(gCalEvent.end?.timeZone, "gCalEvent");
|
||||
|
||||
// Ensure that the start and end times are matching
|
||||
const startTimeMatches = dayjs(booking.startTime).isSame(
|
||||
dayjs(gCalEvent.start.dateTime).tz(gCalEvent.start.timeZone)
|
||||
);
|
||||
const endTimeMatches = dayjs(booking.endTime).isSame(
|
||||
dayjs(gCalEvent.end?.dateTime).tz(gCalEvent.end.timeZone)
|
||||
);
|
||||
expect(startTimeMatches && endTimeMatches).toBe(true);
|
||||
|
||||
// Ensure that the titles are matching
|
||||
expect(booking.title).toBe(gCalEvent.summary);
|
||||
|
||||
// Ensure that the attendee is on the event
|
||||
const bookingAttendee = booking?.attendees[0].email;
|
||||
const attendeeInGCalEvent = gCalEvent.attendees?.find((attendee) => attendee.email === bookingAttendee);
|
||||
expect(attendeeInGCalEvent).toBeTruthy();
|
||||
|
||||
await deleteBookingAndEvent(authedCalendar, booking.uid, gCalReference.uid);
|
||||
});
|
||||
|
||||
test("On reschedule, event should be updated on GCal", async ({ page }) => {
|
||||
// Reschedule the booking and check the gCalEvent's time is also changed
|
||||
// On reschedule gCal UID stays the same
|
||||
const { gCalReference, booking, authedCalendar } = await createBookingAndFetchGCalEvent(
|
||||
page,
|
||||
qaGCalCredential,
|
||||
qaUsername
|
||||
);
|
||||
|
||||
await page.locator('[data-testid="reschedule-link"]').click();
|
||||
|
||||
await selectSecondAvailableTimeSlotNextMonth(page);
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
const rescheduledBookingUrl = await page.url();
|
||||
const rescheduledBookingUid = rescheduledBookingUrl.match(/booking\/([^\/?]+)/);
|
||||
|
||||
assertValueExists(rescheduledBookingUid, "rescheduledBookingUid");
|
||||
|
||||
// Get the rescheduled booking start and end times
|
||||
const rescheduledBooking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: rescheduledBookingUid[1],
|
||||
},
|
||||
select: {
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
},
|
||||
});
|
||||
assertValueExists(rescheduledBooking, "rescheduledBooking");
|
||||
|
||||
// The GCal event UID persists after reschedule but should get the rescheduled data
|
||||
const gCalRescheduledEventResponse = await authedCalendar.events.get({
|
||||
calendarId: "primary",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
eventId: gCalReference.uid,
|
||||
});
|
||||
|
||||
expect(gCalRescheduledEventResponse.status).toBe(200);
|
||||
|
||||
const rescheduledGCalEvent = gCalRescheduledEventResponse.data;
|
||||
|
||||
assertValueExists(rescheduledGCalEvent.start?.timeZone, "rescheduledGCalEvent");
|
||||
assertValueExists(rescheduledGCalEvent.end?.timeZone, "rescheduledGCalEvent");
|
||||
|
||||
// Ensure that the new start and end times are matching
|
||||
const rescheduledStartTimeMatches = dayjs(rescheduledBooking.startTime).isSame(
|
||||
dayjs(rescheduledGCalEvent.start?.dateTime).tz(rescheduledGCalEvent.start?.timeZone)
|
||||
);
|
||||
const rescheduledEndTimeMatches = dayjs(rescheduledBooking.endTime).isSame(
|
||||
dayjs(rescheduledGCalEvent.end?.dateTime).tz(rescheduledGCalEvent.end.timeZone)
|
||||
);
|
||||
expect(rescheduledStartTimeMatches && rescheduledEndTimeMatches).toBe(true);
|
||||
|
||||
// After test passes we can delete the bookings and GCal event
|
||||
await deleteBookingAndEvent(authedCalendar, booking.uid, gCalReference.uid);
|
||||
|
||||
await prisma.booking.delete({
|
||||
where: {
|
||||
uid: rescheduledBookingUid[1],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("When canceling the booking, the GCal event should also be deleted", async ({ page }) => {
|
||||
const { gCalReference, booking, authedCalendar } = await createBookingAndFetchGCalEvent(
|
||||
page,
|
||||
qaGCalCredential,
|
||||
qaUsername
|
||||
);
|
||||
|
||||
// Cancel the booking
|
||||
await page.locator('[data-testid="cancel"]').click();
|
||||
await page.locator('[data-testid="confirm_cancel"]').click();
|
||||
// Query for the bookingUID and ensure that it doesn't exist on GCal
|
||||
|
||||
await page.waitForSelector('[data-testid="cancelled-headline"]');
|
||||
|
||||
const canceledGCalEventResponse = await authedCalendar.events.get({
|
||||
calendarId: "primary",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
eventId: gCalReference.uid,
|
||||
});
|
||||
|
||||
expect(canceledGCalEventResponse.data.status).toBe("cancelled");
|
||||
|
||||
// GCal API sees canceled events as already deleted
|
||||
await deleteBookingAndEvent(authedCalendar, booking.uid);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,127 @@
|
|||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { bookFirstEvent } from "@calcom/web/playwright/lib/testUtils";
|
||||
|
||||
import metadata from "../_metadata";
|
||||
import GoogleCalendarService from "../lib/CalendarService";
|
||||
|
||||
/**
|
||||
* Creates the booking on Cal.com and makes the GCal call to fetch the event.
|
||||
* Ends on the booking success page
|
||||
* @param page
|
||||
*
|
||||
* @returns the raw GCal event GET response and the booking reference
|
||||
*/
|
||||
export const createBookingAndFetchGCalEvent = async (
|
||||
page: Page,
|
||||
qaGCalCredential: Prisma.CredentialGetPayload<{ select: { id: true } }> | null,
|
||||
qaUsername: string
|
||||
) => {
|
||||
await page.goto(`/${qaUsername}`);
|
||||
await bookFirstEvent(page);
|
||||
|
||||
const bookingUrl = await page.url();
|
||||
const bookingUid = bookingUrl.match(/booking\/([^\/?]+)/);
|
||||
assertValueExists(bookingUid, "bookingUid");
|
||||
|
||||
const [gCalReference, booking] = await Promise.all([
|
||||
prisma.bookingReference.findFirst({
|
||||
where: {
|
||||
booking: {
|
||||
uid: bookingUid[1],
|
||||
},
|
||||
type: metadata.type,
|
||||
credentialId: qaGCalCredential?.id,
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
booking: {},
|
||||
},
|
||||
}),
|
||||
prisma.booking.findFirst({
|
||||
where: {
|
||||
uid: bookingUid[1],
|
||||
},
|
||||
select: {
|
||||
uid: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
attendees: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
assertValueExists(gCalReference, "gCalReference");
|
||||
assertValueExists(booking, "booking");
|
||||
|
||||
// Need to refresh keys from DB
|
||||
const refreshedCredential = await prisma.credential.findFirst({
|
||||
where: {
|
||||
id: qaGCalCredential?.id,
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(refreshedCredential).toBeTruthy();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
const googleCalendarService = new GoogleCalendarService(refreshedCredential);
|
||||
|
||||
const authedCalendar = await googleCalendarService.authedCalendar();
|
||||
|
||||
const gCalEventResponse = await authedCalendar.events.get({
|
||||
calendarId: "primary",
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
eventId: gCalReference.uid,
|
||||
});
|
||||
|
||||
expect(gCalEventResponse.status).toBe(200);
|
||||
|
||||
return { gCalEvent: gCalEventResponse.data, gCalReference, booking, authedCalendar };
|
||||
};
|
||||
|
||||
export const deleteBookingAndEvent = async (
|
||||
authedCalendar: any,
|
||||
bookingUid: string,
|
||||
gCalReferenceUid?: string
|
||||
) => {
|
||||
// After test passes we can delete the booking and GCal event
|
||||
await prisma.booking.delete({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
});
|
||||
|
||||
if (gCalReferenceUid) {
|
||||
await authedCalendar.events.delete({
|
||||
calendarId: "primary",
|
||||
eventId: gCalReferenceUid,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export function assertValueExists(value: unknown, variableName?: string): asserts value {
|
||||
if (!value) {
|
||||
throw new Error(`Value is not defined: ${variableName}`);
|
||||
}
|
||||
}
|
|
@ -455,6 +455,22 @@ async function main() {
|
|||
},
|
||||
});
|
||||
|
||||
await createUserAndEventType({
|
||||
user: {
|
||||
email: process.env.E2E_TEST_CALCOM_QA_EMAIL || "qa@example.com",
|
||||
password: process.env.E2E_TEST_CALCOM_QA_PASSWORD || "qa",
|
||||
username: "qa",
|
||||
name: "QA Example",
|
||||
},
|
||||
eventTypes: [
|
||||
{
|
||||
title: "15min",
|
||||
slug: "15min",
|
||||
length: 15,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await createTeamAndAddUsers(
|
||||
{
|
||||
name: "Seeded Team",
|
||||
|
|
|
@ -209,6 +209,8 @@
|
|||
"CALCOM_CREDENTIAL_SYNC_ENDPOINT",
|
||||
"CALCOM_ENV",
|
||||
"CALCOM_LICENSE_KEY",
|
||||
"CALCOM_QA_EMAIL",
|
||||
"CALCOM_QA_PASSWORD",
|
||||
"CALCOM_TELEMETRY_DISABLED",
|
||||
"CALCOM_WEBHOOK_HEADER_NAME",
|
||||
"CALENDSO_ENCRYPTION_KEY",
|
||||
|
@ -222,6 +224,8 @@
|
|||
"DEBUG",
|
||||
"E2E_TEST_APPLE_CALENDAR_EMAIL",
|
||||
"E2E_TEST_APPLE_CALENDAR_PASSWORD",
|
||||
"E2E_TEST_CALCOM_QA_EMAIL",
|
||||
"E2E_TEST_CALCOM_QA_PASSWORD",
|
||||
"E2E_TEST_MAILHOG_ENABLED",
|
||||
"E2E_TEST_OIDC_CLIENT_ID",
|
||||
"E2E_TEST_OIDC_CLIENT_SECRET",
|
||||
|
|
Loading…
Reference in New Issue
Block a user