mirror of
https://github.com/hanzoai/calendar.git
synced 2026-09-24 22:42:24 +00:00
27820ce897
* feat: auto-accept team invitations for existing users - Change email button text from 'View Invitation' to 'Accept Invite' - Implement auto-accept flow when clicking email CTA - Update TeamService.inviteMemberByToken to support auto-acceptance - Add new autoAcceptInvite tRPC endpoint for handling auto-acceptance - Update invitation link generation to include autoAccept parameter - Handle both team and organization invitation scenarios - Maintain payment/billing flow integration with TeamBilling.updateQuantity - Preserve backward compatibility with existing manual flow - Update all locale files with new 'Accept Invite' button text Co-Authored-By: [email protected] <[email protected]> * revert: locale changes except English - Keep only English 'Accept Invite' translation - Revert all other locale files to original 'View Invitation' translations - Maintain core auto-accept invitation functionality Co-Authored-By: [email protected] <[email protected]> * simplify: remove autoAccept parameter and make auto-acceptance default - Remove autoAccept parameter from TeamService.inviteMemberByToken - Always auto-accept invitations for existing users clicking email links - Remove autoAccept logic from teams server-page.tsx - Remove autoAccept=true from invitation URLs - Delete autoAcceptInvite handler and schema files - Remove autoAcceptInvite endpoint from tRPC router - Simplify invitation flow to match new user pattern Co-Authored-By: [email protected] <[email protected]> * test: update teamService test to expect auto-accepted memberships - Change expectation from accepted: false to accepted: true - Update test description to reflect auto-accept behavior - Fix TypeScript type casting to use Pick<TeamRepository, 'deleteById'> - Aligns with new default auto-acceptance for team invitations Co-Authored-By: [email protected] <[email protected]> * update * Update utils.ts * fix type error * delete token * add prisma transaction * update * update param * test: fix mock objects in teamService tests with realistic data - Fix duplicate property assignments in mock user objects - Use proper email format ([email protected]) for email fields - Use proper username format (testuser) for username fields - Fix logic error in acceptInvitationByToken (|| to &&) - Add autoAccept parameter to resendInvitation.handler.ts - All 16 tests passing with proper TypeScript types Co-Authored-By: [email protected] <[email protected]> * test: add e2e tests for team invitation auto-accept flow - Add test for existing user auto-accepting team invitation via email link - Add test for error handling when wrong user tries to use invitation link - Verify proper user identity validation and database state changes - Follow existing e2e test patterns with browser context isolation - Fix ESLint warnings: replace conditional with assertion and remove unused browser parameter Co-Authored-By: [email protected] <[email protected]> * fix * fix * fix: update team owner creation in e2e tests to include proper names - Fix email subject mismatch in auto-accept invitation tests - Team owners now created with explicit names instead of undefined - Matches pattern used in other working team invitation tests Co-Authored-By: [email protected] <[email protected]> * fix: update organization invitation test helper to match new auto-accept link format - Change expectExistingUserToBeInvitedToOrganization to look for 'teams?token' instead of 'settings/team' - Fixes 'Invite link not found' error in organization booking e2e test - Aligns with auto-accept invitation URL changes that use /teams?token= format - Fix eslint disable comment for playwright rule Co-Authored-By: [email protected] <[email protected]> * address coderrabit review * fix failing test * addressed review --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
319 lines
12 KiB
TypeScript
319 lines
12 KiB
TypeScript
import { expect } from "@playwright/test";
|
|
|
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
|
import { prisma } from "@calcom/prisma";
|
|
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
|
|
|
import { test } from "../lib/fixtures";
|
|
import { localize } from "../lib/localize";
|
|
import { getInviteLink } from "../lib/testUtils";
|
|
import { expectInvitationEmailToBeReceived } from "./expects";
|
|
|
|
test.describe.configure({ mode: "parallel" });
|
|
|
|
test.afterEach(async ({ users }) => {
|
|
await users.deleteAll();
|
|
});
|
|
|
|
test.describe("Team", () => {
|
|
test("Invitation (non verified)", async ({ browser, page, users, emails }) => {
|
|
const t = await localize("en");
|
|
const teamOwner = await users.create(undefined, { hasTeam: true });
|
|
const { team } = await teamOwner.getFirstTeamMembership();
|
|
await teamOwner.apiLogin();
|
|
await page.goto(`/settings/teams/${team.id}/settings`);
|
|
|
|
await test.step("To the team by email (external user)", async () => {
|
|
const invitedUserEmail = users.trackEmail({
|
|
username: "rick",
|
|
domain: `domain-${Date.now()}.com`,
|
|
});
|
|
await page.goto(`/settings/teams/${team.id}/members`);
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded
|
|
await page.getByTestId("new-member-button").click();
|
|
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
|
await page.getByText(t("send_invite")).click();
|
|
const inviteLink = await expectInvitationEmailToBeReceived(
|
|
page,
|
|
emails,
|
|
invitedUserEmail,
|
|
`${team.name}'s admin invited you to join the team ${team.name} on Cal.com`,
|
|
"signup?token"
|
|
);
|
|
|
|
//Check newly invited member exists and is pending
|
|
await expect(
|
|
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
|
).toHaveCount(1);
|
|
|
|
expect(inviteLink).toBeTruthy();
|
|
|
|
// Follow invite link to new window
|
|
const context = await browser.newContext();
|
|
const newPage = await context.newPage();
|
|
await newPage.goto(inviteLink);
|
|
await expect(newPage.locator("text=Create your account")).toBeVisible();
|
|
|
|
// Check required fields
|
|
const button = newPage.locator("button[type=submit][disabled]");
|
|
await expect(button).toBeVisible(); // email + 3 password hints
|
|
|
|
// Check required fields
|
|
await newPage.locator("input[name=password]").fill(`P4ssw0rd!`);
|
|
await newPage.locator("button[type=submit]").click();
|
|
await newPage.waitForURL("/getting-started?from=signup");
|
|
await newPage.close();
|
|
await context.close();
|
|
|
|
// Check newly invited member is not pending anymore
|
|
await page.bringToFront();
|
|
await page.goto(`/settings/teams/${team.id}/settings`);
|
|
await expect(
|
|
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
|
).toHaveCount(0);
|
|
});
|
|
|
|
await test.step("To the team by invite link", async () => {
|
|
const user = await users.create({
|
|
email: `user-invite-${Date.now()}@domain.com`,
|
|
password: "P4ssw0rd!",
|
|
});
|
|
|
|
await page.goto(`/settings/teams/${team.id}/members`);
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded
|
|
await page.getByTestId("new-member-button").click();
|
|
const inviteLink = await getInviteLink(page);
|
|
|
|
const context = await browser.newContext();
|
|
const inviteLinkPage = await context.newPage();
|
|
await inviteLinkPage.goto(inviteLink);
|
|
await inviteLinkPage.waitForLoadState("domcontentloaded");
|
|
|
|
await inviteLinkPage.locator("button[type=submit]").click();
|
|
await expect(inviteLinkPage.locator('[data-testid="field-error"]')).toHaveCount(2);
|
|
|
|
await inviteLinkPage.locator("input[name=email]").fill(user.email);
|
|
await inviteLinkPage.locator("input[name=password]").fill(user.username || "P4ssw0rd!");
|
|
await inviteLinkPage.locator("button[type=submit]").click();
|
|
|
|
await inviteLinkPage.waitForURL(`${WEBAPP_URL}/teams**`);
|
|
});
|
|
});
|
|
|
|
test("Invitation (verified)", async ({ page, users, emails }) => {
|
|
const t = await localize("en");
|
|
const teamOwner = await users.create({ name: `team-owner-${Date.now()}` }, { hasTeam: true });
|
|
const { team } = await teamOwner.getFirstTeamMembership();
|
|
await teamOwner.apiLogin();
|
|
await page.goto(`/settings/teams/${team.id}/settings`);
|
|
|
|
await test.step("To the organization by email (internal user)", async () => {
|
|
const invitedUserEmail = users.trackEmail({
|
|
username: "rick",
|
|
domain: `example.com`,
|
|
});
|
|
await page.goto(`/settings/teams/${team.id}/members`);
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded
|
|
await page.getByTestId("new-member-button").click();
|
|
await page.locator('input[name="inviteUser"]').fill(invitedUserEmail);
|
|
await page.getByText(t("send_invite")).click();
|
|
await expectInvitationEmailToBeReceived(
|
|
page,
|
|
emails,
|
|
invitedUserEmail,
|
|
`${teamOwner.name} invited you to join the team ${team.name} on Cal.com`
|
|
);
|
|
|
|
await expect(
|
|
page.locator(`[data-testid="email-${invitedUserEmail.replace("@", "")}-pending"]`)
|
|
).toHaveCount(1);
|
|
});
|
|
});
|
|
|
|
test("Invited member is assigned to existing managed event, after invitation is accepted", async ({
|
|
page,
|
|
users,
|
|
}) => {
|
|
const t = await localize("en");
|
|
const teamEventSlugAndTitle = "managed-event-test";
|
|
const teamMatesObj = [{ name: "teammate-1" }, { name: "teammate-2" }];
|
|
const teamOwner = await users.create(
|
|
{ name: `team-owner-${Date.now()}` },
|
|
{
|
|
hasTeam: true,
|
|
teamRole: MembershipRole.ADMIN,
|
|
teammates: teamMatesObj,
|
|
schedulingType: SchedulingType.MANAGED,
|
|
teamEventSlug: teamEventSlugAndTitle,
|
|
teamEventTitle: teamEventSlugAndTitle,
|
|
teamEventLength: 30,
|
|
addManagedEventToTeamMates: true,
|
|
assignAllTeamMembers: true,
|
|
}
|
|
);
|
|
const invitedMember = await users.create({
|
|
name: `invited-member-${Date.now()}`,
|
|
email: `invited-member-${Date.now()}@example.com`,
|
|
});
|
|
const { team } = await teamOwner.getFirstTeamMembership();
|
|
|
|
await teamOwner.apiLogin();
|
|
await page.goto(`/settings/teams/${team.id}/settings`);
|
|
await page.goto(`/settings/teams/${team.id}/members`);
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await page.waitForTimeout(500); // Add a small delay to ensure UI is fully loaded
|
|
await page.getByTestId("new-member-button").click();
|
|
await page.locator('input[name="inviteUser"]').fill(invitedMember.email);
|
|
await page.getByText(t("send_invite")).click();
|
|
|
|
await invitedMember.apiLogin();
|
|
await page.goto(`/teams`);
|
|
await page.getByTestId(`accept-invitation-${team.id}`).click();
|
|
const response = await page.waitForResponse("/api/trpc/teams/acceptOrLeave?batch=1");
|
|
expect(response.status()).toBe(200);
|
|
await page.goto(`/event-types`);
|
|
|
|
//ensure managed event-type is created for the invited member
|
|
await expect(page.locator(`text="${teamEventSlugAndTitle}"`)).toBeVisible();
|
|
|
|
//ensure the new event-type created for invited member is child of team event-type
|
|
const parentEventType = await prisma.eventType.findFirst({
|
|
where: {
|
|
slug: teamEventSlugAndTitle,
|
|
teamId: team.id,
|
|
},
|
|
select: {
|
|
children: true,
|
|
},
|
|
});
|
|
expect(parentEventType?.children.find((et) => et.userId === invitedMember.id)).toBeTruthy();
|
|
});
|
|
|
|
test("Auto-accept invitation for existing user", async ({ browser, page, users, emails }) => {
|
|
const t = await localize("en");
|
|
const teamOwner = await users.create({ name: "Invited User" }, { hasTeam: true });
|
|
const { team } = await teamOwner.getFirstTeamMembership();
|
|
const invitedUser = await users.create({
|
|
email: `invited-user-${Date.now()}@example.com`,
|
|
name: "Invited User",
|
|
});
|
|
|
|
await teamOwner.apiLogin();
|
|
await page.goto(`/settings/teams/${team.id}/members`);
|
|
|
|
let inviteLink: string;
|
|
|
|
await test.step("Send invitation to existing user", async () => {
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await page.waitForTimeout(500);
|
|
await page.getByTestId("new-member-button").click();
|
|
await page.locator('input[name="inviteUser"]').fill(invitedUser.email);
|
|
await page.getByText(t("send_invite")).click();
|
|
|
|
inviteLink = await expectInvitationEmailToBeReceived(
|
|
page,
|
|
emails,
|
|
invitedUser.email,
|
|
`${teamOwner.name} invited you to join the team ${team.name} on Cal.com`,
|
|
"teams?token"
|
|
);
|
|
|
|
expect(inviteLink).toContain("autoAccept=true");
|
|
|
|
const membership = await prisma.membership.findFirst({
|
|
where: {
|
|
userId: invitedUser.id,
|
|
teamId: team.id,
|
|
},
|
|
});
|
|
expect(membership?.accepted).toBe(false);
|
|
});
|
|
|
|
await test.step("Auto-accept invitation by clicking link", async () => {
|
|
const [secondContext, secondPage] = await invitedUser.apiLoginOnNewBrowser(browser);
|
|
|
|
await secondPage.goto(inviteLink);
|
|
|
|
await expect(secondPage.getByText("Successfully joined")).toBeVisible();
|
|
|
|
const membership = await prisma.membership.findFirst({
|
|
where: {
|
|
userId: invitedUser.id,
|
|
teamId: team.id,
|
|
},
|
|
});
|
|
expect(membership?.accepted).toBe(true);
|
|
|
|
await secondPage.close();
|
|
await secondContext.close();
|
|
});
|
|
});
|
|
|
|
test("Error when wrong user tries to use invitation link", async ({ browser, page, users, emails }) => {
|
|
const t = await localize("en");
|
|
const teamOwner = await users.create({ name: "Wrong User" }, { hasTeam: true });
|
|
const { team } = await teamOwner.getFirstTeamMembership();
|
|
const invitedUser = await users.create({
|
|
email: `invited-user-${Date.now()}@example.com`,
|
|
name: "Invited User",
|
|
});
|
|
const wrongUser = await users.create({
|
|
email: `wrong-user-${Date.now()}@example.com`,
|
|
name: "Wrong User",
|
|
});
|
|
|
|
await teamOwner.apiLogin();
|
|
await page.goto(`/settings/teams/${team.id}/members`);
|
|
|
|
let inviteLink: string;
|
|
|
|
await test.step("Send invitation to specific user", async () => {
|
|
await page.waitForLoadState("domcontentloaded");
|
|
await page.waitForTimeout(500);
|
|
await page.getByTestId("new-member-button").click();
|
|
await page.locator('input[name="inviteUser"]').fill(invitedUser.email);
|
|
await page.getByText(t("send_invite")).click();
|
|
|
|
inviteLink = await expectInvitationEmailToBeReceived(
|
|
page,
|
|
emails,
|
|
invitedUser.email,
|
|
`${teamOwner.name} invited you to join the team ${team.name} on Cal.com`,
|
|
"teams?token"
|
|
);
|
|
|
|
expect(inviteLink).toContain("autoAccept=true");
|
|
});
|
|
|
|
await test.step("Wrong user tries to use invitation link", async () => {
|
|
const [secondContext, secondPage] = await wrongUser.apiLoginOnNewBrowser(browser);
|
|
|
|
await secondPage.goto(inviteLink);
|
|
|
|
await expect(secondPage.getByText("This invitation is not for your account")).toBeVisible();
|
|
|
|
const membership = await prisma.membership.findFirst({
|
|
where: {
|
|
userId: wrongUser.id,
|
|
teamId: team.id,
|
|
},
|
|
});
|
|
expect(membership).toBeNull();
|
|
|
|
const invitedMembership = await prisma.membership.findFirst({
|
|
where: {
|
|
userId: invitedUser.id,
|
|
teamId: team.id,
|
|
},
|
|
});
|
|
expect(invitedMembership?.accepted).toBe(false);
|
|
|
|
await secondPage.close();
|
|
await secondContext.close();
|
|
});
|
|
});
|
|
});
|