From cdf8d052fc068e4aa40126b536dce19d2ea3b325 Mon Sep 17 00:00:00 2001 From: Taliesin67 Date: Wed, 14 May 2025 00:41:19 +0000 Subject: [PATCH 1/5] Start draft PR From 53c93412a09e70f91fb5fe9acbd4f75cd560ee8f Mon Sep 17 00:00:00 2001 From: Taliesin67 Date: Wed, 14 May 2025 00:41:37 +0000 Subject: [PATCH 2/5] Add email validation utility with comprehensive checks --- lib/email-validation.ts | 60 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 lib/email-validation.ts diff --git a/lib/email-validation.ts b/lib/email-validation.ts new file mode 100644 index 0000000..fa65a63 --- /dev/null +++ b/lib/email-validation.ts @@ -0,0 +1,60 @@ +/** + * Validates email format with comprehensive rules + * @param email - Email address to validate + * @returns boolean indicating if email is valid + */ +export function validateEmail(email: string): boolean { + // Check if email is empty or undefined + if (!email) return false; + + // Trim whitespace and convert to lowercase + const trimmedEmail = email.trim().toLowerCase(); + + // Regex for email validation + // Covers most standard email formats with some additional constraints + const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; + + // Additional validation checks + if (!emailRegex.test(trimmedEmail)) return false; + + // Length constraints + if (trimmedEmail.length < 5 || trimmedEmail.length > 100) return false; + + // Split email into local and domain parts + const [localPart, domainPart] = trimmedEmail.split('@'); + + // Additional checks for local and domain parts + if (localPart.length < 1 || localPart.length > 64) return false; + if (domainPart.length < 3 || domainPart.length > 255) return false; + + // Check for consecutive dots + if (/\.{2,}/.test(trimmedEmail)) return false; + + // Ensure valid top-level domain (basic check) + const domainParts = domainPart.split('.'); + if (domainParts.length < 2) return false; + const tld = domainParts[domainParts.length - 1]; + if (tld.length < 2 || tld.length > 63) return false; + + return true; +} + +/** + * Generates a descriptive error message for invalid emails + * @param email - Email address to validate + * @returns Error message or null if email is valid + */ +export function getEmailValidationError(email: string): string | null { + if (!email) return 'Email cannot be empty'; + + const trimmedEmail = email.trim().toLowerCase(); + + if (trimmedEmail.length < 5) return 'Email is too short'; + if (trimmedEmail.length > 100) return 'Email is too long'; + + if (!validateEmail(trimmedEmail)) { + return 'Please enter a valid email address'; + } + + return null; +} \ No newline at end of file From 6375478843d51fce0a0e315b5bdb1048ec56e93a Mon Sep 17 00:00:00 2001 From: Taliesin67 Date: Wed, 14 May 2025 00:41:47 +0000 Subject: [PATCH 3/5] Add comprehensive tests for email validation utility --- __tests__/email-validation.test.ts | 49 ++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 __tests__/email-validation.test.ts diff --git a/__tests__/email-validation.test.ts b/__tests__/email-validation.test.ts new file mode 100644 index 0000000..116479f --- /dev/null +++ b/__tests__/email-validation.test.ts @@ -0,0 +1,49 @@ +import { validateEmail, getEmailValidationError } from '../lib/email-validation'; + +describe('Email Validation', () => { + // Valid email tests + const validEmails = [ + 'user@example.com', + 'first.last@example.co.uk', + 'user+tag@example.org', + 'user123@example-domain.com', + ]; + + validEmails.forEach(email => { + test(`should validate valid email: ${email}`, () => { + expect(validateEmail(email)).toBe(true); + expect(getEmailValidationError(email)).toBeNull(); + }); + }); + + // Invalid email tests + const invalidEmails = [ + '', + ' ', + 'invalid-email', + 'user@', + '@domain.com', + 'user@domain', + 'user@.com', + 'user@domain..com', + 'a'.repeat(101) + '@example.com', + 'user@' + 'a'.repeat(256) + '.com', + ]; + + invalidEmails.forEach(email => { + test(`should invalidate invalid email: ${email}`, () => { + expect(validateEmail(email)).toBe(false); + expect(getEmailValidationError(email)).not.toBeNull(); + }); + }); + + // Edge case tests + test('should handle whitespace and case sensitivity', () => { + expect(validateEmail(' User@Example.COM ')).toBe(true); + }); + + test('should return specific error messages', () => { + expect(getEmailValidationError('')).toBe('Email cannot be empty'); + expect(getEmailValidationError('a@b')).toBe('Please enter a valid email address'); + }); +}); \ No newline at end of file From 09f3cd01a02c6b8aad6aa260d1e24f973e96e29e Mon Sep 17 00:00:00 2001 From: Taliesin67 Date: Wed, 14 May 2025 00:42:51 +0000 Subject: [PATCH 4/5] Enhance email validation with comprehensive checks and uniqueness --- lib/email-validation.ts | 124 ++++++++++++++++++++++++++-------------- 1 file changed, 82 insertions(+), 42 deletions(-) diff --git a/lib/email-validation.ts b/lib/email-validation.ts index fa65a63..c63df75 100644 --- a/lib/email-validation.ts +++ b/lib/email-validation.ts @@ -1,60 +1,100 @@ /** - * Validates email format with comprehensive rules - * @param email - Email address to validate - * @returns boolean indicating if email is valid + * Comprehensive email validation utility */ -export function validateEmail(email: string): boolean { - // Check if email is empty or undefined - if (!email) return false; +export class EmailValidator { + /** + * Advanced email validation regex + * Supports most standard email formats with additional constraints + */ + private static EMAIL_REGEX = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; - // Trim whitespace and convert to lowercase - const trimmedEmail = email.trim().toLowerCase(); + /** + * Validate email format + * @param email - Email address to validate + * @returns boolean indicating if email is valid + */ + static validate(email: string): boolean { + // Check if email is empty or undefined + if (!email) return false; - // Regex for email validation - // Covers most standard email formats with some additional constraints - const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; + // Trim and convert to lowercase + const trimmedEmail = email.trim().toLowerCase(); - // Additional validation checks - if (!emailRegex.test(trimmedEmail)) return false; + // Length checks + if (trimmedEmail.length < 5 || trimmedEmail.length > 320) return false; - // Length constraints - if (trimmedEmail.length < 5 || trimmedEmail.length > 100) return false; + // Regex validation + if (!this.EMAIL_REGEX.test(trimmedEmail)) return false; - // Split email into local and domain parts - const [localPart, domainPart] = trimmedEmail.split('@'); + // Split email into local and domain parts + const [localPart, domainPart] = trimmedEmail.split('@'); - // Additional checks for local and domain parts - if (localPart.length < 1 || localPart.length > 64) return false; - if (domainPart.length < 3 || domainPart.length > 255) return false; + // Additional checks for local and domain parts + if (localPart.length > 64 || domainPart.length > 255) return false; - // Check for consecutive dots - if (/\.{2,}/.test(trimmedEmail)) return false; + // Ensure valid top-level domain + const domainParts = domainPart.split('.'); + if (domainParts.length < 2) return false; + const tld = domainParts[domainParts.length - 1]; + if (tld.length < 2 || tld.length > 63) return false; - // Ensure valid top-level domain (basic check) - const domainParts = domainPart.split('.'); - if (domainParts.length < 2) return false; - const tld = domainParts[domainParts.length - 1]; - if (tld.length < 2 || tld.length > 63) return false; + return true; + } + + /** + * Generate descriptive error message for invalid emails + * @param email - Email address to validate + * @returns Error message or null if email is valid + */ + static getValidationError(email: string): string | null { + if (!email) return 'Email cannot be empty'; + + const trimmedEmail = email.trim().toLowerCase(); + + if (trimmedEmail.length < 5) return 'Email is too short'; + if (trimmedEmail.length > 320) return 'Email is too long'; + + if (!this.validate(trimmedEmail)) { + return 'Please enter a valid email address'; + } + + return null; + } - return true; + /** + * Normalize email for case-insensitive comparison + * @param email - Email to normalize + * @returns Normalized email + */ + static normalize(email: string): string { + return email.trim().toLowerCase(); + } } /** - * Generates a descriptive error message for invalid emails - * @param email - Email address to validate - * @returns Error message or null if email is valid + * Email uniqueness checker (mock implementation) + * In a real application, this would interact with the database */ -export function getEmailValidationError(email: string): string | null { - if (!email) return 'Email cannot be empty'; - - const trimmedEmail = email.trim().toLowerCase(); - - if (trimmedEmail.length < 5) return 'Email is too short'; - if (trimmedEmail.length > 100) return 'Email is too long'; +export class EmailUniquenessChecker { + // Simulated email storage (would be replaced by database check) + private static registeredEmails: Set = new Set(); - if (!validateEmail(trimmedEmail)) { - return 'Please enter a valid email address'; + /** + * Check if email is unique + * @param email - Email to check + * @returns boolean indicating if email is unique + */ + static async isUnique(email: string): Promise { + const normalizedEmail = EmailValidator.normalize(email); + return !this.registeredEmails.has(normalizedEmail); } - return null; -} \ No newline at end of file + /** + * Register an email (simulating database insertion) + * @param email - Email to register + */ + static async registerEmail(email: string): Promise { + const normalizedEmail = EmailValidator.normalize(email); + this.registeredEmails.add(normalizedEmail); + } +} From 54665bfccce7e50e12ed7953b9ce78baf88abf3c Mon Sep 17 00:00:00 2001 From: Taliesin67 Date: Wed, 14 May 2025 00:43:06 +0000 Subject: [PATCH 5/5] Add comprehensive email validation and uniqueness tests --- __tests__/email-validation.test.ts | 64 +++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/__tests__/email-validation.test.ts b/__tests__/email-validation.test.ts index 116479f..42c4a62 100644 --- a/__tests__/email-validation.test.ts +++ b/__tests__/email-validation.test.ts @@ -1,22 +1,28 @@ -import { validateEmail, getEmailValidationError } from '../lib/email-validation'; +import { EmailValidator, EmailUniquenessChecker } from '../lib/email-validation'; describe('Email Validation', () => { - // Valid email tests + // Valid email test cases const validEmails = [ 'user@example.com', 'first.last@example.co.uk', 'user+tag@example.org', 'user123@example-domain.com', + 'very.common@example.com', + 'disposable.style.email.with+symbol@example.com', + 'other.email-with-hyphen@example.com', + 'fully-qualified-domain@example.com', + 'user.name+tag@example.org', + 'x@example.com', // Shortest possible valid email ]; validEmails.forEach(email => { test(`should validate valid email: ${email}`, () => { - expect(validateEmail(email)).toBe(true); - expect(getEmailValidationError(email)).toBeNull(); + expect(EmailValidator.validate(email)).toBe(true); + expect(EmailValidator.getValidationError(email)).toBeNull(); }); }); - // Invalid email tests + // Invalid email test cases const invalidEmails = [ '', ' ', @@ -26,24 +32,52 @@ describe('Email Validation', () => { 'user@domain', 'user@.com', 'user@domain..com', - 'a'.repeat(101) + '@example.com', + 'a'.repeat(321) + '@example.com', // Too long 'user@' + 'a'.repeat(256) + '.com', + 'invalid@domain', + 'invalid@domain.', + 'invalid@.domain', + 'invalid@domain..com', + 'invalid@-domain.com', + 'invalid@domain-.com', ]; invalidEmails.forEach(email => { test(`should invalidate invalid email: ${email}`, () => { - expect(validateEmail(email)).toBe(false); - expect(getEmailValidationError(email)).not.toBeNull(); + expect(EmailValidator.validate(email)).toBe(false); + expect(EmailValidator.getValidationError(email)).not.toBeNull(); }); }); - // Edge case tests - test('should handle whitespace and case sensitivity', () => { - expect(validateEmail(' User@Example.COM ')).toBe(true); + // Email normalization and case-insensitivity tests + describe('Email Normalization', () => { + test('should normalize emails to lowercase', () => { + expect(EmailValidator.normalize('User@Example.COM')).toBe('user@example.com'); + }); + + test('should trim whitespace', () => { + expect(EmailValidator.normalize(' user@example.com ')).toBe('user@example.com'); + }); }); - test('should return specific error messages', () => { - expect(getEmailValidationError('')).toBe('Email cannot be empty'); - expect(getEmailValidationError('a@b')).toBe('Please enter a valid email address'); + // Email uniqueness tests + describe('Email Uniqueness', () => { + beforeEach(async () => { + // Reset uniqueness checker before each test + const registeredEmail = 'existing@example.com'; + await EmailUniquenessChecker.registerEmail(registeredEmail); + }); + + test('should detect non-unique email (case-insensitive)', async () => { + const existingEmail = 'Existing@Example.COM'; + const uniqueResult = await EmailUniquenessChecker.isUnique(existingEmail); + expect(uniqueResult).toBe(false); + }); + + test('should allow unique email', async () => { + const uniqueEmail = 'new.unique@example.com'; + const uniqueResult = await EmailUniquenessChecker.isUnique(uniqueEmail); + expect(uniqueResult).toBe(true); + }); }); -}); \ No newline at end of file +});