import prismock from "@calcom/testing/lib/__mocks__/prisma"; import { intentToCreateOrgHandler } from "./intentToCreateOrg.handler"; import { LicenseKeySingleton } from "@calcom/ee/common/server/LicenseKeyService"; import { OrganizationPaymentService } from "@calcom/features/ee/organizations/lib/OrganizationPaymentService"; import { BillingPeriod, UserPermissionRole, CreationSource } from "@calcom/prisma/enums"; import { TRPCError } from "@trpc/server"; import { describe, expect, it, vi, beforeEach } from "vitest"; vi.mock("@calcom/ee/common/server/LicenseKeyService", () => ({ LicenseKeySingleton: { getInstance: vi.fn(), }, })); vi.mock("@calcom/features/ee/organizations/lib/OrganizationPaymentService"); vi.mock("@calcom/features/ee/teams/repositories/TeamRepository", () => ({ TeamRepository: class { constructor() {} findOwnedTeamsByUserId() { return Promise.resolve([]); } findById() { return Promise.resolve({ id: 1, name: "Test Org", slug: "test-org", logoUrl: null, parentId: null, metadata: {}, isOrganization: true, organizationSettings: null, isPlatform: false, }); } }, })); vi.mock("@calcom/trpc/server/routers/viewer/organizations/createTeams.handler", () => ({ createTeamsHandler: vi.fn().mockResolvedValue({}), })); vi.mock("@calcom/trpc/server/routers/viewer/teams/inviteMember/inviteMember.handler", () => ({ inviteMembersWithNoInviterPermissionCheck: vi.fn().mockResolvedValue({}), })); vi.mock("@calcom/i18n/server", () => ({ getTranslation: vi .fn() .mockImplementation(async (locale: string, namespace: string) => { const t = (key: string) => key; t.locale = locale; t.namespace = namespace; return t; }), })); vi.mock("@calcom/lib/domainManager/organization", () => ({ createDomain: vi.fn().mockResolvedValue({}), })); vi.mock("@calcom/emails/organization-email-service", () => ({ sendOrganizationCreationEmail: vi.fn().mockResolvedValue({}), })); vi.mock("@calcom/features/auth/lib/verifyEmail", () => ({ sendEmailVerification: vi.fn().mockResolvedValue({}), })); const mockInput = { name: "Test Org", slug: "test-org", orgOwnerEmail: "test@example.com", billingPeriod: "MONTHLY", seats: 5, pricePerSeat: 20, isPlatform: false, creationSource: "WEBAPP", }; // Helper functions for creating test data async function createTestUser(data: { email: string; name?: string; username?: string; role?: UserPermissionRole; completedOnboarding?: boolean; emailVerified?: Date | null; }) { return prismock.user.create({ data: { email: data.email, name: data.name || "Test User", username: data.username || "testuser", role: data.role || UserPermissionRole.USER, completedOnboarding: data.completedOnboarding ?? false, emailVerified: data.emailVerified ?? null, }, }); } describe("intentToCreateOrgHandler", () => { beforeEach(async () => { vi.resetAllMocks(); await prismock.reset(); vi.mocked(OrganizationPaymentService).mockImplementation(function () { return { createOrganizationOnboarding: vi.fn().mockImplementation(async (data: any) => { return await prismock.organizationOnboarding.create({ data: { id: "onboarding-123", name: data.name, slug: data.slug, orgOwnerEmail: data.orgOwnerEmail, seats: data.seats ?? 10, pricePerSeat: data.pricePerSeat ?? 15, billingPeriod: data.billingPeriod ?? BillingPeriod.MONTHLY, isComplete: false, stripeCustomerId: null, createdById: data.createdByUserId, teams: data.teams ?? [], invitedMembers: data.invitedMembers ?? [], isPlatform: data.isPlatform ?? false, }, }); }), createPaymentIntent: vi.fn().mockResolvedValue({ checkoutUrl: "https://stripe.com/checkout/session", organizationOnboarding: {}, subscription: {}, sessionId: "session-123", }), } as any; }); }); describe("hosted", () => { beforeEach(() => { vi.mock("@calcom/lib/constants", async () => { const actualImport = await vi.importActual("@calcom/lib/constants"); return { ...actualImport, IS_SELF_HOSTED: false, }; }); vi.mocked(LicenseKeySingleton.getInstance).mockResolvedValue({ checkLicense: vi.fn().mockResolvedValue(true), incrementUsage: vi.fn().mockResolvedValue(undefined), }); process.env.STRIPE_ORG_PRODUCT_ID = "prod_test123"; process.env.STRIPE_ORG_MONTHLY_PRICE_ID = "price_test123"; }); it("should allow admin user to create org for another user", async () => { // Create admin user const adminUser = await createTestUser({ email: "admin@example.com", role: UserPermissionRole.ADMIN, }); // Create target org owner with completed onboarding and verified email await createTestUser({ email: mockInput.orgOwnerEmail, completedOnboarding: true, emailVerified: new Date(), }); const result = await intentToCreateOrgHandler({ input: mockInput, ctx: { user: adminUser, }, }); // Admin creating for different user with no teams/invites triggers handover expect(result).toEqual({ userId: expect.any(Number), orgOwnerEmail: mockInput.orgOwnerEmail, name: mockInput.name, slug: mockInput.slug, seats: mockInput.seats, pricePerSeat: mockInput.pricePerSeat, billingPeriod: mockInput.billingPeriod, isPlatform: mockInput.isPlatform, organizationOnboardingId: expect.any(String), checkoutUrl: null, organizationId: null, // Not created yet - handover flow handoverUrl: expect.stringContaining("/settings/organizations/new/resume?onboardingId="), }); // Verify organization onboarding was created const organizationOnboarding = await prismock.organizationOnboarding.findFirst({ where: { slug: mockInput.slug, }, }); expect(organizationOnboarding).toBeDefined(); expect(organizationOnboarding?.name).toBe(mockInput.name); expect(organizationOnboarding?.slug).toBe(mockInput.slug); expect(organizationOnboarding?.orgOwnerEmail).toBe(mockInput.orgOwnerEmail); }); it("should allow user to create org for themselves", async () => { // Create user const user = await createTestUser({ email: mockInput.orgOwnerEmail, completedOnboarding: true, emailVerified: new Date(), }); const result = await intentToCreateOrgHandler({ input: { ...mockInput, // Use default payment settings seats: undefined, pricePerSeat: undefined, }, ctx: { user, }, }); expect(result.userId).toBe(user.id); expect(result.orgOwnerEmail).toBe(user.email); }); it("should throw unauthorized error when no user is logged in", async () => { await expect( intentToCreateOrgHandler({ input: mockInput, ctx: { user: null as any, }, }) ).rejects.toThrow( new TRPCError({ code: "UNAUTHORIZED", message: "You are not authorized.", }) ); }); it("should throw forbidden error when non-admin tries to create org for another user", async () => { // Create non-admin user const nonAdminUser = await createTestUser({ email: "nonadmin@example.com", role: UserPermissionRole.USER, }); await expect( intentToCreateOrgHandler({ input: mockInput, ctx: { user: nonAdminUser, }, }) ).rejects.toThrow( new TRPCError({ code: "FORBIDDEN", message: "You can only create organization where you are the owner", }) ); }); it("should reject non-admin creating org for another user even with isPlatform flag", async () => { const nonAdminUser = await createTestUser({ email: "nonadmin@example.com", role: UserPermissionRole.USER, }); await expect( intentToCreateOrgHandler({ input: { ...mockInput, isPlatform: true }, ctx: { user: nonAdminUser, }, }) ).rejects.toMatchObject({ code: "FORBIDDEN", message: "You can only create organization where you are the owner", }); }); it("should throw error when target user is not found", async () => { // Create admin user const adminUser = await createTestUser({ email: "admin@example.com", role: UserPermissionRole.ADMIN, }); await expect( intentToCreateOrgHandler({ input: mockInput, ctx: { user: adminUser, }, }) ).rejects.toThrow( new TRPCError({ code: "BAD_REQUEST", message: `No user found with email ${mockInput.orgOwnerEmail}`, }) ); }); it("should throw error when organization onboarding already exists and is complete", async () => { // Create admin user const adminUser = await createTestUser({ email: "admin@example.com", role: UserPermissionRole.ADMIN, }); // Create target org owner await createTestUser({ email: mockInput.orgOwnerEmail, completedOnboarding: true, emailVerified: new Date(), }); // Create existing COMPLETE organization onboarding await prismock.organizationOnboarding.create({ data: { name: mockInput.name, slug: mockInput.slug, orgOwnerEmail: mockInput.orgOwnerEmail, billingPeriod: mockInput.billingPeriod, seats: mockInput.seats, pricePerSeat: mockInput.pricePerSeat, createdById: adminUser.id, isComplete: true, // Must be complete to throw error }, }); await expect( intentToCreateOrgHandler({ input: mockInput, ctx: { user: adminUser, }, }) ).rejects.toThrow("organization_onboarding_already_exists"); }); }); // This endpoint doesn't handle platform organizations as that is completely different flow at the moment // eslint-disable-next-line playwright/no-skipped-test describe.skip("platform organization tests"); describe("self hosted", () => { beforeEach(() => { vi.mock("@calcom/lib/constants", async () => { const actualImport = await vi.importActual("@calcom/lib/constants"); return { ...actualImport, IS_SELF_HOSTED: true, }; }); }); it("should throw error when license is not valid", async () => { vi.mocked(LicenseKeySingleton.getInstance).mockResolvedValue({ checkLicense: vi.fn().mockResolvedValue(false), incrementUsage: vi.fn().mockResolvedValue(undefined), }); const adminUser = await createTestUser({ email: "admin@example.com", role: UserPermissionRole.ADMIN, }); await expect( intentToCreateOrgHandler({ input: mockInput, ctx: { user: adminUser, }, }) ).rejects.toThrow("License is not valid"); }); it("should allow admin user to create org for another user", async () => { vi.mocked(LicenseKeySingleton.getInstance).mockResolvedValue({ checkLicense: vi.fn().mockResolvedValue(true), incrementUsage: vi.fn().mockResolvedValue(undefined), }); // Create admin user const adminUser = await createTestUser({ email: "admin@example.com", role: UserPermissionRole.ADMIN, }); // Create target org owner with completed onboarding and verified email await createTestUser({ email: mockInput.orgOwnerEmail, completedOnboarding: true, emailVerified: new Date(), }); const result = await intentToCreateOrgHandler({ input: mockInput, ctx: { user: adminUser, }, }); // Admin creating for different user with no teams/invites triggers handover (same as hosted) expect(result).toEqual({ userId: expect.any(Number), orgOwnerEmail: mockInput.orgOwnerEmail, name: mockInput.name, slug: mockInput.slug, seats: mockInput.seats, pricePerSeat: mockInput.pricePerSeat, billingPeriod: mockInput.billingPeriod, isPlatform: mockInput.isPlatform, organizationOnboardingId: expect.any(String), checkoutUrl: null, organizationId: null, // Not created yet - handover flow handoverUrl: expect.stringContaining("/settings/organizations/new/resume?onboardingId="), }); // Verify organization onboarding was created const organizationOnboarding = await prismock.organizationOnboarding.findFirst({ where: { slug: mockInput.slug, }, }); expect(organizationOnboarding).toBeDefined(); expect(organizationOnboarding?.name).toBe(mockInput.name); expect(organizationOnboarding?.slug).toBe(mockInput.slug); expect(organizationOnboarding?.orgOwnerEmail).toBe(mockInput.orgOwnerEmail); }); it("should handle teams and invites in the request", async () => { vi.mocked(LicenseKeySingleton.getInstance).mockResolvedValue({ checkLicense: vi.fn().mockResolvedValue(true), incrementUsage: vi.fn().mockResolvedValue(undefined), }); const adminUser = await createTestUser({ email: "admin@example.com", role: UserPermissionRole.ADMIN, }); await createTestUser({ email: mockInput.orgOwnerEmail, completedOnboarding: true, emailVerified: new Date(), }); const inputWithTeamsAndInvites = { ...mockInput, teams: [ { id: -1, name: "Engineering", isBeingMigrated: false, slug: null }, { id: -1, name: "Sales", isBeingMigrated: false, slug: null }, ], invitedMembers: [ { email: "member1@example.com", name: "Member 1" }, { email: "member2@example.com", name: "Member 2" }, ], }; const result = await intentToCreateOrgHandler({ input: inputWithTeamsAndInvites, ctx: { user: adminUser, }, }); expect(result.organizationOnboardingId).toBeDefined(); const organizationOnboarding = await prismock.organizationOnboarding.findFirst({ where: { slug: mockInput.slug, }, }); expect(organizationOnboarding).toBeDefined(); expect(organizationOnboarding?.teams).toEqual(inputWithTeamsAndInvites.teams); expect(organizationOnboarding?.invitedMembers).toEqual(inputWithTeamsAndInvites.invitedMembers); }); it("should preserve teamName, teamId, and role in invites payload", async () => { vi.mocked(LicenseKeySingleton.getInstance).mockResolvedValue({ checkLicense: vi.fn().mockResolvedValue(true), incrementUsage: vi.fn().mockResolvedValue(undefined), }); const adminUser = await createTestUser({ email: "admin@example.com", role: UserPermissionRole.ADMIN, }); await createTestUser({ email: mockInput.orgOwnerEmail, completedOnboarding: true, emailVerified: new Date(), }); // This matches the exact payload from the frontend const inputWithTeamsAndInvites = { ...mockInput, teams: [ { id: -1, name: "New", isBeingMigrated: false, slug: null }, { id: -1, name: "team", isBeingMigrated: false, slug: null }, ], invitedMembers: [ { email: "new@new.com", teamName: "new", teamId: -1, role: "ADMIN" }, { email: "team@new.com", teamName: "team", teamId: -1, role: "ADMIN", }, ], }; const result = await intentToCreateOrgHandler({ input: inputWithTeamsAndInvites, ctx: { user: adminUser, }, }); expect(result.organizationOnboardingId).toBeDefined(); const organizationOnboarding = await prismock.organizationOnboarding.findFirst({ where: { slug: mockInput.slug, }, }); expect(organizationOnboarding).toBeDefined(); expect(organizationOnboarding?.teams).toEqual(inputWithTeamsAndInvites.teams); // Verify invitedMembers are stored with all fields including teamName, teamId, and role expect(organizationOnboarding?.invitedMembers).toBeDefined(); expect(organizationOnboarding?.invitedMembers).toHaveLength(2); const invitedMembers = organizationOnboarding?.invitedMembers as any[]; expect(invitedMembers[0]).toMatchObject({ email: "new@new.com", teamName: "new", teamId: -1, role: "ADMIN", }); expect(invitedMembers[1]).toMatchObject({ email: "team@new.com", teamName: "team", teamId: -1, role: "ADMIN", }); }); }); });