Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
6b9749a
Start draft PR
Merango May 14, 2025
4e6fcc6
Add email validation utility functions
Merango May 14, 2025
5b7d9a3
Add comprehensive email validation tests
Merango May 14, 2025
166b892
Update tsconfig to include jest types and test files
Merango May 14, 2025
07115c5
Update package.json with jest configuration and test scripts
Merango May 14, 2025
d41f3cc
Add jest setup file
Merango May 14, 2025
823e62c
Add example registration form with email validation
Merango May 14, 2025
3735086
Add tests for RegisterForm component
Merango May 14, 2025
e9f201f
Update package.json with testing library dependencies
Merango May 14, 2025
e90e459
Update jest setup to import testing library
Merango May 14, 2025
471256d
Enhance email validation with RFC 5322 compliant regex and normalization
Merango May 14, 2025
12b6b64
Update email validation tests with comprehensive test cases
Merango May 14, 2025
5116d6a
Enhance RegisterForm with robust email validation and normalization
Merango May 14, 2025
4e90fba
Update RegisterForm tests with comprehensive email validation scenarios
Merango May 14, 2025
a76a4ff
Update package.json with comprehensive testing dependencies
Merango May 14, 2025
fecb490
Create jest setup file with testing library
Merango May 14, 2025
15ea169
Add User model with unique email constraint and validation
Merango May 14, 2025
6f61ae5
Add email uniqueness validation utility
Merango May 14, 2025
9ceb9f2
Update RegisterForm with email uniqueness validation
Merango May 14, 2025
6f10451
Add comprehensive tests for email uniqueness validation
Merango May 14, 2025
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
77 changes: 77 additions & 0 deletions __tests__/RegisterForm.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import RegisterForm from '../components/RegisterForm';

describe('RegisterForm', () => {
test('renders email input', () => {
render(<RegisterForm />);
const emailInput = screen.getByLabelText(/email/i);
expect(emailInput).toBeInTheDocument();
});

test.each([
'invalid-email',
'@example.com',
'user@',
'user@example',
'a'.repeat(255) + '@example.com'
])('shows error for invalid email: %s', async (invalidEmail) => {
render(<RegisterForm />);
const emailInput = screen.getByLabelText(/email/i);
const submitButton = screen.getByText(/register/i);

fireEvent.change(emailInput, { target: { value: invalidEmail } });
fireEvent.click(submitButton);

await waitFor(() => {
const errorMessage = screen.getByText(/please enter a valid email address/i);
expect(errorMessage).toBeInTheDocument();
});
});

test.each([
'test@example.com',
'user.name@example.co.uk',
'firstname+lastname@example.com'
])('accepts valid email: %s', async (validEmail) => {
render(<RegisterForm />);
const emailInput = screen.getByLabelText(/email/i);
const submitButton = screen.getByText(/register/i);

// Mock console.log to check registration
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();

fireEvent.change(emailInput, { target: { value: validEmail } });
fireEvent.click(submitButton);

await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Registration with normalized email'),
expect.any(String)
);
});

consoleSpy.mockRestore();
});

test('handles case-insensitive email input', async () => {
render(<RegisterForm />);
const emailInput = screen.getByLabelText(/email/i);
const submitButton = screen.getByText(/register/i);

// Mock console.log to check registration
const consoleSpy = jest.spyOn(console, 'log').mockImplementation();

fireEvent.change(emailInput, { target: { value: 'Test@Example.COM' } });
fireEvent.click(submitButton);

await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining('Registration with normalized email'),
'test@example.com'
);
});

consoleSpy.mockRestore();
});
});
71 changes: 71 additions & 0 deletions __tests__/user-validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import mongoose from 'mongoose';
import { isEmailUnique, validateEmailUniqueness } from '../lib/user-validation';
import User from '../models/User';

// Mock MongoDB connection
beforeAll(async () => {
await mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/testdb');
});

// Clean up and close connection after tests
afterAll(async () => {
await mongoose.connection.dropDatabase();
await mongoose.connection.close();
});

// Clear users before each test
beforeEach(async () => {
await User.deleteMany({});
});

describe('Email Uniqueness Validation', () => {
test('should return true for a new email', async () => {
const uniqueEmail = 'newuser@example.com';
const result = await isEmailUnique(uniqueEmail);
expect(result).toBe(true);
});

test('should return false for an existing email', async () => {
// Create a user first
await User.create({
email: 'existing@example.com',
password: 'password123'
});

const result = await isEmailUnique('existing@example.com');
expect(result).toBe(false);
});

test('should be case-insensitive for email uniqueness', async () => {
// Create a user with lowercase email
await User.create({
email: 'test@example.com',
password: 'password123'
});

// Check with different cases
const result1 = await isEmailUnique('TEST@EXAMPLE.COM');
const result2 = await isEmailUnique('Test@Example.com');

expect(result1).toBe(false);
expect(result2).toBe(false);
});

test('validateEmailUniqueness should provide detailed result', async () => {
// Create an existing user
await User.create({
email: 'existing@example.com',
password: 'password123'
});

// Test unique email
const uniqueResult = await validateEmailUniqueness('newuser@example.com');
expect(uniqueResult.isUnique).toBe(true);
expect(uniqueResult.message).toBeUndefined();

// Test existing email
const existingResult = await validateEmailUniqueness('existing@example.com');
expect(existingResult.isUnique).toBe(false);
expect(existingResult.message).toBe('This email is already registered');
});
});
85 changes: 85 additions & 0 deletions __tests__/validation.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { isValidEmail, normalizeEmail, getEmailValidationError } from '../lib/validation';

