fix: email and phone field validation (#24432)

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
Anik Dhabal Babu
2025-10-13 17:05:11 +00:00
committed by GitHub
co-authored by Udit Takkar
parent e5f14c9316
commit f9b294cfc2
5 changed files with 239 additions and 4 deletions
@@ -3788,6 +3788,10 @@
"configure_agent_to_handle_incoming_calls": "Configure agent to handle incoming calls",
"incoming_calls": "Incoming Calls",
"outgoing_calls": "Outgoing Calls",
"booking_fields_email_and_phone_both_hidden": "Both Email and Attendee Phone Number cannot be hidden",
"booking_fields_email_or_phone_required": "At least Email or Attendee Phone Number must be a required field",
"booking_fields_phone_required_when_email_hidden": "Attendee Phone Number must be required when Email is hidden",
"booking_fields_email_required_when_phone_hidden": "Email must be required when Attendee Phone Number is hidden",
"inbound_agent_setup_success": "Inbound agent setup successful",
"inbound_agent_configured": "Inbound agent configured",
"setup_inbound_agent": "Set up Inbound Agent",
@@ -176,6 +176,66 @@ describe("getBookingResponsesSchema", () => {
);
});
test(`hidden required email field should not be validated`, async () => {
const schema = getBookingResponsesSchema({
bookingFields: [
{
name: "name",
type: "name",
required: true,
},
{
name: "email",
type: "email",
required: true,
hidden: true,
},
{
name: "attendeePhoneNumber",
type: "phone",
required: true,
},
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
view: "ALL_VIEWS",
});
const parsedResponses = await schema.safeParseAsync({
name: "John",
email: "",
attendeePhoneNumber: "+919999999999",
});
expect(parsedResponses.success).toBe(true);
});
test(`hidden required phone field should not be validated`, async () => {
const schema = getBookingResponsesSchema({
bookingFields: [
{
name: "name",
type: "name",
required: true,
},
{
name: "email",
type: "email",
required: true,
},
{
name: "attendeePhoneNumber",
type: "phone",
required: true,
hidden: true,
},
] as z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">,
view: "ALL_VIEWS",
});
const parsedResponses = await schema.safeParseAsync({
name: "John",
email: "john@example.com",
attendeePhoneNumber: "",
});
expect(parsedResponses.success).toBe(true);
});
test(`firstName is required and lastName is optional by default`, async () => {
const schema = getBookingResponsesSchema({
bookingFields: [
@@ -174,7 +174,7 @@ function preprocess<T extends z.ZodType>({
}
if (bookingField.type === "email") {
if (!bookingField.hidden && checkOptional ? true : bookingField.required) {
if (!bookingField.hidden && (checkOptional || bookingField.required)) {
// Email RegExp to validate if the input is a valid email
if (!emailSchema.safeParse(value).success) {
ctx.addIssue({
@@ -7,7 +7,7 @@ import { MembershipRole } from "@calcom/prisma/enums";
import { TRPCError } from "@trpc/server";
import type { authedProcedure } from "../../../procedures/authedProcedure";
import { createEventPbacProcedure } from "../util";
import { createEventPbacProcedure, ensureEmailOrPhoneNumberIsPresent } from "../util";
// Mock dependencies
vi.mock("@calcom/features/pbac/services/permission-check.service");
@@ -513,4 +513,163 @@ describe("createEventPbacProcedure", () => {
);
});
});
describe("ensureEmailOrPhoneNumberIsPresent", () => {
it("should throw error when both email and phone are hidden", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: true,
hidden: true,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: true,
hidden: true,
},
];
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
expect.objectContaining({
code: "BAD_REQUEST",
message: "booking_fields_email_and_phone_both_hidden",
})
);
});
it("should throw error when neither email nor phone is required", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: false,
hidden: false,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: false,
hidden: false,
},
];
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
expect.objectContaining({
code: "BAD_REQUEST",
message: "booking_fields_email_or_phone_required",
})
);
});
it("should throw error when email is hidden and phone is not required", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: true,
hidden: true,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: false,
hidden: false,
},
];
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
expect.objectContaining({
code: "BAD_REQUEST",
message: "booking_fields_phone_required_when_email_hidden",
})
);
});
it("should throw error when phone is hidden and email is not required", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: false,
hidden: false,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: true,
hidden: true,
},
];
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(TRPCError);
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).toThrow(
expect.objectContaining({
code: "BAD_REQUEST",
message: "booking_fields_email_required_when_phone_hidden",
})
);
});
it("should pass when email is visible and required while phone is hidden", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: true,
hidden: false,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: false,
hidden: true,
},
];
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).not.toThrow();
});
it("should pass when phone is visible and required while email is hidden", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: false,
hidden: true,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: true,
hidden: false,
},
];
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).not.toThrow();
});
it("should pass when both email and phone are visible and required", () => {
const fields = [
{
name: "email",
type: "email" as const,
required: true,
hidden: false,
},
{
name: "attendeePhoneNumber",
type: "phone" as const,
required: true,
hidden: false,
},
];
expect(() => ensureEmailOrPhoneNumberIsPresent(fields)).not.toThrow();
});
});
});
@@ -274,13 +274,25 @@ export function ensureEmailOrPhoneNumberIsPresent(fields: TUpdateInputSchema["bo
if (emailField?.hidden && attendeePhoneNumberField?.hidden) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `Both Email and Attendee Phone Number cannot be hidden`,
message: "booking_fields_email_and_phone_both_hidden",
});
}
if (!emailField?.required && !attendeePhoneNumberField?.required) {
throw new TRPCError({
code: "BAD_REQUEST",
message: `At least Email or Attendee Phone Number need to be required field.`,
message: "booking_fields_email_or_phone_required",
});
}
if (emailField?.hidden && !attendeePhoneNumberField?.required) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "booking_fields_phone_required_when_email_hidden",
});
}
if (attendeePhoneNumberField?.hidden && !emailField?.required) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "booking_fields_email_required_when_phone_hidden",
});
}
}