* 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>
304 lines
11 KiB
TypeScript
304 lines
11 KiB
TypeScript
import { CreditsRepository } from "@calcom/features/credits/repositories/CreditsRepository";
|
|
import { sendVerificationCode } from "@calcom/features/ee/workflows/lib/reminders/verifyPhoneNumber";
|
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler";
|
|
import { sendVerificationCodeHandler } from "./sendVerificationCode.handler";
|
|
|
|
vi.mock("@calcom/lib/checkRateLimitAndThrowError");
|
|
vi.mock("@calcom/features/credits/repositories/CreditsRepository");
|
|
vi.mock("@calcom/features/ee/workflows/lib/reminders/verifyPhoneNumber");
|
|
vi.mock("../teams/hasTeamPlan.handler");
|
|
|
|
vi.mock("@calcom/prisma", () => ({
|
|
default: {},
|
|
prisma: {},
|
|
}));
|
|
|
|
describe("sendVerificationCodeHandler", () => {
|
|
const mockUser = {
|
|
id: 123,
|
|
name: "Test User",
|
|
email: "test@example.com",
|
|
metadata: {},
|
|
};
|
|
|
|
const mockCtx = {
|
|
user: mockUser,
|
|
};
|
|
|
|
const mockInput = {
|
|
phoneNumber: "+1234567890",
|
|
};
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
vi.mocked(checkRateLimitAndThrowError).mockResolvedValue({
|
|
success: true,
|
|
remaining: 99,
|
|
limit: 100,
|
|
reset: Date.now() + 60 * 1000,
|
|
});
|
|
vi.mocked(CreditsRepository.findCreditBalance).mockResolvedValue({
|
|
id: 1,
|
|
additionalCredits: 100,
|
|
balance: 100,
|
|
userId: mockUser.id,
|
|
teamId: null,
|
|
limitReachedAt: null,
|
|
warningSentAt: null,
|
|
lockedAt: null,
|
|
});
|
|
vi.mocked(hasTeamPlanHandler).mockResolvedValue({ hasTeamPlan: false });
|
|
vi.mocked(sendVerificationCode).mockResolvedValue({ status: "pending" });
|
|
});
|
|
|
|
describe("Rate Limiting", () => {
|
|
it("should check SMS rate limit with 'sms' type before processing", async () => {
|
|
await sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput });
|
|
|
|
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
|
|
identifier: `sms:verification:${mockUser.id}`,
|
|
rateLimitingType: "sms",
|
|
});
|
|
});
|
|
|
|
it("should check SMS rate limit with 'smsMonth' type before processing", async () => {
|
|
await sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput });
|
|
|
|
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
|
|
identifier: `sms:verification:${mockUser.id}`,
|
|
rateLimitingType: "smsMonth",
|
|
});
|
|
});
|
|
|
|
it("should check both rate limits in order (sms first, then smsMonth)", async () => {
|
|
const callOrder: string[] = [];
|
|
vi.mocked(checkRateLimitAndThrowError).mockImplementation(async ({ rateLimitingType }) => {
|
|
callOrder.push(rateLimitingType as string);
|
|
return { success: true, remaining: 99, limit: 100, reset: Date.now() + 60 * 1000 };
|
|
});
|
|
|
|
await sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput });
|
|
|
|
expect(callOrder).toEqual(["sms", "smsMonth"]);
|
|
});
|
|
|
|
it("should check rate limits before billing gate (credits check)", async () => {
|
|
const callOrder: string[] = [];
|
|
vi.mocked(checkRateLimitAndThrowError).mockImplementation(async () => {
|
|
callOrder.push("rateLimit");
|
|
return { success: true, remaining: 99, limit: 100, reset: Date.now() + 60 * 1000 };
|
|
});
|
|
vi.mocked(CreditsRepository.findCreditBalance).mockImplementation(async () => {
|
|
callOrder.push("credits");
|
|
return {
|
|
id: 1,
|
|
additionalCredits: 100,
|
|
balance: 100,
|
|
userId: mockUser.id,
|
|
teamId: null,
|
|
limitReachedAt: null,
|
|
warningSentAt: null,
|
|
lockedAt: null,
|
|
};
|
|
});
|
|
|
|
await sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput });
|
|
|
|
expect(callOrder.indexOf("rateLimit")).toBeLessThan(callOrder.indexOf("credits"));
|
|
});
|
|
|
|
it("should throw and not check billing when sms rate limit fails", async () => {
|
|
vi.mocked(checkRateLimitAndThrowError).mockRejectedValueOnce(
|
|
new Error("Rate limit exceeded. Try again in 60 seconds.")
|
|
);
|
|
|
|
await expect(sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput })).rejects.toThrow(
|
|
"Rate limit exceeded"
|
|
);
|
|
|
|
expect(CreditsRepository.findCreditBalance).not.toHaveBeenCalled();
|
|
expect(hasTeamPlanHandler).not.toHaveBeenCalled();
|
|
expect(sendVerificationCode).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should throw and not check billing when smsMonth rate limit fails", async () => {
|
|
vi.mocked(checkRateLimitAndThrowError)
|
|
.mockResolvedValueOnce({ success: true, remaining: 99, limit: 100, reset: Date.now() })
|
|
.mockRejectedValueOnce(new Error("Rate limit exceeded. Try again in 2592000 seconds."));
|
|
|
|
await expect(sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput })).rejects.toThrow(
|
|
"Rate limit exceeded"
|
|
);
|
|
|
|
expect(CreditsRepository.findCreditBalance).not.toHaveBeenCalled();
|
|
expect(hasTeamPlanHandler).not.toHaveBeenCalled();
|
|
expect(sendVerificationCode).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("Authorization - Premium Users", () => {
|
|
it("should allow premium users to send verification code", async () => {
|
|
const premiumUser = {
|
|
...mockUser,
|
|
metadata: { isPremium: true },
|
|
};
|
|
|
|
await sendVerificationCodeHandler({ ctx: { user: premiumUser }, input: mockInput });
|
|
|
|
expect(sendVerificationCode).toHaveBeenCalledWith(mockInput.phoneNumber);
|
|
// Premium users skip team plan check
|
|
expect(hasTeamPlanHandler).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should allow premium users even with no additional credits", async () => {
|
|
const premiumUser = {
|
|
...mockUser,
|
|
metadata: { isPremium: true },
|
|
};
|
|
vi.mocked(CreditsRepository.findCreditBalance).mockResolvedValue({
|
|
id: 1,
|
|
additionalCredits: 0,
|
|
balance: 0,
|
|
userId: mockUser.id,
|
|
teamId: null,
|
|
limitReachedAt: null,
|
|
warningSentAt: null,
|
|
lockedAt: null,
|
|
});
|
|
|
|
await sendVerificationCodeHandler({ ctx: { user: premiumUser }, input: mockInput });
|
|
|
|
expect(sendVerificationCode).toHaveBeenCalledWith(mockInput.phoneNumber);
|
|
});
|
|
});
|
|
|
|
describe("Authorization - Team Plan Users", () => {
|
|
it("should allow team plan users to send verification code", async () => {
|
|
vi.mocked(hasTeamPlanHandler).mockResolvedValue({ hasTeamPlan: true });
|
|
vi.mocked(CreditsRepository.findCreditBalance).mockResolvedValue({
|
|
id: 1,
|
|
additionalCredits: 0,
|
|
balance: 0,
|
|
userId: mockUser.id,
|
|
teamId: null,
|
|
limitReachedAt: null,
|
|
warningSentAt: null,
|
|
lockedAt: null,
|
|
});
|
|
|
|
await sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput });
|
|
|
|
expect(sendVerificationCode).toHaveBeenCalledWith(mockInput.phoneNumber);
|
|
});
|
|
|
|
it("should check team plan for non-premium users", async () => {
|
|
await sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput });
|
|
|
|
expect(hasTeamPlanHandler).toHaveBeenCalledWith({ ctx: mockCtx });
|
|
});
|
|
});
|
|
|
|
describe("Authorization - Users with Credits", () => {
|
|
it("should allow users with additional credits to send verification code", async () => {
|
|
vi.mocked(CreditsRepository.findCreditBalance).mockResolvedValue({
|
|
id: 1,
|
|
additionalCredits: 50,
|
|
balance: 50,
|
|
userId: mockUser.id,
|
|
teamId: null,
|
|
limitReachedAt: null,
|
|
warningSentAt: null,
|
|
lockedAt: null,
|
|
});
|
|
vi.mocked(hasTeamPlanHandler).mockResolvedValue({ hasTeamPlan: false });
|
|
|
|
await sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput });
|
|
|
|
expect(sendVerificationCode).toHaveBeenCalledWith(mockInput.phoneNumber);
|
|
});
|
|
});
|
|
|
|
describe("Authorization - Unauthorized Users", () => {
|
|
it("should throw UNAUTHORIZED when user is not premium, has no team plan, and no credits", async () => {
|
|
vi.mocked(CreditsRepository.findCreditBalance).mockResolvedValue({
|
|
id: 1,
|
|
additionalCredits: 0,
|
|
balance: 0,
|
|
userId: mockUser.id,
|
|
teamId: null,
|
|
limitReachedAt: null,
|
|
warningSentAt: null,
|
|
lockedAt: null,
|
|
});
|
|
vi.mocked(hasTeamPlanHandler).mockResolvedValue({ hasTeamPlan: false });
|
|
|
|
await expect(sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput })).rejects.toThrow(
|
|
new TRPCError({ code: "UNAUTHORIZED" })
|
|
);
|
|
|
|
expect(sendVerificationCode).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("should throw UNAUTHORIZED when creditBalance has negative additionalCredits", async () => {
|
|
vi.mocked(CreditsRepository.findCreditBalance).mockResolvedValue({
|
|
id: 1,
|
|
additionalCredits: -5,
|
|
balance: 0,
|
|
userId: mockUser.id,
|
|
teamId: null,
|
|
limitReachedAt: null,
|
|
warningSentAt: null,
|
|
lockedAt: null,
|
|
});
|
|
vi.mocked(hasTeamPlanHandler).mockResolvedValue({ hasTeamPlan: false });
|
|
|
|
await expect(sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput })).rejects.toThrow(
|
|
new TRPCError({ code: "UNAUTHORIZED" })
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Send Verification Code", () => {
|
|
it("should call sendVerificationCode with the phone number from input", async () => {
|
|
await sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput });
|
|
|
|
expect(sendVerificationCode).toHaveBeenCalledWith(mockInput.phoneNumber);
|
|
});
|
|
|
|
it("should return the result from sendVerificationCode", async () => {
|
|
const expectedResult = { status: "pending", sid: "verification-sid" };
|
|
vi.mocked(sendVerificationCode).mockResolvedValue(expectedResult);
|
|
|
|
const result = await sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput });
|
|
|
|
expect(result).toEqual(expectedResult);
|
|
});
|
|
});
|
|
|
|
describe("Edge Cases", () => {
|
|
it("should handle null credit balance gracefully", async () => {
|
|
vi.mocked(CreditsRepository.findCreditBalance).mockResolvedValue(null);
|
|
vi.mocked(hasTeamPlanHandler).mockResolvedValue({ hasTeamPlan: true });
|
|
|
|
await sendVerificationCodeHandler({ ctx: mockCtx, input: mockInput });
|
|
|
|
expect(sendVerificationCode).toHaveBeenCalledWith(mockInput.phoneNumber);
|
|
});
|
|
|
|
it("should handle user without metadata gracefully", async () => {
|
|
const userWithoutMetadata = {
|
|
...mockUser,
|
|
metadata: undefined,
|
|
};
|
|
vi.mocked(hasTeamPlanHandler).mockResolvedValue({ hasTeamPlan: true });
|
|
|
|
await sendVerificationCodeHandler({ ctx: { user: userWithoutMetadata }, input: mockInput });
|
|
|
|
expect(sendVerificationCode).toHaveBeenCalledWith(mockInput.phoneNumber);
|
|
});
|
|
});
|
|
});
|