Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: Add more orgs tests #12241

Merged
merged 1 commit into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/web/pages/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ type SignupProps = inferSSRProps<typeof getServerSideProps>;
const checkValidEmail = (email: string) => z.string().email().safeParse(email).success;

const getOrgUsernameFromEmail = (email: string, autoAcceptEmailDomain: string) => {
const [emailUser, emailDomain] = email.split("@");
const [emailUser, emailDomain = ""] = email.split("@");
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue arises when form is submitted without entering the email or email doesn't have @ in it when it was submitted.

const username =
emailDomain === autoAcceptEmailDomain
? slugify(emailUser)
Expand Down Expand Up @@ -143,7 +143,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug, orgAutoA
methods.clearErrors("apiError");
}

if (methods.getValues().username === undefined && isOrgInviteByLink && orgAutoAcceptEmail) {
if (!methods.getValues().username && isOrgInviteByLink && orgAutoAcceptEmail) {
Copy link
Member Author

@hariombalhara hariombalhara Nov 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

empty username isn't valid so, let's compute username from email in that case as well.

methods.setValue(
"username",
getOrgUsernameFromEmail(methods.getValues().email, orgAutoAcceptEmail)
Expand Down
34 changes: 34 additions & 0 deletions apps/web/playwright/fixtures/clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { Page } from "@playwright/test";

declare global {
interface Window {
E2E_CLIPBOARD_VALUE?: string;
}
}

export type Window = typeof window;
// creates the single server fixture
export const createClipboardFixture = (page: Page) => {
return {
reset: async () => {
await page.evaluate(() => {
delete window.E2E_CLIPBOARD_VALUE;
});
},
get: async () => {
return getClipboardValue({ page });
},
};
};

function getClipboardValue({ page }: { page: Page }) {
return page.evaluate(() => {
return new Promise<string>((resolve, reject) => {
setInterval(() => {
if (!window.E2E_CLIPBOARD_VALUE) return;
resolve(window.E2E_CLIPBOARD_VALUE);
}, 500);
setTimeout(() => reject(new Error("Timeout")), 1000);
});
});
}
20 changes: 17 additions & 3 deletions apps/web/playwright/fixtures/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,12 +78,14 @@ const createTeamAndAddUser = async (
user,
isUnpublished,
isOrg,
isOrgVerified,
hasSubteam,
organizationId,
}: {
user: { id: number; username: string | null; role?: MembershipRole };
user: { id: number; email: string; username: string | null; role?: MembershipRole };
isUnpublished?: boolean;
isOrg?: boolean;
isOrgVerified?: boolean;
hasSubteam?: true;
organizationId?: number | null;
},
Expand All @@ -95,7 +97,14 @@ const createTeamAndAddUser = async (
};
data.metadata = {
...(isUnpublished ? { requestedSlug: slug } : {}),
...(isOrg ? { isOrganization: true } : {}),
...(isOrg
? {
isOrganization: true,
isOrganizationVerified: !!isOrgVerified,
orgAutoAcceptEmail: user.email.split("@")[1],
isOrganizationConfigured: false,
}
: {}),
};
data.slug = !isUnpublished ? slug : undefined;
if (isOrg && hasSubteam) {
Expand Down Expand Up @@ -136,6 +145,7 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
teamEventTitle?: string;
teamEventSlug?: string;
isOrg?: boolean;
isOrgVerified?: boolean;
hasSubteam?: true;
isUnpublished?: true;
} = {}
Expand Down Expand Up @@ -283,9 +293,10 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
if (scenario.hasTeam) {
const team = await createTeamAndAddUser(
{
user: { id: user.id, username: user.username, role: "OWNER" },
user: { id: user.id, email: user.email, username: user.username, role: "OWNER" },
isUnpublished: scenario.isUnpublished,
isOrg: scenario.isOrg,
isOrgVerified: scenario.isOrgVerified,
hasSubteam: scenario.hasSubteam,
organizationId: opts?.organizationId,
},
Expand Down Expand Up @@ -401,6 +412,9 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
routingForms: user.routingForms,
self,
apiLogin: async () => apiLogin({ ...(await self()), password: user.username }, store.page),
/**
* @deprecated use apiLogin instead
*/
login: async () => login({ ...(await self()), password: user.username }, store.page),
logout: async () => {
await page.goto("/auth/logout");
Expand Down
9 changes: 9 additions & 0 deletions apps/web/playwright/lib/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import type { API } from "mailhog";
import mailhog from "mailhog";

import { IS_MAILHOG_ENABLED } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import prisma from "@calcom/prisma";

import type { ExpectedUrlDetails } from "../../../../playwright.config";
import { createBookingsFixture } from "../fixtures/bookings";
import { createClipboardFixture } from "../fixtures/clipboard";
import { createEmbedsFixture } from "../fixtures/embeds";
import { createOrgsFixture } from "../fixtures/orgs";
import { createPaymentsFixture } from "../fixtures/payments";
Expand All @@ -28,6 +30,7 @@ export interface Fixtures {
emails?: API;
routingForms: ReturnType<typeof createRoutingFormsFixture>;
bookingPage: ReturnType<typeof createBookingPageFixture>;
clipboard: ReturnType<typeof createClipboardFixture>;
}

declare global {
Expand Down Expand Up @@ -85,11 +88,17 @@ export const test = base.extend<Fixtures>({
const mailhogAPI = mailhog();
await use(mailhogAPI);
} else {
//FIXME: Ideally we should error out here. If someone is running tests with mailhog disabled, they should be aware of it
logger.warn("Mailhog is not enabled - Skipping Emails verification");
await use(undefined);
}
},
bookingPage: async ({ page }, use) => {
const bookingPage = createBookingPageFixture(page);
await use(bookingPage);
},
clipboard: async ({ page }, use) => {
const clipboard = createClipboardFixture(page);
await use(clipboard);
},
});
11 changes: 11 additions & 0 deletions apps/web/playwright/lib/testUtils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { Frame, Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { createHash } from "crypto";
import EventEmitter from "events";
import type { IncomingMessage, ServerResponse } from "http";
import { createServer } from "http";
// eslint-disable-next-line no-restricted-imports
import { noop } from "lodash";
import type { API, Messages } from "mailhog";
import { totp } from "otplib";

import type { Prisma } from "@calcom/prisma/client";
import { BookingStatus } from "@calcom/prisma/enums";
Expand Down Expand Up @@ -188,7 +190,7 @@
previewLink = `/forms/${formId}`;
}

await page.goto(`${previewLink}${queryString ? `?${queryString}` : ""}`);

Check failure on line 193 in apps/web/playwright/lib/testUtils.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (5/5)

[@calcom/web] › apps/web/playwright/webhook.e2e.ts:612:7 › FORM_SUBMITTED › on submitting team form

1) [@***com/web] › apps/web/playwright/webhook.e2e.ts:612:7 › FORM_SUBMITTED › on submitting team form, triggers team webhook page.goto: net::ERR_ABORTED at http://***:3000/forms/clozh29qs0001jxh6a4sambxp =========================== logs =========================== navigating to "http://***:3000/forms/clozh29qs0001jxh6a4sambxp", waiting until "load" ============================================================ at apps/web/playwright/lib/testUtils.ts:193 191 | } 192 | > 193 | await page.goto(`${previewLink}${queryString ? `?${queryString}` : ""}`); | ^ 194 | 195 | // HACK: There seems to be some issue with the inputs to the form getting reset if we don't wait. 196 | await new Promise((resolve) => setTimeout(resolve, 1000)); at gotoRoutingLink (/home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/lib/testUtils.ts:193:14) at /home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/webhook.e2e.ts:641:26

// HACK: There seems to be some issue with the inputs to the form getting reset if we don't wait.
await new Promise((resolve) => setTimeout(resolve, 1000));
Expand Down Expand Up @@ -278,3 +280,12 @@
});
return { user, eventType, booking };
}

export function generateTotpCode(email: string) {
const secret = createHash("md5")
.update(email + process.env.CALENDSO_ENCRYPTION_KEY)
.digest("hex");

totp.options = { step: 90 };
return totp.generate(secret);
}
28 changes: 28 additions & 0 deletions apps/web/playwright/organization/expects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { JSDOM } from "jsdom";
// eslint-disable-next-line no-restricted-imports
import type { API, Messages } from "mailhog";

import { getEmailsReceivedByUser } from "../lib/testUtils";

export async function expectInvitationEmailToBeReceived(
page: Page,
emails: API | undefined,
userEmail: string,
subject: string,
returnLink?: string
) {
if (!emails) return null;
// We need to wait for the email to go through, otherwise it will fail
// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(5000);
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });
expect(receivedEmails?.total).toBe(1);

