2
0

first commit

This commit is contained in:
2024-08-09 00:39:27 +02:00
commit 79688abe2e
5698 changed files with 497838 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
import { expect } from "@playwright/test";
import { WEBAPP_URL } from "@calcom/lib/constants";
import prisma from "@calcom/prisma";
import { test } from "../../lib/fixtures";
test.afterAll(({ users }) => {
users.deleteAll();
});
test.describe("user1NotMemberOfOrg1 is part of team1MemberOfOrg1", () => {
test("Team1 profile should show correct domain if logged in as User1", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user1NotMemberOfOrg1 = await users.create(undefined, {
hasTeam: true,
});
const { team: team1MemberOfOrg1 } = await user1NotMemberOfOrg1.getFirstTeamMembership();
await moveTeamToOrg({ team: team1MemberOfOrg1, org });
await user1NotMemberOfOrg1.apiLogin();
await page.goto(`/settings/teams/${team1MemberOfOrg1.id}/profile`);
const domain = await page.locator(".testid-leading-text-team-url").textContent();
expect(domain).toContain(org.slug);
});
test("EventTypes listing should show correct link for user events and team1MemberOfOrg1's events", async ({
page,
users,
orgs,
}) => {
const org = await orgs.create({
name: "TestOrg",
});
const user1NotMemberOfOrg1 = await users.create(undefined, {
hasTeam: true,
});
const { team: team1MemberOfOrg1 } = await user1NotMemberOfOrg1.getFirstTeamMembership();
await moveTeamToOrg({ team: team1MemberOfOrg1, org });
await user1NotMemberOfOrg1.apiLogin();
await page.goto("/event-types");
await page.waitForLoadState("networkidle");
const userEventLinksLocators = await page
.locator(`[data-testid=slug-${user1NotMemberOfOrg1.username}] [data-testid="preview-link-button"]`)
.all();
expect(userEventLinksLocators.length).toBeGreaterThan(0);
for (const userEventLinkLocator of userEventLinksLocators) {
const href = await userEventLinkLocator.getAttribute("href");
expect(href).toContain(WEBAPP_URL);
}
const teamEventLinksLocators = await page
.locator(`[data-testid=slug-${team1MemberOfOrg1.slug}] [data-testid="preview-link-button"]`)
.all();
expect(teamEventLinksLocators.length).toBeGreaterThan(0);
for (const teamEventLinksLocator of teamEventLinksLocators) {
const href = await teamEventLinksLocator.getAttribute("href");
expect(href).not.toContain(WEBAPP_URL);
expect(href).toContain(org.slug);
}
});
});
async function moveTeamToOrg({
team,
org,
}: {
team: {
id: number;
};
org: {
id: number;
};
}) {
await prisma.team.update({
where: {
id: team.id,
},
data: {
parent: {
connect: {
id: org.id,
},
},
},
});
}

View File

