test: Integration Test GCal Primary Calendar (#12011)

Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
Joe Au-Yeung 2023-11-22 09:15:47 -05:00 committed by zomars
parent 48dd4048f9
commit 87316c79c9
8 changed files with 389 additions and 13 deletions

View File

@ -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

View File

@ -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
*/

View File

@ -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,

View File

@ -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",

View File

@ -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);
});
});
});

View File

@ -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}`);
}
}

View File

@ -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",

View File

@ -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",