cal.pub0.org/apps/web/playwright/integrations.e2e.ts

357 lines
12 KiB
TypeScript
Raw Normal View History

import type { Page, Route } from "@playwright/test";
import { expect } from "@playwright/test";
import type { DefaultBodyType } from "msw";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { v4 as uuidv4 } from "uuid";
import { prisma } from "@calcom/prisma";
import { test } from "./lib/fixtures";
import { todo } from "./lib/testUtils";
declare let global: {
E2E_EMAILS?: ({ text: string } | Record<string, unknown>)[];
};
const requestInterceptor = setupServer(
rest.post("https://api.hubapi.com/oauth/v1/token", (req, res, ctx) => {
console.log(req.body);
return res(ctx.status(200));
})
);
const addOauthBasedIntegration = async function ({
page,
slug,
authorization,
token,
}: {
page: Page;
slug: string;
authorization: {
url: string;
verify: (config: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
requestHeaders: any;
params: URLSearchParams;
code: string;
}) => Parameters<Route["fulfill"]>[0];
};
token: {
url: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
verify: (config: { requestHeaders: any; params: URLSearchParams; code: string }) => {
status: number;
body: DefaultBodyType;
};
};
}) {
const code = uuidv4();
// Note the difference b/w MSW wildcard and Playwright wildards. Playwright requires query params to be explicitly specified.
page.route(`${authorization.url}?**`, (route, request) => {
const u = new URL(request.url());
const result = authorization.verify({
requestHeaders: request.allHeaders(),
params: u.searchParams,
code,
});
return route.fulfill(result);
});
requestInterceptor.use(
rest.post(token.url, (req, res, ctx) => {
const params = new URLSearchParams(req.body as string);
const result = token.verify({ requestHeaders: req.headers, params, code });
return res(ctx.status(result.status), ctx.json(result.body));
})
);
await page.goto(`/apps/${slug}`);
await page.click('[data-testid="install-app-button"]');
};
const addLocationIntegrationToFirstEvent = async function ({ user }: { user: { username: string | null } }) {
const eventType = await prisma.eventType.findFirst({
where: {
users: {
some: {
username: user.username,
},
},
price: 0,
},
});
if (!eventType) {
throw new Error("Event type not found");
}
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
locations: [{ type: "integrations:zoom" }],
},
});
return eventType;
};
async function bookEvent(page: Page, calLink: string) {
// Let current month dates fully render.
// There is a bug where if we don't let current month fully render and quickly click go to next month, current month get's rendered
// This doesn't seem to be replicable with the speed of a person, only during automation.
// It would also allow correct snapshot to be taken for current month.
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(1000);
await page.goto(`/${calLink}`);
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).click();
page.locator('[data-testid="time"]').nth(0).click();
await page.waitForNavigation({
url(url) {
return url.pathname.includes("/book");
},
});
const meetingId = 123456789;
requestInterceptor.use(
rest.post("https://api.zoom.us/v2/users/me/meetings", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
id: meetingId,
password: "TestPass",
join_url: `https://zoom.us/j/${meetingId}`,
})
);
})
);
// --- fill form
await page.fill('[name="name"]', "Integration User");
await page.fill('[name="email"]', "integration-user@example.com");
await page.press('[name="email"]', "Enter");
const response = await page.waitForResponse("**/api/book/event");
const responseObj = await response.json();
const bookingId = responseObj.uid;
await page.waitForSelector("[data-testid=success-page]");
// Make sure we're navigated to the success page
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
expect(global.E2E_EMAILS?.length).toBe(2);
expect(
global.E2E_EMAILS?.every((email) => (email.text as string).includes(`https://zoom.us/j/${meetingId}`))
).toBe(true);
return bookingId;
}
test.describe.configure({ mode: "parallel" });
// Enable API mocking before tests.
test.beforeAll(() =>
requestInterceptor.listen({
// Comment this to log which all requests are going that are unmocked
onUnhandledRequest: "bypass",
})
);
// Reset any runtime request handlers we may add during the tests.
test.afterEach(() => requestInterceptor.resetHandlers());
// Disable API mocking after the tests are done.
test.afterAll(() => requestInterceptor.close());
2023-03-01 20:18:51 +00:00
test.afterEach(({ users }) => users.deleteAll());
// TODO: Fix MSW mocking
test.fixme("Integrations", () => {
test.beforeEach(() => {
global.E2E_EMAILS = [];
});
const addZoomIntegration = async function ({ page }: { page: Page }) {
await addOauthBasedIntegration({
page,
slug: "zoom",
authorization: {
url: "https://zoom.us/oauth/authorize",
verify({ params, code }) {
expect(params.get("redirect_uri")).toBeTruthy();
return {
status: 307,
headers: {
location: `${params.get("redirect_uri")}?code=${code}`,
},
};
},
},
token: {
url: "https://zoom.us/oauth/token",
verify({ requestHeaders }) {
const authorization = requestHeaders.get("authorization").replace("Basic ", "");
const clientPair = Buffer.from(authorization, "base64").toString();
const [clientId, clientSecret] = clientPair.split(":");
// Ensure that zoom credentials are passed.
// TODO: We should also ensure that these credentials are correct e.g. in this case should be READ from DB
expect(clientId).toBeTruthy();
expect(clientSecret).toBeTruthy();
return {
status: 200,
body: {
access_token:
"eyJhbGciOiJIUzUxMiIsInYiOiIyLjAiLCJraWQiOiI8S0lEPiJ9.eyJ2ZXIiOiI2IiwiY2xpZW50SWQiOiI8Q2xpZW50X0lEPiIsImNvZGUiOiI8Q29kZT4iLCJpc3MiOiJ1cm46em9vbTpjb25uZWN0OmNsaWVudGlkOjxDbGllbnRfSUQ-IiwiYXV0aGVudGljYXRpb25JZCI6IjxBdXRoZW50aWNhdGlvbl9JRD4iLCJ1c2VySWQiOiI8VXNlcl9JRD4iLCJncm91cE51bWJlciI6MCwiYXVkIjoiaHR0cHM6Ly9vYXV0aC56b29tLnVzIiwiYWNjb3VudElkIjoiPEFjY291bnRfSUQ-IiwibmJmIjoxNTgwMTQ2OTkzLCJleHAiOjE1ODAxNTA1OTMsInRva2VuVHlwZSI6ImFjY2Vzc190b2tlbiIsImlhdCI6MTU4MDE0Njk5MywianRpIjoiPEpUST4iLCJ0b2xlcmFuY2VJZCI6MjV9.F9o_w7_lde4Jlmk_yspIlDc-6QGmVrCbe_6El-xrZehnMx7qyoZPUzyuNAKUKcHfbdZa6Q4QBSvpd6eIFXvjHw",
token_type: "bearer",
refresh_token:
"eyJhbGciOiJIUzUxMiIsInYiOiIyLjAiLCJraWQiOiI8S0lEPiJ9.eyJ2ZXIiOiI2IiwiY2xpZW50SWQiOiI8Q2xpZW50X0lEPiIsImNvZGUiOiI8Q29kZT4iLCJpc3MiOiJ1cm46em9vbTpjb25uZWN0OmNsaWVudGlkOjxDbGllbnRfSUQ-IiwiYXV0aGVudGljYXRpb25JZCI6IjxBdXRoZW50aWNhdGlvbl9JRD4iLCJ1c2VySWQiOiI8VXNlcl9JRD4iLCJncm91cE51bWJlciI6MCwiYXVkIjoiaHR0cHM6Ly9vYXV0aC56b29tLnVzIiwiYWNjb3VudElkIjoiPEFjY291bnRfSUQ-IiwibmJmIjoxNTgwMTQ2OTkzLCJleHAiOjIwNTMxODY5OTMsInRva2VuVHlwZSI6InJlZnJlc2hfdG9rZW4iLCJpYXQiOjE1ODAxNDY5OTMsImp0aSI6IjxKVEk-IiwidG9sZXJhbmNlSWQiOjI1fQ.Xcn_1i_tE6n-wy6_-3JZArIEbiP4AS3paSD0hzb0OZwvYSf-iebQBr0Nucupe57HUDB5NfR9VuyvQ3b74qZAfA",
expires_in: 3599,
// Without this permission, meeting can't be created.
scope: "meeting:write",
},
};
},
},
});
};
test.describe("Zoom App", () => {
test("Can add integration", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
await addZoomIntegration({ page });
await page.waitForNavigation({
url: (url) => {
return url.pathname === "/apps/installed";
},
});
//TODO: Check that disconnect button is now visible
});
test("can choose zoom as a location during booking", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
const eventType = await addLocationIntegrationToFirstEvent({ user });
await addZoomIntegration({ page });
await page.waitForNavigation({
url: (url) => {
return url.pathname === "/apps/installed";
},
});
await bookEvent(page, `${user.username}/${eventType.slug}`);
// Ensure that zoom was informed about the meeting
// Verify that email had zoom link
// POST https://api.zoom.us/v2/users/me/meetings
// Verify Header-> Authorization: "Bearer " + accessToken,
/**
* {
topic: event.title,
type: 2, // Means that this is a scheduled meeting
start_time: event.startTime,
duration: (new Date(event.endTime).getTime() - new Date(event.startTime).getTime()) / 60000,
//schedule_for: "string", TODO: Used when scheduling the meeting for someone else (needed?)
timezone: event.attendees[0].timeZone,
//password: "string", TODO: Should we use a password? Maybe generate a random one?
agenda: event.description,
settings: {
host_video: true,
participant_video: true,
cn_meeting: false, // TODO: true if host meeting in China
in_meeting: false, // TODO: true if host meeting in India
join_before_host: true,
mute_upon_entry: false,
watermark: false,
use_pmi: false,
approval_type: 2,
audio: "both",
auto_recording: "none",
enforce_apiLogin: false,
registrants_email_notification: true,
},
};
*/
});
test("Can disconnect from integration", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
await addZoomIntegration({ page });
await page.waitForNavigation({
url: (url) => {
return url.pathname === "/apps/installed";
},
});
// FIXME: First time reaching /apps/installed throws error in UI.
// Temporary use this hack to fix it but remove this HACK before merge.
/** HACK STARTS */
await page.locator('[href="/apps"]').first().click();
await page.waitForNavigation({
url: (url) => {
return url.pathname === "/apps";
},
});
await page.locator('[href="/apps/installed"]').first().click();
/** HACK ENDS */
await page.locator('[data-testid="zoom_video-integration-disconnect-button"]').click();
await page.locator('[data-testid="confirm-button"]').click();
await expect(page.locator('[data-testid="confirm-integration-disconnect-button"]')).toHaveCount(0);
});
});
test.describe("Hubspot App", () => {
test("Can add integration", async ({ page, users }) => {
const user = await users.create();
await user.apiLogin();
await addOauthBasedIntegration({
page,
slug: "hubspot",
authorization: {
url: "https://app.hubspot.com/oauth/authorize",
verify({ params, code }) {
expect(params.get("redirect_uri")).toBeTruthy();
// TODO: We can check if client_id is correctly read from DB or not
expect(params.get("client_id")).toBeTruthy();
expect(params.get("scope")).toBe(
["crm.objects.contacts.read", "crm.objects.contacts.write"].join(" ")
);
return {
// TODO: Should
status: 307,
headers: {
location: `${params.get("redirect_uri")}?code=${code}`,
},
};
},
},
token: {
url: "https://api.hubapi.com/oauth/v1/token",
verify({ params, code }) {
expect(params.get("grant_type")).toBe("authorization_code");
expect(params.get("code")).toBe(code);
expect(params.get("client_id")).toBeTruthy();
expect(params.get("client_secret")).toBeTruthy();
return {
status: 200,
body: {
expiresIn: "3600",
},
};
},
},
});
await page.waitForNavigation({
url: (url) => {
return url.pathname === "/apps/installed";
},
});
});
});
todo("Can add Google Calendar");
todo("Can add Office 365 Calendar");
todo("Can add CalDav Calendar");
todo("Can add Apple Calendar");
});