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>
This commit is contained in:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
hariom@cal.com <hariom@cal.com>
parent
916ed13bea
commit
bbcd0b7e78
@@ -0,0 +1,309 @@
|
||||
import "@calcom/lib/__mocks__/logger";
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { findTeamMembersMatchingAttributeLogic } from "@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic";
|
||||
import { RoutingFormResponseRepository } from "@calcom/lib/server/repository/formResponse";
|
||||
import type { ZResponseInputSchema } from "@calcom/trpc/server/routers/viewer/routing-forms/response.schema";
|
||||
|
||||
import isRouter from "../lib/isRouter";
|
||||
import routerGetCrmContactOwnerEmail from "./crmRouting/routerGetCrmContactOwnerEmail";
|
||||
import type { TargetRoutingFormForResponse } from "./formSubmissionUtils";
|
||||
import { onSubmissionOfFormResponse } from "./formSubmissionUtils";
|
||||
import { handleResponse } from "./handleResponse";
|
||||
|
||||
vi.mock("@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic", () => ({
|
||||
findTeamMembersMatchingAttributeLogic: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/server/repository/formResponse", () => ({
|
||||
RoutingFormResponseRepository: {
|
||||
recordQueuedFormResponse: vi.fn(),
|
||||
recordFormResponse: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./crmRouting/routerGetCrmContactOwnerEmail", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./formSubmissionUtils", () => ({
|
||||
onSubmissionOfFormResponse: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../lib/isRouter", () => ({
|
||||
default: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/lib/sentryWrapper", () => ({
|
||||
withReporting: (fn: unknown) => fn,
|
||||
}));
|
||||
|
||||
const mockForm: TargetRoutingFormForResponse = {
|
||||
id: "form-id",
|
||||
name: "Test Form",
|
||||
fields: [
|
||||
{
|
||||
id: "name",
|
||||
label: "Name",
|
||||
type: "text" as const,
|
||||
required: true,
|
||||
identifier: "name",
|
||||
placeholder: undefined,
|
||||
selectText: undefined,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: "email",
|
||||
label: "Email",
|
||||
type: "email" as const,
|
||||
required: true,
|
||||
identifier: "email",
|
||||
placeholder: undefined,
|
||||
selectText: undefined,
|
||||
deleted: false,
|
||||
},
|
||||
{
|
||||
id: "guests",
|
||||
label: "Guests",
|
||||
type: "text" as const,
|
||||
required: false,
|
||||
identifier: "guests",
|
||||
placeholder: undefined,
|
||||
selectText: undefined,
|
||||
deleted: false,
|
||||
},
|
||||
],
|
||||
routes: [],
|
||||
userId: 1,
|
||||
teamId: 1,
|
||||
user: {
|
||||
id: 1,
|
||||
email: "test@example.com",
|
||||
},
|
||||
team: {
|
||||
parentId: 2,
|
||||
},
|
||||
createdAt: "2023-01-01T00:00:00.000Z",
|
||||
updatedAt: "2023-01-01T00:00:00.000Z",
|
||||
description: null,
|
||||
disabled: false,
|
||||
settings: {
|
||||
emailOwnerOnSubmission: false,
|
||||
},
|
||||
connectedForms: [],
|
||||
routers: [],
|
||||
teamMembers: [],
|
||||
position: 0,
|
||||
updatedById: null,
|
||||
};
|
||||
|
||||
const mockResponse: z.infer<typeof ZResponseInputSchema>["response"] = {
|
||||
name: { value: "John Doe", label: "Name" },
|
||||
email: { value: "john.doe@example.com", label: "Email" },
|
||||
};
|
||||
|
||||
describe("handleResponse", () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
it("should throw a TRPCError for missing required fields", async () => {
|
||||
await expect(
|
||||
handleResponse({
|
||||
response: { email: { value: "test@test.com", label: "Email" } }, // Name is missing
|
||||
form: mockForm,
|
||||
formFillerId: "user1",
|
||||
chosenRouteId: null,
|
||||
isPreview: false,
|
||||
})
|
||||
).rejects.toThrow(/Missing required fields Name/);
|
||||
});
|
||||
|
||||
it("should throw a TRPCError for invalid email", async () => {
|
||||
await expect(
|
||||
handleResponse({
|
||||
response: {
|
||||
name: { value: "John Doe", label: "Name" },
|
||||
email: { value: "invalid-email", label: "Email" },
|
||||
},
|
||||
form: mockForm,
|
||||
formFillerId: "user1",
|
||||
chosenRouteId: null,
|
||||
isPreview: false,
|
||||
})
|
||||
).rejects.toThrow(/Invalid value for fields 'Email' with value 'invalid-email' should be valid email/);
|
||||
});
|
||||
|
||||
it("should record form response when not in preview and not queued", async () => {
|
||||
const dbFormResponse = {
|
||||
id: 1,
|
||||
formId: mockForm.id,
|
||||
response: mockResponse,
|
||||
chosenRouteId: null,
|
||||
formFillerId: "user1",
|
||||
routedToBookingUid: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
vi.mocked(RoutingFormResponseRepository.recordFormResponse).mockResolvedValue(dbFormResponse);
|
||||
|
||||
const result = await handleResponse({
|
||||
response: mockResponse,
|
||||
form: mockForm,
|
||||
formFillerId: "user1",
|
||||
chosenRouteId: null,
|
||||
isPreview: false,
|
||||
});
|
||||
|
||||
expect(RoutingFormResponseRepository.recordFormResponse).toHaveBeenCalledWith({
|
||||
formId: mockForm.id,
|
||||
response: mockResponse,
|
||||
chosenRouteId: null,
|
||||
});
|
||||
expect(onSubmissionOfFormResponse).toHaveBeenCalledWith({
|
||||
form: mockForm,
|
||||
formResponseInDb: dbFormResponse,
|
||||
chosenRouteAction: null,
|
||||
});
|
||||
expect(result.formResponse).toEqual(dbFormResponse);
|
||||
expect(result.queuedFormResponse).toBeNull();
|
||||
});
|
||||
|
||||
it("should queue form response when queueFormResponse is true", async () => {
|
||||
const queuedResponse = {
|
||||
id: "queued-id",
|
||||
formId: mockForm.id,
|
||||
response: mockResponse,
|
||||
chosenRouteId: null,
|
||||
actualResponseId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
vi.mocked(RoutingFormResponseRepository.recordQueuedFormResponse).mockResolvedValue(queuedResponse);
|
||||
|
||||
const result = await handleResponse({
|
||||
response: mockResponse,
|
||||
form: mockForm,
|
||||
formFillerId: "user1",
|
||||
chosenRouteId: null,
|
||||
isPreview: false,
|
||||
queueFormResponse: true,
|
||||
});
|
||||
|
||||
expect(RoutingFormResponseRepository.recordQueuedFormResponse).toHaveBeenCalledWith({
|
||||
formId: mockForm.id,
|
||||
response: mockResponse,
|
||||
chosenRouteId: null,
|
||||
});
|
||||
expect(RoutingFormResponseRepository.recordFormResponse).not.toHaveBeenCalled();
|
||||
expect(onSubmissionOfFormResponse).not.toHaveBeenCalled();
|
||||
expect(result.queuedFormResponse).toEqual(queuedResponse);
|
||||
expect(result.formResponse).toBeNull();
|
||||
});
|
||||
|
||||
describe("Preview mode", () => {
|
||||
it("should send formResponse with id=0 and quueueFormResponse isn't true", async () => {
|
||||
const result = await handleResponse({
|
||||
response: mockResponse,
|
||||
form: mockForm,
|
||||
formFillerId: "user1",
|
||||
chosenRouteId: null,
|
||||
isPreview: true,
|
||||
});
|
||||
|
||||
expect(RoutingFormResponseRepository.recordFormResponse).not.toHaveBeenCalled();
|
||||
expect(onSubmissionOfFormResponse).not.toHaveBeenCalled();
|
||||
expect(result.isPreview).toBe(true);
|
||||
expect(result.formResponse).toBeDefined();
|
||||
expect(result.formResponse?.id).toBe(0);
|
||||
});
|
||||
|
||||
it("should send queuedFormResponse with id=00000000-0000-0000-0000-000000000000 when queueFormResponse is true", async () => {
|
||||
const result = await handleResponse({
|
||||
response: mockResponse,
|
||||
form: mockForm,
|
||||
formFillerId: "user1",
|
||||
chosenRouteId: null,
|
||||
isPreview: true,
|
||||
queueFormResponse: true,
|
||||
});
|
||||
|
||||
expect(RoutingFormResponseRepository.recordQueuedFormResponse).not.toHaveBeenCalled();
|
||||
expect(result.isPreview).toBe(true);
|
||||
expect(result.queuedFormResponse).toBeDefined();
|
||||
expect(result.queuedFormResponse?.id).toBe("00000000-0000-0000-0000-000000000000");
|
||||
expect(result.formResponse).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle chosen route and call routing logic", async () => {
|
||||
const chosenRoute = {
|
||||
id: "route1",
|
||||
queryValue: { type: "group", children1: {} } as any,
|
||||
action: {
|
||||
type: "customPageMessage" as const,
|
||||
value: "Thank you for your submission!",
|
||||
},
|
||||
attributeRoutingConfig: null,
|
||||
attributesQueryValue: { type: "group", children1: {} } as any,
|
||||
fallbackAttributesQueryValue: { type: "group", children1: {} } as any,
|
||||
isFallback: false,
|
||||
};
|
||||
const formWithRoute: TargetRoutingFormForResponse = { ...mockForm, routes: [chosenRoute as any] };
|
||||
|
||||
vi.mocked(routerGetCrmContactOwnerEmail).mockResolvedValue({
|
||||
email: "owner@example.com",
|
||||
recordType: "contact",
|
||||
crmAppSlug: "hubspot",
|
||||
});
|
||||
vi.mocked(findTeamMembersMatchingAttributeLogic).mockResolvedValue({
|
||||
teamMembersMatchingAttributeLogic: [{ userId: 123, result: "MATCH" as any }],
|
||||
checkedFallback: false,
|
||||
fallbackAttributeLogicBuildingWarnings: [],
|
||||
mainAttributeLogicBuildingWarnings: [],
|
||||
timeTaken: {
|
||||
ttGetAttributesForLogic: 50,
|
||||
},
|
||||
});
|
||||
|
||||
const result = await handleResponse({
|
||||
response: mockResponse,
|
||||
form: formWithRoute,
|
||||
formFillerId: "user1",
|
||||
chosenRouteId: "route1",
|
||||
isPreview: false,
|
||||
});
|
||||
|
||||
expect(routerGetCrmContactOwnerEmail).toHaveBeenCalled();
|
||||
expect(findTeamMembersMatchingAttributeLogic).toHaveBeenCalled();
|
||||
expect(result.crmContactOwnerEmail).toBe("owner@example.com");
|
||||
expect(result.teamMembersMatchingAttributeLogic).toEqual([123]);
|
||||
});
|
||||
|
||||
it("should throw error if chosen route is a router", async () => {
|
||||
const chosenRoute = {
|
||||
id: "route1",
|
||||
name: "A router",
|
||||
description: "Router description",
|
||||
isRouter: true as const,
|
||||
routes: [],
|
||||
};
|
||||
vi.mocked(isRouter).mockReturnValue(true);
|
||||
const formWithRoute: TargetRoutingFormForResponse = {
|
||||
...mockForm,
|
||||
routes: [chosenRoute as any],
|
||||
};
|
||||
|
||||
await expect(
|
||||
handleResponse({
|
||||
response: mockResponse,
|
||||
form: formWithRoute,
|
||||
formFillerId: "user1",
|
||||
chosenRouteId: "route1",
|
||||
isPreview: false,
|
||||
})
|
||||
).rejects.toThrow(/Chosen route is a router/);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,15 @@ import slugify from "@calcom/lib/slugify";
|
||||
import type { FormResponse, NonRouterRoute, Field } from "../types/types";
|
||||
import getFieldIdentifier from "./getFieldIdentifier";
|
||||
|
||||
/**
|
||||
* Substitues variables in the target URL identified by routeValue with values from response
|
||||
* e.g. {firstName} is replaced with value of the field with identifier firstName
|
||||
*
|
||||
* @param routeValue - The target URL with variables to be substituted
|
||||
* @param response - The form response containing the values to be substituted
|
||||
* @param fields - The fields of the form
|
||||
* @returns The URL with variables substituted
|
||||
*/
|
||||
export const substituteVariables = (
|
||||
routeValue: NonRouterRoute["action"]["value"],
|
||||
response: FormResponse,
|
||||
|
||||
@@ -0,0 +1,168 @@
|
||||
import "@calcom/lib/__mocks__/logger";
|
||||
|
||||
import { describe, it, vi, expect, beforeEach, afterEach } from "vitest";
|
||||
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/client";
|
||||
|
||||
import { _onFormSubmission } from "./utils";
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock("@calcom/lib/getOrgIdFromMemberOrTeamId", () => ({
|
||||
default: vi.fn(() => Promise.resolve(1)),
|
||||
}));
|
||||
vi.mock("@calcom/features/webhooks/lib/getWebhooks", () => ({
|
||||
default: vi.fn(() => Promise.resolve([])),
|
||||
}));
|
||||
vi.mock("@calcom/features/webhooks/lib/sendPayload", () => ({
|
||||
sendGenericWebhookPayload: vi.fn(() => Promise.resolve()),
|
||||
}));
|
||||
vi.mock("@calcom/features/tasker", () => {
|
||||
const tasker = {
|
||||
create: vi.fn(() => Promise.resolve()),
|
||||
};
|
||||
return { default: Promise.resolve(tasker) };
|
||||
});
|
||||
|
||||
const mockSendEmail = vi.fn(() => Promise.resolve());
|
||||
const mockResponseEmailConstructor = vi.fn();
|
||||
vi.mock("../emails/templates/response-email", () => ({
|
||||
default: class MockResponseEmail {
|
||||
sendEmail = mockSendEmail;
|
||||
constructor(...args: unknown[]) {
|
||||
mockResponseEmailConstructor(...args);
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
describe("_onFormSubmission", () => {
|
||||
const mockForm = {
|
||||
id: "form-1",
|
||||
name: "Test Form",
|
||||
fields: [
|
||||
{ id: "field-1", identifier: "email", label: "Email", type: "email" },
|
||||
{ id: "field-2", identifier: "name", label: "Name", type: "text" },
|
||||
],
|
||||
user: { id: 1, email: "test@example.com" },
|
||||
teamId: null,
|
||||
settings: { emailOwnerOnSubmission: true },
|
||||
};
|
||||
|
||||
const mockResponse = {
|
||||
"field-1": { label: "Email", value: "test@response.com" },
|
||||
"field-2": { label: "Name", value: "Test Name" },
|
||||
};
|
||||
|
||||
const responseId = 123;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("Webhooks", () => {
|
||||
it("should call FORM_SUBMITTED webhooks", async () => {
|
||||
vi.mocked(getWebhooks).mockResolvedValueOnce([{ id: "wh-1", secret: "secret" } as any]);
|
||||
|
||||
await _onFormSubmission(mockForm as any, mockResponse, responseId);
|
||||
|
||||
expect(getWebhooks).toHaveBeenCalledWith({
|
||||
userId: 1,
|
||||
teamId: null,
|
||||
orgId: 1,
|
||||
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
|
||||
});
|
||||
expect(sendGenericWebhookPayload).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should schedule FORM_SUBMITTED_NO_EVENT webhooks via tasker", async () => {
|
||||
const tasker = await (await import("@calcom/features/tasker")).default;
|
||||
const mockWebhook = { id: "wh-no-event-1", secret: "secret" };
|
||||
const chosenAction = { type: "customPageMessage" as const, value: "test" };
|
||||
|
||||
vi.mocked(getWebhooks).mockImplementation(async (options) => {
|
||||
if (options.triggerEvent === WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT) {
|
||||
return [mockWebhook as any];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
await _onFormSubmission(mockForm as any, mockResponse, responseId, chosenAction);
|
||||
|
||||
expect(getWebhooks).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED_NO_EVENT,
|
||||
})
|
||||
);
|
||||
expect(tasker.create).toHaveBeenCalledWith(
|
||||
"triggerFormSubmittedNoEventWebhook",
|
||||
{
|
||||
responseId,
|
||||
form: mockForm,
|
||||
responses: {
|
||||
email: {
|
||||
value: "test@response.com",
|
||||
response: "test@response.com",
|
||||
},
|
||||
name: { value: "Test Name", response: "Test Name" },
|
||||
},
|
||||
redirect: chosenAction,
|
||||
webhook: mockWebhook,
|
||||
},
|
||||
{ scheduledAt: expect.any(Date) }
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Response Email", () => {
|
||||
it("should send response email to team members for a team form", async () => {
|
||||
const teamForm = {
|
||||
...mockForm,
|
||||
teamId: 1,
|
||||
userWithEmails: ["team-member1@example.com", "team-member2@example.com"],
|
||||
user: { id: 1, email: "test@example.com" },
|
||||
};
|
||||
|
||||
await _onFormSubmission(teamForm as any, mockResponse, responseId);
|
||||
|
||||
expect(mockResponseEmailConstructor).toHaveBeenCalledWith({
|
||||
form: teamForm,
|
||||
toAddresses: ["team-member1@example.com", "team-member2@example.com"],
|
||||
orderedResponses: [mockResponse["field-1"], mockResponse["field-2"]],
|
||||
});
|
||||
expect(mockSendEmail).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should send response email to owner when enabled", async () => {
|
||||
const ownerForm = {
|
||||
...mockForm,
|
||||
settings: { emailOwnerOnSubmission: true },
|
||||
};
|
||||
|
||||
await _onFormSubmission(ownerForm as any, mockResponse, responseId);
|
||||
|
||||
expect(mockResponseEmailConstructor).toHaveBeenCalledWith({
|
||||
form: ownerForm,
|
||||
toAddresses: [ownerForm.user.email],
|
||||
orderedResponses: [mockResponse["field-1"], mockResponse["field-2"]],
|
||||
});
|
||||
expect(mockSendEmail).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not send response email to owner when disabled", async () => {
|
||||
const ownerForm = {
|
||||
...mockForm,
|
||||
settings: { emailOwnerOnSubmission: false },
|
||||
};
|
||||
|
||||
await _onFormSubmission(ownerForm as any, mockResponse, responseId);
|
||||
|
||||
expect(mockResponseEmailConstructor).not.toHaveBeenCalled();
|
||||
expect(mockSendEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -85,7 +85,7 @@ export function getFieldResponse({
|
||||
* Not called in preview mode or dry run mode
|
||||
* It takes care of sending webhooks and emails for form submissions
|
||||
*/
|
||||
async function _onFormSubmission(
|
||||
export async function _onFormSubmission(
|
||||
form: Ensure<
|
||||
SerializableForm<App_RoutingForms_Form> & { user: Pick<User, "id" | "email">; userWithEmails?: string[] },
|
||||
"fields"
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
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,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user