* 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>
281 lines
11 KiB
TypeScript
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,
|
|
})
|
|
);
|
|
});
|
|
});
|
|
});
|