Check failure on line 21 in apps/web/playwright/organization/expects.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (4/5)

[@calcom/web] › apps/web/playwright/organization/organization-creation.e2e.ts:21:7 › Organization › should be able to create an organization and complete onboarding

1) [@***com/web] › apps/web/playwright/organization/organization-creation.e2e.ts:21:7 › Organization › should be able to create an organization and complete onboarding Error: expect(received).toBe(expected) // Object.is equality Expected: 1 Received: 0 at apps/web/playwright/organization/expects.ts:21 19 | await page.waitForTimeout(5000); 20 | const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail }); > 21 | expect(receivedEmails?.total).toBe(1); | ^ 22 | const [firstReceivedEmail] = (receivedEmails as Messages).items; 23 | expect(firstReceivedEmail.subject).toBe(subject); 24 | if (!returnLink) return; at expectInvitationEmailToBeReceived (/home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/expects.ts:21:33) at /home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-creation.e2e.ts:111:7 at /home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-creation.e2e.ts:102:5

Check failure on line 21 in apps/web/playwright/organization/expects.ts

View workflow job for this annotation

GitHub Actions / E2E tests / E2E tests (4/5)

[@calcom/web] › apps/web/playwright/organization/organization-creation.e2e.ts:21:7 › Organization › should be able to create an organization and complete onboarding

