Skip to content
This repository has been archived by the owner on Jan 23, 2024. It is now read-only.

Commit

Permalink
test: Add more orgs tests (calcom#12241)
Browse files Browse the repository at this point in the history
  • Loading branch information
hariombalhara authored Nov 21, 2023
1 parent 556b382 commit 48dde24
Show file tree
Hide file tree
Showing 15 changed files with 397 additions and 35 deletions.
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("@");
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) {
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 @@ -86,12 +86,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 @@ -103,7 +105,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 @@ -145,6 +154,7 @@ export const createUsersFixture = (page: Page, emails: API | undefined, workerIn
teamEventSlug?: string;
teamEventLength?: number;
isOrg?: boolean;
isOrgVerified?: boolean;
hasSubteam?: true;
isUnpublished?: true;
} = {}
Expand Down Expand Up @@ -292,9 +302,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 @@ -410,6 +421,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 @@ -278,3 +280,12 @@ export async function createUserWithSeatedEventAndAttendees(
});
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);
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

0 comments on commit 48dde24

Please sign in to comment.