Skip to content

Commit

Permalink
fix: Support adding a non-existent email as the owner of the organiza…
Browse files Browse the repository at this point in the history
…tion (#14569)

* fix: Support adding an non-existent email as the owner of the organization

* Remove dead code

* fixes

* add test

* self review changes

* Update packages/trpc/server/routers/viewer/organizations/create.handler.ts

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>

* Fix typo everywhere

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
  • Loading branch information
3 people committed May 6, 2024
1 parent 4f57e32 commit 3a0c766
Show file tree
Hide file tree
Showing 15 changed files with 556 additions and 212 deletions.
44 changes: 4 additions & 40 deletions apps/web/playwright/organization/booking.e2e.ts
Expand Up @@ -2,7 +2,6 @@ import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";

import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";

import { test } from "../lib/fixtures";
Expand All @@ -14,6 +13,7 @@ import {
testName,
} from "../lib/testUtils";
import { expectExistingUserToBeInvitedToOrganization } from "../team/expects";
import { gotoPathAndExpectRedirectToOrgDomain } from "./lib/gotoPathAndExpectRedirectToOrgDomain";
import { acceptTeamOrOrgInvite, inviteExistingUserToOrganization } from "./lib/inviteUser";

test.describe("Bookings", () => {
Expand Down Expand Up @@ -380,11 +380,11 @@ test.describe("Bookings", () => {

await test.step("Booking through old link redirects to new link on org domain", async () => {
const event = await userOutsideOrganization.getFirstEventAsOwner();
await expectRedirectToOrgDomain({
await gotoPathAndExpectRedirectToOrgDomain({
page,
org,
eventSlug: `/${usernameOutsideOrg}/${event.slug}`,
expectedEventSlug: `/${usernameInOrg}/${event.slug}`,
path: `/${usernameOutsideOrg}/${event.slug}`,
expectedPath: `/${usernameInOrg}/${event.slug}`,
});
// As the redirection correctly happens, the booking would work too which we have verified in previous step. But we can't test that with org domain as that domain doesn't exist.
});
Expand Down Expand Up @@ -463,39 +463,3 @@ async function expectPageToBeNotFound({ page, url }: { page: Page; url: string }
await page.goto(`${url}`);
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
}

async function expectRedirectToOrgDomain({
page,
org,
eventSlug,
expectedEventSlug,
}: {
page: Page;
org: { slug: string | null };
eventSlug: string;
expectedEventSlug: string;
}) {
if (!org.slug) {
throw new Error("Org slug is not defined");
}
page.goto(eventSlug).catch((e) => {
console.log("Expected navigation error to happen");
});

const orgSlug = org.slug;

const orgRedirectUrl = await new Promise(async (resolve) => {
page.on("request", (request) => {
if (request.isNavigationRequest()) {
const requestedUrl = request.url();
console.log("Requested navigation to", requestedUrl);
// Resolve on redirection to org domain
if (requestedUrl.includes(orgSlug)) {
resolve(requestedUrl);
}
}
});
});

expect(orgRedirectUrl).toContain(`${getOrgFullOrigin(org.slug)}${expectedEventSlug}`);
}
@@ -0,0 +1,40 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";

import { getOrgFullOrigin } from "@calcom/features/ee/organizations/lib/orgDomains";

export async function gotoPathAndExpectRedirectToOrgDomain({
page,
org,
path,
expectedPath,
}: {
page: Page;
org: { slug: string | null };
path: string;
expectedPath: string;
}) {
if (!org.slug) {
throw new Error("Org slug is not defined");
}
page.goto(path).catch((e) => {
console.log("Expected navigation error to happen");
});

const orgSlug = org.slug;

const orgRedirectUrl = await new Promise(async (resolve) => {
page.on("request", (request) => {
if (request.isNavigationRequest()) {
const requestedUrl = request.url();
console.log("Requested navigation to", requestedUrl);
// Resolve on redirection to org domain
if (requestedUrl.includes(orgSlug)) {
resolve(requestedUrl);
}
}
});
});

expect(orgRedirectUrl).toContain(`${getOrgFullOrigin(org.slug)}${expectedPath}`);
}
196 changes: 178 additions & 18 deletions apps/web/playwright/organization/organization-creation.e2e.ts
@@ -1,11 +1,94 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { JSDOM } from "jsdom";
import type { Messages } from "mailhog";
import path from "path";
import { uuid } from "short-uuid";

import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";

import type { createEmailsFixture } from "../fixtures/emails";
import { test } from "../lib/fixtures";
import { fillStripeTestCheckout } from "../lib/testUtils";
import { getEmailsReceivedByUser } from "../lib/testUtils";
import { gotoPathAndExpectRedirectToOrgDomain } from "./lib/gotoPathAndExpectRedirectToOrgDomain";

async function expectEmailWithSubject(
page: Page,
emails: ReturnType<typeof createEmailsFixture>,
userEmail: string,
subject: string
) {
if (!emails) return null;

// eslint-disable-next-line playwright/no-wait-for-timeout
await page.waitForTimeout(2000);
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail });

const allEmails = (receivedEmails as Messages).items;
const email = allEmails.find((email) => email.subject === subject);
if (!email) {
throw new Error(`Email with subject ${subject} not found`);
}
const dom = new JSDOM(email.html);
return dom;
}

export async function expectOrganizationCreationEmailToBeSent({
page,
emails,
userEmail,
orgSlug,
}: {
page: Page;
emails: ReturnType<typeof createEmailsFixture>;
userEmail: string;
orgSlug: string;
}) {
const dom = await expectEmailWithSubject(page, emails, userEmail, "Your organization has been created");
const document = dom?.window?.document;
expect(document?.querySelector(`[href*=${orgSlug}]`)).toBeTruthy();
return dom;
}

async function expectOrganizationCreationEmailToBeSentWithLinks({
page,
emails,
userEmail,
oldUsername,
newUsername,
orgSlug,
}: {
page: Page;
emails: ReturnType<typeof createEmailsFixture>;
userEmail: string;
oldUsername: string;
newUsername: string;
orgSlug: string;
}) {
const dom = await expectOrganizationCreationEmailToBeSent({
page,
emails,
userEmail,
orgSlug,
});
const document = dom?.window.document;
const links = document?.querySelectorAll(`[data-testid="organization-link-info"] [href]`);
if (!links) {
throw new Error(`data-testid="organization-link-info doesn't have links`);
}
expect((links[0] as unknown as HTMLAnchorElement).href).toContain(oldUsername);
expect((links[1] as unknown as HTMLAnchorElement).href).toContain(newUsername);
}

export async function expectEmailVerificationEmailToBeSent(
page: Page,
emails: ReturnType<typeof createEmailsFixture>,
userEmail: string
) {
const subject = "Cal.com: Verify your account";
return expectEmailWithSubject(page, emails, userEmail, subject);
}

test.afterAll(({ users, orgs }) => {
users.deleteAll();
Expand All @@ -20,26 +103,33 @@ function capitalize(text: string) {
}

test.describe("Organization", () => {
test("Admin should be able to create an org for a target user", async ({ page, users, emails }) => {
test("Admin should be able to create an org where an existing user is made an owner", async ({
page,
users,
emails,
}) => {
const appLevelAdmin = await users.create({
role: "ADMIN",
});
await appLevelAdmin.apiLogin();
const stringUUID = uuid();

const orgOwnerUsername = `owner-${stringUUID}`;
const orgOwnerUsernamePrefix = "owner";

const targetOrgEmail = users.trackEmail({
username: orgOwnerUsername,
const orgOwnerEmail = users.trackEmail({
username: orgOwnerUsernamePrefix,
domain: `example.com`,
});

const orgOwnerUser = await users.create({
username: orgOwnerUsername,
email: targetOrgEmail,
username: orgOwnerUsernamePrefix,
email: orgOwnerEmail,
role: "ADMIN",
});

const orgName = capitalize(`${orgOwnerUsername}`);
const orgOwnerUsernameOutsideOrg = orgOwnerUser.username;
const orgOwnerUsernameInOrg = orgOwnerEmail.split("@")[0];
const orgName = capitalize(`${orgOwnerUser.username}`);
const orgSlug = `myOrg-${uuid()}`.toLowerCase();
await page.goto("/settings/organizations/new");
await page.waitForLoadState("networkidle");

Expand All @@ -49,17 +139,16 @@ test.describe("Organization", () => {
await expect(page.locator(".text-red-700")).toHaveCount(3);

// Happy path
await page.locator("input[name=orgOwnerEmail]").fill(targetOrgEmail);
// Since we are admin fill in this infomation instead of deriving it
await page.locator("input[name=name]").fill(orgName);
await page.locator("input[name=slug]").fill(orgOwnerUsername);

// Fill in seat infomation
await page.locator("input[name=seats]").fill("30");
await page.locator("input[name=pricePerSeat]").fill("30");
await fillAndSubmitFirstStepAsAdmin(page, orgOwnerEmail, orgName, orgSlug);
});

await page.locator("button[type=submit]").click();
await page.waitForLoadState("networkidle");
await expectOrganizationCreationEmailToBeSentWithLinks({
page,
emails,
userEmail: orgOwnerEmail,
oldUsername: orgOwnerUsernameOutsideOrg || "",
newUsername: orgOwnerUsernameInOrg,
orgSlug,
});

await test.step("About the organization", async () => {
Expand Down Expand Up @@ -149,6 +238,56 @@ test.describe("Organization", () => {

await expect(upgradeButtonHidden).toBeHidden();
});

// Verify that the owner's old username redirect is properly set
await gotoPathAndExpectRedirectToOrgDomain({
page,
org: {
slug: orgSlug,
},
path: `/${orgOwnerUsernameOutsideOrg}`,
expectedPath: `/${orgOwnerUsernameInOrg}`,
});
});

test("Admin should be able to create an org where the owner doesn't exist yet", async ({
page,
users,
emails,
}) => {
const appLevelAdmin = await users.create({
role: "ADMIN",
});
await appLevelAdmin.apiLogin();
const orgOwnerUsername = `owner`;
const orgName = capitalize(`${orgOwnerUsername}`);
const orgSlug = `myOrg-${uuid()}`.toLowerCase();
const orgOwnerEmail = users.trackEmail({
username: orgOwnerUsername,
domain: `example.com`,
});

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 fillAndSubmitFirstStepAsAdmin(page, orgOwnerEmail, orgName, orgSlug);
});

const dom = await expectOrganizationCreationEmailToBeSent({
page,
emails,
userEmail: orgOwnerEmail,
orgSlug,
});
expect(dom?.window.document.querySelector(`[href*=${orgSlug}]`)).toBeTruthy();
await expectEmailVerificationEmailToBeSent(page, emails, orgOwnerEmail);
// Rest of the steps remain same as org creation with existing user as owner. So skipping them
});

test("User can create and upgrade a org", async ({ page, users, emails }) => {
Expand Down Expand Up @@ -426,3 +565,24 @@ test.describe("Organization", () => {
});
});
});

async function fillAndSubmitFirstStepAsAdmin(
page: Page,
targetOrgEmail: string,
orgName: string,
orgSlug: string
) {
await page.locator("input[name=orgOwnerEmail]").fill(targetOrgEmail);
// Since we are admin fill in this infomation instead of deriving it
await page.locator("input[name=name]").fill(orgName);
await page.locator("input[name=slug]").fill(orgSlug);

// Fill in seat infomation
await page.locator("input[name=seats]").fill("30");
await page.locator("input[name=pricePerSeat]").fill("30");

await Promise.all([
page.waitForResponse("**/api/trpc/organizations/create**"),
page.locator("button[type=submit]").click(),
]);
}

0 comments on commit 3a0c766

Please sign in to comment.