Files
calendar/packages/lib/server/getRoutedUrl.test.ts
T
Eunjae LeeGitHubHariom BalharaDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
6fe6540bc6 fix: apply PBAC to routing form CRUD (#22859)
* fix: apply PBAC to routing form CRUD

* apply permission checks to the UI

* moving prisma call to repository [WIP]

* rename repository and fix type error

* update implementation

* fix formMutation handler

* remove unused import

* revert some rename

* add RolePermission for 'routingForm'

* Revert "revert some rename"

This reverts commit 0ef3114c0d27b821271b36296de531654b37749f.

* clean up PrismaRoutingFormRepository

* fix unit test

* remove no longer necessary code

* fix type definition

* explicit permission handling

* do not disable un-editable routing form

* fix: correct property name from readonly to readOnly in ListLinkItem

- Fix TypeScript error in routing forms component
- Change readonly={readOnly} to readOnly={readOnly} to match component interface
- Resolves type check failure in CI

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

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-08-08 13:18:18 +01:00

328 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 { PrismaRoutingFormRepository } from "@calcom/lib/server/repository/PrismaRoutingFormRepository";
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/PrismaRoutingFormRepository");
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, isValidOrgDomain: false });
vi.mocked(PrismaRoutingFormRepository.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(PrismaRoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).mockResolvedValue(null);
const context = mockContext({});
const result = await getRoutedUrl(context);
expect(result).toEqual({ notFound: true });
expect(PrismaRoutingFormRepository.findFormByIdIncludeUserTeamAndOrg).toHaveBeenCalledWith("form-id");
});
it("should return notFound if user is not authorized", async () => {
vi.mocked(PrismaRoutingFormRepository.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(PrismaRoutingFormRepository.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(PrismaRoutingFormRepository.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(PrismaRoutingFormRepository.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(PrismaRoutingFormRepository.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(PrismaRoutingFormRepository.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(PrismaRoutingFormRepository.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,
})
);
});
});
});