* refactor * use abort controller * address comment * fix tests * fix time * fix tests * fix platform library build
541 lines
17 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|