@@ -0,0 +1,528 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { JSDOM } from "jsdom";
import { uuid } from "short-uuid";
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { test } from "../lib/fixtures";
import {
bookTimeSlot,
doOnOrgDomain,
selectFirstAvailableTimeSlotNextMonth,
testName,
} from "../lib/testUtils";
import { expectExistingUserToBeInvitedToOrganization } from "../team/expects";
import { gotoPathAndExpectRedirectToOrgDomain } from "./lib/gotoPathAndExpectRedirectToOrgDomain";
import { acceptTeamOrOrgInvite, inviteExistingUserToOrganization } from "./lib/inviteUser";
function getOrgOrigin(orgSlug: string | null) {
if (!orgSlug) {
throw new Error("orgSlug is required");
}
let orgOrigin = WEBAPP_URL.replace("://app", `://${orgSlug}`);
orgOrigin = orgOrigin.includes(orgSlug) ? orgOrigin : WEBAPP_URL.replace("://", `://${orgSlug}.`);
return orgOrigin;
}
test.describe("Bookings", () => {
test.afterEach(async ({ orgs, users, page }) => {
await users.deleteAll();
await orgs.deleteAll();
});
test.describe("Team Event", () => {
test("Can create a booking for Collective EventType", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
},
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
}
);
const { team } = await owner.getFirstTeamMembership();
const teamEvent = await owner.getFirstTeamEvent(team.id);
await expectPageToBeNotFound({ page, url: `/team/${team.slug}/${teamEvent.slug}` });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookTeamEvent({ page, team, event: teamEvent });
// All the teammates should be in the booking
for (const teammate of teamMatesObj.concat([{ name: owner.name || "" }])) {
await expect(page.getByText(teammate.name, { exact: true })).toBeVisible();
}
}
);
// TODO: Assert whether the user received an email
});
test("Can create a booking for Round Robin EventType", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
},
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.ROUND_ROBIN,
}
);
const { team } = await owner.getFirstTeamMembership();
const teamEvent = await owner.getFirstTeamEvent(team.id);
await expectPageToBeNotFound({ page, url: `/team/${team.slug}/${teamEvent.slug}` });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookTeamEvent({ page, team, event: teamEvent, teamMatesObj });
// Since all the users have the same leastRecentlyBooked value
// Anyone of the teammates could be the Host of the booking.
const chosenUser = await page.getByTestId("booking-host-name").textContent();
expect(chosenUser).not.toBeNull();
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
expect(teamMatesObj.concat([{ name: owner.name! }]).some(({ name }) => name === chosenUser)).toBe(
true
);
}
);
// TODO: Assert whether the user received an email
});
test("Can access booking page with event slug and team page in lowercase/uppercase/mixedcase", async ({
page,
orgs,
users,
}) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
},
{
hasTeam: true,
teammates: teamMatesObj,
schedulingType: SchedulingType.COLLECTIVE,
}
);
const { team } = await owner.getFirstTeamMembership();
const { slug: teamEventSlug } = await owner.getFirstTeamEvent(team.id);
const teamSlugUpperCase = team.slug?.toUpperCase();
const teamEventSlugUpperCase = teamEventSlug.toUpperCase();
// This is the most closest to the actual user flow as org1.cal.com maps to /org/orgSlug
await page.goto(`/org/${org.slug}/${teamSlugUpperCase}/${teamEventSlugUpperCase}`);
await page.waitForSelector("[data-testid=day]");
});
});
test.describe("User Event", () => {
test("Can create a booking", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user = await users.create({
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const event = await user.getFirstEventAsOwner();
await expectPageToBeNotFound({ page, url: `/${user.username}/${event.slug}` });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({ page, user, event });
}
);
});
test.describe("User Event with same slug as another user's", () => {
test("booking is created for first user when first user is booked", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user1 = await users.create({
username: "user1",
name: "User 1",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user2 = await users.create({
username: "user2",
name: "User2",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user1Event = await user1.getFirstEventAsOwner();
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({ page, user: user1, event: user1Event });
}
);
});
test("booking is created for second user when second user is booked", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user1 = await users.create({
username: "user1",
name: "User 1",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user2 = await users.create({
username: "user2",
name: "User2",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const user2Event = await user2.getFirstEventAsOwner();
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({ page, user: user2, event: user2Event });
}
);
});
});
test("check SSR and OG ", async ({ page, users, orgs }) => {
const name = "Test User";
const org = await orgs.create({
name: "TestOrg",
});
const user = await users.create({
name,
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
});
const firstEventType = await user.getFirstEventAsOwner();
const calLink = `/${user.username}/${firstEventType.slug}`;
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
const [response] = await Promise.all([
// This promise resolves to the main resource response
page.waitForResponse(
(response) => response.url().includes(`${calLink}`) && response.status() === 200
),
// Trigger the page navigation
page.goto(`${calLink}`),
]);
const ssrResponse = await response.text();
const document = new JSDOM(ssrResponse).window.document;
const orgOrigin = getOrgOrigin(org.slug);
const titleText = document.querySelector("title")?.textContent;
const ogImage = document.querySelector('meta[property="og:image"]')?.getAttribute("content");
const ogUrl = document.querySelector('meta[property="og:url"]')?.getAttribute("content");
const canonicalLink = document.querySelector('link[rel="canonical"]')?.getAttribute("href");
expect(titleText).toContain(name);
expect(ogUrl).toEqual(`${orgOrigin}${calLink}`);
expect(canonicalLink).toEqual(`${orgOrigin}${calLink}`);
// Verify that there is correct URL that would generate the awesome OG image
expect(ogImage).toContain(
"/_next/image?w=1200&q=100&url=%2Fapi%2Fsocial%2Fog%2Fimage%3Ftype%3Dmeeting%26title%3D"
);
// Verify Organizer Name in the URL
expect(ogImage).toContain("meetingProfileName%3DTest%2520User%26");
}
);
});
});
test.describe("Scenario with same username in and outside organization", () => {
test("Can create a booking for user with same username in and outside organization", async ({
page,
users,
orgs,
}) => {
const org = await orgs.create({
name: "TestOrg",
});
const username = "john";
const userInsideOrganization = await users.create({
username,
useExactUsername: true,
email: `john-inside-${uuid()}@example.com`,
name: "John Inside Organization",
organizationId: org.id,
roleInOrganization: MembershipRole.MEMBER,
eventTypes: [
{
title: "John Inside Org's Meeting",
slug: "john-inside-org-meeting",
length: 15,
},
],
});
const userOutsideOrganization = await users.create({
username,
name: "John Outside Organization",
email: `john-outside-${uuid()}@example.com`,
useExactUsername: true,
eventTypes: [
{
title: "John Outside Org's Meeting",
slug: "john-outside-org-meeting",
length: 15,
},
],
});
const eventForUserInsideOrganization = await userInsideOrganization.getFirstEventAsOwner();
const eventForUserOutsideOrganization = await userOutsideOrganization.getFirstEventAsOwner();
// John Inside Org's meeting can't be accessed on userOutsideOrganization's namespace
await expectPageToBeNotFound({
page,
url: `/${userOutsideOrganization.username}/john-inside-org-meeting`,
});
await bookUserEvent({ page, user: userOutsideOrganization, event: eventForUserOutsideOrganization });
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
// John Outside Org's meeting can't be accessed on userInsideOrganization's namespaces
await expectPageToBeNotFound({
page,
url: `/${userInsideOrganization.username}/john-outside-org-meeting`,
});
await bookUserEvent({ page, user: userInsideOrganization, event: eventForUserInsideOrganization });
}
);
});
});
test.describe("Inviting an existing user and then", () => {
test("create a booking on new link", async ({ page, browser, users, orgs, emails }) => {
const org = await orgs.create({
name: "TestOrg",
});
const owner = await users.create({
username: "owner",
name: "owner",
organizationId: org.id,
roleInOrganization: MembershipRole.OWNER,
});
const userOutsideOrganization = await users.create({
username: "john",
name: "John Outside Organization",
});
await owner.apiLogin();
const { invitedUserEmail } = await inviteExistingUserToOrganization({
page,
organizationId: org.id,
user: userOutsideOrganization,
usersFixture: users,
});
const inviteLink = await expectExistingUserToBeInvitedToOrganization(page, emails, invitedUserEmail);
if (!inviteLink) {
throw new Error("Invite link not found");
}
const usernameInOrg = getOrgUsernameFromEmail(
invitedUserEmail,
org.organizationSettings?.orgAutoAcceptEmail ?? null
);
const usernameOutsideOrg = userOutsideOrganization.username;
// Before invite is accepted the booking page isn't available
await expectPageToBeNotFound({ page, url: `/${usernameInOrg}` });
await userOutsideOrganization.apiLogin();
await acceptTeamOrOrgInvite(page);
await test.step("Book through new link", async () => {
await doOnOrgDomain(
{
orgSlug: org.slug,
page,
},
async () => {
await bookUserEvent({
page,
user: {
username: usernameInOrg,
name: userOutsideOrganization.name,
},
event: await userOutsideOrganization.getFirstEventAsOwner(),
});
}
);
});
await test.step("Booking through old link redirects to new link on org domain", async () => {
const event = await userOutsideOrganization.getFirstEventAsOwner();
await gotoPathAndExpectRedirectToOrgDomain({
page,
org,
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.
});
});
});
});
async function bookUserEvent({
page,
user,
event,
}: {
page: Page;
user: {
username: string | null;
name: string | null;
};
event: { slug: string; title: string };
}) {
await page.goto(`/${user.username}/${event.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.getByTestId("success-page")).toBeVisible();
// The title of the booking
const BookingTitle = `${event.title} between ${user.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
// The booker should be in the attendee list
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
}
async function bookTeamEvent({
page,
team,
event,
teamMatesObj,
}: {
page: Page;
team: {
slug: string | null;
name: string | null;
};
event: { slug: string; title: string; schedulingType: SchedulingType | null };
teamMatesObj?: { name: string }[];
}) {
// Note that even though the default way to access a team booking in an organization is to not use /team in the URL, but it isn't testable with playwright as the rewrite is taken care of by Next.js config which can't handle on the fly org slug's handling
// So, we are using /team in the URL to access the team booking
// There are separate tests to verify that the next.config.js rewrites are working
// Also there are additional checkly tests that verify absolute e2e flow. They are in __checks__/organization.spec.ts
await page.goto(`/team/${team.slug}/${event.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.getByTestId("success-page")).toBeVisible();
// The title of the booking
if (event.schedulingType === SchedulingType.ROUND_ROBIN) {
const bookingTitle = await page.getByTestId("booking-title").textContent();
expect(
teamMatesObj?.some((teamMate) => {
const BookingTitle = `${event.title} between ${teamMate.name} and ${testName}`;
return BookingTitle === bookingTitle;
})
).toBe(true);
} else {
const BookingTitle = `${event.title} between ${team.name} and ${testName}`;
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
}
// The booker should be in the attendee list
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
}
async function expectPageToBeNotFound({ page, url }: { page: Page; url: string }) {
await page.goto(`${url}`);
await expect(page.getByTestId(`404-page`)).toBeVisible();
}

View File

@@ -0,0 +1,29 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { JSDOM } from "jsdom";
// eslint-disable-next-line no-restricted-imports
import type { Messages } from "mailhog";
import type { createEmailsFixture } from "playwright/fixtures/emails";
import { getEmailsReceivedByUser } from "../lib/testUtils";
export async function expectInvitationEmailToBeReceived(
page: Page,
emails: ReturnType<typeof createEmailsFixture>,
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(2000);
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");
}

View File

@@ -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}`);
}

View File

@@ -0,0 +1,57 @@
import type { Page } from "@playwright/test";
import type { createUsersFixture } from "playwright/fixtures/users";
export const inviteUserToOrganization = async ({
page,
organizationId,
email,
usersFixture,
}: {
page: Page;
organizationId: number;
email: string;
usersFixture: ReturnType<typeof createUsersFixture>;
}) => {
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");
const invitedUserEmail = usersFixture.trackEmail({
username: email.split("@")[0],
domain: email.split("@")[1],
});
await inviteAnEmail(page, invitedUserEmail);
return { invitedUserEmail };
};
export const inviteExistingUserToOrganization = async ({
page,
organizationId,
user,
usersFixture,
}: {
page: Page;
organizationId: number;
user: {
email: string;
};
usersFixture: ReturnType<typeof createUsersFixture>;
}) => {
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");
await inviteAnEmail(page, user.email);
await page.waitForSelector('[data-testid="toast-success"]');
return { invitedUserEmail: user.email };
};
export async function acceptTeamOrOrgInvite(page: Page) {
await page.goto("/settings/teams");
await page.click('[data-testid^="accept-invitation"]');
await page.waitForLoadState("networkidle");
}
async function inviteAnEmail(page: Page, invitedUserEmail: string) {
await page.locator('button:text("Add")').click();
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
await page.locator('button:text("Send invite")').click();
await page.waitForLoadState("networkidle");
}

View File

@@ -0,0 +1,588 @@
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();
orgs.deleteAll();
});
function capitalize(text: string) {
if (!text) {
return text;
}
return text.charAt(0).toUpperCase() + text.slice(1);
}
test.describe("Organization", () => {
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 orgOwnerUsernamePrefix = "owner";
const orgOwnerEmail = users.trackEmail({
username: orgOwnerUsernamePrefix,
domain: `example.com`,
});
const orgOwnerUser = await users.create({
username: orgOwnerUsernamePrefix,
email: orgOwnerEmail,
role: "ADMIN",
});
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");
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);
});
await expectOrganizationCreationEmailToBeSentWithLinks({
page,
emails,
userEmail: orgOwnerEmail,
oldUsername: orgOwnerUsernameOutsideOrg || "",
newUsername: orgOwnerUsernameInOrg,
orgSlug,
});
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-members");
});
await test.step("On-board administrators", async () => {
await page.waitForSelector('[data-testid="pending-member-list"]');
expect(await page.getByTestId("pending-member-item").count()).toBe(1);
const adminEmail = users.trackEmail({ username: "rick", domain: `example.com` });
//can add members
await page.getByTestId("new-member-button").click();
await page.locator('[placeholder="email\\@example\\.com"]').fill(adminEmail);
await page.getByTestId("invite-new-member-button").click();
await expect(page.locator(`li:has-text("${adminEmail}")`)).toBeVisible();
// TODO: Check if invited admin received the invitation email
// await expectInvitationEmailToBeReceived(
// page,
// emails,
// adminEmail,
// `${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
// );
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
// can remove members
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
const lastRemoveMemberButton = page.getByTestId("remove-member-button").last();
await lastRemoveMemberButton.click();
await page.waitForLoadState("networkidle");
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
await page.getByTestId("publish-button").click();
// Waiting to be in next step URL
await page.waitForURL("/settings/organizations/*/add-teams");
});
await test.step("Create teams", async () => {
// Filling one team
await page.locator('input[name="teams.0.name"]').fill("Marketing");
// Adding another team
await page.locator('button:text("Add a team")').click();
await page.locator('input[name="teams.1.name"]').fill("Sales");
// Finishing the creation wizard
await page.getByTestId("continue_or_checkout").click();
await page.waitForURL("/event-types");
});
await test.step("Login as org owner and pay", async () => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(!IS_TEAM_BILLING_ENABLED, "Skipping paying for org as stripe is disabled");
await orgOwnerUser.apiLogin();
await page.goto("/event-types");
const upgradeButton = await page.getByTestId("upgrade_org_banner_button");
await expect(upgradeButton).toBeVisible();
await upgradeButton.click();
// Check that stripe checkout is present
const expectedUrl = "https://checkout.stripe.com";
await page.waitForURL((url) => url.href.startsWith(expectedUrl));
const url = page.url();
// Check that the URL matches the expected URL
expect(url).toContain(expectedUrl);
await fillStripeTestCheckout(page);
await page.waitForLoadState("networkidle");
const upgradeButtonHidden = await page.getByTestId("upgrade_org_banner_button");
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 }) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled");
const stringUUID = uuid();
const orgOwnerUsername = `owner-${stringUUID}`;
const targetOrgEmail = users.trackEmail({
username: orgOwnerUsername,
domain: `example.com`,
});
const orgOwnerUser = await users.create({
username: orgOwnerUsername,
email: targetOrgEmail,
});
await orgOwnerUser.apiLogin();
const orgName = capitalize(`${orgOwnerUsername}`);
await page.goto("/settings/organizations/new");
await page.waitForLoadState("networkidle");
await test.step("Basic info", async () => {
// These values are infered due to an existing user being signed
expect(await page.locator("input[name=name]").inputValue()).toBe("Example");
expect(await page.locator("input[name=slug]").inputValue()).toBe("example");
await page.locator("input[name=name]").fill(orgName);
await page.locator("input[name=slug]").fill(orgOwnerUsername);
await page.locator("button[type=submit]").click();
await page.waitForLoadState("networkidle");
});
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-members");
});
await test.step("On-board administrators", async () => {
await page.waitForSelector('[data-testid="pending-member-list"]');
expect(await page.getByTestId("pending-member-item").count()).toBe(1);
const adminEmail = users.trackEmail({ username: "rick", domain: `example.com` });
//can add members
await page.getByTestId("new-member-button").click();
await page.locator('[placeholder="email\\@example\\.com"]').fill(adminEmail);
await page.getByTestId("invite-new-member-button").click();
await expect(page.locator(`li:has-text("${adminEmail}")`)).toBeVisible();
// TODO: Check if invited admin received the invitation email
// await expectInvitationEmailToBeReceived(
// page,
// emails,
// adminEmail,
// `${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
// );
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
// can remove members
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
const lastRemoveMemberButton = page.getByTestId("remove-member-button").last();
await lastRemoveMemberButton.click();
await page.waitForLoadState("networkidle");
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
await page.getByTestId("publish-button").click();
// Waiting to be in next step URL
await page.waitForURL("/settings/organizations/*/add-teams");
});
await test.step("Create teams", async () => {
// Filling one team
await page.locator('input[name="teams.0.name"]').fill("Marketing");
// Adding another team
await page.locator('button:text("Add a team")').click();
await page.locator('input[name="teams.1.name"]').fill("Sales");
// Finishing the creation wizard
await page.getByTestId("continue_or_checkout").click();
});
await test.step("Login as org owner and pay", async () => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(!IS_TEAM_BILLING_ENABLED, "Skipping paying for org as stripe is disabled");
await orgOwnerUser.apiLogin();
await page.goto("/event-types");
const upgradeButton = await page.getByTestId("upgrade_org_banner_button");
await expect(upgradeButton).toBeVisible();
await upgradeButton.click();
// Check that stripe checkout is present
const expectedUrl = "https://checkout.stripe.com";
await page.waitForURL((url) => url.href.startsWith(expectedUrl));
const url = page.url();
// Check that the URL matches the expected URL
expect(url).toContain(expectedUrl);
await fillStripeTestCheckout(page);
await page.waitForLoadState("networkidle");
const upgradeButtonHidden = await page.getByTestId("upgrade_org_banner_button");
await expect(upgradeButtonHidden).toBeHidden();
});
});
test("User gets prompted with >=3 teams to upgrade & can transfer existing teams to org", async ({
page,
users,
}) => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(process.env.NEXT_PUBLIC_ORG_SELF_SERVE_ENABLED !== "1", "Org self serve is not enabled");
const numberOfTeams = 3;
const stringUUID = uuid();
const orgOwnerUsername = `owner-${stringUUID}`;
const targetOrgEmail = users.trackEmail({
username: orgOwnerUsername,
domain: `example.com`,
});
const orgOwnerUser = await users.create(
{
username: orgOwnerUsername,
email: targetOrgEmail,
},
{ hasTeam: true, numberOfTeams }
);
await orgOwnerUser.apiLogin();
await page.goto("/teams");
await test.step("Has org self serve banner", async () => {
// These values are infered due to an existing user being signed
const selfServeButtonLocator = await page.getByTestId("setup_your_org_action_button");
await expect(selfServeButtonLocator).toBeVisible();
await selfServeButtonLocator.click();
await page.waitForURL("/settings/organizations/new");
});
await test.step("Basic info", async () => {
// These values are infered due to an existing user being signed
const slugLocator = await page.locator("input[name=slug]");
expect(await page.locator("input[name=name]").inputValue()).toBe("Example");
expect(await slugLocator.inputValue()).toBe("example");
await slugLocator.fill(`example-${stringUUID}`);
await page.locator("button[type=submit]").click();
await page.waitForLoadState("networkidle");
});
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-members");
});
await test.step("On-board administrators", async () => {
await page.waitForSelector('[data-testid="pending-member-list"]');
expect(await page.getByTestId("pending-member-item").count()).toBe(1);
const adminEmail = users.trackEmail({ username: "rick", domain: `example.com` });
//can add members
await page.getByTestId("new-member-button").click();
await page.locator('[placeholder="email\\@example\\.com"]').fill(adminEmail);
await page.getByTestId("invite-new-member-button").click();
await expect(page.locator(`li:has-text("${adminEmail}")`)).toBeVisible();
// TODO: Check if invited admin received the invitation email
// await expectInvitationEmailToBeReceived(
// page,
// emails,
// adminEmail,
// `${orgName}'s admin invited you to join the organization ${orgName} on Cal.com`
// );
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
// can remove members
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
const lastRemoveMemberButton = page.getByTestId("remove-member-button").last();
await lastRemoveMemberButton.click();
await page.waitForLoadState("networkidle");
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
await page.getByTestId("publish-button").click();
// Waiting to be in next step URL
await page.waitForURL("/settings/organizations/*/add-teams");
});
await test.step("Move existing teams to org", async () => {
// No easy way to get all team checkboxes so we fill all checkboxes on the page in
const foundCheckboxes = page.locator('input[type="checkbox"]');
const count = await foundCheckboxes.count();
for (let i = 0; i < count; i++) {
const checkbox = foundCheckboxes.nth(i);
await checkbox.click();
}
});
await test.step("Create teams", async () => {
// Filling one team
await page.locator('input[name="teams.0.name"]').fill("Marketing");
// Adding another team
await page.locator('button:text("Add a team")').click();
await page.locator('input[name="teams.1.name"]').fill("Sales");
// Finishing the creation wizard
await page.getByTestId("continue_or_checkout").click();
});
await test.step("Login as org owner and pay", async () => {
// eslint-disable-next-line playwright/no-skipped-test
test.skip(!IS_TEAM_BILLING_ENABLED, "Skipping paying for org as stripe is disabled");
await orgOwnerUser.apiLogin();
await page.goto("/event-types");
const upgradeButton = await page.getByTestId("upgrade_org_banner_button");
await expect(upgradeButton).toBeVisible();
await upgradeButton.click();
// Check that stripe checkout is present
const expectedUrl = "https://checkout.stripe.com";
await page.waitForURL((url) => url.href.startsWith(expectedUrl));
const url = page.url();
// Check that the URL matches the expected URL
expect(url).toContain(expectedUrl);
await fillStripeTestCheckout(page);
await page.waitForLoadState("networkidle");
const upgradeButtonHidden = await page.getByTestId("upgrade_org_banner_button");
await expect(upgradeButtonHidden).toBeHidden();
});
await test.step("Ensure correctnumberOfTeams are migrated", async () => {
// eslint-disable-next-line playwright/no-skipped-test
await page.goto("/teams");
await page.waitForLoadState("networkidle");
const teamListItems = await page.getByTestId("team-list-item-link").all();
// Number of teams migrated + the two created in the create teams step
expect(teamListItems.length).toBe(numberOfTeams + 2);
});
});
});
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(),
]);
}

