From 983af06787fd7bb11e347dd5438469100a331ae1 Mon Sep 17 00:00:00 2001 From: Dhairyashil Shinde <93669429+dhairyashiil@users.noreply.github.com> Date: Sat, 13 Dec 2025 02:06:42 +0530 Subject: [PATCH] fix: allow two dots in exclude email (#24968) * add test and fix two dots bug in exclude email * add test and fix two dots bug in exclude email --- packages/prisma/zod-utils.test.ts | 91 +++++++++++++++++++++++++++++++ packages/prisma/zod-utils.ts | 27 ++++++--- 2 files changed, 111 insertions(+), 7 deletions(-) create mode 100644 packages/prisma/zod-utils.test.ts diff --git a/packages/prisma/zod-utils.test.ts b/packages/prisma/zod-utils.test.ts new file mode 100644 index 0000000000..a45123f7c7 --- /dev/null +++ b/packages/prisma/zod-utils.test.ts @@ -0,0 +1,91 @@ +import { describe, it, expect } from "vitest"; +import z from "zod"; + +import { excludeOrRequireEmailSchema } from "./zod-utils"; + +describe("excludeOrRequireEmailSchema", () => { + const parse = (input: string) => z.object({ v: excludeOrRequireEmailSchema }).safeParse({ v: input }); + + describe("valid inputs", () => { + it("accepts single TLD domains", () => { + expect(parse("gmail.com").success).toBe(true); + }); + + it("accepts uppercase domains", () => { + expect(parse("GMAIL.COM").success).toBe(true); + }); + + it("accepts multi-level TLD domains (co.uk)", () => { + expect(parse("hotmail.co.uk").success).toBe(true); + }); + + it("accepts multi-level TLD domains (k12.us)", () => { + expect(parse("mail.school.k12.us").success).toBe(true); + }); + + it("accepts full email addresses", () => { + expect(parse("user@example.co.uk").success).toBe(true); + }); + + it("accepts @domain format", () => { + expect(parse("@example.co.uk").success).toBe(true); + }); + + it("accepts multiple comma-separated entries", () => { + expect(parse("gmail.com, @example.com, user@example.co.uk").success).toBe(true); + }); + + it("accepts domains with trailing comma (ignores empty entry)", () => { + expect(parse("gmail.com, hotmail.co.uk,").success).toBe(true); + }); + + it("accepts domains with extra spaces", () => { + expect(parse(" gmail.com , hotmail.co.uk ").success).toBe(true); + }); + + it("accepts domains with numbers and hyphens", () => { + expect(parse("example123.com").success).toBe(true); + expect(parse("my-domain.com").success).toBe(true); + }); + + it("accepts very short domains (min 2-char TLD)", () => { + expect(parse("a.co").success).toBe(true); + }); + + it("accepts long multi-level domains", () => { + expect(parse("example.co.uk.test.com").success).toBe(true); + }); + }); + + describe("invalid inputs", () => { + it("rejects single-label domains and missing dots", () => { + expect(parse("example").success).toBe(false); + expect(parse("@example").success).toBe(false); + }); + + it("rejects invalid TLD lengths", () => { + expect(parse("example.c").success).toBe(false); + }); + + it("rejects leading/trailing hyphens in labels", () => { + expect(parse("-bad.com").success).toBe(false); + expect(parse("bad-.com").success).toBe(false); + }); + + it("rejects leading/trailing dots and consecutive dots", () => { + expect(parse(".bad.com").success).toBe(false); + expect(parse("bad.com.").success).toBe(false); + expect(parse("ex..ample.com").success).toBe(false); + }); + + it("allows empty input but rejects commas-only", () => { + expect(parse("").success).toBe(true); + expect(parse(" ").success).toBe(true); + expect(parse(",,,").success).toBe(false); + }); + + it("rejects Unicode domains (ASCII-only)", () => { + expect(parse("münchen.de").success).toBe(false); + }); + }); +}); diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index f24741d5f1..3ed7664b38 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -950,16 +950,29 @@ export const fieldTypeEnum = z.enum([ export type FieldType = z.infer; export const excludeOrRequireEmailSchema = z.string().superRefine((val, ctx) => { - const allDomains = val.split(",").map((dom) => dom.trim()); + // Allow empty input: field is optional at the form level but may come through as empty string + if (val.trim() === "") return; - const regex = /^(?:@?[a-z0-9-]+(?:\.[a-z]{2,})?)?(?:@[a-z0-9-]+\.[a-z]{2,})?$/i; + const allDomains = val + .split(",") + .map((dom) => dom.trim()) + .filter(Boolean); - /* - Valid patterns - [ example, example.anything, anyone@example.anything ] - Invalid patterns - Patterns involving capital letter [ Example, Example.anything, Anyone@example.anything ] -*/ + // If user entered only separators/commas, treat as invalid input + if (allDomains.length === 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Enter valid domain or email", + }); + return; + } - const isValid = !allDomains.some((domain) => !regex.test(domain)); + // Accept forms: domain-only, `@domain`, or `local@domain` + // - Domain labels: alnum, hyphens allowed internally, no leading/trailing hyphen + // - Require at least one dot and end with an alpha TLD of length ≥2 + const EMAIL_OR_DOMAIN_PATTERN = /^(?:[a-z0-9._+'-]+@|@)?(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$/i; + + const isValid = allDomains.every((entry) => EMAIL_OR_DOMAIN_PATTERN.test(entry)); if (!isValid) { ctx.addIssue({