1) [@***com/web] › apps/web/playwright/organization/organization-creation.e2e.ts:21:7 › Organization › should be able to create an organization and complete onboarding Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: expect(received).toBe(expected) // Object.is equality Expected: 1 Received: 0 at apps/web/playwright/organization/expects.ts:21 19 | await page.waitForTimeout(5000); 20 | const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail }); > 21 | expect(receivedEmails?.total).toBe(1); | ^ 22 | const [firstReceivedEmail] = (receivedEmails as Messages).items; 23 | expect(firstReceivedEmail.subject).toBe(subject); 24 | if (!returnLink) return; at expectInvitationEmailToBeReceived (/home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/expects.ts:21:33) at /home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-creation.e2e.ts:46:7 at /home/runner/actions-runner/_work/***.com/***.com/apps/web/playwright/organization/organization-creation.e2e.ts:33:5
const [firstReceivedEmail] = (receivedEmails as Messages).items;
expect(firstReceivedEmail.subject).toBe(subject);
if (!returnLink) return;
const dom = new JSDOM(firstReceivedEmail.html);
const anchor = dom.window.document.querySelector(`a[href*="${returnLink}"]`);
return anchor?.getAttribute("href");
}
143 changes: 143 additions & 0 deletions apps/web/playwright/organization/organization-creation.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { expect } from "@playwright/test";
import path from "path";

import { test } from "../lib/fixtures";
import { generateTotpCode } from "../lib/testUtils";
import { expectInvitationEmailToBeReceived } from "./expects";

test.afterAll(({ users, emails }) => {
users.deleteAll();
emails?.deleteAll();
});

function capitalize(text: string) {
if (!text) {
return text;
}
return text.charAt(0).toUpperCase() + text.slice(1);
}

