* 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>
254 lines
8.7 KiB
TypeScript
254 lines
8.7 KiB
TypeScript
import { handleAutoLock } from "@calcom/features/ee/api-keys/lib/autoLock";
|
|
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
|
|
import { HttpError } from "@calcom/lib/http-error";
|
|
import type { RatelimitResponse } from "@unkey/ratelimit";
|
|
import type { Request, Response } from "express";
|
|
import type { NextApiRequest, NextApiResponse } from "next";
|
|
import { createMocks } from "node-mocks-http";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { rateLimitApiKey } from "~/lib/helpers/rateLimitApiKey";
|
|
|
|
type CustomNextApiRequest = NextApiRequest & Request;
|
|
type CustomNextApiResponse = NextApiResponse & Response;
|
|
|
|
const testUserId = 123;
|
|
|
|
vi.mock("@calcom/lib/checkRateLimitAndThrowError", () => ({
|
|
checkRateLimitAndThrowError: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@calcom/features/ee/api-keys/lib/autoLock", () => ({
|
|
handleAutoLock: vi.fn(),
|
|
}));
|
|
|
|
vi.mock("@calcom/prisma", () => ({
|
|
default: {},
|
|
prisma: {},
|
|
}));
|
|
|
|
describe("rateLimitApiKey middleware", () => {
|
|
it("should return 401 if no apiKey is provided", async () => {
|
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
|
method: "GET",
|
|
query: {},
|
|
userId: testUserId,
|
|
});
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
|
|
|
expect(res._getStatusCode()).toBe(401);
|
|
expect(res._getJSONData()).toEqual({ message: "No apiKey provided" });
|
|
});
|
|
|
|
it("should call checkRateLimitAndThrowError with correct parameters", async () => {
|
|
const { req, res } = createMocks({
|
|
method: "GET",
|
|
query: { apiKey: "test-key" },
|
|
userId: testUserId,
|
|
});
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(checkRateLimitAndThrowError as any).mockResolvedValueOnce({
|
|
limit: 100,
|
|
remaining: 99,
|
|
reset: Date.now(),
|
|
});
|
|
|
|
// @ts-expect-error weird typing between middleware and createMocks
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
|
|
|
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
|
|
identifier: testUserId.toString(),
|
|
rateLimitingType: "api",
|
|
onRateLimiterResponse: expect.any(Function),
|
|
});
|
|
});
|
|
|
|
it("should set rate limit headers correctly", async () => {
|
|
const { req, res } = createMocks({
|
|
method: "GET",
|
|
query: { apiKey: "test-key" },
|
|
userId: testUserId,
|
|
});
|
|
|
|
const rateLimiterResponse: RatelimitResponse = {
|
|
limit: 100,
|
|
remaining: 99,
|
|
reset: Date.now(),
|
|
success: true,
|
|
};
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
|
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
|
onRateLimiterResponse(rateLimiterResponse);
|
|
}
|
|
);
|
|
// @ts-expect-error weird typing between middleware and createMocks
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
|
|
|
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
|
|
expect(res.getHeader("X-RateLimit-Remaining")).toBe(rateLimiterResponse.remaining);
|
|
expect(res.getHeader("X-RateLimit-Reset")).toBe(rateLimiterResponse.reset);
|
|
});
|
|
|
|
it("should return 429 if rate limit is exceeded", async () => {
|
|
const { req, res } = createMocks({
|
|
method: "GET",
|
|
query: { apiKey: "test-key" },
|
|
userId: testUserId,
|
|
});
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(checkRateLimitAndThrowError as any).mockRejectedValue(new Error("Rate limit exceeded"));
|
|
|
|
// @ts-expect-error weird typing between middleware and createMocks
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
|
|
|
expect(res._getStatusCode()).toBe(429);
|
|
expect(res._getJSONData()).toEqual({ message: "Rate limit exceeded" });
|
|
});
|
|
|
|
it("should lock API key when rate limit is repeatedly exceeded", async () => {
|
|
const { req, res } = createMocks({
|
|
method: "GET",
|
|
query: { apiKey: "test-key" },
|
|
userId: testUserId,
|
|
});
|
|
|
|
const rateLimiterResponse: RatelimitResponse = {
|
|
success: false,
|
|
remaining: 0,
|
|
limit: 100,
|
|
reset: Date.now(),
|
|
};
|
|
|
|
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
|
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
|
onRateLimiterResponse(rateLimiterResponse);
|
|
}
|
|
);
|
|
|
|
// Mock handleAutoLock to indicate the key was locked
|
|
vi.mocked(handleAutoLock).mockResolvedValueOnce(true);
|
|
|
|
// @ts-expect-error weird typing between middleware and createMocks
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
|
|
|
expect(handleAutoLock).toHaveBeenCalledWith({
|
|
identifier: testUserId.toString(),
|
|
identifierType: "userId",
|
|
rateLimitResponse: rateLimiterResponse,
|
|
});
|
|
|
|
expect(res._getStatusCode()).toBe(429);
|
|
expect(res._getJSONData()).toEqual({ message: "Too many requests" });
|
|
});
|
|
|
|
it("should handle API key not found error during auto-lock", async () => {
|
|
const { req, res } = createMocks({
|
|
method: "GET",
|
|
query: { apiKey: "test-key" },
|
|
userId: testUserId,
|
|
});
|
|
|
|
const rateLimiterResponse: RatelimitResponse = {
|
|
success: false,
|
|
remaining: 0,
|
|
limit: 100,
|
|
reset: Date.now(),
|
|
};
|
|
|
|
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
|
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
|
onRateLimiterResponse(rateLimiterResponse);
|
|
}
|
|
);
|
|
|
|
// Mock handleAutoLock to throw a "No user found" error
|
|
vi.mocked(handleAutoLock).mockRejectedValueOnce(new Error("No user found for this API key."));
|
|
|
|
// @ts-expect-error weird typing between middleware and createMocks
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
|
|
|
expect(handleAutoLock).toHaveBeenCalledWith({
|
|
identifier: testUserId.toString(),
|
|
identifierType: "userId",
|
|
rateLimitResponse: rateLimiterResponse,
|
|
});
|
|
|
|
expect(res._getStatusCode()).toBe(401);
|
|
expect(res._getJSONData()).toEqual({ message: "No user found for this API key." });
|
|
});
|
|
|
|
it("should continue if auto-lock returns false (not locked)", async () => {
|
|
const { req, res } = createMocks({
|
|
method: "GET",
|
|
query: { apiKey: "test-key" },
|
|
userId: testUserId,
|
|
});
|
|
|
|
const rateLimiterResponse: RatelimitResponse = {
|
|
success: false,
|
|
remaining: 0,
|
|
limit: 100,
|
|
reset: Date.now(),
|
|
};
|
|
|
|
// Mock rate limiter to trigger the onRateLimiterResponse callback
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
(checkRateLimitAndThrowError as any).mockImplementationOnce(
|
|
({ onRateLimiterResponse }: { onRateLimiterResponse: (response: RatelimitResponse) => void }) => {
|
|
onRateLimiterResponse(rateLimiterResponse);
|
|
}
|
|
);
|
|
|
|
// Mock handleAutoLock to indicate the key was not locked
|
|
vi.mocked(handleAutoLock).mockResolvedValueOnce(false);
|
|
|
|
const next = vi.fn();
|
|
// @ts-expect-error weird typing between middleware and createMocks
|
|
await rateLimitApiKey(req, res, next);
|
|
|
|
expect(handleAutoLock).toHaveBeenCalledWith({
|
|
identifier: testUserId.toString(),
|
|
identifierType: "userId",
|
|
rateLimitResponse: rateLimiterResponse,
|
|
});
|
|
|
|
// Verify headers were set but request continued
|
|
expect(res.getHeader("X-RateLimit-Limit")).toBe(rateLimiterResponse.limit);
|
|
expect(next).toHaveBeenCalled();
|
|
});
|
|
|
|
it("should handle HttpError during rate limiting", async () => {
|
|
const { req, res } = createMocks({
|
|
method: "GET",
|
|
query: { apiKey: "test-key" },
|
|
userId: testUserId,
|
|
});
|
|
|
|
// Mock checkRateLimitAndThrowError to throw HttpError
|
|
vi.mocked(checkRateLimitAndThrowError).mockRejectedValueOnce(
|
|
new HttpError({
|
|
statusCode: 429,
|
|
message: "Custom rate limit error",
|
|
})
|
|
);
|
|
|
|
// @ts-expect-error weird typing between middleware and createMocks
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
await rateLimitApiKey(req, res, vi.fn() as any);
|
|
|
|
expect(res._getStatusCode()).toBe(429);
|
|
expect(res._getJSONData()).toEqual({ message: "Custom rate limit error" });
|
|
});
|
|
});
|