Zoom/Hubspot Tests with MSW mocking of requests initiated from Next.js server (#3210)

* Add code from previous PR

* Remove env variables from wrong place

* Fix tests

* Remove dead code

* Package.json updates

* Fix eslint error

* Fix issue due to conflict resolution

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
Hariom Balhara 2022-08-27 00:14:02 +05:30 committed by GitHub
parent e0744a4857
commit 4ef666a610
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 916 additions and 106 deletions

View File

@ -0,0 +1,104 @@
name: E2E Test - Integrations with Third Party
on:
push:
branches: [ tests/with-msw ]
pull_request_target: # So we can test on forks
branches:
- main
paths-ignore:
- apps/api/**
- apps/console/**
- apps/docs/**
- apps/swagger/**
- apps/website/**
- apps/web/public/**
jobs:
test:
timeout-minutes: 20
name: E2E Integration
strategy:
matrix:
node: ["16.x"]
os: [ubuntu-latest]
runs-on: ${{ matrix.os }}
env:
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
NEXT_PUBLIC_WEBAPP_URL: http://localhost:3000
NEXT_PUBLIC_WEBSITE_URL: http://localhost:3000
NEXTAUTH_SECRET: secret
GOOGLE_API_CREDENTIALS: ${{ secrets.CI_GOOGLE_API_CREDENTIALS }}
GOOGLE_LOGIN_ENABLED: true
# CRON_API_KEY: xxx
CALENDSO_ENCRYPTION_KEY: ${{ secrets.CI_CALENDSO_ENCRYPTION_KEY }}
NEXT_PUBLIC_STRIPE_PUBLIC_KEY: ${{ secrets.CI_NEXT_PUBLIC_STRIPE_PUBLIC_KEY }}
STRIPE_PRIVATE_KEY: ${{ secrets.CI_STRIPE_PRIVATE_KEY }}
STRIPE_CLIENT_ID: ${{ secrets.CI_STRIPE_CLIENT_ID }}
STRIPE_WEBHOOK_SECRET: ${{ secrets.CI_STRIPE_WEBHOOK_SECRET }}
PAYMENT_FEE_PERCENTAGE: 0.005
PAYMENT_FEE_FIXED: 10
SAML_DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
SAML_ADMINS: pro@example.com
NEXTAUTH_URL: http://localhost:3000/api/auth
ZOOM_CLIENT_ID: ZOOM_CLIENT_ID
ZOOM_CLIENT_SECRET: ZOOM_CLIENT_SECRET
HUBSPOT_CLIENT_ID: HUBSPOT_CLIENT_ID
HUBSPOT_CLIENT_SECRET: HUBSPOT_CLIENT_SECRET
NEXT_PUBLIC_IS_E2E: 1
# EMAIL_FROM: e2e@cal.com
# EMAIL_SERVER_HOST: ${{ secrets.CI_EMAIL_SERVER_HOST }}
# EMAIL_SERVER_PORT: ${{ secrets.CI_EMAIL_SERVER_PORT }}
# EMAIL_SERVER_USER: ${{ secrets.CI_EMAIL_SERVER_USER }}
# EMAIL_SERVER_PASSWORD: ${{ secrets.CI_EMAIL_SERVER_PASSWORD }}
# MS_GRAPH_CLIENT_ID: xxx
# MS_GRAPH_CLIENT_SECRET: xxx
# ZOOM_CLIENT_ID: xxx
# ZOOM_CLIENT_SECRET: xxx
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ secrets.TURBO_TEAM }}
services:
postgres:
image: postgres:12.1
env:
POSTGRES_USER: postgres
POSTGRES_DB: calendso
ports:
- 5432:5432
steps:
- name: Checkout repo
uses: actions/checkout@v2
with:
ref: ${{ github.event.pull_request.head.sha }} # So we can test on forks
fetch-depth: 2
- run: echo 'NODE_OPTIONS="--max_old_space_size=4096"' >> $GITHUB_ENV
- name: Use Node ${{ matrix.node }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
cache: "yarn"
- name: Cache playwright binaries
uses: actions/cache@v2
id: playwright-cache
with:
path: |
~/Library/Caches/ms-playwright
~/.cache/ms-playwright
${{ github.workspace }}/node_modules/playwright
key: cache-playwright-${{ hashFiles('**/yarn.lock') }}
restore-keys: cache-playwright-
- run: yarn --frozen-lockfile
- name: Install playwright deps
# if: steps.playwright-cache.outputs.cache-hit != 'true'
run: yarn playwright install --with-deps
- name: Run Tests
# Force bypass cache because new environment variables were added that caused DB to change but build remains cached
run: yarn test-e2e-integrations --force
- name: Upload Test Results
if: ${{ always() }}
uses: actions/upload-artifact@v2
with:
name: test-results-core
path: apps/web/playwright-integrations/test-results

View File

@ -10,8 +10,10 @@
"dev": "next dev",
"dx": "yarn dev",
"test": "dotenv -e ./test/.env.test -- jest",
"test-e2e": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --config=../../tests/config/playwright.config.ts --project=chromium",
"test-e2e-integrations": "NEXT_PUBLIC_IS_E2E=1 yarn playwright test --config=playwright-integrations/config/playwright.config.ts --project=chromium",
"test-e2e-integrations-quick": "QUICK=true E2E_DEV_SERVER=1 yarn test-e2e-integrations",
"db-setup-tests": "dotenv -e ./test/.env.test -- yarn workspace @calcom/prisma prisma generate",
"test-e2e": "cd ../.. && yarn playwright test --config=tests/config/playwright.config.ts --project=chromium",
"playwright-report": "playwright show-report playwright/reports/playwright-html-report",
"test-codegen": "yarn playwright codegen http://localhost:3000",
"type-check": "tsc --pretty --noEmit",
@ -132,6 +134,7 @@
"@types/accept-language-parser": "1.5.2",
"@types/async": "^3.2.15",
"@types/bcryptjs": "^2.4.2",
"@types/detect-port": "^1.3.2",
"@types/glidejs__glide": "^3.4.2",
"@types/jest": "^28.1.7",
"@types/lodash": "^4.14.182",
@ -150,6 +153,7 @@
"@types/stripe": "^8.0.417",
"@types/uuid": "8.3.1",
"autoprefixer": "^10.4.7",
"detect-port": "^1.3.0",
"babel-jest": "^28.1.0",
"copy-webpack-plugin": "^11.0.0",
"env-cmd": "^10.1.0",
@ -159,6 +163,7 @@
"jest-mock-extended": "^2.0.7",
"mockdate": "^3.0.5",
"module-alias": "^2.2.2",
"msw": "^0.42.3",
"postcss": "^8.4.13",
"tailwindcss": "^3.1.6",
"ts-jest": "^28.0.8",

View File

@ -45,7 +45,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
<DisconnectIntegration
id={credentialId}
render={(btnProps) => (
<Button {...btnProps} color="warn" data-testid="integration-connection-button">
<Button {...btnProps} color="warn" data-testid={type + "-integration-disconnect-button"}>
{t("disconnect")}
</Button>
)}
@ -57,7 +57,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
<DisconnectIntegration
id={credentialId}
render={(btnProps) => (
<Button {...btnProps} color="warn" data-testid="integration-connection-button">
<Button {...btnProps} color="warn" data-testid={type + "-integration-disconnect-button"}>
{t("disconnect")}
</Button>
)}
@ -100,7 +100,7 @@ interface IntegrationsContainerProps {
const IntegrationsContainer = ({ variant, className = "" }: IntegrationsContainerProps): JSX.Element => {
const { t } = useLocale();
const query = trpc.useQuery(["viewer.integrations", { variant, onlyInstalled: true }], { suspense: true });
const query = trpc.useQuery(["viewer.integrations", { variant, onlyInstalled: true }]);
return (
<QueryCell
query={query}

View File

@ -0,0 +1 @@
test-results

View File

@ -0,0 +1,68 @@
import { devices, PlaywrightTestConfig } from "@playwright/test";
import dotenv from "dotenv";
import { addAliases } from "module-alias";
import * as path from "path";
dotenv.config({ path: "./env" });
// Add aliases for the paths specified in the tsconfig.json file.
// This is needed because playwright does not consider tsconfig.json
// For more info, see:
// https://stackoverflow.com/questions/69023682/typescript-playwright-error-cannot-find-module
// https://github.com/microsoft/playwright/issues/7066#issuecomment-983984496
addAliases({
"@components": __dirname + "/apps/web/components",
"@lib": __dirname + "/apps/web/lib",
"@server": __dirname + "/apps/web/server",
"@ee": __dirname + "/apps/web/ee",
});
const outputDir = path.join(__dirname, "..", "test-results");
const testDir = path.join(__dirname, "..", "tests");
const DEFAULT_NAVIGATION_TIMEOUT = 600000;
const headless = !!process.env.CI || !!process.env.PLAYWRIGHT_HEADLESS;
process.env.PLAYWRIGHT_TEST_BASE_URL = "http://localhost:3000";
const quickMode = process.env.QUICK === "true";
const config: PlaywrightTestConfig = {
forbidOnly: !!process.env.CI,
retries: 0,
workers: quickMode ? 1 : 1,
timeout: 60_000,
maxFailures: headless ? 10 : undefined,
reporter: [
[process.env.CI ? "github" : "list"],
["html", { outputFolder: path.join(outputDir, "reports/playwright-html-report"), open: "never" }],
["junit", { outputFile: path.join(outputDir, "reports/results.xml") }],
],
outputDir,
use: {
baseURL: "http://localhost:3000/",
locale: "en-US",
trace: "retain-on-failure",
headless,
},
projects: [
{
name: "chromium",
testDir,
use: {
...devices["Desktop Chrome"],
/** If navigation takes more than this, then something's wrong, let's fail fast. */
navigationTimeout: DEFAULT_NAVIGATION_TIMEOUT,
},
},
/* {
name: "firefox",
use: { ...devices["Desktop Firefox"] },
},
{
name: "webkit",
use: { ...devices["Desktop Safari"] },
}, */
],
};
export default config;

View File

@ -0,0 +1,51 @@
import { test as base } from "@playwright/test";
import { Server } from "http";
import { rest } from "msw";
import type { SetupServerApi } from "msw/node";
import { nextServer } from "../../playwright-integrations/next-server";
import { createBookingsFixture } from "../../playwright/fixtures/bookings";
import { createPaymentsFixture } from "../../playwright/fixtures/payments";
import { createUsersFixture } from "../../playwright/fixtures/users";
interface Fixtures {
users: ReturnType<typeof createUsersFixture>;
bookings: ReturnType<typeof createBookingsFixture>;
payments: ReturnType<typeof createPaymentsFixture>;
server: Server;
requestInterceptor: SetupServerApi;
rest: typeof rest;
}
/**
* @see https://playwright.dev/docs/test-fixtures
*/
export const test = base.extend<Fixtures>({
users: async ({ page }, use, workerInfo) => {
const usersFixture = createUsersFixture(page, workerInfo);
await use(usersFixture);
},
bookings: async ({ page }, use) => {
const bookingsFixture = createBookingsFixture(page);
await use(bookingsFixture);
},
payments: async ({ page }, use) => {
const payemntsFixture = createPaymentsFixture(page);
await use(payemntsFixture);
},
// This fixture runs for each worker, ensuring that every worker starts it's own Next.js instance on which we can attach MSW
// A single worker can run many tests
server: [
async ({}, use) => {
const server = await nextServer();
await use(server);
server.close();
},
{
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
scope: "worker",
auto: true,
},
],
});

View File

@ -0,0 +1,52 @@
import detect from "detect-port";
import { createServer, Server } from "http";
import next from "next";
import { parse } from "url";
// eslint-disable-next-line @typescript-eslint/no-namespace
declare let process: {
env: {
E2E_DEV_SERVER: string;
PLAYWRIGHT_TEST_BASE_URL: string;
NEXT_PUBLIC_WEBAPP_URL: string;
NEXT_PUBLIC_WEBSITE_URL: string;
};
};
export const nextServer = async ({ port = 3000 } = { port: 3000 }) => {
// eslint-disable-next-line turbo/no-undeclared-env-vars
const dev = process.env.E2E_DEV_SERVER === "1" ? true : false;
if (dev) {
port = await detect(Math.round((1 + Math.random()) * 3000));
}
process.env.PLAYWRIGHT_TEST_BASE_URL =
process.env.NEXT_PUBLIC_WEBAPP_URL =
process.env.NEXT_PUBLIC_WEBSITE_URL =
"http://localhost:" + port;
const app = next({
dev: dev,
port,
hostname: "localhost",
});
console.log("Started Next Server", { dev, port });
await app.prepare();
const handle = app.getRequestHandler();
// start next server on arbitrary port
const server: Server = await new Promise((resolve) => {
const server = createServer((req, res) => {
if (!req.url) {
throw new Error("URL not present");
}
const parsedUrl = parse(req.url, true);
handle(req, res, parsedUrl);
});
server.listen({ port: port }, () => {
resolve(server);
});
server.on("error", (error) => {
if (error) throw new Error("Could not start Next.js server -" + error.message);
});
});
return server;
};

View File

@ -0,0 +1,430 @@
import { expect, Page, Route } from "@playwright/test";
import { rest } from "msw";
import { setupServer } from "msw/node";
import { v4 as uuidv4 } from "uuid";
import { prisma } from "@calcom/prisma";
import {
createHttpServer,
selectFirstAvailableTimeSlotNextMonth,
todo,
waitFor,
} from "@calcom/web/playwright/lib/testUtils";
import { test } from "../lib/fixtures";
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));
})
);
requestInterceptor.listen({
// Comment this to log which all requests are going that are unmocked
onUnhandledRequest: "bypass",
});
requestInterceptor.use();
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: any;
};
};
}) {
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(`${process.env.PLAYWRIGHT_TEST_BASE_URL}/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(`${process.env.PLAYWRIGHT_TEST_BASE_URL}/${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" });
test.describe("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, code }) {
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.afterEach(async () => {
await prisma?.credential.deleteMany({
where: {
user: {
email: "pro@example.com",
},
type: "zoom_video",
},
});
});
test("Can add integration", async ({ page, users }) => {
const user = await users.create();
await user.login();
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.login();
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_login: false,
registrants_email_notification: true,
},
};
*/
});
test("Can disconnect from integration", async ({ page, users }) => {
const user = await users.create();
await user.login();
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.login();
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");
test("add webhook & test that creating an event triggers a webhook call", async ({
page,
users,
}, testInfo) => {
const webhookReceiver = createHttpServer();
const user = await users.create();
const [eventType] = user.eventTypes;
await user.login();
await page.goto(`${process.env.PLAYWRIGHT_TEST_BASE_URL}/settings/developer`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await expect(page.locator(`[data-testid='WebhookDialogForm']`)).toBeVisible();
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await page.click("[type=submit]");
// dialog is closed
await expect(page.locator(`[data-testid='WebhookDialogForm']`)).not.toBeVisible();
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
// --- Book the first available day next month in the pro user's "30min"-event
await page.goto(`${process.env.PLAYWRIGHT_TEST_BASE_URL}/${user.username}/${eventType.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
// --- fill form
await page.fill('[name="name"]', "Test Testson");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
// if we change the shape of our webhooks, we can simply update this by clicking `u`
// console.log("BODY", body);
// Text files shouldn't have platform specific suffixes
testInfo.snapshotSuffix = "";
expect(JSON.stringify(body)).toMatchSnapshot(`webhookResponse.txt`);
webhookReceiver.close();
});
});

View File

@ -1,13 +1,19 @@
import type { Page, WorkerInfo } from "@playwright/test";
import type Prisma from "@prisma/client";
import { Prisma as PrismaType, UserPlan } from "@prisma/client";
import { hash } from "bcryptjs";
import { hashPassword } from "@calcom/lib/auth";
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
import { prisma } from "@calcom/prisma";
import { TimeZoneEnum } from "./types";
// Don't import hashPassword from app as that ends up importing next-auth and initializing it before NEXTAUTH_URL can be updated during tests.
export async function hashPassword(password: string) {
const hashedPassword = await hash(password, 12);
return hashedPassword;
}
type UserFixture = ReturnType<typeof createUserFixture>;
const userIncludes = PrismaType.validator<PrismaType.UserInclude>()({
@ -66,7 +72,7 @@ export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
},
get: () => store.users,
logout: async () => {
await page.goto("/auth/logout");
await page.goto(`${process.env.PLAYWRIGHT_TEST_BASE_URL}/auth/logout`);
},
deleteAll: async () => {
const ids = store.users.map((u) => u.id);
@ -161,8 +167,10 @@ export async function login(
const signInLocator = loginLocator.locator('[type="submit"]');
//login
await page.goto("/");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await page.goto(process.env.PLAYWRIGHT_TEST_BASE_URL!);
await emailLocator.fill(user.email ?? `${user.username}@example.com`);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await passwordLocator.fill(user.password ?? user.username!);
await signInLocator.click();

View File

@ -25,7 +25,7 @@ test.describe("Stripe integration", () => {
/** If Stripe is added correctly we should see the "Disconnect" button */
await expect(
page.locator(`li:has-text("Stripe") >> [data-testid="integration-connection-button"]`)
page.locator(`li:has-text("Stripe") >> [data-testid="stripe_payment-integration-disconnect-button"]`)
).toContainText("Disconnect");
// Cleanup

View File

@ -1,89 +0,0 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
import { createHttpServer, selectFirstAvailableTimeSlotNextMonth, todo, waitFor } from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test.describe("Integrations", () => {
todo("Can add Zoom integration");
todo("Can add Google Calendar");
todo("Can add Office 365 Calendar");
todo("Can add CalDav Calendar");
todo("Can add Apple Calendar");
test("add webhook & test that creating an event triggers a webhook call", async ({
page,
users,
}, testInfo) => {
const webhookReceiver = createHttpServer();
const user = await users.create();
const [eventType] = user.eventTypes;
await user.login();
await page.goto("/settings/developer");
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await expect(page.locator(`[data-testid='WebhookDialogForm']`)).toBeVisible();
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await page.click("[type=submit]");
// dialog is closed
await expect(page.locator(`[data-testid='WebhookDialogForm']`)).not.toBeVisible();
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
// --- Book the first available day next month in the pro user's "30min"-event
await page.goto(`/${user.username}/${eventType.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
// --- fill form
await page.fill('[name="name"]', "Test Testson");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
// if we change the shape of our webhooks, we can simply update this by clicking `u`
// console.log("BODY", body);
// Text files shouldn't have platform specific suffixes
testInfo.snapshotSuffix = "";
expect(JSON.stringify(body)).toMatchSnapshot(`webhookResponse.txt`);
webhookReceiver.close();
});
});

View File

@ -51,7 +51,8 @@
"test-playwright": "yarn playwright test --config=tests/config/playwright.config.ts",
"test": "turbo run test",
"turbo-w": "node turbo-wrapper.js",
"type-check": "turbo run type-check"
"type-check": "turbo run type-check",
"test-e2e-integrations": "turbo run test-e2e-integrations --scope=\"@calcom/web\" --concurrency=1"
},
"devDependencies": {
"@snaplet/copycat": "^0.3.0",

View File

@ -21,8 +21,24 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
);
if (result.status !== 200) {
let errorMessage = "Something is wrong with Zoom API";
try {
const responseBody = await result.json();
errorMessage = responseBody.error;
} catch (e) {}
res.status(400).json({ message: errorMessage });
return;
}
const responseBody = await result.json();
if (responseBody.error) {
res.status(400).json({ message: responseBody.error });
return;
}
responseBody.expiry_date = Math.round(Date.now() + responseBody.expires_in * 1000);
delete responseBody.expires_in;

View File

@ -244,7 +244,7 @@ const ZoomVideoApiAdapter = (credential: Credential): VideoApiAdapter => {
url: result.join_url,
});
}
return Promise.reject(new Error("Failed to create meeting"));
return Promise.reject(new Error("Failed to create meeting. Response is " + JSON.stringify(result)));
},
deleteMeeting: async (uid: string): Promise<void> => {
await fetchZoomApi(`meetings/${uid}`, {

View File

@ -4,6 +4,10 @@ import dayjs, { Dayjs } from "@calcom/dayjs";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { serverConfig } from "@calcom/lib/serverConfig";
declare let global: {
E2E_EMAILS?: Record<string, unknown>[];
};
export default class BaseEmail {
name = "";
@ -23,7 +27,12 @@ export default class BaseEmail {
return {};
}
public sendEmail() {
if (process.env.NEXT_PUBLIC_IS_E2E) return new Promise((r) => r("Skipped sendEmail for E2E"));
if (process.env.NEXT_PUBLIC_IS_E2E) {
global.E2E_EMAILS = global.E2E_EMAILS || [];
global.E2E_EMAILS.push(this.getNodeMailerPayload());
console.log("Skipped Sending Email");
return new Promise((r) => r("Skipped sendEmail for E2E"));
}
new Promise((resolve, reject) =>
nodemailer
.createTransport(this.getMailerOptions().transport)

View File

@ -63,7 +63,7 @@ export default function ConfirmationDialogContent(props: PropsWithChildren<Confi
<div className="mt-5 flex flex-row-reverse gap-x-2 sm:mt-8">
<DialogClose disabled={isLoading} onClick={onConfirm} asChild>
{confirmBtn || (
<Button color="primary" loading={isLoading}>
<Button data-testid="confirm-button" color="primary" loading={isLoading}>
{isLoading ? loadingText : confirmBtnText}
</Button>
)}

View File

@ -28,6 +28,7 @@ export async function loginAsUser(username: string, browser: Browser) {
async function globalSetup(/* config: FullConfig */) {
loadEnvConfig(process.env.PWD);
const browser = await chromium.launch();
await loginAsUser("onboarding", browser);
// await loginAsUser("free-first-hidden", browser);
await loginAsUser("pro", browser);

View File

@ -137,6 +137,10 @@
"cache": false,
"dependsOn": ["@calcom/prisma#db-seed", "@calcom/web#build"]
},
"test-e2e-integrations": {
"cache": false,
"dependsOn": ["@calcom/prisma#db-seed", "@calcom/web#build"]
},
"type-check": {
"cache": false,
"outputs": []

159
yarn.lock
View File

@ -3213,6 +3213,26 @@
call-me-maybe "^1.0.1"
glob-to-regexp "^0.3.0"
"@mswjs/cookies@^0.2.0":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@mswjs/cookies/-/cookies-0.2.1.tgz#66a283b45970ffc5350d22657983a1377ea6bf97"
integrity sha512-0tDfcPw5/s7QsNQqS3knAvAD5w5PF1nNPagRhKO/yECY+sMbJxoC2sLWnH7Lzmh52mTSVLKDhd1r92Q3kfljnQ==
dependencies:
"@types/set-cookie-parser" "^2.4.0"
set-cookie-parser "^2.4.6"
"@mswjs/interceptors@^0.16.3":
version "0.16.6"
resolved "https://registry.yarnpkg.com/@mswjs/interceptors/-/interceptors-0.16.6.tgz#c1a777ed3f69b55bbbc725b2deb827f160c0107c"
integrity sha512-7ax1sRx5s4ZWl0KvVhhcPOUoPbCCkVh8M8hYaqOyvoAQOiqLVzy+Z6Mh2ywPhYw4zudr5Mo/E8UT/zJBO/Wxrw==
dependencies:
"@open-draft/until" "^1.0.3"
"@xmldom/xmldom" "^0.7.5"
debug "^4.3.3"
headers-polyfill "^3.0.4"
outvariant "^1.2.1"
strict-event-emitter "^0.2.4"
"@next-auth/prisma-adapter@^1.0.4":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@next-auth/prisma-adapter/-/prisma-adapter-1.0.4.tgz#3e7304ac0615b8bfe425c81f96c40b40cafb59f0"
@ -3369,6 +3389,11 @@
mkdirp "^1.0.4"
rimraf "^3.0.2"
"@open-draft/until@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca"
integrity sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==
"@otplib/core@^12.0.1":
version "12.0.1"
resolved "https://registry.yarnpkg.com/@otplib/core/-/core-12.0.1.tgz#73720a8cedce211fe5b3f683cd5a9c098eaf0f8d"
@ -5919,6 +5944,11 @@
dependencies:
"@types/node" "*"
"@types/cookie@^0.4.1":
version "0.4.1"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d"
integrity sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==
"@types/cross-spawn@6.0.2":
version "6.0.2"
resolved "https://registry.yarnpkg.com/@types/cross-spawn/-/cross-spawn-6.0.2.tgz#168309de311cd30a2b8ae720de6475c2fbf33ac7"
@ -5938,6 +5968,11 @@
dependencies:
"@types/ms" "*"
"@types/detect-port@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/detect-port/-/detect-port-1.3.2.tgz#8c06a975e472803b931ee73740aeebd0a2eb27ae"
integrity sha512-xxgAGA2SAU4111QefXPSp5eGbDm/hW6zhvYl9IeEPZEry9F4d66QAHm5qpUXjb6IsevZV/7emAEx5MhP6O192g==
"@types/eslint-scope@^3.7.3":
version "3.7.4"
resolved "https://registry.yarnpkg.com/@types/eslint-scope/-/eslint-scope-3.7.4.tgz#37fc1223f0786c39627068a12e94d6e6fc61de16"
@ -6121,6 +6156,11 @@
expect "^28.0.0"
pretty-format "^28.0.0"
"@types/js-levenshtein@^1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5"
integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g==
"@types/js-yaml@^4.0.0":
version "4.0.5"
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.5.tgz#738dd390a6ecc5442f35e7f03fa1431353f7e138"
@ -6433,6 +6473,13 @@
"@types/mime" "^1"
"@types/node" "*"
"@types/set-cookie-parser@^2.4.0":
version "2.4.2"
resolved "https://registry.yarnpkg.com/@types/set-cookie-parser/-/set-cookie-parser-2.4.2.tgz#b6a955219b54151bfebd4521170723df5e13caad"
integrity sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==
dependencies:
"@types/node" "*"
"@types/source-list-map@*":
version "0.1.2"
resolved "https://registry.yarnpkg.com/@types/source-list-map/-/source-list-map-0.1.2.tgz#0078836063ffaf17412349bba364087e0ac02ec9"
@ -7009,7 +7056,7 @@
react-date-picker "^8.4.0"
react-fit "^1.4.0"
"@xmldom/xmldom@0.7.5", "@xmldom/xmldom@^0.7.0":
"@xmldom/xmldom@0.7.5", "@xmldom/xmldom@^0.7.0", "@xmldom/xmldom@^0.7.5":
version "0.7.5"
resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.5.tgz#09fa51e356d07d0be200642b0e4f91d8e6dd408d"
integrity sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==
@ -8753,6 +8800,14 @@ chalk@2.3.0:
escape-string-regexp "^1.0.5"
supports-color "^4.0.0"
chalk@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.1.tgz#c80b3fab28bf6371e6863325eee67e618b77e6ad"
integrity sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==
dependencies:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1, chalk@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
@ -9401,7 +9456,7 @@ cookie@0.4.1:
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
cookie@0.4.2, cookie@^0.4.1:
cookie@0.4.2, cookie@^0.4.1, cookie@^0.4.2:
version "0.4.2"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.2.tgz#0e41f24de5ecf317947c82fc789e06a884824432"
integrity sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==
@ -9830,7 +9885,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9:
dependencies:
ms "2.0.0"
debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@ -11400,7 +11455,7 @@ eventemitter3@^4.0.4:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f"
integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==
events@^3.0.0, events@^3.1.0, events@^3.2.0:
events@^3.0.0, events@^3.1.0, events@^3.2.0, events@^3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400"
integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==
@ -12827,6 +12882,11 @@ grapheme-splitter@^1.0.4:
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
graphql@^16.3.0:
version "16.5.0"
resolved "https://registry.yarnpkg.com/graphql/-/graphql-16.5.0.tgz#41b5c1182eaac7f3d47164fb247f61e4dfb69c85"
integrity sha512-qbHgh8Ix+j/qY+a/ZcJnFQ+j8ezakqPiHwPiZhV/3PgGlgf96QMBB5/f2rkiC9sgLoy/xvT6TSiaf2nTHJh5iA==
gray-matter@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/gray-matter/-/gray-matter-4.0.3.tgz#e893c064825de73ea1f5f7d88c7a9f7274288798"
@ -13142,6 +13202,11 @@ he@^1.2.0:
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
headers-polyfill@^3.0.4:
version "3.0.7"
resolved "https://registry.yarnpkg.com/headers-polyfill/-/headers-polyfill-3.0.7.tgz#725c4f591e6748f46b036197eae102c92b959ff4"
integrity sha512-JoLCAdCEab58+2/yEmSnOlficyHFpIl0XJqwu3l+Unkm1gXpFUYsThz6Yha3D6tNhocWkCPfyW0YVIGWFqTi7w==
highlight.js@^10.4.1, highlight.js@^10.7.1, highlight.js@~10.7.0:
version "10.7.3"
resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.7.3.tgz#697272e3991356e40c3cac566a74eef681756531"
@ -13671,6 +13736,27 @@ inquirer@8.2.1:
strip-ansi "^6.0.0"
through "^2.3.6"
inquirer@^8.2.0:
version "8.2.4"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.4.tgz#ddbfe86ca2f67649a67daa6f1051c128f684f0b4"
integrity sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==
dependencies:
ansi-escapes "^4.2.1"
chalk "^4.1.1"
cli-cursor "^3.1.0"
cli-width "^3.0.0"
external-editor "^3.0.3"
figures "^3.0.0"
lodash "^4.17.21"
mute-stream "0.0.8"
ora "^5.4.1"
run-async "^2.4.0"
rxjs "^7.5.5"
string-width "^4.1.0"
strip-ansi "^6.0.0"
through "^2.3.6"
wrap-ansi "^7.0.0"
internal-slot@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
@ -14045,6 +14131,11 @@ is-negative-zero@^2.0.2:
resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==
is-node-process@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-node-process/-/is-node-process-1.0.1.tgz#4fc7ac3a91e8aac58175fe0578abbc56f2831b23"
integrity sha512-5IcdXuf++TTNt3oGl9EBdkvndXA8gmc4bz/Y+mdEpWh3Mcn/+kOw6hI7LD5CocqJWMzeb0I0ClndRVNdEPuJXQ==
is-number-object@^1.0.4:
version "1.0.7"
resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc"
@ -14863,6 +14954,11 @@ js-file-download@^0.4.12:
resolved "https://registry.yarnpkg.com/js-file-download/-/js-file-download-0.4.12.tgz#10c70ef362559a5b23cdbdc3bd6f399c3d91d821"
integrity sha512-rML+NkoD08p5Dllpjo0ffy4jRHeY6Zsapvr/W86N7E0yuzAO6qa5X9+xog6zQNlH102J7IXljNY2FtS6Lj3ucg==
js-levenshtein@^1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
js-sha3@0.8.0, js-sha3@^0.8.0:
version "0.8.0"
resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.8.0.tgz#b9b7a5da73afad7dedd0f8c463954cbde6818840"
@ -16905,6 +17001,32 @@ ms@2.1.3, ms@^2.1.1, ms@^2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
msw@^0.42.3:
version "0.42.3"
resolved "https://registry.yarnpkg.com/msw/-/msw-0.42.3.tgz#150c475e2cb6d53c67503bd0e3f6251bfd075328"
integrity sha512-zrKBIGCDsNUCZLd3DLSeUtRruZ0riwJgORg9/bSDw3D0PTI8XUGAK3nC0LJA9g0rChGuKaWK/SwObA8wpFrz4g==
dependencies:
"@mswjs/cookies" "^0.2.0"
"@mswjs/interceptors" "^0.16.3"
"@open-draft/until" "^1.0.3"
"@types/cookie" "^0.4.1"
"@types/js-levenshtein" "^1.1.1"
chalk "4.1.1"
chokidar "^3.4.2"
cookie "^0.4.2"
graphql "^16.3.0"
headers-polyfill "^3.0.4"
inquirer "^8.2.0"
is-node-process "^1.0.1"
js-levenshtein "^1.1.6"
node-fetch "^2.6.7"
outvariant "^1.3.0"
path-to-regexp "^6.2.0"
statuses "^2.0.0"
strict-event-emitter "^0.2.0"
type-fest "^1.2.2"
yargs "^17.3.1"
multibase@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/multibase/-/multibase-0.7.0.tgz#1adfc1c50abe05eefeb5091ac0c2728d6b84581b"
@ -17796,6 +17918,11 @@ otplib@^12.0.1:
"@otplib/preset-default" "^12.0.1"
"@otplib/preset-v11" "^12.0.1"
outvariant@^1.2.1, outvariant@^1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/outvariant/-/outvariant-1.3.0.tgz#c39723b1d2cba729c930b74bf962317a81b9b1c9"
integrity sha512-yeWM9k6UPfG/nzxdaPlJkB2p08hCg4xP6Lx99F+vP8YF7xyZVfTmJjrrNalkmzudD4WFvNLVudQikqUmF8zhVQ==
p-all@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/p-all/-/p-all-2.1.0.tgz#91419be56b7dee8fe4c5db875d55e0da084244a0"
@ -18217,6 +18344,11 @@ path-to-regexp@0.1.7:
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
path-to-regexp@^6.2.0:
version "6.2.1"
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.2.1.tgz#d54934d6798eb9e5ef14e7af7962c945906918e5"
integrity sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==
path-type@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
@ -20664,6 +20796,11 @@ set-blocking@^2.0.0, set-blocking@~2.0.0:
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc=
set-cookie-parser@^2.4.6:
version "2.5.0"
resolved "https://registry.yarnpkg.com/set-cookie-parser/-/set-cookie-parser-2.5.0.tgz#96b59525e1362c94335c3c761100bb6e8f2da4b0"
integrity sha512-cHMAtSXilfyBePduZEBVPTCftTQWz6ehWJD5YNUg4mqvRosrrjKbo4WS8JkB0/RxonMoohHm7cOGH60mDkRQ9w==
set-value@^2.0.0, set-value@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/set-value/-/set-value-2.0.1.tgz#a18d40530e6f07de4228c7defe4227af8cad005b"
@ -21113,7 +21250,7 @@ static-extend@^0.1.1:
define-property "^0.2.5"
object-copy "^0.1.0"
statuses@2.0.1:
statuses@2.0.1, statuses@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
@ -21189,6 +21326,13 @@ stream-shift@^1.0.0:
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
strict-event-emitter@^0.2.0, strict-event-emitter@^0.2.4:
version "0.2.4"
resolved "https://registry.yarnpkg.com/strict-event-emitter/-/strict-event-emitter-0.2.4.tgz#365714f0c95f059db31064ca745d5b33e5b30f6e"
integrity sha512-xIqTLS5azUH1djSUsLH9DbP6UnM/nI18vu8d43JigCQEoVsnY+mrlE+qv6kYqs6/1OkMnMIiL6ffedQSZStuoQ==
dependencies:
events "^3.3.0"
strict-uri-encode@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713"
@ -22496,6 +22640,11 @@ type-fest@^0.8.1:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
type-fest@^1.2.2:
version "1.4.0"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-1.4.0.tgz#e9fb813fe3bf1744ec359d55d1affefa76f14be1"
integrity sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==
type-flag@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/type-flag/-/type-flag-2.2.0.tgz#56ed3a79a3011bafba3ceb9d1a5acb633ade7884"