// Mock express-validator
const mockValidationResult = jest.fn();
const mockBody = jest.fn();
jest.mock('express-validator', () => ({
body: jest.fn(() => ({
isEmail: jest.fn().mockReturnThis(),
normalizeEmail: jest.fn().mockReturnThis(),
withMessage: jest.fn().mockReturnThis(),
isLength: jest.fn().mockReturnThis(),
matches: jest.fn().mockReturnThis(),
custom: jest.fn().mockReturnThis(),
trim: jest.fn().mockReturnThis(),
optional: jest.fn().mockReturnThis(),
isMobilePhone: jest.fn().mockReturnThis(),
notEmpty: jest.fn().mockReturnThis(),
isFloat: jest.fn().mockReturnThis(),
toFloat: jest.fn().mockReturnThis()
})),
query: jest.fn(() => ({
optional: jest.fn().mockReturnThis(),
isFloat: jest.fn().mockReturnThis(),
withMessage: jest.fn().mockReturnThis(),
toFloat: jest.fn().mockReturnThis()
})),
validationResult: jest.fn()
}));
// Mock DOMPurify
const mockSanitize = jest.fn();
jest.mock('dompurify', () => jest.fn(() => ({
sanitize: mockSanitize
})));
// Mock JSDOM
jest.mock('jsdom', () => ({
JSDOM: jest.fn(() => ({
window: {}
}))
}));
const {
sanitizeInput,
handleValidationErrors,
validateRegistration,
validateLogin,
validateGoogleAuth,
validateProfileUpdate,
validatePasswordChange,
validateForgotPassword,
validateResetPassword,
validateVerifyResetToken,
validateFeedback,
validateCoordinatesQuery,
validateCoordinatesBody,
validateTotpCode,
validateEmailOtp,
validateRecoveryCode
} = require('../../../middleware/validation');
describe('Validation Middleware', () => {
let req, res, next;
beforeEach(() => {
req = {
body: {},
query: {},
params: {}
};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
next = jest.fn();
// Reset mocks
jest.clearAllMocks();
// Set up mock sanitize function default behavior
mockSanitize.mockImplementation((value) => value); // Default passthrough
mockValidationResult.mockReturnValue({ isEmpty: jest.fn(() => true), array: jest.fn(() => []) });
const { validationResult } = require('express-validator');
validationResult.mockImplementation(mockValidationResult);
});
describe('sanitizeInput', () => {
describe('String sanitization', () => {
it('should sanitize string values in req.body', () => {
req.body = {
name: 'John',
message: 'Hello World'
};
mockSanitize
.mockReturnValueOnce('John')
.mockReturnValueOnce('Hello World');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('John', { ALLOWED_TAGS: [] });
expect(mockSanitize).toHaveBeenCalledWith('Hello World', { ALLOWED_TAGS: [] });
expect(req.body).toEqual({
name: 'John',
message: 'Hello World'
});
expect(next).toHaveBeenCalled();
});
it('should sanitize string values in req.query', () => {
req.query = {
search: '
test',
filter: 'normal text'
};
mockSanitize
.mockReturnValueOnce('test')
.mockReturnValueOnce('normal text');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('
test', { ALLOWED_TAGS: [] });
expect(req.query).toEqual({
search: 'test',
filter: 'normal text'
});
});
it('should sanitize string values in req.params', () => {
req.params = {
id: '123',
slug: 'safe-slug'
};
mockSanitize
.mockReturnValueOnce('123')
.mockReturnValueOnce('safe-slug');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('123', { ALLOWED_TAGS: [] });
expect(req.params).toEqual({
id: '123',
slug: 'safe-slug'
});
});
});
describe('Object sanitization', () => {
it('should recursively sanitize nested objects', () => {
req.body = {
user: {
name: 'John',
profile: {
bio: 'Bold text'
}
},
tags: ['tag1', 'tag2']
};
mockSanitize
.mockReturnValueOnce('John')
.mockReturnValueOnce('Bold text')
.mockReturnValueOnce('tag1')
.mockReturnValueOnce('tag2');
sanitizeInput(req, res, next);
expect(req.body.user.name).toBe('John');
expect(req.body.user.profile.bio).toBe('Bold text');
expect(req.body.tags['0']).toBe('tag1');
expect(req.body.tags['1']).toBe('tag2');
});
it('should handle arrays within objects', () => {
req.body = {
items: [
{ name: 'Item1' },
{ name: 'Item2' }
]
};
mockSanitize
.mockReturnValueOnce('Item1')
.mockReturnValueOnce('Item2');
sanitizeInput(req, res, next);
expect(req.body.items[0].name).toBe('Item1');
expect(req.body.items[1].name).toBe('Item2');
});
});
describe('Non-string values', () => {
it('should preserve numbers', () => {
req.body = {
age: 25,
price: 99.99,
count: 0
};
sanitizeInput(req, res, next);
expect(req.body).toEqual({
age: 25,
price: 99.99,
count: 0
});
expect(mockSanitize).not.toHaveBeenCalled();
});
it('should preserve booleans', () => {
req.body = {
isActive: true,
isDeleted: false
};
sanitizeInput(req, res, next);
expect(req.body).toEqual({
isActive: true,
isDeleted: false
});
expect(mockSanitize).not.toHaveBeenCalled();
});
it('should preserve null values', () => {
req.body = {
nullValue: null
};
sanitizeInput(req, res, next);
expect(req.body.nullValue).toBeNull();
expect(mockSanitize).not.toHaveBeenCalled();
});
it('should preserve undefined values', () => {
req.body = {
undefinedValue: undefined
};
sanitizeInput(req, res, next);
expect(req.body.undefinedValue).toBeUndefined();
expect(mockSanitize).not.toHaveBeenCalled();
});
});
describe('Edge cases', () => {
it('should handle empty objects', () => {
req.body = {};
req.query = {};
req.params = {};
sanitizeInput(req, res, next);
expect(req.body).toEqual({});
expect(req.query).toEqual({});
expect(req.params).toEqual({});
expect(next).toHaveBeenCalled();
});
it('should handle missing req properties', () => {
delete req.body;
delete req.query;
delete req.params;
sanitizeInput(req, res, next);
expect(next).toHaveBeenCalled();
expect(mockSanitize).not.toHaveBeenCalled();
});
it('should handle mixed data types in objects', () => {
req.body = {
string: 'Hello',
number: 42,
boolean: true,
array: ['item', 123, false],
object: {
nested: 'nestedvalue'
}
};
mockSanitize
.mockReturnValueOnce('Hello')
.mockReturnValueOnce('item')
.mockReturnValueOnce('nestedvalue');
sanitizeInput(req, res, next);
expect(req.body.string).toBe('Hello');
expect(req.body.number).toBe(42);
expect(req.body.boolean).toBe(true);
expect(req.body.array['0']).toBe('item');
expect(req.body.array['1']).toBe(123);
expect(req.body.array['2']).toBe(false);
expect(req.body.object.nested).toBe('nestedvalue');
});
});
});
describe('handleValidationErrors', () => {
it('should call next when no validation errors', () => {
const mockResult = {
isEmpty: jest.fn(() => true),
array: jest.fn(() => [])
};
mockValidationResult.mockReturnValue(mockResult);
handleValidationErrors(req, res, next);
expect(mockValidationResult).toHaveBeenCalledWith(req);
expect(mockResult.isEmpty).toHaveBeenCalled();
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
it('should return 400 with error details when validation fails', () => {
const mockErrors = [
{ path: 'email', msg: 'Invalid email format' },
{ path: 'password', msg: 'Password too short' }
];
const mockResult = {
isEmpty: jest.fn(() => false),
array: jest.fn(() => mockErrors)
};
mockValidationResult.mockReturnValue(mockResult);
handleValidationErrors(req, res, next);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.json).toHaveBeenCalledWith({
error: 'Validation failed',
details: [
{ field: 'email', message: 'Invalid email format' },
{ field: 'password', message: 'Password too short' }
]
});
expect(next).not.toHaveBeenCalled();
});
it('should handle single validation error', () => {
const mockErrors = [
{ path: 'username', msg: 'Username is required' }
];
const mockResult = {
isEmpty: jest.fn(() => false),
array: jest.fn(() => mockErrors)
};
mockValidationResult.mockReturnValue(mockResult);
handleValidationErrors(req, res, next);
expect(res.json).toHaveBeenCalledWith({
error: 'Validation failed',
details: [
{ field: 'username', message: 'Username is required' }
]
});
});
it('should handle errors with different field names', () => {
const mockErrors = [
{ path: 'firstName', msg: 'First name is required' },
{ path: 'lastName', msg: 'Last name is required' },
{ path: 'phone', msg: 'Invalid phone number' }
];
const mockResult = {
isEmpty: jest.fn(() => false),
array: jest.fn(() => mockErrors)
};
mockValidationResult.mockReturnValue(mockResult);
handleValidationErrors(req, res, next);
expect(res.json).toHaveBeenCalledWith({
error: 'Validation failed',
details: [
{ field: 'firstName', message: 'First name is required' },
{ field: 'lastName', message: 'Last name is required' },
{ field: 'phone', message: 'Invalid phone number' }
]
});
});
});
describe('Validation rule arrays', () => {
const { body } = require('express-validator');
describe('validateRegistration', () => {
it('should be an array of validation middlewares', () => {
expect(Array.isArray(validateRegistration)).toBe(true);
expect(validateRegistration.length).toBeGreaterThan(1);
expect(validateRegistration[validateRegistration.length - 1]).toBe(handleValidationErrors);
});
it('should include validation fields', () => {
// Since we're mocking express-validator, we can't test the actual calls
// but we can verify the validation array structure
expect(validateRegistration.length).toBeGreaterThan(5); // Should have multiple validators
expect(validateRegistration[validateRegistration.length - 1]).toBe(handleValidationErrors);
});
});
describe('validateLogin', () => {
it('should be an array of validation middlewares', () => {
expect(Array.isArray(validateLogin)).toBe(true);
expect(validateLogin.length).toBeGreaterThan(1);
expect(validateLogin[validateLogin.length - 1]).toBe(handleValidationErrors);
});
it('should include validation fields', () => {
expect(validateLogin.length).toBeGreaterThan(2); // Should have email, password, and handler
expect(validateLogin[validateLogin.length - 1]).toBe(handleValidationErrors);
});
});
describe('validateGoogleAuth', () => {
it('should be an array of validation middlewares', () => {
expect(Array.isArray(validateGoogleAuth)).toBe(true);
expect(validateGoogleAuth.length).toBeGreaterThan(1);
expect(validateGoogleAuth[validateGoogleAuth.length - 1]).toBe(handleValidationErrors);
});
it('should include validation fields', () => {
expect(validateGoogleAuth.length).toBeGreaterThan(1);
expect(validateGoogleAuth[validateGoogleAuth.length - 1]).toBe(handleValidationErrors);
});
});
describe('validateProfileUpdate', () => {
it('should be an array of validation middlewares', () => {
expect(Array.isArray(validateProfileUpdate)).toBe(true);
expect(validateProfileUpdate.length).toBeGreaterThan(1);
expect(validateProfileUpdate[validateProfileUpdate.length - 1]).toBe(handleValidationErrors);
});
it('should include validation fields', () => {
expect(validateProfileUpdate.length).toBeGreaterThan(5);
expect(validateProfileUpdate[validateProfileUpdate.length - 1]).toBe(handleValidationErrors);
});
});
describe('validatePasswordChange', () => {
it('should be an array of validation middlewares', () => {
expect(Array.isArray(validatePasswordChange)).toBe(true);
expect(validatePasswordChange.length).toBeGreaterThan(1);
expect(validatePasswordChange[validatePasswordChange.length - 1]).toBe(handleValidationErrors);
});
it('should include validation fields', () => {
expect(validatePasswordChange.length).toBeGreaterThan(3);
expect(validatePasswordChange[validatePasswordChange.length - 1]).toBe(handleValidationErrors);
});
});
});
describe('Integration with express-validator', () => {
const { body } = require('express-validator');
it('should create validation chains with proper methods', () => {
// Verify that body() returns an object with chaining methods
const validationChain = body('test');
expect(validationChain.isEmail).toBeDefined();
expect(validationChain.isLength).toBeDefined();
expect(validationChain.matches).toBeDefined();
expect(validationChain.custom).toBeDefined();
expect(validationChain.trim).toBeDefined();
expect(validationChain.optional).toBeDefined();
expect(validationChain.withMessage).toBeDefined();
});
it('should chain validation methods correctly', () => {
const validationChain = body('email');
// Test method chaining
const result = validationChain
.isEmail()
.normalizeEmail()
.withMessage('Test message')
.isLength({ max: 255 });
expect(result).toBe(validationChain); // Should return the same object for chaining
});
});
describe('DOMPurify integration', () => {
const DOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
it('should use mocked DOMPurify and JSDOM', () => {
const { JSDOM } = require('jsdom');
const DOMPurify = require('dompurify');
// Test that our mocks are in place
expect(typeof JSDOM).toBe('function');
expect(typeof DOMPurify).toBe('function');
// Test that DOMPurify returns our mock sanitize function
const purifyInstance = DOMPurify();
expect(purifyInstance.sanitize).toBe(mockSanitize);
});
it('should call DOMPurify.sanitize with correct options', () => {
req.body = { test: 'Hello' };
mockSanitize.mockReturnValue('Hello');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('Hello', { ALLOWED_TAGS: [] });
});
it('should strip all HTML tags by default', () => {
req.body = {
input1: '
content
',
input2: 'text',
input3: ''
};
mockSanitize
.mockReturnValueOnce('content')
.mockReturnValueOnce('text')
.mockReturnValueOnce('');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('content
', { ALLOWED_TAGS: [] });
expect(mockSanitize).toHaveBeenCalledWith('text', { ALLOWED_TAGS: [] });
expect(mockSanitize).toHaveBeenCalledWith('', { ALLOWED_TAGS: [] });
});
});
describe('Module exports', () => {
it('should export all required validation functions and middlewares', () => {
const validationModule = require('../../../middleware/validation');
expect(validationModule).toHaveProperty('sanitizeInput');
expect(validationModule).toHaveProperty('handleValidationErrors');
expect(validationModule).toHaveProperty('validateRegistration');
expect(validationModule).toHaveProperty('validateLogin');
expect(validationModule).toHaveProperty('validateGoogleAuth');
expect(validationModule).toHaveProperty('validateProfileUpdate');
expect(validationModule).toHaveProperty('validatePasswordChange');
});
it('should export functions and arrays with correct types', () => {
const validationModule = require('../../../middleware/validation');
expect(typeof validationModule.sanitizeInput).toBe('function');
expect(typeof validationModule.handleValidationErrors).toBe('function');
expect(Array.isArray(validationModule.validateRegistration)).toBe(true);
expect(Array.isArray(validationModule.validateLogin)).toBe(true);
expect(Array.isArray(validationModule.validateGoogleAuth)).toBe(true);
expect(Array.isArray(validationModule.validateProfileUpdate)).toBe(true);
expect(Array.isArray(validationModule.validatePasswordChange)).toBe(true);
});
});
describe('Password strength validation', () => {
// Since we're testing the module structure, we can verify that password validation
// includes the expected patterns and common password checks
it('should include password strength requirements in registration', () => {
const registrationValidation = validateRegistration;
// The password validation should be one of the middleware functions
expect(registrationValidation.length).toBeGreaterThan(1);
expect(Array.isArray(registrationValidation)).toBe(true);
});
it('should include password change validation with multiple fields', () => {
const passwordChangeValidation = validatePasswordChange;
// Should have current password, new password, and confirm password validation
expect(passwordChangeValidation.length).toBeGreaterThan(1);
expect(Array.isArray(passwordChangeValidation)).toBe(true);
});
describe('Password pattern validation', () => {
it('should accept strong passwords with all required elements', () => {
// Test passwords that should pass the regex: /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/
const strongPasswords = [
'Password123!',
'MyStr0ngP@ss',
'C0mpl3xP@ssw0rd',
'Secure123#Pass',
'TestUser2024!'
];
strongPasswords.forEach(password => {
const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
expect(passwordRegex.test(password)).toBe(true);
});
});
it('should reject passwords missing uppercase letters', () => {
const weakPasswords = [
'password123!',
'weak@pass1',
'lowercase123#'
];
weakPasswords.forEach(password => {
const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
expect(passwordRegex.test(password)).toBe(false);
});
});
it('should reject passwords missing lowercase letters', () => {
const weakPasswords = [
'PASSWORD123!',
'UPPERCASE@123',
'ALLCAPS456#'
];
weakPasswords.forEach(password => {
const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
expect(passwordRegex.test(password)).toBe(false);
});
});
it('should reject passwords missing numbers', () => {
const weakPasswords = [
'Password!',
'NoNumbers@Pass',
'WeakPassword#'
];
weakPasswords.forEach(password => {
const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
expect(passwordRegex.test(password)).toBe(false);
});
});
it('should reject passwords under 8 characters', () => {
const shortPasswords = [
'Pass1!',
'Ab3#',
'Short1!'
];
shortPasswords.forEach(password => {
const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
expect(passwordRegex.test(password)).toBe(false);
});
});
});
describe('Common password validation', () => {
it('should reject common passwords regardless of case', () => {
const commonPasswords = [
'password',
'PASSWORD',
'Password',
'123456',
'qwerty',
'QWERTY',
'admin',
'ADMIN',
'password123',
'PASSWORD123'
];
// Test the actual common passwords array from validation.js
const validationCommonPasswords = [
'password',
'123456',
'123456789',
'qwerty',
'abc123',
'password123',
'admin',
'letmein',
'welcome',
'monkey',
'1234567890'
];
commonPasswords.forEach(password => {
const isCommon = validationCommonPasswords.includes(password.toLowerCase());
if (validationCommonPasswords.includes(password.toLowerCase())) {
expect(isCommon).toBe(true);
}
});
});
it('should accept strong passwords not in common list', () => {
const uniquePasswords = [
'MyUniqueP@ss123',
'Str0ngP@ssw0rd2024',
'C0mpl3xS3cur3P@ss',
'UnguessableP@ss456'
];
const validationCommonPasswords = [
'password',
'123456',
'123456789',
'qwerty',
'abc123',
'password123',
'admin',
'letmein',
'welcome',
'monkey',
'1234567890'
];
uniquePasswords.forEach(password => {
const isCommon = validationCommonPasswords.includes(password.toLowerCase());
expect(isCommon).toBe(false);
});
});
});
describe('Password length boundaries', () => {
it('should reject passwords exactly 7 characters', () => {
const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
expect(passwordRegex.test('Pass12!')).toBe(false); // 7 chars
});
it('should accept passwords exactly 8 characters', () => {
const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
expect(passwordRegex.test('Pass123!')).toBe(true); // 8 chars
});
it('should accept very long passwords up to 128 characters', () => {
const longPassword = 'A1!' + 'a'.repeat(125); // 128 chars total
const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
expect(passwordRegex.test(longPassword)).toBe(true);
expect(longPassword.length).toBe(128);
});
});
describe('Password change specific validation', () => {
it('should ensure new password differs from current password', () => {
// This tests the custom validation logic in validatePasswordChange
const samePassword = 'SamePassword123!';
// Mock request object for password change
const mockReq = {
body: {
currentPassword: samePassword,
newPassword: samePassword,
confirmPassword: samePassword
}
};
// Test that passwords are identical (this would trigger custom validation error)
expect(mockReq.body.currentPassword).toBe(mockReq.body.newPassword);
});
it('should ensure confirm password matches new password', () => {
const mockReq = {
body: {
currentPassword: 'OldPassword123!',
newPassword: 'NewPassword123!',
confirmPassword: 'DifferentPassword123!'
}
};
// Test that passwords don't match (this would trigger custom validation error)
expect(mockReq.body.newPassword).not.toBe(mockReq.body.confirmPassword);
});
it('should accept valid password change with all different passwords', () => {
const mockReq = {
body: {
currentPassword: 'OldPassword123!',
newPassword: 'NewPassword456!',
confirmPassword: 'NewPassword456!'
}
};
// All validations should pass
expect(mockReq.body.currentPassword).not.toBe(mockReq.body.newPassword);
expect(mockReq.body.newPassword).toBe(mockReq.body.confirmPassword);
const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$/;
expect(passwordRegex.test(mockReq.body.newPassword)).toBe(true);
});
});
});
describe('Field-specific validation tests', () => {
describe('Email validation', () => {
it('should accept valid email formats', () => {
const validEmails = [
'user@example.com',
'test.email@domain.co.uk',
'user+tag@example.org',
'firstname.lastname@company.com',
'user123@domain123.com'
];
validEmails.forEach(email => {
// Basic email regex test (simplified version of what express-validator uses)
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
expect(emailRegex.test(email)).toBe(true);
});
});
it('should reject invalid email formats', () => {
const invalidEmails = [
'invalid-email',
'@domain.com',
'user@',
'user@domain',
'user.domain.com'
];
invalidEmails.forEach(email => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
expect(emailRegex.test(email)).toBe(false);
});
});
it('should handle email normalization', () => {
const emailsToNormalize = [
{ input: 'User@Example.COM', normalized: 'user@example.com' },
{ input: 'TEST.USER@DOMAIN.ORG', normalized: 'test.user@domain.org' },
{ input: ' user@domain.com ', normalized: 'user@domain.com' }
];
emailsToNormalize.forEach(({ input, normalized }) => {
// Test email normalization (lowercase and trim)
const result = input.toLowerCase().trim();
expect(result).toBe(normalized);
});
});
it('should enforce email length limits', () => {
const longEmail = 'a'.repeat(244) + '@example.com'; // > 255 chars
expect(longEmail.length).toBeGreaterThan(255);
const validEmail = 'user@example.com';
expect(validEmail.length).toBeLessThanOrEqual(255);
});
});
describe('Name validation (firstName/lastName)', () => {
it('should accept valid name formats', () => {
const validNames = [
'John',
'Mary-Jane',
"O'Connor",
'Jean-Pierre',
'Anna Maria',
'McDonalds'
];
validNames.forEach(name => {
const nameRegex = /^[a-zA-Z\s\-']+$/;
expect(nameRegex.test(name)).toBe(true);
expect(name.length).toBeGreaterThan(0);
expect(name.length).toBeLessThanOrEqual(50);
});
});
it('should reject invalid name formats', () => {
const invalidNames = [
'John123',
'Mary@Jane',
'User$Name',
'Name#WithSymbols',
'John.Doe',
'Name_With_Underscores',
'Name+Plus',
'Name(Parens)'
];
invalidNames.forEach(name => {
const nameRegex = /^[a-zA-Z\s\-']+$/;
expect(nameRegex.test(name)).toBe(false);
});
});
it('should enforce name length limits', () => {
const tooLongName = 'a'.repeat(51);
expect(tooLongName.length).toBeGreaterThan(50);
const emptyName = '';
expect(emptyName.length).toBe(0);
const validName = 'John';
expect(validName.length).toBeGreaterThan(0);
expect(validName.length).toBeLessThanOrEqual(50);
});
it('should handle name trimming', () => {
const namesToTrim = [
{ input: ' John ', trimmed: 'John' },
{ input: '\tMary\t', trimmed: 'Mary' },
{ input: ' Jean-Pierre ', trimmed: 'Jean-Pierre' }
];
namesToTrim.forEach(({ input, trimmed }) => {
expect(input.trim()).toBe(trimmed);
});
});
});
describe('Username validation', () => {
it('should accept valid username formats', () => {
const validUsernames = [
'user123',
'test_user',
'user-name',
'username',
'user_123',
'test-user-123',
'USER123',
'TestUser'
];
validUsernames.forEach(username => {
const usernameRegex = /^[a-zA-Z0-9_-]+$/;
expect(usernameRegex.test(username)).toBe(true);
expect(username.length).toBeGreaterThanOrEqual(3);
expect(username.length).toBeLessThanOrEqual(30);
});
});
it('should reject invalid username formats', () => {
const invalidUsernames = [
'user@name',
'user.name',
'user+name',
'user name',
'user#name',
'user$name',
'user%name',
'user!name'
];
invalidUsernames.forEach(username => {
const usernameRegex = /^[a-zA-Z0-9_-]+$/;
expect(usernameRegex.test(username)).toBe(false);
});
});
it('should enforce username length limits', () => {
const tooShort = 'ab';
expect(tooShort.length).toBeLessThan(3);
const tooLong = 'a'.repeat(31);
expect(tooLong.length).toBeGreaterThan(30);
const validUsername = 'user123';
expect(validUsername.length).toBeGreaterThanOrEqual(3);
expect(validUsername.length).toBeLessThanOrEqual(30);
});
});
describe('Phone number validation', () => {
it('should accept valid phone number formats', () => {
const validPhones = [
'+1234567890',
'+12345678901',
'1234567890',
'+447912345678', // UK
'+33123456789', // France
'+81312345678' // Japan
];
// Since we're using isMobilePhone(), we test the general format
validPhones.forEach(phone => {
expect(phone).toMatch(/^\+?\d{10,15}$/);
});
});
it('should reject invalid phone number formats', () => {
const invalidPhones = [
'123',
'abc123456',
'123-456-7890',
'(123) 456-7890',
'+1 234 567 890',
'phone123',
'12345678901234567890' // Too long
];
invalidPhones.forEach(phone => {
expect(phone).not.toMatch(/^\+?\d{10,15}$/);
});
});
});
describe('Address validation', () => {
it('should enforce address field length limits', () => {
const validAddress1 = '123 Main Street';
expect(validAddress1.length).toBeLessThanOrEqual(255);
const validAddress2 = 'Apt 4B';
expect(validAddress2.length).toBeLessThanOrEqual(255);
const validCity = 'New York';
expect(validCity.length).toBeLessThanOrEqual(100);
const validState = 'California';
expect(validState.length).toBeLessThanOrEqual(100);
const validCountry = 'United States';
expect(validCountry.length).toBeLessThanOrEqual(100);
});
it('should accept valid city formats', () => {
const validCities = [
'New York',
'Los Angeles',
'Saint-Pierre',
"O'Fallon"
];
validCities.forEach(city => {
const cityRegex = /^[a-zA-Z\s\-']+$/;
expect(cityRegex.test(city)).toBe(true);
});
});
it('should reject invalid city formats', () => {
const invalidCities = [
'City123',
'City@Name',
'City_Name',
'City.Name',
'City+Name'
];
invalidCities.forEach(city => {
const cityRegex = /^[a-zA-Z\s\-']+$/;
expect(cityRegex.test(city)).toBe(false);
});
});
});
describe('ZIP code validation', () => {
it('should accept valid ZIP code formats', () => {
const validZipCodes = [
'12345',
'12345-6789',
'90210',
'00501-0001'
];
validZipCodes.forEach(zip => {
const zipRegex = /^[0-9]{5}(-[0-9]{4})?$/;
expect(zipRegex.test(zip)).toBe(true);
});
});
it('should reject invalid ZIP code formats', () => {
const invalidZipCodes = [
'1234',
'123456',
'12345-678',
'12345-67890',
'abcde',
'12345-abcd',
'12345 6789',
'12345_6789'
];
invalidZipCodes.forEach(zip => {
const zipRegex = /^[0-9]{5}(-[0-9]{4})?$/;
expect(zipRegex.test(zip)).toBe(false);
});
});
});
describe('Google Auth token validation', () => {
it('should enforce token length limits', () => {
const validToken = 'eyJ' + 'a'.repeat(2000); // Under 2048 chars
expect(validToken.length).toBeLessThanOrEqual(2048);
const tooLongToken = 'a'.repeat(2049);
expect(tooLongToken.length).toBeGreaterThan(2048);
});
it('should require non-empty token', () => {
const emptyToken = '';
expect(emptyToken.length).toBe(0);
const validToken = 'eyJhbGciOiJSUzI1NiIsImtpZCI6...';
expect(validToken.length).toBeGreaterThan(0);
});
});
});
describe('Custom validation logic tests', () => {
describe('Optional field validation', () => {
it('should allow optional fields to be undefined', () => {
const optionalFields = {
username: undefined,
phone: undefined,
address1: undefined,
address2: undefined,
city: undefined,
state: undefined,
zipCode: undefined,
country: undefined
};
// These fields should be valid when undefined/optional
Object.entries(optionalFields).forEach(([field, value]) => {
expect(value).toBeUndefined();
});
});
it('should allow optional fields to be empty strings after trimming', () => {
const optionalFieldsEmpty = {
username: ' ',
address1: '\t\t',
address2: ' \n '
};
Object.entries(optionalFieldsEmpty).forEach(([field, value]) => {
const trimmed = value.trim();
expect(trimmed).toBe('');
});
});
it('should validate optional fields when they have values', () => {
const validOptionalFields = {
username: 'validuser123',
phone: '+1234567890',
address1: '123 Main St',
city: 'New York',
zipCode: '12345'
};
// Test that optional fields with valid values pass validation
expect(validOptionalFields.username).toMatch(/^[a-zA-Z0-9_-]+$/);
expect(validOptionalFields.phone).toMatch(/^\+?\d{10,15}$/);
expect(validOptionalFields.city).toMatch(/^[a-zA-Z\s\-']+$/);
expect(validOptionalFields.zipCode).toMatch(/^[0-9]{5}(-[0-9]{4})?$/);
});
});
describe('Required field validation', () => {
it('should enforce required fields for registration', () => {
const requiredRegistrationFields = {
email: 'user@example.com',
password: 'Password123!',
firstName: 'John',
lastName: 'Doe'
};
// All required fields should have values
Object.entries(requiredRegistrationFields).forEach(([field, value]) => {
expect(value).toBeDefined();
expect(value.toString().trim()).not.toBe('');
});
});
it('should enforce required fields for login', () => {
const requiredLoginFields = {
email: 'user@example.com',
password: 'anypassword'
};
Object.entries(requiredLoginFields).forEach(([field, value]) => {
expect(value).toBeDefined();
expect(value.toString().trim()).not.toBe('');
});
});
it('should enforce required fields for password change', () => {
const requiredPasswordChangeFields = {
currentPassword: 'OldPassword123!',
newPassword: 'NewPassword456!',
confirmPassword: 'NewPassword456!'
};
Object.entries(requiredPasswordChangeFields).forEach(([field, value]) => {
expect(value).toBeDefined();
expect(value.toString().trim()).not.toBe('');
});
});
it('should enforce required field for Google auth', () => {
const requiredGoogleAuthFields = {
idToken: 'eyJhbGciOiJSUzI1NiIsImtpZCI6...'
};
Object.entries(requiredGoogleAuthFields).forEach(([field, value]) => {
expect(value).toBeDefined();
expect(value.toString().trim()).not.toBe('');
});
});
});
describe('Conditional validation logic', () => {
it('should validate password confirmation matches new password', () => {
const passwordChangeScenarios = [
{
newPassword: 'NewPassword123!',
confirmPassword: 'NewPassword123!',
shouldMatch: true
},
{
newPassword: 'NewPassword123!',
confirmPassword: 'DifferentPassword456!',
shouldMatch: false
},
{
newPassword: 'Password',
confirmPassword: 'password', // Case sensitive
shouldMatch: false
}
];
passwordChangeScenarios.forEach(({ newPassword, confirmPassword, shouldMatch }) => {
const matches = newPassword === confirmPassword;
expect(matches).toBe(shouldMatch);
});
});
it('should validate new password differs from current password', () => {
const passwordChangeScenarios = [
{
currentPassword: 'OldPassword123!',
newPassword: 'NewPassword456!',
shouldBeDifferent: true
},
{
currentPassword: 'SamePassword123!',
newPassword: 'SamePassword123!',
shouldBeDifferent: false
},
{
currentPassword: 'password123',
newPassword: 'PASSWORD123', // Case sensitive
shouldBeDifferent: true
}
];
passwordChangeScenarios.forEach(({ currentPassword, newPassword, shouldBeDifferent }) => {
const isDifferent = currentPassword !== newPassword;
expect(isDifferent).toBe(shouldBeDifferent);
});
});
});
describe('Field interdependency validation', () => {
it('should validate complete address sets when partially provided', () => {
const addressCombinations = [
{
address1: '123 Main St',
city: 'New York',
state: 'NY',
zipCode: '12345',
isComplete: true
},
{
address1: '123 Main St',
city: undefined,
state: 'NY',
zipCode: '12345',
isComplete: false
},
{
address1: undefined,
city: undefined,
state: undefined,
zipCode: undefined,
isComplete: true // All empty is valid
}
];
addressCombinations.forEach(({ address1, city, state, zipCode, isComplete }) => {
const hasAnyAddress = [address1, city, state, zipCode].some(field =>
field !== undefined && (typeof field === 'string' ? field.trim() !== '' : true)
);
const hasAllRequired = !!(address1 && city && state && zipCode);
if (hasAnyAddress) {
expect(hasAllRequired).toBe(isComplete);
} else {
expect(true).toBe(true); // All empty is valid
}
});
});
});
describe('Data type validation', () => {
it('should handle string input validation correctly', () => {
const stringInputs = [
{ value: 'validstring', isValid: true },
{ value: '', isValid: false }, // Empty after trim
{ value: ' ', isValid: false }, // Whitespace only
{ value: 'a'.repeat(256), isValid: false }, // Too long for most fields
{ value: 'Valid String 123', isValid: true }
];
stringInputs.forEach(({ value, isValid }) => {
const trimmed = value.trim();
const hasContent = trimmed.length > 0;
const isReasonableLength = trimmed.length <= 255;
expect(hasContent && isReasonableLength).toBe(isValid);
});
});
it('should handle numeric string validation', () => {
const numericInputs = [
{ value: '12345', isNumeric: true },
{ value: '123abc', isNumeric: false },
{ value: '12345-6789', isNumeric: false }, // Contains hyphen
{ value: '', isNumeric: false },
{ value: '000123', isNumeric: true }
];
numericInputs.forEach(({ value, isNumeric }) => {
const isDigitsOnly = /^\d+$/.test(value);
expect(isDigitsOnly).toBe(isNumeric);
});
});
});
describe('Edge case handling', () => {
it('should handle unicode characters appropriately', () => {
const unicodeTestCases = [
{ value: 'Москва', field: 'city', shouldPass: false }, // Cyrillic not allowed in name regex
{ value: '北京', field: 'city', shouldPass: false } // Chinese characters
];
unicodeTestCases.forEach(({ value, field, shouldPass }) => {
let regex;
switch (field) {
case 'name':
case 'city':
regex = /^[a-zA-Z\s\-']+$/;
break;
case 'email':
regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
break;
default:
regex = /./;
}
expect(regex.test(value)).toBe(shouldPass);
});
});
it('should handle very long input strings', () => {
const longInputTests = [
{ field: 'email', maxLength: 255, value: 'user@' + 'a'.repeat(250) + '.com' },
{ field: 'firstName', maxLength: 50, value: 'a'.repeat(51) },
{ field: 'username', maxLength: 30, value: 'a'.repeat(31) },
{ field: 'password', maxLength: 128, value: 'A1!' + 'a'.repeat(126) }
];
longInputTests.forEach(({ field, maxLength, value }) => {
const exceedsLimit = value.length > maxLength;
expect(exceedsLimit).toBe(true);
});
});
it('should handle malformed input gracefully', () => {
const malformedInputs = [
null,
undefined,
{},
[],
123,
true,
false
];
malformedInputs.forEach(input => {
const isString = typeof input === 'string';
const isValidForStringValidation = isString || input == null;
// Only strings and null/undefined should pass initial type checks
expect(isValidForStringValidation).toBe(
typeof input === 'string' || input == null
);
});
});
});
});
describe('Edge cases and security tests', () => {
describe('Advanced sanitization tests', () => {
it('should sanitize complex XSS attack vectors', () => {
const xssPayloads = [
'',
'
',
'