import type { Browser, Page } from "@playwright/test"; import { expect } from "@playwright/test"; import prisma from "@calcom/prisma"; import { MembershipRole } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; import { test } from "../lib/fixtures"; import { moveUserToOrg } from "../lib/orgMigration"; import { bookTeamEvent, doOnOrgDomain, expectPageToBeNotFound, 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/${org.slug}/members`); 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, orgSlug: org.slug, 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, orgSlug: org.slug, 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, orgSlug: org.slug, 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`); 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, true); await expectUserToBeAMemberOfTeam({ page, teamId: team.id, username: usernameDerivedFromEmail, role: "member", isMemberShipAccepted: false, email: invitedUserEmail, }); await expectUserToBeAMemberOfOrganization({ page, orgSlug: org.slug, username: usernameDerivedFromEmail, role: "member", isMemberShipAccepted: false, 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, orgSlug: org.slug, 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, true); 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, orgSlug: org.slug, 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/${org.slug}/members`); 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, orgSlug: org.slug, 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, orgSlug: org.slug, 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, orgSlug: org.slug, 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"); await expect(page.locator("text=Create your account")).toBeVisible(); await expect(page.locator('[data-testid="continue-with-email-button"]')).toBeVisible(); await page.locator('[data-testid="continue-with-email-button"]').click(); await expect(page.locator('[data-testid="signup-submit-button"]')).toBeVisible(); // 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 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`); const invitedUserEmail = users.trackEmail({ username: "rick", domain: "example.com" }); const usernameDerivedFromEmail = invitedUserEmail.split("@")[0]; await inviteAnEmail(page, invitedUserEmail, true); await expectUserToBeAMemberOfTeam({ page, teamId: team.id, username: usernameDerivedFromEmail, role: "member", isMemberShipAccepted: true, email: invitedUserEmail, }); await expectUserToBeAMemberOfOrganization({ page, orgSlug: org.slug, 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, orgSlug: org.slug, 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, true); 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, orgSlug: org.slug, username: usernameDerivedFromEmail, role: "member", isMemberShipAccepted: true, email: email, }); }); }); test("can book an event with auto accepted invitee (not completed on-boarding) added as fixed host.", async ({ page, users, }) => { const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, hasSubteam: true, isOrgVerified: true, isDnsSetup: true, orgRequestedSlug: "example", schedulingType: SchedulingType.ROUND_ROBIN, }); const { team: org } = await orgOwner.getOrgMembership(); const { team } = await orgOwner.getFirstTeamMembership(); await orgOwner.apiLogin(); await page.goto(`/settings/teams/${team.id}/members`); const invitedUserEmail = users.trackEmail({ username: "rick", domain: "example.com" }); await inviteAnEmail(page, invitedUserEmail, true); //add invitee as fixed host to team event const teamEvent = await orgOwner.getFirstTeamEvent(team.id); await page.goto(`/event-types/${teamEvent.id}?tabName=team`); await page.locator('[data-testid="fixed-hosts-switch"]').click(); await page.locator('[data-testid="fixed-hosts-select"]').click(); await page.locator(`text="${invitedUserEmail}"`).click(); await page.locator('[data-testid="update-eventtype"]').click(); await page.waitForResponse("/api/trpc/eventTypes/update?batch=1"); await expectPageToBeNotFound({ page, url: `/team/${team.slug}/${teamEvent.slug}` }); await doOnOrgDomain( { orgSlug: org.slug, page, }, async ({ page, goToUrlWithErrorHandling }) => { const result = await goToUrlWithErrorHandling(`/team/${team.slug}/${teamEvent.slug}`); await bookTeamEvent({ page, team, event: teamEvent }); await expect(page.getByText(invitedUserEmail, { exact: true })).toBeVisible(); return { url: result.url }; } ); }); }); }); 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 expect(inviteLinkPage.locator("text=Create your account")).toBeVisible(); // 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 expect(signupPage.locator("text=Create your account")).toBeVisible(); 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); } // 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, teamPage?: boolean) { if (teamPage) { await page.getByTestId("new-member-button").click(); } else { await page.getByTestId("new-organization-member-button").click(); } await page.locator('input[name="inviteUser"]').fill(invitedUserEmail); const submitPromise = page.waitForResponse("/api/trpc/teams/inviteMember?batch=1"); await page.getByTestId("invite-new-member-button").click(); const response = await submitPromise; expect(response.status()).toBe(200); } async function expectUserToBeAMemberOfOrganization({ page, orgSlug, username, email, role, isMemberShipAccepted, }: { page: Page; orgSlug: string | null; username: string; role: string; isMemberShipAccepted: boolean; email: string; }) { // Check newly invited member is not pending anymore await page.goto(`/settings/organizations/${orgSlug}/members`); await expect(page.locator(`[data-testid="member-${username}-username"]`)).toHaveText(username); await expect(page.locator(`[data-testid="member-${username}-email"]`)).toHaveText(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(); await page.waitForLoadState("domcontentloaded"); expect( ( await page .locator( `[data-testid="member-${username}"] [data-testid="${ isMemberShipAccepted ? "member-email" : `email-${email.replace("@", "")}-pending` }"]` ) .textContent() )?.toLowerCase() ).toBe(email.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, teamPage?: boolean) { if (teamPage) { await page.getByTestId("new-member-button").click(); } else { await page.getByTestId("new-organization-member-button").click(); } const inviteLink = await getInviteLink(page); return inviteLink; }