Files
calendar/packages/features/ee/organizations/lib/server/createOrganizationFromOnboarding.test.ts
T
Benny JooandGitHub 4061bfb3fe fix: refactor i18n loadTranslations and set timeout to 3s (#22878)
* refactor

* use abort controller

* address comment

* fix tests

* fix time

* fix tests

* fix platform library build
2025-08-04 10:30:18 +01:00

541 lines
17 KiB
TypeScript

/**
* Goal is to e2e test createOrganizationFromOnboarding except inviteMembersWithNoInviterPermissionCheck, createTeamsHandler and createDomain
* Those are either already tested or should be tested out separately
*/
import prismock from "../../../../../../tests/libs/__mocks__/prisma";
import { describe, expect, it, vi, beforeEach } from "vitest";
import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService";
import * as constants from "@calcom/lib/constants";
import { createDomain } from "@calcom/lib/domainManager/organization";
import type { OrganizationOnboarding } from "@calcom/prisma/client";
import { MembershipRole } from "@calcom/prisma/enums";
import { createTeamsHandler } from "@calcom/trpc/server/routers/viewer/organizations/createTeams.handler";
import { inviteMembersWithNoInviterPermissionCheck } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler";
import { createOrganizationFromOnboarding } from "./createOrganizationFromOnboarding";
vi.mock("@calcom/ee/common/server/LicenseKeyService", () => ({
LicenseKeySingleton: {
getInstance: vi.fn(),
},
}));
// Mock the external functions
vi.mock("@calcom/features/auth/lib/verifyEmail", () => ({
sendEmailVerification: vi.fn(),
}));
vi.mock("@calcom/emails/email-manager", () => ({
sendOrganizationCreationEmail: vi.fn(),
}));
vi.mock("@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler", () => ({
inviteMembersWithNoInviterPermissionCheck: vi.fn(),
}));
vi.mock("@calcom/trpc/server/routers/viewer/organizations/createTeams.handler", () => ({
createTeamsHandler: vi.fn(),
}));
vi.mock("@calcom/lib/domainManager/organization", () => ({
createDomain: vi.fn(),
}));
vi.mock("@calcom/lib/server/i18n", () => {
return {
getTranslation: async (locale: string, namespace: string) => {
const t = (key: string) => key;
t.locale = locale;
t.namespace = namespace;
return t;
},
};
});
const mockOrganizationOnboarding = {
name: "Test Org",
slug: "test-org",
orgOwnerEmail: "test@example.com",
billingPeriod: "MONTHLY",
seats: 5,
pricePerSeat: 20,
isComplete: false,
stripeCustomerId: "mock_stripe_customer_id",
createdById: 1,
invitedMembers: [{ email: "member1@example.com" }, { email: "member2@example.com" }],
teams: [
{ id: 1, name: "Team To Move", isBeingMigrated: true, slug: "new-team-slug" },
{ id: 2, name: "New Team", isBeingMigrated: false, slug: null },
],
logo: null,
bio: null,
organizationId: null,
isDomainConfigured: false,
isPlatform: false,
};
// Helper functions for creating test scenarios
async function createTestUser(data: {
email: string;
name?: string;
username?: string;
metadata?: any;
onboardingCompleted?: boolean;
emailVerified?: Date | null;
}) {
return prismock.user.create({
data: {
email: data.email,
name: data.name || "Test User",
username: data.username || "testuser",
metadata: data.metadata || {},
completedOnboarding: data.onboardingCompleted,
emailVerified: data.emailVerified,
},
});
}
async function createTestOrganization(data: {
name: string;
slug: string;
isOrganization?: boolean;
metadata?: any;
}) {
return prismock.team.create({
data: {
name: data.name,
slug: data.slug,
isOrganization: data.isOrganization ?? true,
metadata: data.metadata || {},
},
});
}
async function createTestTeam(data: { name: string; slug: string }) {
return prismock.team.create({
data: {
name: data.name,
slug: data.slug,
isOrganization: false,
},
});
}
async function createTestOrganizationOnboarding(
data?: Partial<typeof mockOrganizationOnboarding> & { organizationId?: number | null }
) {
console.log("TEST: Creating organization onboarding", data);
return prismock.organizationOnboarding.create({
data: {
...mockOrganizationOnboarding,
...data,
},
});
}
async function createOnboardingEligibleUserAndOnboarding(data: {
user?: {
email: string;
name?: string;
username?: string;
};
organizationOnboarding?: Partial<typeof mockOrganizationOnboarding> & { organizationId?: number | null };
}) {
const user = await createTestUser({
...data.user,
onboardingCompleted: true,
emailVerified: new Date(),
});
const organizationOnboarding = await createTestOrganizationOnboarding({
...data.organizationOnboarding,
createdById: user.id,
});
return { user, organizationOnboarding };
}
async function createTestMembership(data: { userId: number; teamId: number; role?: MembershipRole }) {
return prismock.membership.create({
data: {
createdAt: new Date(),
userId: data.userId,
teamId: data.teamId,
role: data.role || MembershipRole.MEMBER,
accepted: true,
},
});
}
async function expectOrganizationOnboardingToHaveDataContaining({
onboardingId,
expectedData,
}: {
onboardingId: string;
expectedData: Partial<OrganizationOnboarding>;
}) {
const updatedOrganizationOnboarding = await prismock.organizationOnboarding.findUnique({
where: { id: onboardingId },
});
expect(updatedOrganizationOnboarding).toEqual(expect.objectContaining(expectedData));
}
describe("createOrganizationFromOnboarding", () => {
beforeEach(async () => {
vi.resetAllMocks();
// @ts-expect-error reset is a method on Prismock
await prismock.reset();
});
describe("hosted", () => {
beforeEach(() => {
vi.spyOn(constants, "IS_SELF_HOSTED", "get").mockReturnValue(false);
});
// Skipped because non-existend user creation support isn't there through onboarding now.
it.skip("should create an organization with a non-existent user as owner", async () => {
const organizationOnboarding = await createTestOrganizationOnboarding();
const { organization, owner } = await createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
});
// Verify organization creation
expect(organization).toBeDefined();
expect(organization.name).toBe(organizationOnboarding.name);
expect(organization.slug).toBe(organizationOnboarding.slug);
expect(organization.metadata).toEqual({
orgSeats: 5,
orgPricePerSeat: 20,
billingPeriod: "MONTHLY",
subscriptionId: "mock_subscription_id",
subscriptionItemId: "mock_subscription_item_id",
});
// Verify owner creation
expect(owner).toBeDefined();
expect(owner.email).toBe(organizationOnboarding.orgOwnerEmail);
});
it("should create an organization with an existing user as owner", async () => {
const orgOwnerEmail = "test@example.com";
const existingUser = await createTestUser({
email: orgOwnerEmail,
name: "Existing User",
username: "existinguser",
onboardingCompleted: true,
emailVerified: new Date(),
});
const organizationOnboarding = await createTestOrganizationOnboarding({
orgOwnerEmail,
});
const { organization, owner } = await createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
});
// Verify organization creation
expect(organization).toBeDefined();
expect(organization.name).toBe(organizationOnboarding.name);
expect(organization.slug).toBe(organizationOnboarding.slug);
// Verify owner is the existing user
expect(owner.id).toBe(existingUser.id);
expect(owner.email).toBe(existingUser.email);
expect(createDomain).toHaveBeenCalledWith(organization.slug);
// Verify team creation and member invites still work
expect(inviteMembersWithNoInviterPermissionCheck).toHaveBeenCalledWith(
expect.objectContaining({
orgSlug: organization.slug,
teamId: organization.id,
invitations: organizationOnboarding.invitedMembers?.map((member) => ({
usernameOrEmail: member.email,
role: MembershipRole.MEMBER,
})),
})
);
expect(createTeamsHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
orgId: organization.id,
teamNames: ["New Team"],
moveTeams: expect.arrayContaining([expect.objectContaining({ id: 1, newSlug: "new-team-slug" })]),
}),
})
);
});
it("should reuse existing organization if organizationId is already set in onboarding. Covers the retry scenario where org got created but then error occurred", async () => {
const orgName = "Existing Org";
const orgSlug = "existing-org";
const existingOrg = await createTestOrganization({
name: orgName,
slug: orgSlug,
});
const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
user: {
name: "Existing User",
username: "existinguser",
email: "test@example.com",
},
organizationOnboarding: {
organizationId: existingOrg.id,
name: orgName,
slug: orgSlug,
},
});
const { organization } = await createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
});
// Verify the existing organization was reused
expect(organization.id).toBe(existingOrg.id);
});
it("should throw error if organization with same slug exists", async () => {
const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
user: {
email: "test@example.com",
},
organizationOnboarding: {
slug: "conflicting-org-with-same-slug",
},
});
await createTestOrganization({
name: "Conflicting Org with same slug",
slug: organizationOnboarding.slug,
});
await expect(
createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
})
).rejects.toThrow("organization_url_taken");
});
it("should create team with slugified slug", async () => {
const teamToMove = await createTestTeam({
name: "Sales",
slug: "Company Sales",
});
const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
user: {
email: "test@example.com",
},
organizationOnboarding: {
slug: "UPPERCASE-SLUG",
teams: [{ id: teamToMove.id, name: "Sales", isBeingMigrated: true, slug: "sales-team" }],
},
});
const { organization } = await createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
});
expect(createTeamsHandler).toHaveBeenCalledWith(
expect.objectContaining({
input: expect.objectContaining({
orgId: organization.id,
teamNames: [],
moveTeams: expect.arrayContaining([expect.objectContaining({ id: 1, newSlug: "sales-team" })]),
}),
})
);
});
it("should invite members with isDirectUserAction set to false", async () => {
const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
user: {
username: "org-owner",
email: "owner@example.com",
},
});
// Call createOrganizationFromOnboarding
const result = await createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "sub_123",
paymentSubscriptionItemId: "si_123",
});
// Verify inviteMembersWithNoInviterPermissionCheck was called with isDirectUserAction: false
expect(inviteMembersWithNoInviterPermissionCheck).toHaveBeenCalledWith(
expect.objectContaining({
teamId: result.organization.id,
invitations: [
{ usernameOrEmail: "member1@example.com", role: MembershipRole.MEMBER },
{ usernameOrEmail: "member2@example.com", role: MembershipRole.MEMBER },
],
isDirectUserAction: false,
})
);
});
it("should update stripe customer ID for existing user", async () => {
const { user: existingUser, organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
user: {
email: "test@example.com",
},
organizationOnboarding: {
slug: "conflicting-org-with-same-slug",
},
});
await createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
});
// Verify user's stripe customer ID was updated
const updatedUser = await prismock.user.findUnique({
where: { id: existingUser.id },
});
expect(updatedUser?.metadata).toEqual(
expect.objectContaining({
stripeCustomerId: organizationOnboarding.stripeCustomerId,
})
);
});
it("should create organization in with slug set even if there is a team with same slug of which orgOwner(to-be) is a member", async () => {
const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
user: {
email: "test@example.com",
},
organizationOnboarding: {
slug: "conflicting-org-with-same-slug",
},
});
const teamWithConflictingSlug = await createTestTeam({
name: "TestTeamWithConflictingSlug",
slug: organizationOnboarding.slug,
});
// Make the orgOwner a member of the team
await createTestMembership({
userId: organizationOnboarding.createdById,
teamId: teamWithConflictingSlug.id,
role: MembershipRole.ADMIN,
});
const { organization } = await createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
});
expect(organization.slug).toBe(organizationOnboarding.slug);
});
it("should not create organization if there is a team with same slug of which orgOwner(to-be) is not a member", async () => {
const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
user: {
email: "test@example.com",
},
organizationOnboarding: {
slug: "conflicting-org-with-same-slug",
},
});
await createTestTeam({
name: "TestTeamWithConflictingSlugNotOwnedByOrgOwner",
slug: organizationOnboarding.slug,
});
// Don't make the orgOwner a member of the team
// await createTestMembership({
// userId: organizationOnboarding.createdById,
// teamId: teamWithConflictingSlug.id,
// role: MembershipRole.ADMIN,
// });
await expect(
createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
})
).rejects.toThrow("organization_url_taken");
});
it("should not mark the Onboarding as completed(isComplete=true)", async () => {
const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
user: {
email: "test@example.com",
},
});
await expectOrganizationOnboardingToHaveDataContaining({
onboardingId: organizationOnboarding.id,
expectedData: {
isComplete: false,
},
});
});
});
describe("self-hosted", () => {
beforeEach(() => {
vi.spyOn(constants, "IS_SELF_HOSTED", "get").mockReturnValue(true);
});
it("should fail if the license is invalid", async () => {
vi.mocked(LicenseKeySingleton.getInstance as ReturnType<typeof vi.fn>).mockResolvedValue({
checkLicense: vi.fn().mockResolvedValue(false),
});
const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
user: {
email: "test@example.com",
},
});
await expect(
createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
})
).rejects.toThrow("Self hosted license not valid");
});
it("should create an organization with a valid license", async () => {
vi.mocked(LicenseKeySingleton.getInstance).mockResolvedValue({
checkLicense: vi.fn().mockResolvedValue(true),
});
const { organizationOnboarding } = await createOnboardingEligibleUserAndOnboarding({
user: {
email: "test@example.com",
},
});
const { organization, owner } = await createOrganizationFromOnboarding({
organizationOnboarding,
paymentSubscriptionId: "mock_subscription_id",
paymentSubscriptionItemId: "mock_subscription_item_id",
});
expect(organization).toBeDefined();
expect(owner).toBeDefined();
});
});
});