* fix: add missing vi.mock() calls to prevent vitest worker shutdown flakiness
Add vi.mock() calls for modules that trigger background network requests
or database connections during import. These transitive imports can cause
the vitest worker RPC to shut down while pending fetch/network operations
are still in flight, resulting in flaky test failures with:
Error: [vitest-worker]: Closing rpc while "fetch" was pending
The primary modules mocked are:
- @calcom/app-store/delegationCredential (triggers credential lookups)
- @calcom/prisma (triggers database initialization)
- @calcom/features/calendars/lib/CalendarManager (triggers calendar API calls)
- @calcom/features/auth/lib/verifyEmail (triggers email service)
- @calcom/lib/domainManager/organization (triggers domain lookups)
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: remove conflicting empty prisma mocks from files with prismock/prismaMock setups
- Remove vi.mock('@calcom/prisma', () => ({ default: {}, prisma: {} })) from 28 files
that already have prismock/prismaMock test doubles. Vitest hoists all vi.mock() calls
and the last one wins, so these empty mocks were overriding the functional test doubles.
- Fix CalendarSubscriptionService.test.ts to reuse the shared mock from
__mocks__/delegationCredential instead of creating a new unconfigured vi.fn()
- Remove DelegationCredentialRepository.test.ts empty prisma mock (different pattern)
- Remove vi.mock from inside beforeEach in intentToCreateOrg.handler.test.ts
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: add comprehensive delegationCredential mock exports to prevent CI test failures
The vi.mock blocks for @calcom/app-store/delegationCredential were missing
exports that the code under test transitively imports (e.g.
enrichUsersWithDelegationCredentials, enrichUserWithDelegationCredentialsIncludeServiceAccountKey,
buildAllCredentials, getFirstDelegationConferencingCredentialAppLocation).
Added all exports with passthrough implementations so the booking flow
works correctly without triggering real network requests.
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: correct credential mock return shapes to match real module API
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
* fix: revert unintended yarn.lock changes
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
---------
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
432 lines
12 KiB
TypeScript
432 lines
12 KiB
TypeScript
import prismock from "@calcom/testing/lib/__mocks__/prisma";
|
|
|
|
import type { DirectorySyncEvent } from "@boxyhq/saml-jackson";
|
|
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
|
|
import { IdentityProvider, MembershipRole } from "@calcom/prisma/enums";
|
|
|
|
import handleUserEvents from "./handleUserEvents";
|
|
|
|
vi.mock("@calcom/lib/logger", () => ({
|
|
default: {
|
|
getSubLogger: () => ({
|
|
debug: vi.fn(),
|
|
error: vi.fn(),
|
|
warn: vi.fn(),
|
|
}),
|
|
},
|
|
}));
|
|
|
|
vi.mock("@calcom/i18n/server", () => ({
|
|
getTranslation: vi.fn().mockResolvedValue((key: string) => key),
|
|
}));
|
|
|
|
vi.mock("@calcom/features/ee/teams/lib/inviteMemberUtils", () => ({
|
|
getTeamOrThrow: vi.fn(),
|
|
sendExistingUserTeamInviteEmails: vi.fn(),
|
|
sendSignupToOrganizationEmail: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./assignValueToUser", () => ({
|
|
assignValueToUserInOrgBulk: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./users/createUsersAndConnectToOrg", () => ({
|
|
default: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("./users/inviteExistingUserToOrg", () => ({
|
|
default: vi.fn().mockResolvedValue({
|
|
id: 1,
|
|
username: "testuser",
|
|
email: "test@example.com",
|
|
}),
|
|
}));
|
|
|
|
vi.mock("./removeUserFromOrg", () => ({
|
|
default: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@calcom/features/users/repositories/UserRepository", () => ({
|
|
UserRepository: vi.fn().mockImplementation(function () {
|
|
return {
|
|
isAMemberOfOrganization: vi.fn().mockResolvedValue(false),
|
|
};
|
|
}),
|
|
}));
|
|
async function createMockOrganization({ id, name, slug }: { id: number; name: string; slug: string }) {
|
|
return prismock.team.create({
|
|
data: {
|
|
id,
|
|
name,
|
|
slug,
|
|
isOrganization: true,
|
|
},
|
|
});
|
|
}
|
|
|
|
async function createMockUser({ email, organizationId }: { email: string; organizationId: number | null }) {
|
|
return prismock.user.create({
|
|
data: {
|
|
email,
|
|
username: email.split("@")[0],
|
|
organizationId,
|
|
completedOnboarding: true,
|
|
identityProvider: IdentityProvider.CAL,
|
|
locale: "en",
|
|
},
|
|
});
|
|
}
|
|
|
|
async function createMockMembership({
|
|
userId,
|
|
teamId,
|
|
role = MembershipRole.MEMBER,
|
|
}: {
|
|
userId: number;
|
|
teamId: number;
|
|
role?: MembershipRole;
|
|
}) {
|
|
return prismock.membership.create({
|
|
data: {
|
|
userId,
|
|
teamId,
|
|
role,
|
|
accepted: true,
|
|
disableImpersonation: false,
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("handleUserEvents", () => {
|
|
const directoryId = "test-directory-id";
|
|
const organizationId = 1001;
|
|
const organizationName = "Test Organization";
|
|
const organizationSlug = "test-org";
|
|
|
|
beforeEach(async () => {
|
|
vi.clearAllMocks();
|
|
|
|
await createMockOrganization({
|
|
id: organizationId,
|
|
name: organizationName,
|
|
slug: organizationSlug,
|
|
});
|
|
|
|
const { getTeamOrThrow } = await import("@calcom/features/ee/teams/lib/inviteMemberUtils");
|
|
vi.mocked(getTeamOrThrow).mockResolvedValue({
|
|
id: organizationId,
|
|
name: organizationName,
|
|
slug: organizationSlug,
|
|
isOrganization: true,
|
|
parent: null,
|
|
parentId: null,
|
|
metadata: null,
|
|
} as Awaited<ReturnType<typeof getTeamOrThrow>>);
|
|
});
|
|
|
|
describe("Cross-tenant hijack prevention", () => {
|
|
it("should throw an error when user belongs to a different organization", async () => {
|
|
const userEmail = "user@example.com";
|
|
const differentOrgId = 2002;
|
|
|
|
await createMockOrganization({
|
|
id: differentOrgId,
|
|
name: "Different Organization",
|
|
slug: "different-org",
|
|
});
|
|
|
|
await createMockUser({
|
|
email: userEmail,
|
|
organizationId: 9999,
|
|
});
|
|
|
|
const event: DirectorySyncEvent = {
|
|
event: "user.created",
|
|
tenant: "test-tenant",
|
|
directory_id: directoryId,
|
|
data: {
|
|
id: "user-123",
|
|
email: userEmail,
|
|
first_name: "Test",
|
|
last_name: "User",
|
|
active: true,
|
|
raw: {
|
|
schemas: [],
|
|
},
|
|
},
|
|
};
|
|
|
|
await expect(handleUserEvents(event, organizationId)).rejects.toThrow(
|
|
"User belongs to another organization."
|
|
);
|
|
});
|
|
|
|
it("should succeed when user belongs to the correct organization", async () => {
|
|
const userEmail = "user@example.com";
|
|
|
|
const user = await createMockUser({
|
|
email: userEmail,
|
|
organizationId: organizationId,
|
|
});
|
|
|
|
await createMockMembership({
|
|
userId: user.id,
|
|
teamId: organizationId,
|
|
});
|
|
|
|
const event: DirectorySyncEvent = {
|
|
event: "user.created",
|
|
tenant: "test-tenant",
|
|
directory_id: directoryId,
|
|
data: {
|
|
id: "user-123",
|
|
email: userEmail,
|
|
first_name: "Test",
|
|
last_name: "User",
|
|
active: true,
|
|
raw: {
|
|
schemas: [],
|
|
},
|
|
},
|
|
};
|
|
|
|
const { UserRepository } = await import("@calcom/features/users/repositories/UserRepository");
|
|
vi.mocked(UserRepository).mockImplementation(function () {
|
|
return {
|
|
isAMemberOfOrganization: vi.fn().mockResolvedValue(true),
|
|
} as unknown as InstanceType<typeof UserRepository>;
|
|
});
|
|
|
|
await expect(handleUserEvents(event, organizationId)).resolves.not.toThrow();
|
|
});
|
|
|
|
it("should pass when user has no organizationId (allow existing user to be added to an org)", async () => {
|
|
const userEmail = "legacy@example.com";
|
|
|
|
await createMockUser({
|
|
email: userEmail,
|
|
organizationId: null,
|
|
});
|
|
|
|
const event: DirectorySyncEvent = {
|
|
event: "user.created",
|
|
tenant: "test-tenant",
|
|
directory_id: directoryId,
|
|
data: {
|
|
id: "user-123",
|
|
email: userEmail,
|
|
first_name: "Legacy",
|
|
last_name: "User",
|
|
active: true,
|
|
raw: {
|
|
schemas: [],
|
|
},
|
|
},
|
|
};
|
|
|
|
await expect(handleUserEvents(event, organizationId)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it("should succeed when user does not exist yet (new user creation)", async () => {
|
|
const userEmail = "newuser@example.com";
|
|
|
|
const event: DirectorySyncEvent = {
|
|
event: "user.created",
|
|
tenant: "test-tenant",
|
|
directory_id: directoryId,
|
|
data: {
|
|
id: "user-123",
|
|
email: userEmail,
|
|
first_name: "New",
|
|
last_name: "User",
|
|
active: true,
|
|
raw: {
|
|
schemas: [],
|
|
},
|
|
},
|
|
};
|
|
|
|
const createUsersAndConnectToOrg = (await import("./users/createUsersAndConnectToOrg")).default;
|
|
vi.mocked(createUsersAndConnectToOrg).mockResolvedValue(undefined);
|
|
|
|
await expect(handleUserEvents(event, organizationId)).resolves.not.toThrow();
|
|
|
|
expect(createUsersAndConnectToOrg).toHaveBeenCalledWith({
|
|
createUsersAndConnectToOrgProps: {
|
|
emailsToCreate: [userEmail],
|
|
identityProvider: IdentityProvider.CAL,
|
|
identityProviderId: null,
|
|
},
|
|
org: expect.objectContaining({
|
|
id: organizationId,
|
|
name: organizationName,
|
|
}),
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("User activation and deactivation", () => {
|
|
it("should invite existing user when active is true and user is not a member", async () => {
|
|
const userEmail = "user@example.com";
|
|
|
|
await createMockUser({
|
|
email: userEmail,
|
|
organizationId: organizationId,
|
|
});
|
|
|
|
const event: DirectorySyncEvent = {
|
|
event: "user.updated",
|
|
tenant: "test-tenant",
|
|
directory_id: directoryId,
|
|
data: {
|
|
id: "user-123",
|
|
email: userEmail,
|
|
first_name: "Test",
|
|
last_name: "User",
|
|
active: true,
|
|
raw: {
|
|
schemas: [],
|
|
},
|
|
},
|
|
};
|
|
|
|
const { UserRepository } = await import("@calcom/features/users/repositories/UserRepository");
|
|
vi.mocked(UserRepository).mockImplementation(function () {
|
|
return {
|
|
isAMemberOfOrganization: vi.fn().mockResolvedValue(false),
|
|
} as unknown as InstanceType<typeof UserRepository>;
|
|
});
|
|
|
|
const inviteExistingUserToOrg = (await import("./users/inviteExistingUserToOrg")).default;
|
|
const sendExistingUserTeamInviteEmails = (
|
|
await import("@calcom/features/ee/teams/lib/inviteMemberUtils")
|
|
).sendExistingUserTeamInviteEmails;
|
|
|
|
await handleUserEvents(event, organizationId);
|
|
|
|
expect(inviteExistingUserToOrg).toHaveBeenCalled();
|
|
expect(sendExistingUserTeamInviteEmails).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should remove user from organization when active is false", async () => {
|
|
const userEmail = "user@example.com";
|
|
|
|
const user = await createMockUser({
|
|
email: userEmail,
|
|
organizationId: organizationId,
|
|
});
|
|
|
|
const event: DirectorySyncEvent = {
|
|
event: "user.updated",
|
|
tenant: "test-tenant",
|
|
directory_id: directoryId,
|
|
data: {
|
|
id: "user-123",
|
|
email: userEmail,
|
|
first_name: "Test",
|
|
last_name: "User",
|
|
active: false,
|
|
raw: {
|
|
schemas: [],
|
|
},
|
|
},
|
|
};
|
|
|
|
const removeUserFromOrg = (await import("./removeUserFromOrg")).default;
|
|
|
|
await handleUserEvents(event, organizationId);
|
|
|
|
expect(removeUserFromOrg).toHaveBeenCalledWith({
|
|
userId: user.id,
|
|
orgId: organizationId,
|
|
});
|
|
});
|
|
|
|
it("should sync custom attributes when user is already a member and active", async () => {
|
|
const userEmail = "user@example.com";
|
|
|
|
const user = await createMockUser({
|
|
email: userEmail,
|
|
organizationId: organizationId,
|
|
});
|
|
|
|
await createMockMembership({
|
|
userId: user.id,
|
|
teamId: organizationId,
|
|
});
|
|
|
|
const event: DirectorySyncEvent = {
|
|
event: "user.updated",
|
|
tenant: "test-tenant",
|
|
directory_id: directoryId,
|
|
data: {
|
|
id: "user-123",
|
|
email: userEmail,
|
|
first_name: "Test",
|
|
last_name: "User",
|
|
active: true,
|
|
raw: {
|
|
schemas: ["custom:enterprise"],
|
|
"custom:enterprise": {
|
|
department: "Engineering",
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const { UserRepository } = await import("@calcom/features/users/repositories/UserRepository");
|
|
vi.mocked(UserRepository).mockImplementation(function () {
|
|
return {
|
|
isAMemberOfOrganization: vi.fn().mockResolvedValue(true),
|
|
} as unknown as InstanceType<typeof UserRepository>;
|
|
});
|
|
|
|
const { assignValueToUserInOrgBulk } = await import("./assignValueToUser");
|
|
|
|
await handleUserEvents(event, organizationId);
|
|
|
|
expect(assignValueToUserInOrgBulk).toHaveBeenCalledWith({
|
|
orgId: organizationId,
|
|
userId: user.id,
|
|
attributeLabelToValueMap: {
|
|
department: "Engineering",
|
|
},
|
|
updater: {
|
|
dsyncId: directoryId,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Error handling", () => {
|
|
it("should throw an error when organization is not found", async () => {
|
|
const userEmail = "user@example.com";
|
|
const nonExistentOrgId = 9999;
|
|
|
|
const event: DirectorySyncEvent = {
|
|
event: "user.created",
|
|
tenant: "test-tenant",
|
|
directory_id: directoryId,
|
|
data: {
|
|
id: "user-123",
|
|
email: userEmail,
|
|
first_name: "Test",
|
|
last_name: "User",
|
|
active: true,
|
|
raw: {
|
|
schemas: [],
|
|
},
|
|
},
|
|
};
|
|
|
|
const { getTeamOrThrow } = await import("@calcom/features/ee/teams/lib/inviteMemberUtils");
|
|
vi.mocked(getTeamOrThrow).mockResolvedValue(
|
|
null as unknown as Awaited<ReturnType<typeof getTeamOrThrow>>
|
|
);
|
|
|
|
await expect(handleUserEvents(event, nonExistentOrgId)).rejects.toThrow("Org not found");
|
|
});
|
|
});
|
|
});
|