2
0
Files
cal/calcom/apps/web/playwright/organization/organization-invitation.e2e.ts
2024-08-09 16:18:09 +02:00

560 lines
19 KiB
TypeScript

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 BLS cal`,
"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 BLS cal`,
"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 BLS cal`,
"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 BLS cal`,
"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;
}