View File

@@ -0,0 +1,559 @@
import type { Browser, Page } from "@playwright/test";
import { expect } from "@playwright/test";
import prisma from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/client";
import { moveUserToOrg } from "@lib/orgMigration";
import { test } from "../lib/fixtures";
import { getInviteLink } from "../lib/testUtils";
import { expectInvitationEmailToBeReceived } from "./expects";
test.describe.configure({ mode: "parallel" });
test.afterEach(async ({ users, orgs }) => {
await users.deleteAll();
await orgs.deleteAll();
});
test.describe("Organization", () => {
test.describe("Email not matching orgAutoAcceptEmail", () => {
test("nonexisting user invited to an organization", async ({ browser, page, users, emails }) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");
await test.step("By email", async () => {
const invitedUserEmail = users.trackEmail({ username: "rick", domain: "domain.com" });
// '-domain' because the email doesn't match orgAutoAcceptEmail
const usernameDerivedFromEmail = `${invitedUserEmail.split("@")[0]}-domain`;
await inviteAnEmail(page, invitedUserEmail);
const inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
invitedUserEmail,
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`,
"signup?token"
);
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: false,
email: invitedUserEmail,
});
assertInviteLink(inviteLink);
await signupFromEmailInviteLink({
browser,
inviteLink,
expectedEmail: invitedUserEmail,
expectedUsername: usernameDerivedFromEmail,
});
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: invitedUserEmail,
});
});
await test.step("By invite link", async () => {
const inviteLink = await copyInviteLink(page);
const email = users.trackEmail({ username: "rick", domain: "domain.com" });
// '-domain' because the email doesn't match orgAutoAcceptEmail
const usernameDerivedFromEmail = `${email.split("@")[0]}-domain`;
await signupFromInviteLink({ browser, inviteLink, email });
const dbUser = await prisma.user.findUnique({ where: { email } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email,
});
});
});
// This test is already covered by booking.e2e.ts where existing user is invited and his booking links are tested.
// We can re-test here when we want to test some more scenarios.
// eslint-disable-next-line @typescript-eslint/no-empty-function
test("existing user invited to an organization", () => {});
test("nonexisting user invited to a Team inside organization", async ({
browser,
page,
users,
emails,
}) => {
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, hasSubteam: true });
await orgOwner.apiLogin();
const { team } = await orgOwner.getFirstTeamMembership();
const { team: org } = await orgOwner.getOrgMembership();
await test.step("By email", async () => {
await page.goto(`/settings/teams/${team.id}/members`);
await page.waitForLoadState("networkidle");
const invitedUserEmail = users.trackEmail({ username: "rick", domain: "domain.com" });
// '-domain' because the email doesn't match orgAutoAcceptEmail
const usernameDerivedFromEmail = `${invitedUserEmail.split("@")[0]}-domain`;
await inviteAnEmail(page, invitedUserEmail);
await expectUserToBeAMemberOfTeam({
page,
teamId: team.id,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: false,
email: invitedUserEmail,
});
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: false,
email: invitedUserEmail,
});
await page.waitForLoadState("networkidle");
const inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
invitedUserEmail,
`${team.name}'s admin invited you to join the team ${team.name} of organization ${org.name} on Cal.com`,
"signup?token"
);
assertInviteLink(inviteLink);
await signupFromEmailInviteLink({
browser,
inviteLink,
expectedEmail: invitedUserEmail,
expectedUsername: usernameDerivedFromEmail,
});
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
await expectUserToBeAMemberOfTeam({
page,
teamId: team.id,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: invitedUserEmail,
});
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: invitedUserEmail,
});
});
await test.step("By invite link", async () => {
await page.goto(`/settings/teams/${team.id}/members`);
const inviteLink = await copyInviteLink(page);
const email = users.trackEmail({ username: "rick", domain: "domain.com" });
// '-domain' because the email doesn't match orgAutoAcceptEmail
const usernameDerivedFromEmail = `${email.split("@")[0]}-domain`;
await signupFromInviteLink({ browser, inviteLink, email });
const dbUser = await prisma.user.findUnique({ where: { email } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
await expectUserToBeAMemberOfTeam({
teamId: team.id,
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: email,
});
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: email,
});
});
});
});
test.describe("Email matching orgAutoAcceptEmail and a Verified Organization with DNS Setup Done", () => {
test("nonexisting user is invited to Org", async ({ browser, page, users, emails }) => {
const orgOwner = await users.create(undefined, {
hasTeam: true,
isOrg: true,
isOrgVerified: true,
isDnsSetup: true,
});
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
await page.goto("/settings/organizations/members");
await page.waitForLoadState("networkidle");
await test.step("By email", async () => {
const invitedUserEmail = users.trackEmail({ username: "rick", domain: "example.com" });
const usernameDerivedFromEmail = invitedUserEmail.split("@")[0];
await inviteAnEmail(page, invitedUserEmail);
const inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
invitedUserEmail,
`${org.name}'s admin invited you to join the organization ${org.name} on Cal.com`,
"signup?token"
);
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: invitedUserEmail,
});
assertInviteLink(inviteLink);
await signupFromEmailInviteLink({
browser,
inviteLink,
expectedEmail: invitedUserEmail,
expectedUsername: usernameDerivedFromEmail,
});
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: invitedUserEmail,
});
});
await test.step("By invite link", async () => {
const inviteLink = await copyInviteLink(page);
const email = users.trackEmail({ username: "rick", domain: "example.com" });
const usernameDerivedFromEmail = email.split("@")[0];
await signupFromInviteLink({ browser, inviteLink, email });
const dbUser = await prisma.user.findUnique({ where: { email } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email,
});
});
});
// Such a user has user.username changed directly in addition to having the new username in the profile.username
test("existing user migrated to an organization", async ({ users, page, emails }) => {
const orgOwner = await users.create(undefined, {
hasTeam: true,
isOrg: true,
isOrgVerified: true,
isDnsSetup: true,
});
const { team: org } = await orgOwner.getOrgMembership();
await orgOwner.apiLogin();
const { existingUser } = await test.step("Invite an existing user to an organization", async () => {
const existingUser = await users.create({
username: "john",
emailDomain: org.organizationSettings?.orgAutoAcceptEmail ?? "",
name: "John Outside Organization",
});
await moveUserToOrg({
user: existingUser,
targetOrg: {
username: `${existingUser.username}-org`,
id: org.id,
membership: {
role: MembershipRole.MEMBER,
accepted: true,
},
},
shouldMoveTeams: false,
});
return { existingUser };
});
await test.step("Signing up with the previous username of the migrated user - shouldn't be allowed", async () => {
await page.goto("/signup");
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
await page.locator('input[name="username"]').fill(existingUser.username!);
await page
.locator('input[name="email"]')
.fill(`${existingUser.username}-differnet-email@example.com`);
await page.locator('input[name="password"]').fill("Password99!");
await page.waitForLoadState("networkidle");
await expect(page.locator('button[type="submit"]')).toBeDisabled();
});
});
test("nonexisting user is invited to a team inside organization", async ({
browser,
page,
users,
emails,
}) => {
const orgOwner = await users.create(undefined, {
hasTeam: true,
isOrg: true,
hasSubteam: true,
isOrgVerified: true,
isDnsSetup: true,
});
const { team: org } = await orgOwner.getOrgMembership();
const { team } = await orgOwner.getFirstTeamMembership();
await orgOwner.apiLogin();
await test.step("By email", async () => {
await page.goto(`/settings/teams/${team.id}/members`);
await page.waitForLoadState("networkidle");
const invitedUserEmail = users.trackEmail({ username: "rick", domain: "example.com" });
const usernameDerivedFromEmail = invitedUserEmail.split("@")[0];
await inviteAnEmail(page, invitedUserEmail);
await expectUserToBeAMemberOfTeam({
page,
teamId: team.id,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: invitedUserEmail,
});
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: invitedUserEmail,
});
const inviteLink = await expectInvitationEmailToBeReceived(
page,
emails,
invitedUserEmail,
`${team.name}'s admin invited you to join the team ${team.name} of organization ${org.name} on Cal.com`,
"signup?token"
);
assertInviteLink(inviteLink);
await signupFromEmailInviteLink({
browser,
inviteLink,
expectedEmail: invitedUserEmail,
expectedUsername: usernameDerivedFromEmail,
});
const dbUser = await prisma.user.findUnique({ where: { email: invitedUserEmail } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
await expectUserToBeAMemberOfTeam({
page,
teamId: team.id,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: invitedUserEmail,
});
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: invitedUserEmail,
});
});
await test.step("By invite link", async () => {
await page.goto(`/settings/teams/${team.id}/members`);
const inviteLink = await copyInviteLink(page);
const email = users.trackEmail({ username: "rick", domain: "example.com" });
// '-domain' because the email doesn't match orgAutoAcceptEmail
const usernameDerivedFromEmail = `${email.split("@")[0]}`;
await signupFromInviteLink({ browser, inviteLink, email });
const dbUser = await prisma.user.findUnique({ where: { email } });
expect(dbUser?.username).toBe(usernameDerivedFromEmail);
await expectUserToBeAMemberOfTeam({
teamId: team.id,
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: email,
});
await expectUserToBeAMemberOfOrganization({
page,
username: usernameDerivedFromEmail,
role: "member",
isMemberShipAccepted: true,
email: email,
});
});
});
});
});
async function signupFromInviteLink({
browser,
inviteLink,
email,
}: {
browser: Browser;
inviteLink: string;
email: string;
}) {
const context = await browser.newContext();
const inviteLinkPage = await context.newPage();
await inviteLinkPage.goto(inviteLink);
await inviteLinkPage.waitForLoadState("networkidle");
// Check required fields
const button = inviteLinkPage.locator("button[type=submit][disabled]");
await expect(button).toBeVisible(); // email + 3 password hints
await inviteLinkPage.locator("input[name=email]").fill(email);
await inviteLinkPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await inviteLinkPage.locator("button[type=submit]").click();
await inviteLinkPage.waitForURL("/getting-started");
return { email };
}
export async function signupFromEmailInviteLink({
browser,
inviteLink,
expectedUsername,
expectedEmail,
}: {
browser: Browser;
inviteLink: string;
expectedUsername?: string;
expectedEmail?: string;
}) {
// Follow invite link in new window
const context = await browser.newContext();
const signupPage = await context.newPage();
signupPage.goto(inviteLink);
await signupPage.locator(`[data-testid="signup-usernamefield"]`).waitFor({ state: "visible" });
await expect(signupPage.locator(`[data-testid="signup-usernamefield"]`)).toBeDisabled();
// await for value. initial value is ""
if (expectedUsername) {
await expect(signupPage.locator(`[data-testid="signup-usernamefield"]`)).toHaveValue(expectedUsername);
}
await expect(signupPage.locator(`[data-testid="signup-emailfield"]`)).toBeDisabled();
if (expectedEmail) {
await expect(signupPage.locator(`[data-testid="signup-emailfield"]`)).toHaveValue(expectedEmail);
}
await signupPage.waitForLoadState("networkidle");
// Check required fields
await signupPage.locator("input[name=password]").fill(`P4ssw0rd!`);
await signupPage.locator("button[type=submit]").click();
await signupPage.waitForURL("/getting-started?from=signup");
await context.close();
await signupPage.close();
}
async function inviteAnEmail(page: Page, invitedUserEmail: string) {
await page.locator('button:text("Add")').click();
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
await page.locator('button:text("Send invite")').click();
await page.waitForLoadState("networkidle");
}
async function expectUserToBeAMemberOfOrganization({
page,
username,
email,
role,
isMemberShipAccepted,
}: {
page: Page;
username: string;
role: string;
isMemberShipAccepted: boolean;
email: string;
}) {
// Check newly invited member is not pending anymore
await page.goto("/settings/organizations/members");
expect(await page.locator(`[data-testid="member-${username}-username"]`).textContent()).toBe(username);
expect(await page.locator(`[data-testid="member-${username}-email"]`).textContent()).toBe(email);
expect((await page.locator(`[data-testid="member-${username}-role"]`).textContent())?.toLowerCase()).toBe(
role.toLowerCase()
);
if (isMemberShipAccepted) {
await expect(page.locator(`[data-testid2="member-${username}-pending"]`)).toBeHidden();
} else {
await expect(page.locator(`[data-testid2="member-${username}-pending"]`)).toBeVisible();
}
}
async function expectUserToBeAMemberOfTeam({
page,
teamId,
email,
role,
username,
isMemberShipAccepted,
}: {
page: Page;
username: string;
role: string;
teamId: number;
isMemberShipAccepted: boolean;
email: string;
}) {
// Check newly invited member is not pending anymore
await page.goto(`/settings/teams/${teamId}/members`);
await page.reload();
expect(
(
await page.locator(`[data-testid="member-${username}"] [data-testid=member-role]`).textContent()
)?.toLowerCase()
).toBe(role.toLowerCase());
if (isMemberShipAccepted) {
await expect(page.locator(`[data-testid="email-${email.replace("@", "")}-pending"]`)).toBeHidden();
} else {
await expect(page.locator(`[data-testid="email-${email.replace("@", "")}-pending"]`)).toBeVisible();
}
}
function assertInviteLink(inviteLink: string | null | undefined): asserts inviteLink is string {
if (!inviteLink) throw new Error("Invite link not found");
}
async function copyInviteLink(page: Page) {
await page.locator('button:text("Add")').click();
await page.locator(`[data-testid="copy-invite-link-button"]`).click();
const inviteLink = await getInviteLink(page);
return inviteLink;
}