test.describe("Organization", () => {
test("should be able to create an organization and complete onboarding", async ({
page,
users,
emails,
}) => {
const orgOwner = await users.create();
const orgDomain = `${orgOwner.username}-org`;
const orgName = capitalize(`${orgOwner.username}-org`);
await orgOwner.apiLogin();
await page.goto("/settings/organizations/new");
await page.waitForLoadState("networkidle");

await test.step("Basic info", async () => {
// Check required fields
await page.locator("button[type=submit]").click();
await expect(page.locator(".text-red-700")).toHaveCount(3);

// Happy path
await page.locator("input[name=adminEmail]").fill(`john@${orgDomain}.com`);
expect(await page.locator("input[name=name]").inputValue()).toEqual(orgName);
expect(await page.locator("input[name=slug]").inputValue()).toEqual(orgDomain);
await page.locator("button[type=submit]").click();
await page.waitForLoadState("networkidle");

// Check admin email about code verification
await expectInvitationEmailToBeReceived(
page,
emails,
`john@${orgOwner.username}-org.com`,
"Verify your email to create an organization"
);

await test.step("Verification", async () => {
// Code verification
await expect(page.locator("#modal-title")).toBeVisible();
await page.locator("input[name='2fa1']").fill(generateTotpCode(`john@${orgDomain}.com`));
await page.locator("button:text('Verify')").click();

// Check admin email about DNS pending action
await expectInvitationEmailToBeReceived(
page,
emails,
"[email protected]",
"New organization created: pending action"
);

// Waiting to be in next step URL
await page.waitForURL("/settings/organizations/*/set-password");
});
});

await test.step("Admin password", async () => {
// Check required fields
await page.locator("button[type=submit]").click();
await expect(page.locator(".text-red-700")).toHaveCount(3); // 3 password hints

// Happy path
await page.locator("input[name='password']").fill("ADMIN_user2023$");
await page.locator("button[type=submit]").click();

// Waiting to be in next step URL
await page.waitForURL("/settings/organizations/*/about");
});

await test.step("About the organization", async () => {
// Choosing an avatar
await page.locator('button:text("Upload")').click();
const fileChooserPromise = page.waitForEvent("filechooser");
await page.getByText("Choose a file...").click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, "../../public/apple-touch-icon.png"));
await page.locator('button:text("Save")').click();

// About text
await page.locator('textarea[name="about"]').fill("This is a testing org");
await page.locator("button[type=submit]").click();

// Waiting to be in next step URL
await page.waitForURL("/settings/organizations/*/onboard-admins");
});

await test.step("On-board administrators", async () => {
// Required field
await page.locator("button[type=submit]").click();

// Happy path
await page.locator('textarea[name="emails"]').fill(`rick@${orgDomain}.com`);
await page.locator("button[type=submit]").click();

// Check if invited admin received the invitation email
await expectInvitationEmailToBeReceived(
page,
emails,
`rick@${orgDomain}.com`,
`${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
);

// Waiting to be in next step URL
await page.waitForURL("/settings/organizations/*/add-teams");
});

await test.step("Create teams", async () => {
// Initial state
await expect(page.locator('input[name="teams.0.name"]')).toHaveCount(1);
await expect(page.locator('button:text("Continue")')).toBeDisabled();

// Filling one team
await page.locator('input[name="teams.0.name"]').fill("Marketing");
await expect(page.locator('button:text("Continue")')).toBeEnabled();

// Adding another team
await page.locator('button:text("Add a team")').click();
await expect(page.locator('button:text("Continue")')).toBeDisabled();
await expect(page.locator('input[name="teams.1.name"]')).toHaveCount(1);
await page.locator('input[name="teams.1.name"]').fill("Sales");
await expect(page.locator('button:text("Continue")')).toBeEnabled();

// Finishing the creation wizard
await page.locator('button:text("Continue")').click();
await page.waitForURL("/event-types");
});
});
});
Loading
Loading