Files
calendar/packages/lib/server/getRoutedUrl.test.ts
T
Hariom BalharaGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>hariom@cal.com <hariom@cal.com>
bbcd0b7e78 test: add unit tests to various important fns in routing flow (#21902)
* Add unit tests to routing flow

* fix: resolve TypeScript errors in routing forms test files

- Fix mock form structure to match TargetRoutingFormForResponse type
- Add missing properties like selectText, deleted, updatedById to form fields
- Correct route action types to use valid RouteActionType enum values
- Fix response objects to include required label property
- Update mock return values for findTeamMembersMatchingAttributeLogic
- Use proper type assertions for complex route objects

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

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: hariom@cal.com <hariom@cal.com>
2025-06-18 14:14:14 +00:00

281 lines
11 KiB
TypeScript

import "@calcom/lib/__mocks__/logger";
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 { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm";
import { UserRepository } from "@calcom/lib/server/repository/user";
import { getRoutedUrl } from "./getRoutedUrl";
// Mock dependencies
vi.mock("@calcom/app-store/routing-forms/lib/handleResponse");
vi.mock("@calcom/lib/server/repository/routingForm");
vi.mock("@calcom/lib/server/repository/user");
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);
vi.mocked(UserRepository.enrichUserWithItsProfile).mockImplementation(async ({ user }) => user);
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" } } })
);
});
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,
})
);
});
});
});