View File

@@ -0,0 +1,195 @@
import { expect } from "@playwright/test";
import prisma from "@calcom/prisma";
import { test } from "../lib/fixtures";
test.describe.configure({ mode: "parallel" });
test.afterEach(async ({ users, orgs }) => {
await users.deleteAll();
await orgs.deleteAll();
});
test.describe("Organization - Privacy", () => {
test(`Private Org \n
1) Org Member cannot see members of orgs\n
2) Org Owner/Admin can see members`, async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
isPrivate: true,
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: "OWNER",
},
{
hasTeam: true,
teammates: teamMatesObj,
}
);
const memberInOrg = await users.create({
username: "org-member-user",
name: "org-member-user",
organizationId: org.id,
roleInOrganization: "MEMBER",
});
await owner.apiLogin();
await page.goto("/settings/organizations/members");
await page.waitForLoadState("domcontentloaded");
const tableLocator = await page.getByTestId("user-list-data-table");
await expect(tableLocator).toBeVisible();
await memberInOrg.apiLogin();
await page.goto("/settings/organizations/members");
await page.waitForLoadState("domcontentloaded");
const userDataTable = await page.getByTestId("user-list-data-table");
const membersPrivacyWarning = await page.getByTestId("members-privacy-warning");
await expect(userDataTable).toBeHidden();
await expect(membersPrivacyWarning).toBeVisible();
});
test(`Private Org - Private Team\n
1) Team Member cannot see members in team\n
2) Team Admin/Owner can see members in team`, async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
isPrivate: true,
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: "OWNER",
},
{
hasTeam: true,
teammates: teamMatesObj,
}
);
await owner.apiLogin();
const membership = await owner.getFirstTeamMembership();
const teamId = membership.team.id;
// Update team to be private
await page.goto(`/settings/teams/${teamId}/members`);
await page.waitForLoadState("domcontentloaded");
const togglePrivateSwitch = await page.getByTestId("make-team-private-check");
await togglePrivateSwitch.click();
// As admin/owner we can see the user list
const tableLocator = await page.getByTestId("team-member-list-container");
await expect(tableLocator).toBeVisible();
const memberUser = await prisma.membership.findFirst({
where: {
teamId,
role: "MEMBER",
},
select: {
user: {
select: {
email: true,
},
},
},
});
expect(memberUser?.user.email).toBeDefined();
// @ts-expect-error expect doesnt assert on a type level
const memberOfTeam = await users.set(memberUser?.user.email);
await memberOfTeam.apiLogin();
await page.goto(`/settings/teams/${teamId}/members`);
await page.waitForLoadState("domcontentloaded");
// As a user we can not see the user list when a team is private
const hiddenTableLocator = await page.getByTestId("team-member-list-container");
await expect(hiddenTableLocator).toBeHidden();
});
test(`Private Org - Public Team\n
1) All team members can see members in team \n
2) Team Admin/Owner can see members in team`, async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const teamMatesObj = [
{ name: "teammate-1" },
{ name: "teammate-2" },
{ name: "teammate-3" },
{ name: "teammate-4" },
];
const owner = await users.create(
{
username: "pro-user",
name: "pro-user",
organizationId: org.id,
roleInOrganization: "OWNER",
},
{
hasTeam: true,
teammates: teamMatesObj,
}
);
await owner.apiLogin();
const membership = await owner.getFirstTeamMembership();
const teamId = membership.team.id;
// Update team to be private
await page.goto(`/settings/teams/${teamId}/members`);
await page.waitForLoadState("domcontentloaded");
// As admin/owner we can see the user list
const tableLocator = await page.getByTestId("team-member-list-container");
await expect(tableLocator).toBeVisible();
const memberUser = await prisma.membership.findFirst({
where: {
teamId,
role: "MEMBER",
},
select: {
user: {
select: {
email: true,
},
},
},
});
expect(memberUser?.user.email).toBeDefined();
// @ts-expect-error expect doesnt assert on a type level
const memberOfTeam = await users.set(memberUser?.user.email);
await memberOfTeam.apiLogin();
await page.goto(`/settings/teams/${teamId}/members`);
await page.waitForLoadState("domcontentloaded");
// As a user we can not see the user list when a team is private
const hiddenTableLocator = await page.getByTestId("team-member-list-container");
await expect(hiddenTableLocator).toBeVisible();
});
});

