From 6b9749aa0d82a8398f39d42fa5580484843509b7 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:34:41 +0000 Subject: [PATCH 01/20] Start draft PR From 4e6fcc67caa365d80e334235780535ded37f9137 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:34:56 +0000 Subject: [PATCH 02/20] Add email validation utility functions --- lib/validation.ts | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 lib/validation.ts diff --git a/lib/validation.ts b/lib/validation.ts new file mode 100644 index 0000000..5a88828 --- /dev/null +++ b/lib/validation.ts @@ -0,0 +1,24 @@ +/** + * Validates an email address using a robust regex pattern + * @param email The email address to validate + * @returns Boolean indicating if the email is valid + */ +export const isValidEmail = (email: string): boolean => { + // RFC 5322 Official Standard email regex with some practical limitations + 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,}))$/; + + // Check if email is not empty and matches the regex pattern + return email.trim().length > 0 && emailRegex.test(email.trim()); +}; + +/** + * Generate a user-friendly email validation error message + * @param email The email address that failed validation + * @returns Descriptive error message + */ +export const getEmailValidationError = (email: string): string => { + if (email.trim().length === 0) { + return "Email cannot be empty"; + } + return "Please enter a valid email address"; +}; \ No newline at end of file From 5b7d9a3225377d4440fb0e0bf3870d1811f0c64e Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:35:17 +0000 Subject: [PATCH 03/20] Add comprehensive email validation tests --- __tests__/validation.test.ts | 61 ++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 __tests__/validation.test.ts diff --git a/__tests__/validation.test.ts b/__tests__/validation.test.ts new file mode 100644 index 0000000..a7507d0 --- /dev/null +++ b/__tests__/validation.test.ts @@ -0,0 +1,61 @@ +import { isValidEmail, getEmailValidationError } from '../lib/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', + '1234567890@example.com', + 'email@example-one.com', + '_______@example.com', + 'email@example.name', + 'email@example.museum', + 'email@example.co.jp', + ]; + + // Invalid email test cases + const invalidEmails = [ + '', + ' ', + 'plainaddress', + '@example.com', + 'Joe Smith ', + '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', + ]; + + // 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 error message generation + describe('Email Validation Error Messages', () => { + test('Empty email error message', () => { + expect(getEmailValidationError('')).toBe('Email cannot be empty'); + }); + + test('Invalid email error message', () => { + expect(getEmailValidationError('invalid-email')).toBe('Please enter a valid email address'); + }); + }); +}); \ No newline at end of file From 166b89206785396809a683a1ab6925f6af8fc743 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:35:39 +0000 Subject: [PATCH 04/20] Update tsconfig to include jest types and test files --- tsconfig.json | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 7a7ed11..a23f5f7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -21,14 +21,16 @@ ], "paths": { "@/*": ["./*"] - } + }, + "types": ["jest", "node"] }, "include": [ "next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", - "components/index.js" + "components/index.js", + "__tests__/**/*.test.ts" ], "exclude": ["node_modules"] -} +} \ No newline at end of file From 07115c5252418d88faa93a55aad1b710e72b6f44 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:36:06 +0000 Subject: [PATCH 05/20] Update package.json with jest configuration and test scripts --- package.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 5f77db5..903153b 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,8 @@ "build": "next build", "start": "next start", "lint": "next lint", + "test": "jest", + "test:watch": "jest --watch", "ionic:build": "npm run build", "ionic:serve": "npm run start" }, @@ -60,6 +62,7 @@ "zod": "^3.21.4" }, "devDependencies": { + "@types/jest": "^29.5.12", "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", @@ -72,8 +75,18 @@ "eslint-plugin-n": "^16.3.1", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.32.2", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", "prettier": "^3.0.0", "prettier-plugin-tailwindcss": "^0.4.0", + "ts-jest": "^29.1.2", "typescript": "^5.1.6" + }, + "jest": { + "testEnvironment": "jsdom", + "setupFilesAfterEnv": ["/jest.setup.js"], + "transform": { + "^.+\\.tsx?$": "ts-jest" + } } -} +} \ No newline at end of file From d41f3cc3afa10efeee1f113b9bc863b61c3e0a2c Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:36:11 +0000 Subject: [PATCH 06/20] Add jest setup file --- jest.setup.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 jest.setup.js diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..d2fd03f --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,2 @@ +// Optional setup for Jest +// Add any global test configurations or mocks here \ No newline at end of file From 823e62c9949aa4481e399dec1047b0de3635b663 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:36:22 +0000 Subject: [PATCH 07/20] Add example registration form with email validation --- components/RegisterForm.tsx | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 components/RegisterForm.tsx diff --git a/components/RegisterForm.tsx b/components/RegisterForm.tsx new file mode 100644 index 0000000..424a680 --- /dev/null +++ b/components/RegisterForm.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { useForm } from 'react-hook-form'; +import { isValidEmail, getEmailValidationError } from '../lib/validation'; + +interface RegisterFormData { + email: string; + password: string; +} + +const RegisterForm: React.FC = () => { + const { + register, + handleSubmit, + formState: { errors } + } = useForm(); + + const onSubmit = (data: RegisterFormData) => { + // Proceed with registration + console.log('Registration data:', data); + }; + + return ( +
+
+ + + isValidEmail(value) || getEmailValidationError(value) + })} + className={`w-full p-2 border ${errors.email ? 'border-red-500' : 'border-gray-300'}`} + /> + {errors.email && ( +

{errors.email.message}

+ )} +
+ {/* Other form fields would be added here */} + +
+ ); +}; + +export default RegisterForm; \ No newline at end of file From 37350860f56a1047b6843bac2fa6a6ca577726e7 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:36:34 +0000 Subject: [PATCH 08/20] Add tests for RegisterForm component --- __tests__/RegisterForm.test.tsx | 35 +++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 __tests__/RegisterForm.test.tsx diff --git a/__tests__/RegisterForm.test.tsx b/__tests__/RegisterForm.test.tsx new file mode 100644 index 0000000..21ba42e --- /dev/null +++ b/__tests__/RegisterForm.test.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import RegisterForm from '../components/RegisterForm'; + +describe('RegisterForm', () => { + test('renders email input', () => { + render(); + const emailInput = screen.getByLabelText(/email/i); + expect(emailInput).toBeInTheDocument(); + }); + + test('shows error for invalid email', () => { + render(); + const emailInput = screen.getByLabelText(/email/i); + const submitButton = screen.getByText(/register/i); + + fireEvent.change(emailInput, { target: { value: 'invalid-email' } }); + fireEvent.click(submitButton); + + const errorMessage = screen.getByText(/please enter a valid email address/i); + expect(errorMessage).toBeInTheDocument(); + }); + + test('accepts valid email', () => { + render(); + const emailInput = screen.getByLabelText(/email/i); + const submitButton = screen.getByText(/register/i); + + fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + fireEvent.click(submitButton); + + const errorMessage = screen.queryByText(/please enter a valid email address/i); + expect(errorMessage).not.toBeInTheDocument(); + }); +}); \ No newline at end of file From e9f201f4e690d92a2a25e84ca09e3a4d87a45b82 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:36:56 +0000 Subject: [PATCH 09/20] Update package.json with testing library dependencies --- package.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/package.json b/package.json index 903153b..19c95bf 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,10 @@ "zod": "^3.21.4" }, "devDependencies": { + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/react": "^14.2.1", "@types/jest": "^29.5.12", + "@types/testing-library__jest-dom": "^6.0.0", "@types/uuid": "^9.0.8", "@typescript-eslint/eslint-plugin": "^6.12.0", "@typescript-eslint/parser": "^6.12.0", @@ -87,6 +90,9 @@ "setupFilesAfterEnv": ["/jest.setup.js"], "transform": { "^.+\\.tsx?$": "ts-jest" + }, + "moduleNameMapper": { + "\\.(css|less|sass|scss)$": "identity-obj-proxy" } } } \ No newline at end of file From e90e45902dff610f3788a12a231ecca92642fb0e Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:37:00 +0000 Subject: [PATCH 10/20] Update jest setup to import testing library --- jest.setup.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jest.setup.js b/jest.setup.js index d2fd03f..058c368 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,2 +1,3 @@ -// Optional setup for Jest -// Add any global test configurations or mocks here \ No newline at end of file +import '@testing-library/jest-dom'; + +// Additional global setup can be added here if needed \ No newline at end of file From 471256dcfc7efd73353a5ea6c15704969f092212 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:37:40 +0000 Subject: [PATCH 11/20] Enhance email validation with RFC 5322 compliant regex and normalization --- lib/validation.ts | 33 +++++++++++++++++++++++++++------ 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/lib/validation.ts b/lib/validation.ts index 5a88828..586c3f7 100644 --- a/lib/validation.ts +++ b/lib/validation.ts @@ -1,14 +1,28 @@ /** - * Validates an email address using a robust regex pattern + * Validates an email address using a comprehensive RFC 5322 compliant regex * @param email The email address to validate * @returns Boolean indicating if the email is valid */ export const isValidEmail = (email: string): boolean => { - // RFC 5322 Official Standard email regex with some practical limitations - 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,}))$/; + // Comprehensive RFC 5322 email validation regex + 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,}))$/; - // Check if email is not empty and matches the regex pattern - return email.trim().length > 0 && emailRegex.test(email.trim()); + // Trim and validate email + const trimmedEmail = email.trim(); + + // Check length, format, and basic structure + return trimmedEmail.length > 0 && + trimmedEmail.length <= 254 && + emailRegex.test(trimmedEmail); +}; + +/** + * Normalize email for case-insensitive comparison + * @param email The email to normalize + * @returns Normalized email address + */ +export const normalizeEmail = (email: string): string => { + return email.trim().toLowerCase(); }; /** @@ -17,8 +31,15 @@ export const isValidEmail = (email: string): boolean => { * @returns Descriptive error message */ export const getEmailValidationError = (email: string): string => { - if (email.trim().length === 0) { + const trimmedEmail = email.trim(); + + if (trimmedEmail.length === 0) { return "Email cannot be empty"; } + + if (trimmedEmail.length > 254) { + return "Email address is too long"; + } + return "Please enter a valid email address"; }; \ No newline at end of file From 12b6b64f5e6260aaeef5429db2f8205b2c394b0e Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:38:07 +0000 Subject: [PATCH 12/20] Update email validation tests with comprehensive test cases --- __tests__/validation.test.ts | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/__tests__/validation.test.ts b/__tests__/validation.test.ts index a7507d0..67d38ca 100644 --- a/__tests__/validation.test.ts +++ b/__tests__/validation.test.ts @@ -1,7 +1,7 @@ -import { isValidEmail, getEmailValidationError } from '../lib/validation'; +import { isValidEmail, normalizeEmail, getEmailValidationError } from '../lib/validation'; describe('Email Validation', () => { - // Valid email test cases + // Valid email test cases covering various formats const validEmails = [ 'user@example.com', 'firstname.lastname@example.com', @@ -14,6 +14,13 @@ describe('Email Validation', () => { '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 @@ -32,6 +39,8 @@ describe('Email Validation', () => { 'email@example', 'email@-example.com', 'email@example..com', + // Emails exceeding 254 characters + 'a'.repeat(255) + '@example.com' ]; // Test valid email validation @@ -48,12 +57,27 @@ describe('Email Validation', () => { }); }); + // 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'); }); From 5116d6a388a84fa054e822eb97158818073d3218 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:38:20 +0000 Subject: [PATCH 13/20] Enhance RegisterForm with robust email validation and normalization --- components/RegisterForm.tsx | 51 ++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/components/RegisterForm.tsx b/components/RegisterForm.tsx index 424a680..7cfab48 100644 --- a/components/RegisterForm.tsx +++ b/components/RegisterForm.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, { useState } from 'react'; import { useForm } from 'react-hook-form'; -import { isValidEmail, getEmailValidationError } from '../lib/validation'; +import { isValidEmail, normalizeEmail, getEmailValidationError } from '../lib/validation'; interface RegisterFormData { email: string; @@ -8,15 +8,43 @@ interface RegisterFormData { } const RegisterForm: React.FC = () => { + const [emailError, setEmailError] = useState(null); + const { register, handleSubmit, - formState: { errors } + formState: { errors }, + setError, + clearErrors } = useForm(); + const validateEmail = (email: string): boolean => { + const normalizedEmail = normalizeEmail(email); + + if (!isValidEmail(normalizedEmail)) { + setEmailError(getEmailValidationError(email)); + setError('email', { + type: 'manual', + message: getEmailValidationError(email) + }); + return false; + } + + // Clear any previous email errors + setEmailError(null); + clearErrors('email'); + return true; + }; + const onSubmit = (data: RegisterFormData) => { - // Proceed with registration - console.log('Registration data:', data); + const isValid = validateEmail(data.email); + + if (isValid) { + // Proceed with registration using normalized email + const normalizedEmail = normalizeEmail(data.email); + console.log('Registration with normalized email:', normalizedEmail); + // Add actual registration logic here + } }; return ( @@ -28,13 +56,18 @@ const RegisterForm: React.FC = () => { type="email" {...register('email', { required: 'Email is required', - validate: (value) => - isValidEmail(value) || getEmailValidationError(value) + validate: validateEmail })} + onChange={(e) => { + // Optional: Real-time validation + validateEmail(e.target.value); + }} className={`w-full p-2 border ${errors.email ? 'border-red-500' : 'border-gray-300'}`} /> - {errors.email && ( -

{errors.email.message}

+ {(emailError || errors.email) && ( +

+ {emailError || errors.email?.message} +

)} {/* Other form fields would be added here */} From 4e90fba51bff4e579c080932e01acc89515848fd Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:38:34 +0000 Subject: [PATCH 14/20] Update RegisterForm tests with comprehensive email validation scenarios --- __tests__/RegisterForm.test.tsx | 60 ++++++++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/__tests__/RegisterForm.test.tsx b/__tests__/RegisterForm.test.tsx index 21ba42e..12762f6 100644 --- a/__tests__/RegisterForm.test.tsx +++ b/__tests__/RegisterForm.test.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { render, screen, fireEvent } from '@testing-library/react'; +import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import RegisterForm from '../components/RegisterForm'; describe('RegisterForm', () => { @@ -9,27 +9,69 @@ describe('RegisterForm', () => { expect(emailInput).toBeInTheDocument(); }); - test('shows error for invalid email', () => { + test.each([ + 'invalid-email', + '@example.com', + 'user@', + 'user@example', + 'a'.repeat(255) + '@example.com' + ])('shows error for invalid email: %s', async (invalidEmail) => { render(); const emailInput = screen.getByLabelText(/email/i); const submitButton = screen.getByText(/register/i); - fireEvent.change(emailInput, { target: { value: 'invalid-email' } }); + fireEvent.change(emailInput, { target: { value: invalidEmail } }); fireEvent.click(submitButton); - const errorMessage = screen.getByText(/please enter a valid email address/i); - expect(errorMessage).toBeInTheDocument(); + await waitFor(() => { + const errorMessage = screen.getByText(/please enter a valid email address/i); + expect(errorMessage).toBeInTheDocument(); + }); }); - test('accepts valid email', () => { + test.each([ + 'test@example.com', + 'user.name@example.co.uk', + 'firstname+lastname@example.com' + ])('accepts valid email: %s', async (validEmail) => { render(); const emailInput = screen.getByLabelText(/email/i); const submitButton = screen.getByText(/register/i); - fireEvent.change(emailInput, { target: { value: 'test@example.com' } }); + // 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(); + 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); - const errorMessage = screen.queryByText(/please enter a valid email address/i); - expect(errorMessage).not.toBeInTheDocument(); + await waitFor(() => { + expect(consoleSpy).toHaveBeenCalledWith( + expect.stringContaining('Registration with normalized email'), + 'test@example.com' + ); + }); + + consoleSpy.mockRestore(); }); }); \ No newline at end of file From a76a4ff8cd1cb04b929c1805c9b14c3357a7aadf Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:39:01 +0000 Subject: [PATCH 15/20] Update package.json with comprehensive testing dependencies --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 19c95bf..b06b96a 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "eslint-plugin-n": "^16.3.1", "eslint-plugin-promise": "^6.1.1", "eslint-plugin-react": "^7.32.2", + "identity-obj-proxy": "^3.0.0", "jest": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "prettier": "^3.0.0", From fecb490157ac4301eb3e5c4082469f6354bcabf0 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:39:19 +0000 Subject: [PATCH 16/20] Create jest setup file with testing library --- jest.setup.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/jest.setup.js b/jest.setup.js index 058c368..331666c 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,3 +1 @@ -import '@testing-library/jest-dom'; - -// Additional global setup can be added here if needed \ No newline at end of file +import '@testing-library/jest-dom'; \ No newline at end of file From 15ea1696660de967dbcba7219513fb309ef8b9a1 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:40:13 +0000 Subject: [PATCH 17/20] Add User model with unique email constraint and validation --- models/User.ts | 68 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 models/User.ts diff --git a/models/User.ts b/models/User.ts new file mode 100644 index 0000000..a565942 --- /dev/null +++ b/models/User.ts @@ -0,0 +1,68 @@ +import mongoose from 'mongoose'; +import { normalizeEmail } from '../lib/validation'; + +// Define the User interface +export interface IUser extends mongoose.Document { + email: string; + password: string; + createdAt: Date; + updatedAt: Date; +} + +// Create the User schema +const UserSchema = new mongoose.Schema({ + email: { + type: String, + required: [true, 'Email is required'], + unique: true, // Database-level unique constraint + lowercase: true, // Store email in lowercase + trim: true, // Remove whitespace + validate: { + validator: function(value: string) { + // Reuse existing email validation + const { isValidEmail } = require('../lib/validation'); + return isValidEmail(value); + }, + message: 'Please provide a valid email address' + }, + index: true // Create an index for faster querying + }, + password: { + type: String, + required: [true, 'Password is required'], + minlength: [8, 'Password must be at least 8 characters long'] + }, + createdAt: { + type: Date, + default: Date.now + }, + updatedAt: { + type: Date, + default: Date.now + } +}, { + timestamps: true, // Automatically manage createdAt and updatedAt + // Ensure unique constraint is case-insensitive + autoIndex: true +}); + +// Pre-save middleware to normalize email +UserSchema.pre('save', function(next) { + // Normalize email before saving + if (this.isModified('email')) { + this.email = normalizeEmail(this.email); + } + next(); +}); + +// Custom method to check if email exists +UserSchema.statics.emailExists = async function(email: string): Promise { + const normalizedEmail = normalizeEmail(email); + const existingUser = await this.findOne({ email: normalizedEmail }); + return !!existingUser; +}; + +// Create and export the User model +const User = mongoose.models.User || mongoose.model('User', UserSchema); + +export default User; \ No newline at end of file From 6f61ae57f6a99cc626c3f6959ea75bacc5d9f89f Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:40:23 +0000 Subject: [PATCH 18/20] Add email uniqueness validation utility --- lib/user-validation.ts | 47 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 lib/user-validation.ts diff --git a/lib/user-validation.ts b/lib/user-validation.ts new file mode 100644 index 0000000..796475b --- /dev/null +++ b/lib/user-validation.ts @@ -0,0 +1,47 @@ +import { normalizeEmail } from './validation'; +import User from '../models/User'; + +/** + * Check if an email is already registered + * @param email Email to check for uniqueness + * @returns Promise resolving to boolean indicating email uniqueness + */ +export const isEmailUnique = async (email: string): Promise => { + try { + const normalizedEmail = normalizeEmail(email); + const existingUser = await User.findOne({ email: normalizedEmail }); + return !existingUser; + } catch (error) { + console.error('Error checking email uniqueness:', error); + return false; + } +}; + +/** + * Validate email uniqueness with detailed error handling + * @param email Email to validate + * @returns Validation result with message + */ +export const validateEmailUniqueness = async (email: string): Promise<{ + isUnique: boolean; + message?: string; +}> => { + try { + const isUnique = await isEmailUnique(email); + + if (!isUnique) { + return { + isUnique: false, + message: 'This email is already registered' + }; + } + + return { isUnique: true }; + } catch (error) { + console.error('Email uniqueness validation error:', error); + return { + isUnique: false, + message: 'Error validating email uniqueness' + }; + } +}; \ No newline at end of file From 9ceb9f2c5059fc7a1dbcd5bdada0d2cb1e539273 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:40:40 +0000 Subject: [PATCH 19/20] Update RegisterForm with email uniqueness validation --- components/RegisterForm.tsx | 70 ++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 20 deletions(-) diff --git a/components/RegisterForm.tsx b/components/RegisterForm.tsx index 7cfab48..a98e805 100644 --- a/components/RegisterForm.tsx +++ b/components/RegisterForm.tsx @@ -1,6 +1,7 @@ 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; @@ -9,6 +10,7 @@ interface RegisterFormData { const RegisterForm: React.FC = () => { const [emailError, setEmailError] = useState(null); + const [isSubmitting, setIsSubmitting] = useState(false); const { register, @@ -18,32 +20,60 @@ const RegisterForm: React.FC = () => { clearErrors } = useForm(); - const validateEmail = (email: string): boolean => { + const validateEmail = async (email: string): Promise => { const normalizedEmail = normalizeEmail(email); + // Clear previous errors + setEmailError(null); + clearErrors('email'); + + // Check email format if (!isValidEmail(normalizedEmail)) { - setEmailError(getEmailValidationError(email)); + const errorMessage = getEmailValidationError(email); + setEmailError(errorMessage); setError('email', { type: 'manual', - message: getEmailValidationError(email) + message: errorMessage }); return false; } - - // Clear any previous email errors - setEmailError(null); - clearErrors('email'); + + // 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 = (data: RegisterFormData) => { - const isValid = validateEmail(data.email); + const onSubmit = async (data: RegisterFormData) => { + setIsSubmitting(true); - if (isValid) { - // Proceed with registration using normalized email - const normalizedEmail = normalizeEmail(data.email); - console.log('Registration with normalized email:', normalizedEmail); - // Add actual registration logic here + 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); } }; @@ -58,10 +88,6 @@ const RegisterForm: React.FC = () => { required: 'Email is required', validate: validateEmail })} - onChange={(e) => { - // Optional: Real-time validation - validateEmail(e.target.value); - }} className={`w-full p-2 border ${errors.email ? 'border-red-500' : 'border-gray-300'}`} /> {(emailError || errors.email) && ( @@ -71,8 +97,12 @@ const RegisterForm: React.FC = () => { )} {/* Other form fields would be added here */} - ); From 6f1045173a8f4dd0fd8525fd7383abe953af3ab5 Mon Sep 17 00:00:00 2001 From: Merango Date: Wed, 14 May 2025 12:40:53 +0000 Subject: [PATCH 20/20] Add comprehensive tests for email uniqueness validation --- __tests__/user-validation.test.ts | 71 +++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 __tests__/user-validation.test.ts diff --git a/__tests__/user-validation.test.ts b/__tests__/user-validation.test.ts new file mode 100644 index 0000000..a6a7873 --- /dev/null +++ b/__tests__/user-validation.test.ts @@ -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'); + }); +}); \ No newline at end of file