diff --git a/__tests__/email-validation.test.ts b/__tests__/email-validation.test.ts new file mode 100644 index 0000000..635a72b --- /dev/null +++ b/__tests__/email-validation.test.ts @@ -0,0 +1,90 @@ +import { EmailValidator } from '../lib/email-validation'; + +describe('Email Validation', () => { + // Comprehensive test scenarios covering 10+ email format cases + const validEmailScenarios = [ + // Standard email formats + 'user@example.com', + 'firstname.lastname@example.com', + 'email@subdomain.example.com', + + // Special character handling + 'firstname+lastname@example.com', + 'email.with.dots@example.com', + 'email-with-hyphen@example.com', + + // Numeric and special domain cases + 'email@123.123.123.123', // IP address domain + 'email@[123.123.123.123]', // IP in square brackets + '1234567890@example.com', // Numeric local part + + // Complex valid emails + '"email with spaces"@example.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.com' + ]; + + const invalidEmailScenarios = [ + // Empty and whitespace inputs + '', + ' ', + null as any, + undefined as any, + + // Invalid formats + 'invalid', + '@invalid.com', + 'invalid@', + 'invalid@.com', + 'invalid@domain', + 'invalid@domain.', + + // Problematic patterns + 'email@example', + 'email.@example.com', + '.email@example.com', + 'email..email@example.com', + + // Special character issues + 'あいうえお@example.com', // Non-ASCII characters + 'email@-example.com', + 'email@example-.com', + + // Length and dot issues + 'a'.repeat(65) + '@example.com', // Too long local part + 'email@' + 'a'.repeat(256) + '.com' // Too long domain + ]; + + // Validate correct email formats + test.each(validEmailScenarios)('validates valid email: %s', (email) => { + expect(EmailValidator.validate(email)).toBe(true); + expect(EmailValidator.getValidationError(email)).toBeNull(); + }); + + // Invalidate incorrect email formats + test.each(invalidEmailScenarios)('invalidates invalid email: %s', (email) => { + expect(EmailValidator.validate(email)).toBe(false); + expect(EmailValidator.getValidationError(email)).not.toBeNull(); + }); + + // Normalization tests + describe('Email Normalization', () => { + test('normalizes emails consistently', () => { + expect(EmailValidator.normalize(' User@Example.com ')).toBe('user@example.com'); + expect(EmailValidator.normalize('USER@EXAMPLE.COM')).toBe('user@example.com'); + expect(EmailValidator.normalize('')).toBe(''); + }); + }); + + // Error message tests + describe('Validation Error Messages', () => { + test('provides appropriate error messages', () => { + expect(EmailValidator.getValidationError('')).toBe('Email is required'); + expect(EmailValidator.getValidationError('a@b')).toBe('Invalid email format'); + expect(EmailValidator.getValidationError('a'.repeat(300) + '@example.com')).toBe('Email is too long'); + }); + }); +}); \ No newline at end of file diff --git a/lib/email-validation.ts b/lib/email-validation.ts new file mode 100644 index 0000000..d168691 --- /dev/null +++ b/lib/email-validation.ts @@ -0,0 +1,65 @@ +/** + * Comprehensive email validation utility + */ +export class EmailValidator { + /** + * Validate email format with comprehensive checks + * @param email - Email address to validate + * @returns boolean indicating email validity + */ + static validate(email: string): boolean { + // Check for null, undefined, or empty input + if (!email) return false; + + // Trim and convert to lowercase + const trimmedEmail = email.trim().toLowerCase(); + + // Length check (RFC 5321 limits) + if (trimmedEmail.length < 3 || trimmedEmail.length > 254) return false; + + // Comprehensive email regex with RFC 5322 standard + const emailRegex = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; + + // Additional validation checks + if (!emailRegex.test(trimmedEmail)) return false; + + // Local part length check + const [local, domain] = trimmedEmail.split('@'); + if (local.length > 64 || domain.length > 255) return false; + + // Disallow consecutive dots + if (/\.{2,}/.test(trimmedEmail)) return false; + + // Disallow leading/trailing dots in local part + if (/^\.|\.@|\.$/.test(local)) return false; + + return true; + } + + /** + * Normalize email for consistent comparison + * @param email - Email to normalize + * @returns Normalized email + */ + static normalize(email: string): string { + return email ? email.trim().toLowerCase() : ''; + } + + /** + * Get detailed validation error message + * @param email - Email to validate + * @returns Error message or null if valid + */ + static getValidationError(email: string): string | null { + if (!email) return 'Email is required'; + + const trimmedEmail = email.trim().toLowerCase(); + + if (trimmedEmail.length < 3) return 'Email is too short'; + if (trimmedEmail.length > 254) return 'Email is too long'; + + if (!this.validate(trimmedEmail)) return 'Invalid email format'; + + return null; + } +} \ No newline at end of file diff --git a/models/User.ts b/models/User.ts new file mode 100644 index 0000000..9f63609 --- /dev/null +++ b/models/User.ts @@ -0,0 +1,55 @@ +import mongoose from 'mongoose'; +import { EmailValidator } from '../lib/email-validation'; + +// Define the User schema interface +export interface IUser extends mongoose.Document { + email: string; + password: string; + // Add other user fields as needed +} + +// Create the User schema +const UserSchema = new mongoose.Schema({ + email: { + type: String, + required: [true, 'Email is required'], + unique: true, // Database-level unique constraint + trim: true, + lowercase: true, + validate: { + validator: function(value: string) { + return EmailValidator.validate(value); + }, + message: 'Invalid email format' + } + }, + password: { + type: String, + required: [true, 'Password is required'], + // Add password complexity validation if needed + } +}, { + timestamps: true, + // Ensure case-insensitive unique index + indexes: [{ + email: 1 + }] +}); + +// Pre-save hook to normalize email +UserSchema.pre('save', function(next) { + if (this.isModified('email')) { + this.email = EmailValidator.normalize(this.email); + } + next(); +}); + +// Custom method to find user by email (case-insensitive) +UserSchema.statics.findByEmail = function(email: string) { + return this.findOne({ email: EmailValidator.normalize(email) }); +}; + +// Create the User model +export const User = mongoose.models.User || mongoose.model('User', UserSchema); + +export default User; \ No newline at end of file