View File

@@ -0,0 +1,80 @@
import { expect } from "@playwright/test";
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
import { prisma } from "@calcom/prisma";
import { MembershipRole } from "@calcom/prisma/enums";
import { test } from "../lib/fixtures";
import { fillStripeTestCheckout } from "../lib/testUtils";
test.describe("Teams", () => {
test.afterEach(({ orgs, users }) => {
orgs.deleteAll();
users.deleteAll();
});
test("Can create teams via Wizard", async ({ page, users, orgs }) => {
const org = await orgs.create({
name: "TestOrg",
});
const user = await users.create({
organizationId: org.id,
roleInOrganization: MembershipRole.ADMIN,
});
const inviteeEmail = `${user.username}+invitee@example.com`;
await user.apiLogin();
await page.goto("/teams");
await test.step("Can create team", async () => {
// Click text=Create Team
await page.locator("text=Create a new Team").click();
await page.waitForURL((url) => url.pathname === "/settings/teams/new");
// Fill input[name="name"]
await page.locator('input[name="name"]').fill(`${user.username}'s Team`);
// Click text=Continue
await page.click("[type=submit]");
// TODO: Figure out a way to make this more reliable
// eslint-disable-next-line playwright/no-conditional-in-test
if (IS_TEAM_BILLING_ENABLED) await fillStripeTestCheckout(page);
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/onboard-members.*$/i);
await page.waitForSelector('[data-testid="pending-member-list"]');
expect(await page.getByTestId("pending-member-item").count()).toBe(1);
});
await test.step("Can add members", async () => {
await page.getByTestId("new-member-button").click();
await page.locator('[placeholder="email\\@example\\.com"]').fill(inviteeEmail);
await page.getByTestId("invite-new-member-button").click();
await expect(page.locator(`li:has-text("${inviteeEmail}")`)).toBeVisible();
// locator.count() does not await for the expected number of elements
// https://github.com/microsoft/playwright/issues/14278
// using toHaveCount() is more reliable
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
});
await test.step("Can remove members", async () => {
await expect(page.getByTestId("pending-member-item")).toHaveCount(2);
const lastRemoveMemberButton = page.getByTestId("remove-member-button").last();
await lastRemoveMemberButton.click();
await page.waitForLoadState("networkidle");
await expect(page.getByTestId("pending-member-item")).toHaveCount(1);
// Cleanup here since this user is created without our fixtures.
await prisma.user.delete({ where: { email: inviteeEmail } });
});
await test.step("Can finish team creation", async () => {
await page.getByTestId("publish-button").click();
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/profile$/i);
});
await test.step("Can disband team", async () => {
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
await page.getByTestId("disband-team-button").click();
await page.getByTestId("dialog-confirmation").click();
await page.waitForURL("/teams");
expect(await page.locator(`text=${user.username}'s Team`).count()).toEqual(0);
});
});
});