Files
calendar/packages/lib/server/getRoutedUrl.test.ts
T
devin-ai-integration[bot]GitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>morgan@cal.com <morgan@cal.com>Morgan
e4c47640fc refactor: convert UserRepository to use dependency injection pattern (#22360)
* refactor: convert UserRepository to use dependency injection pattern

- Convert all static methods to public instance methods
- Add constructor that takes PrismaClient parameter
- Update all usage sites to use new instantiation pattern: new UserRepository(prisma).method()
- Follow same pattern as PrismaOOORepository for consistency
- Maintain all existing method logic and signatures unchanged
- Update 125+ files across the codebase to adapt to new pattern

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* optimize: reuse UserRepository instances within same function scope

- Create single UserRepository instance per function scope
- Reuse instance for multiple method calls within same function
- Reduces object instantiation overhead and improves performance
- Apply optimization pattern consistently across codebase

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: repository

* fixup! fix: repository

* fixup! fixup! fix: repository

* fixup! fixup! fixup! fix: repository

* fix: update test mocking strategies for UserRepository dependency injection

- Convert static method mocks to instance method mocks in userCreationService.test.ts
- Update vi.spyOn calls to work with constructor injection pattern in getAllCredentials.test.ts
- Fix UserRepository mocking in getRoutedUrl.test.ts to use constructor injection
- Ensure consistent mocking approach across all test files
- Fix 'UserRepository is not a constructor' errors in tests

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* feat: optimize UserRepository instance reuse and add SessionUser type

- Reuse UserRepository instance in OrganizationRepository.createWithNonExistentOwner
- Add comprehensive SessionUser type definition for type safety
- Improve type constraints in enrichUserWithTheProfile and enrichUserWithItsProfile
- Ensure proper return types with profile information

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fix: make UserRepository mocking strategy more robust for CI environments

- Add defensive checks for vi.mocked() to handle CI environment differences
- Ensure mockImplementation is available before calling it
- Maintain consistent mocking pattern across all test files
- Fix 'Cannot read properties of undefined' error in CI

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* fixup! fix: make UserRepository mocking strategy more robust for CI environments

* refactor: convert direct UserRepository instantiations to two-step pattern

- Change await new UserRepository(prisma).method(...) to const userRepo = new UserRepository(prisma); await userRepo.method(...)
- Optimize instance reuse within same function scopes
- Apply pattern consistently across all modified files in PR
- Fix type errors in organization.ts and sessionMiddleware.ts

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* refactor: complete two-step UserRepository pattern for remaining files

- Apply two-step instantiation pattern to all remaining modified files in PR
- Ensure consistent UserRepository usage across entire codebase
- Maintain instance reuse optimization within function scopes

Co-Authored-By: morgan@cal.com <morgan@cal.com>

* chore: bump platform libs

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: morgan@cal.com <morgan@cal.com>
Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
2025-07-10 12:11:14 +00:00

314 lines
12 KiB
TypeScript

import "@calcom/lib/__mocks__/logger";
import { createHash } from "crypto";
import type { GetServerSidePropsContext } from "next";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { getAbsoluteEventTypeRedirectUrlWithEmbedSupport } from "@calcom/app-store/routing-forms/getEventTypeRedirectUrl";
import { getResponseToStore } from "@calcom/app-store/routing-forms/lib/getResponseToStore";
import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSerializableForm";
import { handleResponse } from "@calcom/app-store/routing-forms/lib/handleResponse";
import { findMatchingRoute } from "@calcom/app-store/routing-forms/lib/processRoute";
import { substituteVariables } from "@calcom/app-store/routing-forms/lib/substituteVariables";
import { getUrlSearchParamsToForward } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward";
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm";
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { getRoutedUrl } from "./getRoutedUrl";
// Mock dependencies
vi.mock("@calcom/lib/checkRateLimitAndThrowError");
vi.mock("@calcom/app-store/routing-forms/lib/handleResponse");
vi.mock("@calcom/lib/server/repository/routingForm");
vi.mock("@calcom/lib/server/repository/user", () => {
return {
UserRepository: vi.fn().mockImplementation(() => ({
enrichUserWithItsProfile: vi.fn(),
})),
};
});
vi.mock("@calcom/features/ee/organizations/lib/orgDomains");
vi.mock("@calcom/features/routing-forms/lib/isAuthorizedToViewForm");
vi.mock("@calcom/app-store/routing-forms/lib/getSerializableForm");
vi.mock("@calcom/app-store/routing-forms/lib/getResponseToStore");
vi.mock("@calcom/app-store/routing-forms/lib/processRoute");
vi.mock("@calcom/app-store/routing-forms/lib/substituteVariables");
vi.mock("@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward");
vi.mock("@calcom/app-store/routing-forms/getEventTypeRedirectUrl");
vi.mock("@calcom/app-store/routing-forms/enrichFormWithMigrationData", () => ({
enrichFormWithMigrationData: vi.fn((form) => form),
}));
vi.mock("@calcom/lib/sentryWrapper", () => ({
withReporting: (fn: unknown) => fn,
}));
const mockForm = {
id: "form-id",
user: { id: 1, name: "Test User" },
team: null,
name: "Test Form",
fields: [],
routes: [],
userId: 1,
teamId: null,
createdAt: new Date(),
updatedAt: new Date(),
description: null,
enabled: true,
isDefault: false,
fieldsOrder: [],
position: 0,
slug: "test-form",
};
const mockSerializableForm = {
id: "form-id",
fields: [{ id: "email", type: "email", label: "Email", identifier: "email" }],
routes: [],
user: { id: 1 },
};
const mockContext = (
query: Record<string, unknown>,
url = "/link/form-id"
): Pick<GetServerSidePropsContext, "query" | "req"> => ({
query: { form: "form-id", ...query },
req: { url },
});
describe("getRoutedUrl", () => {
beforeEach(() => {
vi.resetAllMocks();
// Provide default mock implementations
vi.mocked(orgDomainConfig).mockReturnValue({ currentOrgDomain: null });
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(null);
const mockEnrichUserWithItsProfile = vi.fn().mockImplementation(async ({ user }) => user);
const mockUserRepository = vi.mocked(UserRepository);
if (mockUserRepository && typeof mockUserRepository.mockImplementation === "function") {
mockUserRepository.mockImplementation(
() =>
({
enrichUserWithItsProfile: mockEnrichUserWithItsProfile,
} as any)
);
}
vi.mocked(isAuthorizedToViewFormOnOrgDomain).mockReturnValue(true);
vi.mocked(getSerializableForm).mockResolvedValue(mockSerializableForm as never);
vi.mocked(findMatchingRoute).mockReturnValue(null);
vi.mocked(handleResponse).mockResolvedValue({
teamMembersMatchingAttributeLogic: null,
formResponse: { id: 1 },
queuedFormResponse: null,
attributeRoutingConfig: null,
timeTaken: {},
crmContactOwnerEmail: null,
crmContactOwnerRecordType: null,
crmAppSlug: null,
isPreview: false,
});
vi.mocked(substituteVariables).mockImplementation((value) => value);
vi.mocked(getUrlSearchParamsToForward).mockReturnValue(new URLSearchParams());
vi.mocked(getAbsoluteEventTypeRedirectUrlWithEmbedSupport).mockImplementation(
({ eventTypeRedirectUrl }) => eventTypeRedirectUrl
);
vi.mocked(getResponseToStore).mockReturnValue({ email: "test@cal.com" });
});
it("should return notFound if query params are invalid", async () => {
const context = {
query: { wrong_param: "value" }, // 'form' is missing
req: { url: "/link/form-id" },
};
// @ts-expect-error we are testing invalid input
const result = await getRoutedUrl(context);
expect(result).toEqual({ notFound: true });
});
it("should return notFound if form is not found", async () => {
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(null);
const context = mockContext({});
const result = await getRoutedUrl(context);
expect(result).toEqual({ notFound: true });
expect(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).toHaveBeenCalledWith("form-id");
});
it("should return notFound if user is not authorized", async () => {
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);
vi.mocked(isAuthorizedToViewFormOnOrgDomain).mockReturnValue(false);
const context = mockContext({});
const result = await getRoutedUrl(context);
expect(result).toEqual({ notFound: true });
expect(isAuthorizedToViewFormOnOrgDomain).toHaveBeenCalledWith({
user: mockForm.user,
team: mockForm.team,
currentOrgDomain: null,
});
});
it("should throw an error if no matching route is found", async () => {
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);
vi.mocked(getSerializableForm).mockResolvedValue(mockSerializableForm as never);
vi.mocked(findMatchingRoute).mockReturnValue(null);
const context = mockContext({});
await expect(getRoutedUrl(context)).rejects.toThrow("No matching route could be found");
expect(findMatchingRoute).toHaveBeenCalledWith(
expect.objectContaining({
form: mockSerializableForm,
})
);
});
it("should return props with a message for customPageMessage action", async () => {
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);
const message = "This is a custom message";
const mockRoute = { id: "route1", action: { type: "customPageMessage", value: message } };
vi.mocked(findMatchingRoute).mockReturnValue(mockRoute as never);
const context = mockContext({});
const result = await getRoutedUrl(context);
expect(result).toEqual({
props: {
isEmbed: false,
form: mockSerializableForm,
message: message,
errorMessage: null,
},
});
expect(handleResponse).toHaveBeenCalledWith(
expect.objectContaining({
form: mockSerializableForm,
})
);
expect(findMatchingRoute).toHaveBeenCalledWith(
expect.objectContaining({
form: mockSerializableForm,
})
);
});
it("should return a redirect for eventTypeRedirectUrl action and substitute variables", async () => {
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);
const redirectUrl = "test-user/30min/{email}";
const mockRoute = { id: "route1", action: { type: "eventTypeRedirectUrl", value: redirectUrl } };
vi.mocked(findMatchingRoute).mockReturnValue(mockRoute as never);
const substitutedUrl = "test-user/30min/test@cal.com";
vi.mocked(substituteVariables).mockReturnValue(substitutedUrl);
vi.mocked(getAbsoluteEventTypeRedirectUrlWithEmbedSupport).mockReturnValue(`/${substitutedUrl}`);
const context = mockContext({ email: "test@cal.com" });
const result = await getRoutedUrl(context);
expect(substituteVariables).toHaveBeenCalledWith(
redirectUrl,
{ email: "test@cal.com" },
mockSerializableForm.fields
);
expect(getUrlSearchParamsToForward).toHaveBeenCalledWith(
expect.objectContaining({
searchParams: expect.any(URLSearchParams),
formResponse: { email: "test@cal.com" },
fields: mockSerializableForm.fields,
})
);
expect(getAbsoluteEventTypeRedirectUrlWithEmbedSupport).toHaveBeenCalledWith(
expect.objectContaining({
eventTypeRedirectUrl: substitutedUrl,
})
);
expect(result).toEqual({
redirect: {
destination: `/${substitutedUrl}`,
permanent: false,
},
});
});
it("should return a redirect for externalRedirectUrl action", async () => {
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);
const redirectUrl = "https://example.com";
const mockRoute = { id: "route1", action: { type: "externalRedirectUrl", value: redirectUrl } };
vi.mocked(findMatchingRoute).mockReturnValue(mockRoute as never);
const context = mockContext({ email: "test@cal.com" });
const result = await getRoutedUrl(context);
const searchParams = new URLSearchParams({
...(context.query as Record<string, string>),
"cal.action": "externalRedirectUrl",
});
expect(result).toEqual({
redirect: {
destination: `${redirectUrl}?${searchParams.toString()}`,
permanent: false,
},
});
expect(findMatchingRoute).toHaveBeenCalledWith(
expect.objectContaining({
form: mockSerializableForm,
})
);
});
it("should call handleResponse with correct response prop obtained from getResponseToStore", async () => {
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);
const redirectUrl = "test-user/30min";
const mockRoute = { id: "route1", action: { type: "eventTypeRedirectUrl", value: redirectUrl } };
vi.mocked(findMatchingRoute).mockReturnValue(mockRoute as never);
vi.mocked(getAbsoluteEventTypeRedirectUrlWithEmbedSupport).mockReturnValue(`/${redirectUrl}`);
vi.mocked(getResponseToStore).mockReturnValue({ "xxx-xxx": { value: "test@cal.com" } } as never);
const context = mockContext({ email: "test@cal.com" });
await getRoutedUrl(context);
expect(getResponseToStore).toHaveBeenCalledWith({
formFields: mockSerializableForm.fields,
fieldsResponses: { email: "test@cal.com" },
});
expect(handleResponse).toHaveBeenCalledWith(
expect.objectContaining({ response: { "xxx-xxx": { value: "test@cal.com" } } })
);
});
it("should throw an error if rate limit is exceeded", async () => {
vi.mocked(checkRateLimitAndThrowError).mockRejectedValue(new Error("Rate limit exceeded"));
const context = mockContext({ email: "test@cal.com" });
const expectedHash = createHash("sha256")
.update(JSON.stringify({ email: "test@cal.com" }))
.digest("hex");
await expect(getRoutedUrl(context)).rejects.toThrow("Rate limit exceeded");
expect(checkRateLimitAndThrowError).toHaveBeenCalledWith({
identifier: `form:form-id:hash:${expectedHash}`,
});
});
describe("Dry Run", () => {
it("should call handleResponse with isPreview:true", async () => {
vi.mocked(RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(mockForm as never);
const redirectUrl = "test-user/30min";
const mockRoute = { id: "route1", action: { type: "eventTypeRedirectUrl", value: redirectUrl } };
vi.mocked(findMatchingRoute).mockReturnValue(mockRoute as never);
vi.mocked(getAbsoluteEventTypeRedirectUrlWithEmbedSupport).mockReturnValue(`/${redirectUrl}`);
const context = mockContext({ "cal.isBookingDryRun": "true" });
await getRoutedUrl(context);
expect(handleResponse).toHaveBeenCalledWith(
expect.objectContaining({
isPreview: true,
})
);
});
});
});