Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 83 additions & 0 deletions __tests__/email-validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { EmailValidator, EmailUniquenessChecker } from '../lib/email-validation';

describe('Email Validation', () => {
// 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(EmailValidator.validate(email)).toBe(true);
expect(EmailValidator.getValidationError(email)).toBeNull();
});
});

// Invalid email test cases
const invalidEmails = [
'',
' ',
'invalid-email',
'user@',
'@domain.com',
'user@domain',
'user@.com',
'user@domain..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(EmailValidator.validate(email)).toBe(false);
expect(EmailValidator.getValidationError(email)).not.toBeNull();
});
});

// 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');
});
});

// 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);
});
});
});
100 changes: 100 additions & 0 deletions lib/email-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Comprehensive email validation utility
*/
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])?)*$/;

/**
* 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;

// Trim and convert to lowercase
const trimmedEmail = email.trim().toLowerCase();

// Length checks
if (trimmedEmail.length < 5 || trimmedEmail.length > 320) return false;

// Regex validation
if (!this.EMAIL_REGEX.test(trimmedEmail)) return false;

// Split email into local and domain parts
const [localPart, domainPart] = trimmedEmail.split('@');

// Additional checks for local and domain parts
if (localPart.length > 64 || domainPart.length > 255) 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;

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;
}

/**
* Normalize email for case-insensitive comparison
* @param email - Email to normalize
* @returns Normalized email
*/
static normalize(email: string): string {
return email.trim().toLowerCase();
}
}

/**
* Email uniqueness checker (mock implementation)
* In a real application, this would interact with the database
*/
export class EmailUniquenessChecker {
// Simulated email storage (would be replaced by database check)
private static registeredEmails: Set<string> = new Set();

/**
* Check if email is unique
* @param email - Email to check
* @returns boolean indicating if email is unique
*/
static async isUnique(email: string): Promise<boolean> {
const normalizedEmail = EmailValidator.normalize(email);
return !this.registeredEmails.has(normalizedEmail);
}

/**
* Register an email (simulating database insertion)
* @param email - Email to register
*/
static async registerEmail(email: string): Promise<void> {
const normalizedEmail = EmailValidator.normalize(email);
this.registeredEmails.add(normalizedEmail);
}
}