Files
calendar/packages/app-store/routing-forms/lib/formSubmissionUtils.test.ts
T
Syed Ali ShahbazandGitHub 056922b6ee feat: add webhook versioning (#23861)
* add webhook version schema

* version the code

* update version from numeric to date val

* migration

* update schema and build factory

* update string

* move version picker

* tooltip instead of infobadge

* --

* fix type

* --

* fix type

* fix type

* --

* fix messed up merge

* improvements to payloadfactory

* extract version off of DB and instead keep it in IWebhookRepository

* fix webhookform

* fix type safety and routing ambiguity

* scalable with easier factory extensions and base definition

* fix types

* --

* --

* clean up prisma/client type imports

* fix

* type fix

* type fix

* cleanup

* add tests and registry changes

* unintended file inclusion

* type-fix

* select in repo

* --

* explicit return type

* --

* fix type

* fixes

* feedback 1

* feedback 2

* use enum instead of string

* fixes
2025-12-18 09:36:22 +02:00

397 lines
12 KiB
TypeScript

import "@calcom/lib/__mocks__/logger";
import { prisma } from "@calcom/prisma/__mocks__/prisma";
import { describe, it, vi, expect, beforeEach, afterEach } from "vitest";
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
import type { WebhookSubscriber } from "@calcom/features/webhooks/lib/dto/types";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { sendGenericWebhookPayload } from "@calcom/features/webhooks/lib/sendPayload";
import {
WebhookTriggerEvents,
WorkflowTriggerEvents,
WorkflowActions,
WorkflowTemplates,
TimeUnit,
} from "@calcom/prisma/enums";
import { WebhookVersion as WebhookVersionEnum } from "@calcom/features/webhooks/lib/interface/IWebhookRepository";
import type { FormResponse, Field } from "../types/types";
import { _onFormSubmission } from "./formSubmissionUtils";
vi.mock("@calcom/prisma", () => ({
prisma,
}));
// 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) };
});
// Mock workflow dependencies
vi.mock("@calcom/features/ee/workflows/lib/service/WorkflowService", () => ({
WorkflowService: {
getAllWorkflowsFromRoutingForm: vi.fn(() => Promise.resolve([])),
scheduleFormWorkflows: vi.fn(() => Promise.resolve()),
},
}));
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",
disabled: false,
userId: 1,
position: 0,
description: null,
updatedById: null,
fields: [
{ id: "field-1", identifier: "email", label: "Email", type: "email", required: false },
{ id: "field-2", identifier: "name", label: "Name", type: "text", required: false },
] as Field[],
user: { id: 1, email: "test@example.com", timeFormat: 12, locale: "en" },
teamId: null,
settings: { emailOwnerOnSubmission: true },
routes: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
connectedForms: [],
routers: [],
teamMembers: [],
};
const mockResponse: FormResponse = {
"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 () => {
const mockWebhook: WebhookSubscriber = {
id: "wh-1",
secret: "secret",
subscriberUrl: "https://example.com/webhook",
payloadTemplate: null,
appId: null,
eventTriggers: [WebhookTriggerEvents.FORM_SUBMITTED],
time: null,
timeUnit: null,
version: WebhookVersionEnum.V_2021_10_20,
};
vi.mocked(getWebhooks).mockResolvedValueOnce([mockWebhook]);
await _onFormSubmission(mockForm, mockResponse, responseId);
expect(getWebhooks).toHaveBeenCalledWith({
userId: 1,
teamId: null,
orgId: 1,
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
});
expect(sendGenericWebhookPayload).toHaveBeenCalledTimes(1);
});
it("should normalize identifiers with spaces to hyphens in rootData for webhook templates", async () => {
const formWithSpaces = {
...mockForm,
fields: [
{
id: "field-1",
identifier: "attendee name",
label: "Attendee Name",
type: "text",
required: false,
},
] as Field[],
};
const responseWithSpaces: FormResponse = {
"field-1": { label: "Attendee Name", value: "John Doe" },
};
const mockWebhook: WebhookSubscriber = {
id: "wh-1",
secret: "secret",
subscriberUrl: "https://example.com/webhook",
payloadTemplate: null,
appId: null,
eventTriggers: [WebhookTriggerEvents.FORM_SUBMITTED],
time: null,
timeUnit: null,
version: WebhookVersionEnum.V_2021_10_20,
};
vi.mocked(getWebhooks).mockResolvedValueOnce([mockWebhook]);
await _onFormSubmission(formWithSpaces, responseWithSpaces, responseId);
expect(sendGenericWebhookPayload).toHaveBeenCalledWith(
expect.objectContaining({
rootData: expect.objectContaining({
"attendee-name": "John Doe", // Spaces replaced with hyphens for template access
}),
})
);
});
});
describe("Workflows", () => {
it("should call WorkflowService.scheduleFormWorkflows for FORM_SUBMITTED workflows", async () => {
const mockWorkflows: Workflow[] = [
{
id: 1,
name: "Form Submitted Workflow",
userId: 1,
teamId: null,
trigger: WorkflowTriggerEvents.FORM_SUBMITTED,
time: null,
timeUnit: null,
steps: [
{
id: 1,
action: WorkflowActions.EMAIL_ATTENDEE,
sendTo: null,
sender: null,
reminderBody: "Thank you for your submission!",
emailSubject: "Form Received",
template: WorkflowTemplates.CUSTOM,
verifiedAt: new Date(),
includeCalendarEvent: false,
numberVerificationPending: false,
numberRequired: false,
},
],
},
];
vi.mocked(WorkflowService.getAllWorkflowsFromRoutingForm).mockResolvedValueOnce(mockWorkflows);
await _onFormSubmission(mockForm, mockResponse, responseId);
expect(WorkflowService.getAllWorkflowsFromRoutingForm).toHaveBeenCalledWith(mockForm);
expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith(
expect.objectContaining({
workflows: mockWorkflows,
responses: {
email: {
value: "test@response.com",
response: "test@response.com",
},
name: { value: "Test Name", response: "Test Name" },
},
responseId,
routedEventTypeId: null,
form: {
...mockForm,
fields: mockForm.fields.map((field) => ({
type: field.type,
identifier: field.identifier,
})),
},
creditCheckFn: expect.any(Function),
})
);
});
it("should call WorkflowService.scheduleFormWorkflows for FORM_SUBMITTED_NO_EVENT workflows", async () => {
const mockWorkflows: Workflow[] = [
{
id: 2,
name: "Form Follow-up Workflow",
userId: 1,
teamId: null,
trigger: WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT,
time: 30,
timeUnit: TimeUnit.MINUTE,
steps: [
{
id: 2,
action: WorkflowActions.EMAIL_ATTENDEE,
sendTo: null,
sender: null,
reminderBody: "Follow up on your form submission",
emailSubject: "Follow Up",
template: WorkflowTemplates.CUSTOM,
verifiedAt: new Date(),
includeCalendarEvent: false,
numberVerificationPending: false,
numberRequired: false,
},
],
},
];
vi.mocked(WorkflowService.getAllWorkflowsFromRoutingForm).mockResolvedValueOnce(mockWorkflows);
await _onFormSubmission(mockForm, mockResponse, responseId);
expect(WorkflowService.getAllWorkflowsFromRoutingForm).toHaveBeenCalledWith(mockForm);
expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith(
expect.objectContaining({
workflows: mockWorkflows,
responses: {
email: {
value: "test@response.com",
response: "test@response.com",
},
name: { value: "Test Name", response: "Test Name" },
},
routedEventTypeId: null,
responseId,
form: {
...mockForm,
fields: mockForm.fields.map((field) => ({
type: field.type,
identifier: field.identifier,
})),
},
creditCheckFn: expect.any(Function),
})
);
});
it("should pass routedEventTypeId when chosenAction is eventTypeRedirectUrl", async () => {
const mockWorkflows: Workflow[] = [
{
id: 3,
name: "Event Type Workflow",
userId: 1,
teamId: null,
trigger: WorkflowTriggerEvents.FORM_SUBMITTED,
time: null,
timeUnit: null,
steps: [
{
id: 3,
action: WorkflowActions.CAL_AI_PHONE_CALL,
sendTo: null,
sender: null,
reminderBody: null,
emailSubject: null,
template: WorkflowTemplates.CUSTOM,
verifiedAt: new Date(),
includeCalendarEvent: false,
numberVerificationPending: false,
numberRequired: false,
},
],
},
];
const chosenAction = {
type: "eventTypeRedirectUrl" as const,
value: "/team/test-team/test-event",
eventTypeId: 42,
};
vi.mocked(WorkflowService.getAllWorkflowsFromRoutingForm).mockResolvedValueOnce(mockWorkflows);
await _onFormSubmission(mockForm, mockResponse, responseId, chosenAction);
expect(WorkflowService.scheduleFormWorkflows).toHaveBeenCalledWith(
expect.objectContaining({
workflows: mockWorkflows,
responses: {
email: {
value: "test@response.com",
response: "test@response.com",
},
name: { value: "Test Name", response: "Test Name" },
},
routedEventTypeId: 42,
responseId,
form: {
...mockForm,
fields: mockForm.fields.map((field) => ({
type: field.type,
identifier: field.identifier,
})),
},
creditCheckFn: expect.any(Function),
})
);
});
});
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", timeFormat: 12, locale: "en" },
};
await _onFormSubmission(teamForm, 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, 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, mockResponse, responseId);
expect(mockResponseEmailConstructor).not.toHaveBeenCalled();
expect(mockSendEmail).not.toHaveBeenCalled();
});
});
});