describe('Email Validation', () => {
// Valid email test cases covering various formats
const validEmails = [
'user@example.com',
'firstname.lastname@example.com',
'email@subdomain.example.com',
'firstname+lastname@example.com',
'email@123.123.123.123',
'1234567890@example.com',
'email@example-one.com',
'_______@example.com',
'email@example.name',
'email@example.museum',
'email@example.co.jp',
'very.common@example.com',
'disposable.style.email@example.com',
'other.email-with-hyphen@example.com',
'fully-qualified-domain@example.com',
// Test IP and domain variations
'user@[123.123.123.123]',
'user@[IPv6:2001:0db8:85a3:0000:0000:8a2e:0370:7334]'
];

// Invalid email test cases
const invalidEmails = [
'',
' ',
'plainaddress',
'@example.com',
'Joe Smith <email@example.com>',
'email.example.com',
'email@example@example.com',
'.email@example.com',
'email.@example.com',
'email..email@example.com',
'email@example.com (Joe Smith)',
'email@example',
'email@-example.com',
'email@example..com',
// Emails exceeding 254 characters
'a'.repeat(255) + '@example.com'
];

// Test valid email validation
validEmails.forEach(email => {
test(`Valid email: ${email}`, () => {
expect(isValidEmail(email)).toBe(true);
});
});

// Test invalid email validation
invalidEmails.forEach(email => {
test(`Invalid email: ${email}`, () => {
expect(isValidEmail(email)).toBe(false);
});
});

// Test email normalization
describe('Email Normalization', () => {
test('Normalize email to lowercase', () => {
expect(normalizeEmail('Test@Example.COM')).toBe('test@example.com');
});

test('Trim whitespace in email', () => {
expect(normalizeEmail(' test@example.com ')).toBe('test@example.com');
});
});

// Test error message generation
describe('Email Validation Error Messages', () => {
test('Empty email error message', () => {
expect(getEmailValidationError('')).toBe('Email cannot be empty');
});

test('Too long email error message', () => {
expect(getEmailValidationError('a'.repeat(255) + '@example.com')).toBe('Email address is too long');
});

test('Invalid email error message', () => {
expect(getEmailValidationError('invalid-email')).toBe('Please enter a valid email address');
});
});
});
111 changes: 111 additions & 0 deletions components/RegisterForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useState } from 'react';
import { useForm } from 'react-hook-form';
import { isValidEmail, normalizeEmail, getEmailValidationError } from '../lib/validation';
import { validateEmailUniqueness } from '../lib/user-validation';

interface RegisterFormData {
email: string;
password: string;
}

const RegisterForm: React.FC = () => {
const [emailError, setEmailError] = useState<string | null>(null);
const [isSubmitting, setIsSubmitting] = useState(false);

const {
register,
handleSubmit,
formState: { errors },
setError,
clearErrors
} = useForm<RegisterFormData>();

const validateEmail = async (email: string): Promise<boolean> => {
const normalizedEmail = normalizeEmail(email);

// Clear previous errors
setEmailError(null);
clearErrors('email');

// Check email format
if (!isValidEmail(normalizedEmail)) {
const errorMessage = getEmailValidationError(email);
setEmailError(errorMessage);
setError('email', {
type: 'manual',
message: errorMessage
});
return false;
}

// Check email uniqueness
try {
const uniquenessResult = await validateEmailUniqueness(normalizedEmail);

if (!uniquenessResult.isUnique) {
setEmailError(uniquenessResult.message || 'Email is already registered');
setError('email', {
type: 'manual',
message: uniquenessResult.message || 'Email is already registered'
});
return false;
}
} catch (error) {
setEmailError('Error validating email');
return false;
}

return true;
};

const onSubmit = async (data: RegisterFormData) => {
setIsSubmitting(true);

try {
const isValid = await validateEmail(data.email);

if (isValid) {
const normalizedEmail = normalizeEmail(data.email);
console.log('Registration with normalized email:', normalizedEmail);
// Add actual registration logic here
}
} catch (error) {
console.error('Registration error:', error);
setEmailError('Registration failed');
} finally {
setIsSubmitting(false);
}
};

return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium">Email</label>
<input
id="email"
type="email"
{...register('email', {
required: 'Email is required',
validate: validateEmail
})}
className={`w-full p-2 border ${errors.email ? 'border-red-500' : 'border-gray-300'}`}
/>
{(emailError || errors.email) && (
<p className="text-red-500 text-sm mt-1">
{emailError || errors.email?.message}
</p>
)}
</div>
{/* Other form fields would be added here */}
<button
type="submit"
disabled={isSubmitting}
className="w-full bg-blue-500 text-white p-2 disabled:opacity-50"
>
{isSubmitting ? 'Registering...' : 'Register'}
</button>
</form>
);
};

export default RegisterForm;
1 change: 1 addition & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import '@testing-library/jest-dom';
Loading