From 83a575c35da4248e58ba8c3309ec8657b1ddd216 Mon Sep 17 00:00:00 2001 From: Vuk7912 Date: Wed, 14 May 2025 11:26:34 +0000 Subject: [PATCH 1/9] Start draft PR From 06afd00c15c662bee0345d0cc9cb4bcca9505074 Mon Sep 17 00:00:00 2001 From: Vuk7912 Date: Wed, 14 May 2025 11:26:52 +0000 Subject: [PATCH 2/9] Add email validation utility with comprehensive validation --- lib/email-validation.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 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..b1992c2 --- /dev/null +++ b/lib/email-validation.ts @@ -0,0 +1,37 @@ +/** + * Validates email format using a comprehensive regex pattern + * @param email - The email address to validate + * @returns boolean indicating whether the email is valid + */ +export function validateEmail(email: string): boolean { + // RFC 5322 compliant email regex with some additional restrictions + const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; + + // Check for null, undefined, or empty string + if (!email) return false; + + // Trim whitespace and convert to lowercase for consistent validation + const trimmedEmail = email.trim().toLowerCase(); + + // Check email length constraints + if (trimmedEmail.length < 3 || trimmedEmail.length > 254) return false; + + // Perform regex validation + return emailRegex.test(trimmedEmail); +} + +/** + * Get a descriptive error message for invalid email + * @param email - The email address to validate + * @returns string with error message or null if email is valid + */ +export function getEmailValidationError(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'; + + return validateEmail(trimmedEmail) ? null : 'Invalid email format'; +} \ No newline at end of file From 621ff50c353d73f3ff4061ccddd542759ee5ce80 Mon Sep 17 00:00:00 2001 From: Vuk7912 Date: Wed, 14 May 2025 11:27:04 +0000 Subject: [PATCH 3/9] Add comprehensive email validation tests --- __tests__/email-validation.test.ts | 68 ++++++++++++++++++++++++++++++ 1 file changed, 68 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..6c54729 --- /dev/null +++ b/__tests__/email-validation.test.ts @@ -0,0 +1,68 @@ +import { validateEmail, getEmailValidationError } from '../lib/email-validation'; + +describe('Email Validation', () => { + // Valid email test cases + const validEmails = [ + 'user@example.com', + 'firstname.lastname@example.com', + 'email@subdomain.example.com', + 'firstname+lastname@example.com', + 'email@123.123.123.123', + 'email@[123.123.123.123]', + '"email"@example.com', + '1234567890@example.com', + 'email@example-one.com', + '_______@example.com', + 'email@example.name', + 'email@example.museum', + 'email@example.co.jp', + 'firstname-lastname@example.com' + ]; + + // Invalid email test cases + const invalidEmails = [ + '', + 'invalid', + '@invalid.com', + 'invalid@', + 'invalid@.com', + 'invalid@domain', + 'invalid@domain.', + ' @example.com', + 'email@example', + 'email.@example.com', + '.email@example.com', + 'email..email@example.com', + 'あいうえお@example.com' + ]; + + // Validate email format tests + test.each(validEmails)('validates valid email: %s', (email) => { + expect(validateEmail(email)).toBe(true); + expect(getEmailValidationError(email)).toBeNull(); + }); + + test.each(invalidEmails)('invalidates invalid email: %s', (email) => { + expect(validateEmail(email)).toBe(false); + expect(getEmailValidationError(email)).not.toBeNull(); + }); + + // Edge case tests + test('handles null and undefined inputs', () => { + expect(validateEmail('')).toBe(false); + expect(validateEmail(null as any)).toBe(false); + expect(validateEmail(undefined as any)).toBe(false); + }); + + test('trims whitespace around email', () => { + expect(validateEmail(' user@example.com ')).toBe(true); + }); + + test('checks email length constraints', () => { + const shortEmail = 'a@b'; + const longEmail = 'a'.repeat(255) + '@example.com'; + + expect(validateEmail(shortEmail)).toBe(false); + expect(validateEmail(longEmail)).toBe(false); + }); +}); \ No newline at end of file From 4d4ab83bca1bbcfd6199559570abe23527b8df83 Mon Sep 17 00:00:00 2001 From: Vuk7912 Date: Wed, 14 May 2025 11:27:59 +0000 Subject: [PATCH 4/9] Update email validation with normalization and comprehensive checks --- lib/email-validation.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/email-validation.ts b/lib/email-validation.ts index b1992c2..2fdb146 100644 --- a/lib/email-validation.ts +++ b/lib/email-validation.ts @@ -4,7 +4,7 @@ * @returns boolean indicating whether the email is valid */ export function validateEmail(email: string): boolean { - // RFC 5322 compliant email regex with some additional restrictions + // RFC 5322 compliant email regex with additional restrictions const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; // Check for null, undefined, or empty string @@ -20,6 +20,15 @@ export function validateEmail(email: string): boolean { return emailRegex.test(trimmedEmail); } +/** + * Normalize email for consistent comparison + * @param email - The email address to normalize + * @returns normalized email (lowercase, trimmed) + */ +export function normalizeEmail(email: string): string { + return email ? email.trim().toLowerCase() : ''; +} + /** * Get a descriptive error message for invalid email * @param email - The email address to validate From 9db6f9296ff1f6d393741fa4941f92657c68e7a4 Mon Sep 17 00:00:00 2001 From: Vuk7912 Date: Wed, 14 May 2025 11:28:09 +0000 Subject: [PATCH 5/9] Create User model with email uniqueness and validation --- models/User.ts | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 models/User.ts diff --git a/models/User.ts b/models/User.ts new file mode 100644 index 0000000..ff90002 --- /dev/null +++ b/models/User.ts @@ -0,0 +1,56 @@ +import mongoose from 'mongoose'; +import { validateEmail, normalizeEmail } 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, // Ensure lowercase storage + validate: { + validator: function(value: string) { + return validateEmail(value); + }, + message: 'Invalid email format' + } + }, + password: { + type: String, + required: [true, 'Password is required'], + // Add password complexity validation if needed + } + // Add other user schema fields +}, { + timestamps: true, // Add createdAt and updatedAt fields + // 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 = normalizeEmail(this.email); + } + next(); +}); + +// Custom method to find user by email (case-insensitive) +UserSchema.statics.findByEmail = function(email: string) { + return this.findOne({ email: normalizeEmail(email) }); +}; + +// Create the User model +export const User = mongoose.models.User || mongoose.model('User', UserSchema); + +export default User; \ No newline at end of file From 0607733de26a29942fa03db1384fb55deffd8c89 Mon Sep 17 00:00:00 2001 From: Vuk7912 Date: Wed, 14 May 2025 11:28:25 +0000 Subject: [PATCH 6/9] Update email validation tests with database uniqueness checks --- __tests__/email-validation.test.ts | 72 ++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 18 deletions(-) diff --git a/__tests__/email-validation.test.ts b/__tests__/email-validation.test.ts index 6c54729..d7254a3 100644 --- a/__tests__/email-validation.test.ts +++ b/__tests__/email-validation.test.ts @@ -1,7 +1,22 @@ -import { validateEmail, getEmailValidationError } from '../lib/email-validation'; +import { validateEmail, normalizeEmail, getEmailValidationError } from '../lib/email-validation'; +import mongoose from 'mongoose'; +import User from '../models/User'; describe('Email Validation', () => { - // Valid email test cases + // Setup and teardown for MongoDB connection + beforeAll(async () => { + await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/testdb'); + }); + + afterAll(async () => { + await mongoose.connection.close(); + }); + + beforeEach(async () => { + await User.deleteMany({}); + }); + + // Existing email format validation tests const validEmails = [ 'user@example.com', 'firstname.lastname@example.com', @@ -19,7 +34,6 @@ describe('Email Validation', () => { 'firstname-lastname@example.com' ]; - // Invalid email test cases const invalidEmails = [ '', 'invalid', @@ -36,7 +50,7 @@ describe('Email Validation', () => { 'あいうえお@example.com' ]; - // Validate email format tests + // Existing format validation tests test.each(validEmails)('validates valid email: %s', (email) => { expect(validateEmail(email)).toBe(true); expect(getEmailValidationError(email)).toBeNull(); @@ -47,22 +61,44 @@ describe('Email Validation', () => { expect(getEmailValidationError(email)).not.toBeNull(); }); - // Edge case tests - test('handles null and undefined inputs', () => { - expect(validateEmail('')).toBe(false); - expect(validateEmail(null as any)).toBe(false); - expect(validateEmail(undefined as any)).toBe(false); + // Email normalization tests + test('normalizes email consistently', () => { + expect(normalizeEmail(' User@Example.com ')).toBe('user@example.com'); + expect(normalizeEmail('USER@EXAMPLE.COM')).toBe('user@example.com'); }); - test('trims whitespace around email', () => { - expect(validateEmail(' user@example.com ')).toBe(true); - }); + // Database-level unique constraint tests + describe('Database Email Uniqueness', () => { + test('prevents duplicate emails (case-insensitive)', async () => { + // Create a user with a specific email + const initialUser = new User({ + email: 'test@example.com', + password: 'password123' + }); + await initialUser.save(); + + // Try to create another user with the same email (different case) + const duplicateUser = new User({ + email: 'TEST@EXAMPLE.COM', + password: 'differentpassword' + }); + + // Expect an error about duplicate email + await expect(duplicateUser.save()).rejects.toThrow(); + }); + + test('allows unique emails', async () => { + const user1 = new User({ + email: 'user1@example.com', + password: 'password123' + }); + const user2 = new User({ + email: 'user2@example.com', + password: 'password456' + }); - test('checks email length constraints', () => { - const shortEmail = 'a@b'; - const longEmail = 'a'.repeat(255) + '@example.com'; - - expect(validateEmail(shortEmail)).toBe(false); - expect(validateEmail(longEmail)).toBe(false); + await expect(user1.save()).resolves.toBeTruthy(); + await expect(user2.save()).resolves.toBeTruthy(); + }); }); }); \ No newline at end of file From cba25c64d0d2e033e0f46ec4dfccd3307da623c4 Mon Sep 17 00:00:00 2001 From: Vuk7912 Date: Wed, 14 May 2025 11:29:13 +0000 Subject: [PATCH 7/9] Enhance email validation with comprehensive checks --- lib/email-validation.ts | 95 ++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 38 deletions(-) diff --git a/lib/email-validation.ts b/lib/email-validation.ts index 2fdb146..d168691 100644 --- a/lib/email-validation.ts +++ b/lib/email-validation.ts @@ -1,46 +1,65 @@ /** - * Validates email format using a comprehensive regex pattern - * @param email - The email address to validate - * @returns boolean indicating whether the email is valid + * Comprehensive email validation utility */ -export function validateEmail(email: string): boolean { - // RFC 5322 compliant email regex with additional restrictions - const emailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; - - // Check for null, undefined, or empty string - if (!email) return false; +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 whitespace and convert to lowercase for consistent validation - const trimmedEmail = email.trim().toLowerCase(); + // Trim and convert to lowercase + const trimmedEmail = email.trim().toLowerCase(); - // Check email length constraints - if (trimmedEmail.length < 3 || trimmedEmail.length > 254) return false; + // Length check (RFC 5321 limits) + if (trimmedEmail.length < 3 || trimmedEmail.length > 254) return false; - // Perform regex validation - return emailRegex.test(trimmedEmail); -} + // 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,}))$/; -/** - * Normalize email for consistent comparison - * @param email - The email address to normalize - * @returns normalized email (lowercase, trimmed) - */ -export function normalizeEmail(email: string): string { - return email ? email.trim().toLowerCase() : ''; -} + // Additional validation checks + if (!emailRegex.test(trimmedEmail)) return false; -/** - * Get a descriptive error message for invalid email - * @param email - The email address to validate - * @returns string with error message or null if email is valid - */ -export function getEmailValidationError(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'; - - return validateEmail(trimmedEmail) ? null : 'Invalid email format'; + // 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 From 20fb26018f9d492167829c2ebd037de4fe06ec70 Mon Sep 17 00:00:00 2001 From: Vuk7912 Date: Wed, 14 May 2025 11:29:31 +0000 Subject: [PATCH 8/9] Add comprehensive email validation test scenarios --- __tests__/email-validation.test.ts | 134 +++++++++++++---------------- 1 file changed, 60 insertions(+), 74 deletions(-) diff --git a/__tests__/email-validation.test.ts b/__tests__/email-validation.test.ts index d7254a3..635a72b 100644 --- a/__tests__/email-validation.test.ts +++ b/__tests__/email-validation.test.ts @@ -1,104 +1,90 @@ -import { validateEmail, normalizeEmail, getEmailValidationError } from '../lib/email-validation'; -import mongoose from 'mongoose'; -import User from '../models/User'; +import { EmailValidator } from '../lib/email-validation'; describe('Email Validation', () => { - // Setup and teardown for MongoDB connection - beforeAll(async () => { - await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/testdb'); - }); - - afterAll(async () => { - await mongoose.connection.close(); - }); - - beforeEach(async () => { - await User.deleteMany({}); - }); - - // Existing email format validation tests - const validEmails = [ + // 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@123.123.123.123', - 'email@[123.123.123.123]', - '"email"@example.com', - '1234567890@example.com', - 'email@example-one.com', - '_______@example.com', - 'email@example.name', - 'email@example.museum', - 'email@example.co.jp', - '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 invalidEmails = [ + const invalidEmailScenarios = [ + // Empty and whitespace inputs '', + ' ', + null as any, + undefined as any, + + // Invalid formats 'invalid', '@invalid.com', 'invalid@', 'invalid@.com', 'invalid@domain', 'invalid@domain.', - ' @example.com', + + // Problematic patterns 'email@example', 'email.@example.com', '.email@example.com', 'email..email@example.com', - 'あいうえお@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 ]; - // Existing format validation tests - test.each(validEmails)('validates valid email: %s', (email) => { - expect(validateEmail(email)).toBe(true); - expect(getEmailValidationError(email)).toBeNull(); + // Validate correct email formats + test.each(validEmailScenarios)('validates valid email: %s', (email) => { + expect(EmailValidator.validate(email)).toBe(true); + expect(EmailValidator.getValidationError(email)).toBeNull(); }); - test.each(invalidEmails)('invalidates invalid email: %s', (email) => { - expect(validateEmail(email)).toBe(false); - expect(getEmailValidationError(email)).not.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(); }); - // Email normalization tests - test('normalizes email consistently', () => { - expect(normalizeEmail(' User@Example.com ')).toBe('user@example.com'); - expect(normalizeEmail('USER@EXAMPLE.COM')).toBe('user@example.com'); - }); - - // Database-level unique constraint tests - describe('Database Email Uniqueness', () => { - test('prevents duplicate emails (case-insensitive)', async () => { - // Create a user with a specific email - const initialUser = new User({ - email: 'test@example.com', - password: 'password123' - }); - await initialUser.save(); - - // Try to create another user with the same email (different case) - const duplicateUser = new User({ - email: 'TEST@EXAMPLE.COM', - password: 'differentpassword' - }); - - // Expect an error about duplicate email - await expect(duplicateUser.save()).rejects.toThrow(); + // 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(''); }); + }); - test('allows unique emails', async () => { - const user1 = new User({ - email: 'user1@example.com', - password: 'password123' - }); - const user2 = new User({ - email: 'user2@example.com', - password: 'password456' - }); - - await expect(user1.save()).resolves.toBeTruthy(); - await expect(user2.save()).resolves.toBeTruthy(); + // 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 From 84d84ccf5d5f1f6add9a3c9f0dbaec14fd0c49d4 Mon Sep 17 00:00:00 2001 From: Vuk7912 Date: Wed, 14 May 2025 11:29:42 +0000 Subject: [PATCH 9/9] Update User model with email validation and unique constraint --- models/User.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/models/User.ts b/models/User.ts index ff90002..9f63609 100644 --- a/models/User.ts +++ b/models/User.ts @@ -1,5 +1,5 @@ import mongoose from 'mongoose'; -import { validateEmail, normalizeEmail } from '../lib/email-validation'; +import { EmailValidator } from '../lib/email-validation'; // Define the User schema interface export interface IUser extends mongoose.Document { @@ -15,10 +15,10 @@ const UserSchema = new mongoose.Schema({ required: [true, 'Email is required'], unique: true, // Database-level unique constraint trim: true, - lowercase: true, // Ensure lowercase storage + lowercase: true, validate: { validator: function(value: string) { - return validateEmail(value); + return EmailValidator.validate(value); }, message: 'Invalid email format' } @@ -28,9 +28,8 @@ const UserSchema = new mongoose.Schema({ required: [true, 'Password is required'], // Add password complexity validation if needed } - // Add other user schema fields }, { - timestamps: true, // Add createdAt and updatedAt fields + timestamps: true, // Ensure case-insensitive unique index indexes: [{ email: 1 @@ -40,14 +39,14 @@ const UserSchema = new mongoose.Schema({ // Pre-save hook to normalize email UserSchema.pre('save', function(next) { if (this.isModified('email')) { - this.email = normalizeEmail(this.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: normalizeEmail(email) }); + return this.findOne({ email: EmailValidator.normalize(email) }); }; // Create the User model