diff --git a/.github/workflows/integrations-third-party.yml b/.github/workflows/integrations-third-party.yml new file mode 100644 index 0000000000..2a7876deda --- /dev/null +++ b/.github/workflows/integrations-third-party.yml @@ -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 \ No newline at end of file diff --git a/apps/web/package.json b/apps/web/package.json index f804fd4add..18efc8d9a8 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", diff --git a/apps/web/pages/apps/installed.tsx b/apps/web/pages/apps/installed.tsx index 3ffc1a66a9..cc173a08a1 100644 --- a/apps/web/pages/apps/installed.tsx +++ b/apps/web/pages/apps/installed.tsx @@ -45,7 +45,7 @@ function ConnectOrDisconnectIntegrationButton(props: { ( - )} @@ -57,7 +57,7 @@ function ConnectOrDisconnectIntegrationButton(props: { ( - )} @@ -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 ( ; + bookings: ReturnType; + payments: ReturnType; + server: Server; + requestInterceptor: SetupServerApi; + rest: typeof rest; +} + +/** + * @see https://playwright.dev/docs/test-fixtures + */ +export const test = base.extend({ + 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, + }, + ], +}); diff --git a/apps/web/playwright-integrations/next-server.ts b/apps/web/playwright-integrations/next-server.ts new file mode 100644 index 0000000000..34c57969a4 --- /dev/null +++ b/apps/web/playwright-integrations/next-server.ts @@ -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; +}; diff --git a/apps/web/playwright-integrations/tests/integrations.test.ts b/apps/web/playwright-integrations/tests/integrations.test.ts new file mode 100644 index 0000000000..389ca569f1 --- /dev/null +++ b/apps/web/playwright-integrations/tests/integrations.test.ts @@ -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)[]; +}; + +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[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(); + }); +}); diff --git a/apps/web/playwright/integrations.test.ts-snapshots/webhookResponse-chromium.txt b/apps/web/playwright-integrations/tests/integrations.test.ts-snapshots/webhookResponse-chromium.txt similarity index 100% rename from apps/web/playwright/integrations.test.ts-snapshots/webhookResponse-chromium.txt rename to apps/web/playwright-integrations/tests/integrations.test.ts-snapshots/webhookResponse-chromium.txt diff --git a/apps/web/playwright/fixtures/users.ts b/apps/web/playwright/fixtures/users.ts index 074bb94ce6..b636f4ee7d 100644 --- a/apps/web/playwright/fixtures/users.ts +++ b/apps/web/playwright/fixtures/users.ts @@ -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; const userIncludes = PrismaType.validator()({ @@ -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(); diff --git a/apps/web/playwright/integrations-stripe.test.ts b/apps/web/playwright/integrations-stripe.test.ts index d6558170fd..678b53b523 100644 --- a/apps/web/playwright/integrations-stripe.test.ts +++ b/apps/web/playwright/integrations-stripe.test.ts @@ -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 diff --git a/apps/web/playwright/integrations.test.ts b/apps/web/playwright/integrations.test.ts deleted file mode 100644 index cf641886d7..0000000000 --- a/apps/web/playwright/integrations.test.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/package.json b/package.json index 974084fd27..24c0a4d50a 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/app-store/zoomvideo/api/callback.ts b/packages/app-store/zoomvideo/api/callback.ts index fbe7b6fd2a..7d874a7f83 100644 --- a/packages/app-store/zoomvideo/api/callback.ts +++ b/packages/app-store/zoomvideo/api/callback.ts @@ -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; diff --git a/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts b/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts index f5d6539455..fe5971cf58 100644 --- a/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts +++ b/packages/app-store/zoomvideo/lib/VideoApiAdapter.ts @@ -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 => { await fetchZoomApi(`meetings/${uid}`, { diff --git a/packages/emails/templates/_base-email.ts b/packages/emails/templates/_base-email.ts index e9e79d1045..78f72a6f9b 100644 --- a/packages/emails/templates/_base-email.ts +++ b/packages/emails/templates/_base-email.ts @@ -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[]; +}; + 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) diff --git a/packages/ui/ConfirmationDialogContent.tsx b/packages/ui/ConfirmationDialogContent.tsx index 57d7d699da..1acbbd3324 100644 --- a/packages/ui/ConfirmationDialogContent.tsx +++ b/packages/ui/ConfirmationDialogContent.tsx @@ -63,7 +63,7 @@ export default function ConfirmationDialogContent(props: PropsWithChildren {confirmBtn || ( - )} diff --git a/tests/config/globalSetup.ts b/tests/config/globalSetup.ts index ab069d7504..63fd56d4bd 100644 --- a/tests/config/globalSetup.ts +++ b/tests/config/globalSetup.ts @@ -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); diff --git a/turbo.json b/turbo.json index 4d6dbd1358..3e63780668 100644 --- a/turbo.json +++ b/turbo.json @@ -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": [] diff --git a/yarn.lock b/yarn.lock index 7bf243f2d8..92f6aee758 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"