Files
rentall-app/backend/tests/unit/middleware/validation.test.js
2026-01-19 19:22:01 -05:00

2466 lines
84 KiB
JavaScript

// 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: '<script>alert("xss")</script>John',
message: '<b>Hello</b> World'
};
mockSanitize
.mockReturnValueOnce('John')
.mockReturnValueOnce('Hello World');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('<script>alert("xss")</script>John', { ALLOWED_TAGS: [] });
expect(mockSanitize).toHaveBeenCalledWith('<b>Hello</b> 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: '<img src="x" onerror="alert(1)">test',
filter: 'normal text'
};
mockSanitize
.mockReturnValueOnce('test')
.mockReturnValueOnce('normal text');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('<img src="x" onerror="alert(1)">test', { ALLOWED_TAGS: [] });
expect(req.query).toEqual({
search: 'test',
filter: 'normal text'
});
});
it('should sanitize string values in req.params', () => {
req.params = {
id: '<script>malicious</script>123',
slug: 'safe-slug'
};
mockSanitize
.mockReturnValueOnce('123')
.mockReturnValueOnce('safe-slug');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('<script>malicious</script>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: '<script>alert("nested")</script>John',
profile: {
bio: '<b>Bold</b> text'
}
},
tags: ['<em>tag1</em>', '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: '<script>test</script>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: '<script>test</script>Hello',
number: 42,
boolean: true,
array: ['<em>item</em>', 123, false],
object: {
nested: '<b>nested</b>value'
}
};
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: '<script>alert("xss")</script>Hello' };
mockSanitize.mockReturnValue('Hello');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('<script>alert("xss")</script>Hello', { ALLOWED_TAGS: [] });
});
it('should strip all HTML tags by default', () => {
req.body = {
input1: '<div>content</div>',
input2: '<span>text</span>',
input3: '<script>malicious</script>'
};
mockSanitize
.mockReturnValueOnce('content')
.mockReturnValueOnce('text')
.mockReturnValueOnce('');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('<div>content</div>', { ALLOWED_TAGS: [] });
expect(mockSanitize).toHaveBeenCalledWith('<span>text</span>', { ALLOWED_TAGS: [] });
expect(mockSanitize).toHaveBeenCalledWith('<script>malicious</script>', { 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 = [
'<script>alert("XSS")</script>',
'<img src="x" onerror="alert(1)">',
'<svg onload="alert(\'XSS\')">',
'<iframe src="javascript:alert(\'XSS\')"></iframe>',
'<object data="javascript:alert(\'XSS\')"></object>',
'<embed src="javascript:alert(\'XSS\')">',
'<link rel="stylesheet" href="javascript:alert(\'XSS\')">',
'<style>@import "javascript:alert(\'XSS\')";</style>',
'<div onclick="alert(\'XSS\')">Click me</div>',
'<input type="image" src="x" onerror="alert(\'XSS\')">'
];
req.body = { maliciousInput: '<script>alert("test")</script>Hello' };
mockSanitize.mockReturnValue('Hello');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('<script>alert("test")</script>Hello', { ALLOWED_TAGS: [] });
expect(req.body.maliciousInput).toBe('Hello');
});
it('should sanitize SQL injection attempts', () => {
const sqlInjectionPayloads = [
"'; DROP TABLE users; --",
"' OR '1'='1",
"1'; UNION SELECT * FROM users--",
"'; INSERT INTO admin VALUES ('hacker', 'password'); --",
"1' AND (SELECT COUNT(*) FROM users) > 0 --"
];
sqlInjectionPayloads.forEach((payload, index) => {
req.body = { userInput: payload };
mockSanitize.mockReturnValue('sanitized');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith(payload, { ALLOWED_TAGS: [] });
});
});
it('should handle deeply nested objects with malicious content', () => {
req.body = {
level1: {
level2: {
level3: {
level4: {
malicious: '<script>alert("deep")</script>',
array: [
'<img src="x" onerror="alert(1)">',
{
nested: '<svg onload="alert(2)">'
}
]
}
}
}
}
};
mockSanitize
.mockReturnValueOnce('sanitized1')
.mockReturnValueOnce('sanitized2')
.mockReturnValueOnce('sanitized3');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledTimes(3);
expect(req.body.level1.level2.level3.level4.malicious).toBe('sanitized1');
});
it('should handle mixed content types in arrays', () => {
req.body = {
mixedArray: [
'<script>alert("xss")</script>',
123,
true,
{
nested: '<img src="x" onerror="alert(1)">'
},
null,
undefined,
false
]
};
mockSanitize
.mockReturnValueOnce('cleaned')
.mockReturnValueOnce('cleaned_nested');
sanitizeInput(req, res, next);
expect(req.body.mixedArray[0]).toBe('cleaned');
expect(req.body.mixedArray[1]).toBe(123);
expect(req.body.mixedArray[2]).toBe(true);
expect(req.body.mixedArray[3].nested).toBe('cleaned_nested');
expect(req.body.mixedArray[4]).toBeNull();
expect(req.body.mixedArray[5]).toBeUndefined();
expect(req.body.mixedArray[6]).toBe(false);
});
});
describe('Large payload handling', () => {
it('should handle extremely large input strings', () => {
const largeString = '<script>alert("xss")</script>' + 'A'.repeat(10000);
req.body = { largeInput: largeString };
mockSanitize.mockReturnValue('A'.repeat(10000));
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith(largeString, { ALLOWED_TAGS: [] });
expect(req.body.largeInput.length).toBe(10000);
});
it('should handle objects with many properties', () => {
const manyProps = {};
for (let i = 0; i < 1000; i++) {
manyProps[`prop${i}`] = `<script>alert(${i})</script>value${i}`;
}
req.body = manyProps;
// Mock to return cleaned values
mockSanitize.mockImplementation((value) => value.replace(/<script>.*?<\/script>/, ''));
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledTimes(1000);
expect(req.body.prop0).toContain('value0');
expect(req.body.prop999).toContain('value999');
});
it('should handle deeply nested object structures', () => {
let deepObject = { value: '<script>alert("deep")</script>content' };
for (let i = 0; i < 100; i++) {
deepObject = { nested: deepObject };
}
req.body = deepObject;
mockSanitize.mockReturnValue('content');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith('<script>alert("deep")</script>content', { ALLOWED_TAGS: [] });
});
});
describe('Unicode and encoding attacks', () => {
it('should handle various unicode XSS attempts', () => {
const unicodeXSS = [
'\u003cscript\u003ealert("XSS")\u003c/script\u003e',
'%3Cscript%3Ealert("XSS")%3C/script%3E',
'&lt;script&gt;alert("XSS")&lt;/script&gt;',
'\x3cscript\x3ealert("XSS")\x3c/script\x3e'
];
unicodeXSS.forEach((payload, index) => {
req.body = { unicodeInput: payload };
mockSanitize.mockReturnValue('sanitized');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith(payload, { ALLOWED_TAGS: [] });
});
});
it('should handle null bytes and control characters', () => {
const controlCharacters = [
'test\x00script',
'test\x01alert',
'test\n\r\tscript',
'test\u0000null',
'test\uFEFFbom'
];
controlCharacters.forEach((payload) => {
req.body = { controlInput: payload };
mockSanitize.mockReturnValue('test');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith(payload, { ALLOWED_TAGS: [] });
});
});
});
describe('Memory and performance edge cases', () => {
it('should handle circular object references gracefully', () => {
const circularObj = { name: '<script>alert("test")</script>safe' };
circularObj.self = circularObj;
// This should not cause infinite recursion
expect(() => {
// Test that we can detect circular references
try {
JSON.stringify(circularObj);
} catch (error) {
expect(error.message).toContain('circular');
}
}).not.toThrow();
});
it('should handle empty and null edge cases', () => {
const edgeCases = [
{ body: {}, expected: {} },
{ body: { empty: '' }, expected: { empty: '' } },
{ body: { nullValue: null }, expected: { nullValue: null } },
{ body: { undefinedValue: undefined }, expected: { undefinedValue: undefined } },
{ body: { emptyObject: {} }, expected: { emptyObject: {} } }
];
edgeCases.forEach(({ body, expected }) => {
req.body = body;
mockSanitize.mockImplementation((value) => value);
sanitizeInput(req, res, next);
expect(req.body).toEqual(expected);
});
});
});
describe('Validation bypass attempts', () => {
it('should handle case variations in malicious input', () => {
const caseVariations = [
'<Script>alert("XSS")</Script>',
'<SCRIPT>alert("XSS")</SCRIPT>',
'<ScRiPt>alert("XSS")</ScRiPt>',
'<sCrIpT>alert("XSS")</ScRiPt>'
];
caseVariations.forEach((payload) => {
req.body = { caseInput: payload };
mockSanitize.mockReturnValue('');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith(payload, { ALLOWED_TAGS: [] });
});
});
it('should handle whitespace variations in malicious input', () => {
const whitespaceVariations = [
'< script >alert("XSS")< /script >',
'<\tscript\t>alert("XSS")<\t/script\t>',
'<\nscript\n>alert("XSS")<\n/script\n>',
'<\rscript\r>alert("XSS")<\r/script\r>'
];
whitespaceVariations.forEach((payload) => {
req.body = { whitespaceInput: payload };
mockSanitize.mockReturnValue('alert("XSS")');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith(payload, { ALLOWED_TAGS: [] });
});
});
it('should handle attribute-based XSS attempts', () => {
const attributeXSS = [
'<div onclick="alert(\'XSS\')">Click</div>',
'<img src="x" onerror="alert(\'XSS\')">',
'<body onload="alert(\'XSS\')">',
'<input onfocus="alert(\'XSS\')" autofocus>',
'<select onfocus="alert(\'XSS\')" autofocus><option>test</option></select>'
];
attributeXSS.forEach((payload) => {
req.body = { attributeInput: payload };
mockSanitize.mockReturnValue('Click');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith(payload, { ALLOWED_TAGS: [] });
});
});
});
describe('Protocol-based attacks', () => {
it('should handle javascript protocol attempts', () => {
const jsProtocols = [
'javascript:alert("XSS")',
'JAVASCRIPT:alert("XSS")',
'JaVaScRiPt:alert("XSS")',
'javascript&colon;alert("XSS")',
'javascript&#x3A;alert("XSS")'
];
jsProtocols.forEach((payload) => {
req.body = { jsInput: payload };
mockSanitize.mockReturnValue('');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith(payload, { ALLOWED_TAGS: [] });
});
});
it('should handle data URI attempts', () => {
const dataURIs = [
'data:text/html,<script>alert("XSS")</script>',
'data:text/html;base64,PHNjcmlwdD5hbGVydCgiWFNTIik8L3NjcmlwdD4=',
'data:application/javascript,alert("XSS")'
];
dataURIs.forEach((payload) => {
req.body = { dataInput: payload };
mockSanitize.mockReturnValue('');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith(payload, { ALLOWED_TAGS: [] });
});
});
});
describe('Input mutation and normalization', () => {
it('should handle HTML entity encoding attacks', () => {
const htmlEntities = [
'&lt;script&gt;alert("XSS")&lt;/script&gt;',
'&#60;script&#62;alert("XSS")&#60;/script&#62;',
'&#x3C;script&#x3E;alert("XSS")&#x3C;/script&#x3E;',
'&amp;lt;script&amp;gt;alert("XSS")&amp;lt;/script&amp;gt;'
];
htmlEntities.forEach((payload) => {
req.body = { entityInput: payload };
mockSanitize.mockReturnValue('alert("XSS")');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith(payload, { ALLOWED_TAGS: [] });
});
});
it('should handle URL encoding attacks', () => {
const urlEncoded = [
'%3Cscript%3Ealert%28%22XSS%22%29%3C%2Fscript%3E',
'%253Cscript%253Ealert%2528%2522XSS%2522%2529%253C%252Fscript%253E',
'%2527%253E%253Cscript%253Ealert%2528String.fromCharCode%252888%252C83%252C83%2529%2529%253C%252Fscript%253E'
];
urlEncoded.forEach((payload) => {
req.body = { urlInput: payload };
mockSanitize.mockReturnValue('');
sanitizeInput(req, res, next);
expect(mockSanitize).toHaveBeenCalledWith(payload, { ALLOWED_TAGS: [] });
});
});
});
});
describe('Error message validation tests', () => {
describe('Validation error message structure', () => {
it('should return properly structured error messages', () => {
const mockErrors = [
{ path: 'email', msg: 'Please provide a valid email address' },
{ path: 'password', msg: 'Password must be between 8 and 128 characters' },
{ path: 'firstName', msg: 'First name must be between 1 and 50 characters' }
];
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: 'Please provide a valid email address' },
{ field: 'password', message: 'Password must be between 8 and 128 characters' },
{ field: 'firstName', message: 'First name must be between 1 and 50 characters' }
]
});
});
it('should handle single field with multiple validation errors', () => {
const mockErrors = [
{ path: 'password', msg: 'Password must be between 8 and 128 characters' },
{ path: 'password', msg: 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character' },
{ path: 'password', msg: 'Password is too common. Please choose a stronger password' }
];
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: 'password', message: 'Password must be between 8 and 128 characters' },
{ field: 'password', message: 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character' },
{ field: 'password', message: 'Password is too common. Please choose a stronger password' }
]
});
});
});
describe('Specific validation error messages', () => {
it('should provide specific error messages for email validation', () => {
const emailErrors = [
{ field: 'email', message: 'Please provide a valid email address', input: 'invalid-email' },
{ field: 'email', message: 'Email must be less than 255 characters', input: 'a'.repeat(250) + '@example.com' }
];
emailErrors.forEach(({ field, message, input }) => {
expect(field).toBe('email');
expect(message.toLowerCase()).toContain('email');
expect(typeof message).toBe('string');
expect(message.length).toBeGreaterThan(0);
});
});
it('should provide specific error messages for password validation', () => {
const passwordErrors = [
{ field: 'password', message: 'Password must be between 8 and 128 characters', input: '1234567' },
{ field: 'password', message: 'Password must contain at least one uppercase letter, one lowercase letter, one number, and one special character', input: 'password' },
{ field: 'password', message: 'Password is too common. Please choose a stronger password', input: 'password123' }
];
passwordErrors.forEach(({ field, message, input }) => {
expect(field).toBe('password');
expect(message).toContain('Password');
expect(typeof message).toBe('string');
expect(message.length).toBeGreaterThan(0);
});
});
it('should provide specific error messages for name validation', () => {
const nameErrors = [
{ field: 'firstName', message: 'First name must be between 1 and 50 characters', input: '' },
{ field: 'firstName', message: 'First name can only contain letters, spaces, hyphens, and apostrophes', input: 'John123' },
{ field: 'lastName', message: 'Last name must be between 1 and 50 characters', input: 'a'.repeat(51) },
{ field: 'lastName', message: 'Last name can only contain letters, spaces, hyphens, and apostrophes', input: 'Doe@Domain' }
];
nameErrors.forEach(({ field, message, input }) => {
expect(['firstName', 'lastName']).toContain(field);
expect(message).toMatch(/name/i);
expect(typeof message).toBe('string');
expect(message.length).toBeGreaterThan(0);
});
});
it('should provide specific error messages for username validation', () => {
const usernameErrors = [
{ field: 'username', message: 'Username must be between 3 and 30 characters', input: 'ab' },
{ field: 'username', message: 'Username can only contain letters, numbers, underscores, and hyphens', input: 'user@name' }
];
usernameErrors.forEach(({ field, message, input }) => {
expect(field).toBe('username');
expect(message).toContain('Username');
expect(typeof message).toBe('string');
expect(message.length).toBeGreaterThan(0);
});
});
it('should provide specific error messages for phone validation', () => {
const phoneErrors = [
{ field: 'phone', message: 'Please provide a valid phone number', input: 'invalid-phone' }
];
phoneErrors.forEach(({ field, message, input }) => {
expect(field).toBe('phone');
expect(message).toContain('phone');
expect(typeof message).toBe('string');
expect(message.length).toBeGreaterThan(0);
});
});
it('should provide specific error messages for address validation', () => {
const addressErrors = [
{ field: 'address1', message: 'Address line 1 must be less than 255 characters', input: 'a'.repeat(256) },
{ field: 'city', message: 'City can only contain letters, spaces, hyphens, and apostrophes', input: 'City123' },
{ field: 'zipCode', message: 'Please provide a valid ZIP code', input: '1234' }
];
addressErrors.forEach(({ field, message, input }) => {
expect(['address1', 'address2', 'city', 'state', 'zipCode', 'country']).toContain(field);
expect(typeof message).toBe('string');
expect(message.length).toBeGreaterThan(0);
});
});
});
describe('Custom validation error messages', () => {
it('should provide specific error messages for password change validation', () => {
const passwordChangeErrors = [
{ field: 'currentPassword', message: 'Current password is required', input: '' },
{ field: 'newPassword', message: 'New password must be different from current password', input: 'samePassword' },
{ field: 'confirmPassword', message: 'Password confirmation does not match', input: 'differentPassword' }
];
passwordChangeErrors.forEach(({ field, message, input }) => {
expect(['currentPassword', 'newPassword', 'confirmPassword']).toContain(field);
expect(message).toMatch(/password/i);
expect(typeof message).toBe('string');
expect(message.length).toBeGreaterThan(0);
});
});
it('should provide specific error messages for Google auth validation', () => {
const googleAuthErrors = [
{ field: 'idToken', message: 'Google ID token is required', input: '' },
{ field: 'idToken', message: 'Invalid token format', input: 'a'.repeat(2049) }
];
googleAuthErrors.forEach(({ field, message, input }) => {
expect(field).toBe('idToken');
expect(message).toMatch(/token/i);
expect(typeof message).toBe('string');
expect(message.length).toBeGreaterThan(0);
});
});
});
describe('Error message formatting and consistency', () => {
it('should format error messages consistently', () => {
const consistencyTests = [
{ message: 'Please provide a valid email address', hasProperCapitalization: true },
{ message: 'Password must be between 8 and 128 characters', hasProperCapitalization: true },
{ message: 'First name must be between 1 and 50 characters', hasProperCapitalization: true },
{ message: 'Username can only contain letters, numbers, underscores, and hyphens', hasProperCapitalization: true }
];
consistencyTests.forEach(({ message, hasProperCapitalization }) => {
// Check that message starts with capital letter
expect(message.charAt(0)).toBe(message.charAt(0).toUpperCase());
// Check that message doesn't end with period (consistent format)
expect(message.endsWith('.')).toBe(false);
// Check minimum length
expect(message.length).toBeGreaterThan(10);
// Check for proper capitalization
expect(hasProperCapitalization).toBe(true);
});
});
it('should handle error message field mapping correctly', () => {
const fieldMappingTests = [
{ originalPath: 'email', expectedField: 'email' },
{ originalPath: 'password', expectedField: 'password' },
{ originalPath: 'firstName', expectedField: 'firstName' },
{ originalPath: 'lastName', expectedField: 'lastName' },
{ originalPath: 'username', expectedField: 'username' },
{ originalPath: 'phone', expectedField: 'phone' }
];
fieldMappingTests.forEach(({ originalPath, expectedField }) => {
const mockErrors = [{ path: originalPath, msg: 'Test error message' }];
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: expectedField, message: 'Test error message' }]
});
});
});
it('should maintain consistent error response structure', () => {
const mockErrors = [
{ path: 'testField', msg: 'Test error message' }
];
const mockResult = {
isEmpty: jest.fn(() => false),
array: jest.fn(() => mockErrors)
};
mockValidationResult.mockReturnValue(mockResult);
handleValidationErrors(req, res, next);
const responseCall = res.json.mock.calls[0][0];
// Verify response structure
expect(responseCall).toHaveProperty('error');
expect(responseCall).toHaveProperty('details');
expect(responseCall.error).toBe('Validation failed');
expect(Array.isArray(responseCall.details)).toBe(true);
expect(responseCall.details.length).toBeGreaterThan(0);
// Verify detail structure
responseCall.details.forEach(detail => {
expect(detail).toHaveProperty('field');
expect(detail).toHaveProperty('message');
expect(typeof detail.field).toBe('string');
expect(typeof detail.message).toBe('string');
expect(detail.field.length).toBeGreaterThan(0);
expect(detail.message.length).toBeGreaterThan(0);
});
});
});
describe('Edge cases in error handling', () => {
it('should handle empty error arrays gracefully', () => {
const mockResult = {
isEmpty: jest.fn(() => false),
array: jest.fn(() => [])
};
mockValidationResult.mockReturnValue(mockResult);
handleValidationErrors(req, res, next);
expect(res.json).toHaveBeenCalledWith({
error: 'Validation failed',
details: []
});
});
it('should handle malformed error objects', () => {
const malformedErrors = [
{ path: undefined, msg: 'Test message' },
{ path: 'testField', msg: undefined },
{ path: null, msg: 'Test message' },
{ path: 'testField', msg: null }
];
const mockResult = {
isEmpty: jest.fn(() => false),
array: jest.fn(() => malformedErrors)
};
mockValidationResult.mockReturnValue(mockResult);
handleValidationErrors(req, res, next);
const responseCall = res.json.mock.calls[0][0];
expect(responseCall.details).toHaveLength(4);
// Verify handling of undefined/null values
responseCall.details.forEach(detail => {
expect(detail).toHaveProperty('field');
expect(detail).toHaveProperty('message');
});
});
it('should handle very long error messages', () => {
const longMessage = 'a'.repeat(1000);
const mockErrors = [{ path: 'testField', msg: longMessage }];
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: 'testField', message: longMessage }]
});
});
it('should handle special characters in error messages', () => {
const specialCharMessages = [
'Message with "quotes" and \'apostrophes\'',
'Message with <html> tags',
'Message with unicode characters: ñáéíóú',
'Message with newlines\nand\ttabs',
'Message with symbols: @#$%^&*()'
];
specialCharMessages.forEach((message) => {
const mockErrors = [{ path: 'testField', msg: message }];
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: 'testField', message: message }]
});
});
});
});
});
describe('Two-Factor Authentication Validation', () => {
describe('validateTotpCode', () => {
it('should be an array ending with handleValidationErrors', () => {
expect(Array.isArray(validateTotpCode)).toBe(true);
expect(validateTotpCode.length).toBeGreaterThan(1);
expect(validateTotpCode[validateTotpCode.length - 1]).toBe(handleValidationErrors);
});
it('should validate 6-digit numeric format', () => {
const validCodes = ['123456', '000000', '999999', '012345'];
const invalidCodes = ['12345', '1234567', 'abcdef', '12345a', '12 345', ''];
const totpRegex = /^\d{6}$/;
validCodes.forEach(code => {
expect(totpRegex.test(code)).toBe(true);
});
invalidCodes.forEach(code => {
expect(totpRegex.test(code)).toBe(false);
});
});
it('should have at least 2 middleware functions', () => {
expect(validateTotpCode.length).toBeGreaterThanOrEqual(2);
});
});
describe('validateEmailOtp', () => {
it('should be an array ending with handleValidationErrors', () => {
expect(Array.isArray(validateEmailOtp)).toBe(true);
expect(validateEmailOtp.length).toBeGreaterThan(1);
expect(validateEmailOtp[validateEmailOtp.length - 1]).toBe(handleValidationErrors);
});
it('should validate 6-digit numeric format', () => {
const validCodes = ['123456', '000000', '999999', '654321'];
const invalidCodes = ['12345', '1234567', 'abcdef', '12345a', '', ' '];
const otpRegex = /^\d{6}$/;
validCodes.forEach(code => {
expect(otpRegex.test(code)).toBe(true);
});
invalidCodes.forEach(code => {
expect(otpRegex.test(code)).toBe(false);
});
});
it('should have at least 2 middleware functions', () => {
expect(validateEmailOtp.length).toBeGreaterThanOrEqual(2);
});
});
describe('validateRecoveryCode', () => {
it('should be an array ending with handleValidationErrors', () => {
expect(Array.isArray(validateRecoveryCode)).toBe(true);
expect(validateRecoveryCode.length).toBeGreaterThan(1);
expect(validateRecoveryCode[validateRecoveryCode.length - 1]).toBe(handleValidationErrors);
});
it('should validate XXXX-XXXX format', () => {
const validCodes = ['ABCD-1234', 'abcd-efgh', '1234-5678', 'A1B2-C3D4', 'aaaa-bbbb'];
const invalidCodes = ['ABCD1234', 'ABCD-12345', 'ABC-1234', 'ABCD-123', '', 'ABCD--1234', 'ABCD_1234'];
const recoveryRegex = /^[A-Za-z0-9]{4}-[A-Za-z0-9]{4}$/i;
validCodes.forEach(code => {
expect(recoveryRegex.test(code)).toBe(true);
});
invalidCodes.forEach(code => {
expect(recoveryRegex.test(code)).toBe(false);
});
});
it('should have at least 2 middleware functions', () => {
expect(validateRecoveryCode.length).toBeGreaterThanOrEqual(2);
});
});
});
describe('Password Reset Validation', () => {
describe('validateForgotPassword', () => {
it('should be an array ending with handleValidationErrors', () => {
expect(Array.isArray(validateForgotPassword)).toBe(true);
expect(validateForgotPassword.length).toBeGreaterThan(1);
expect(validateForgotPassword[validateForgotPassword.length - 1]).toBe(handleValidationErrors);
});
it('should validate email format', () => {
const validEmails = ['user@example.com', 'test.user@domain.co.uk', 'email@test.org'];
const invalidEmails = ['invalid-email', '@domain.com', 'user@', 'user.domain.com'];
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
validEmails.forEach(email => {
expect(emailRegex.test(email)).toBe(true);
});
invalidEmails.forEach(email => {
expect(emailRegex.test(email)).toBe(false);
});
});
it('should enforce email length limits', () => {
const longEmail = 'a'.repeat(250) + '@example.com';
expect(longEmail.length).toBeGreaterThan(255);
const validEmail = 'user@example.com';
expect(validEmail.length).toBeLessThanOrEqual(255);
});
});
describe('validateResetPassword', () => {
it('should be an array ending with handleValidationErrors', () => {
expect(Array.isArray(validateResetPassword)).toBe(true);
expect(validateResetPassword.length).toBeGreaterThan(1);
expect(validateResetPassword[validateResetPassword.length - 1]).toBe(handleValidationErrors);
});
it('should validate 64-character token format', () => {
const valid64CharToken = 'a'.repeat(64);
const shortToken = 'a'.repeat(63);
const longToken = 'a'.repeat(65);
expect(valid64CharToken.length).toBe(64);
expect(shortToken.length).toBe(63);
expect(longToken.length).toBe(65);
});
it('should validate password strength requirements', () => {
const passwordRegex = /^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z])(?=.*[-@$!%*?&#^]).{8,}$/;
const strongPasswords = ['Password123!', 'MyStr0ng@Pass', 'Secure1#Test'];
const weakPasswords = ['password', 'PASSWORD123', 'Password', '12345678'];
strongPasswords.forEach(password => {
expect(passwordRegex.test(password)).toBe(true);
});
weakPasswords.forEach(password => {
expect(passwordRegex.test(password)).toBe(false);
});
});
it('should have multiple middleware functions for token and password', () => {
expect(validateResetPassword.length).toBeGreaterThanOrEqual(3);
});
});
describe('validateVerifyResetToken', () => {
it('should be an array ending with handleValidationErrors', () => {
expect(Array.isArray(validateVerifyResetToken)).toBe(true);
expect(validateVerifyResetToken.length).toBeGreaterThan(1);
expect(validateVerifyResetToken[validateVerifyResetToken.length - 1]).toBe(handleValidationErrors);
});
it('should validate 64-character token format', () => {
const valid64CharToken = 'abcdef1234567890'.repeat(4);
expect(valid64CharToken.length).toBe(64);
const shortToken = 'abc123'.repeat(10);
expect(shortToken.length).toBe(60);
const longToken = 'a'.repeat(65);
expect(longToken.length).toBe(65);
});
it('should have at least 2 middleware functions', () => {
expect(validateVerifyResetToken.length).toBeGreaterThanOrEqual(2);
});
});
});
describe('Feedback Validation', () => {
describe('validateFeedback', () => {
it('should be an array ending with handleValidationErrors', () => {
expect(Array.isArray(validateFeedback)).toBe(true);
expect(validateFeedback.length).toBeGreaterThan(1);
expect(validateFeedback[validateFeedback.length - 1]).toBe(handleValidationErrors);
});
it('should validate text length (5-5000 chars)', () => {
const tooShort = 'abcd'; // 4 chars
const minValid = 'abcde'; // 5 chars
const maxValid = 'a'.repeat(5000);
const tooLong = 'a'.repeat(5001);
expect(tooShort.length).toBe(4);
expect(minValid.length).toBe(5);
expect(maxValid.length).toBe(5000);
expect(tooLong.length).toBe(5001);
// Validate boundaries
expect(tooShort.length).toBeLessThan(5);
expect(minValid.length).toBeGreaterThanOrEqual(5);
expect(maxValid.length).toBeLessThanOrEqual(5000);
expect(tooLong.length).toBeGreaterThan(5000);
});
it('should have at least 2 middleware functions', () => {
expect(validateFeedback.length).toBeGreaterThanOrEqual(2);
});
it('should include optional URL validation', () => {
// The feedback validation should include url field as optional
expect(validateFeedback.length).toBeGreaterThanOrEqual(2);
});
});
});
describe('Coordinates Validation', () => {
describe('validateCoordinatesQuery', () => {
it('should be an array ending with handleValidationErrors', () => {
expect(Array.isArray(validateCoordinatesQuery)).toBe(true);
expect(validateCoordinatesQuery.length).toBeGreaterThan(1);
expect(validateCoordinatesQuery[validateCoordinatesQuery.length - 1]).toBe(handleValidationErrors);
});
it('should validate latitude range (-90 to 90)', () => {
const validLatitudes = [0, 45, -45, 90, -90, 37.7749];
const invalidLatitudes = [91, -91, 180, -180, 1000];
validLatitudes.forEach(lat => {
expect(lat).toBeGreaterThanOrEqual(-90);
expect(lat).toBeLessThanOrEqual(90);
});
invalidLatitudes.forEach(lat => {
expect(lat < -90 || lat > 90).toBe(true);
});
});
it('should validate longitude range (-180 to 180)', () => {
const validLongitudes = [0, 90, -90, 180, -180, -122.4194];
const invalidLongitudes = [181, -181, 360, -360];
validLongitudes.forEach(lng => {
expect(lng).toBeGreaterThanOrEqual(-180);
expect(lng).toBeLessThanOrEqual(180);
});
invalidLongitudes.forEach(lng => {
expect(lng < -180 || lng > 180).toBe(true);
});
});
it('should validate radius range (0.1 to 100)', () => {
const validRadii = [0.1, 1, 50, 100, 0.5, 99.9];
const invalidRadii = [0, 0.05, 100.1, 200, -1];
validRadii.forEach(radius => {
expect(radius).toBeGreaterThanOrEqual(0.1);
expect(radius).toBeLessThanOrEqual(100);
});
invalidRadii.forEach(radius => {
expect(radius < 0.1 || radius > 100).toBe(true);
});
});
it('should have middleware for lat, lng, and radius', () => {
expect(validateCoordinatesQuery.length).toBeGreaterThanOrEqual(4);
});
});
describe('validateCoordinatesBody', () => {
it('should be an array with validation middleware', () => {
expect(Array.isArray(validateCoordinatesBody)).toBe(true);
expect(validateCoordinatesBody.length).toBeGreaterThan(0);
});
it('should validate body latitude range (-90 to 90)', () => {
const validLatitudes = [0, 45.5, -89.99, 90, -90];
const invalidLatitudes = [90.1, -90.1, 100, -100];
validLatitudes.forEach(lat => {
expect(lat).toBeGreaterThanOrEqual(-90);
expect(lat).toBeLessThanOrEqual(90);
});
invalidLatitudes.forEach(lat => {
expect(lat < -90 || lat > 90).toBe(true);
});
});
it('should validate body longitude range (-180 to 180)', () => {
const validLongitudes = [0, 179.99, -179.99, 180, -180];
const invalidLongitudes = [180.1, -180.1, 200, -200];
validLongitudes.forEach(lng => {
expect(lng).toBeGreaterThanOrEqual(-180);
expect(lng).toBeLessThanOrEqual(180);
});
invalidLongitudes.forEach(lng => {
expect(lng < -180 || lng > 180).toBe(true);
});
});
it('should have middleware for latitude and longitude', () => {
expect(validateCoordinatesBody.length).toBeGreaterThanOrEqual(2);
});
});
});
describe('Module Exports Completeness', () => {
it('should export all validators from the module', () => {
const validationModule = require('../../../middleware/validation');
// Core middleware
expect(validationModule).toHaveProperty('sanitizeInput');
expect(validationModule).toHaveProperty('handleValidationErrors');
// Auth validators
expect(validationModule).toHaveProperty('validateRegistration');
expect(validationModule).toHaveProperty('validateLogin');
expect(validationModule).toHaveProperty('validateGoogleAuth');
// Profile validators
expect(validationModule).toHaveProperty('validateProfileUpdate');
expect(validationModule).toHaveProperty('validatePasswordChange');
// Password reset validators
expect(validationModule).toHaveProperty('validateForgotPassword');
expect(validationModule).toHaveProperty('validateResetPassword');
expect(validationModule).toHaveProperty('validateVerifyResetToken');
// Feedback validator
expect(validationModule).toHaveProperty('validateFeedback');
// Coordinate validators
expect(validationModule).toHaveProperty('validateCoordinatesQuery');
expect(validationModule).toHaveProperty('validateCoordinatesBody');
// 2FA validators
expect(validationModule).toHaveProperty('validateTotpCode');
expect(validationModule).toHaveProperty('validateEmailOtp');
expect(validationModule).toHaveProperty('validateRecoveryCode');
});
it('should export functions and arrays with correct types', () => {
const validationModule = require('../../../middleware/validation');
// Functions
expect(typeof validationModule.sanitizeInput).toBe('function');
expect(typeof validationModule.handleValidationErrors).toBe('function');
// Arrays (validation chains)
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);
expect(Array.isArray(validationModule.validateForgotPassword)).toBe(true);
expect(Array.isArray(validationModule.validateResetPassword)).toBe(true);
expect(Array.isArray(validationModule.validateVerifyResetToken)).toBe(true);
expect(Array.isArray(validationModule.validateFeedback)).toBe(true);
expect(Array.isArray(validationModule.validateCoordinatesQuery)).toBe(true);
expect(Array.isArray(validationModule.validateCoordinatesBody)).toBe(true);
expect(Array.isArray(validationModule.validateTotpCode)).toBe(true);
expect(Array.isArray(validationModule.validateEmailOtp)).toBe(true);
expect(Array.isArray(validationModule.validateRecoveryCode)).toBe(true);
});
it('should have all validation arrays end with handleValidationErrors', () => {
const validationModule = require('../../../middleware/validation');
const validatorsWithHandler = [
'validateRegistration',
'validateLogin',
'validateGoogleAuth',
'validateProfileUpdate',
'validatePasswordChange',
'validateForgotPassword',
'validateResetPassword',
'validateVerifyResetToken',
'validateFeedback',
'validateCoordinatesQuery',
'validateTotpCode',
'validateEmailOtp',
'validateRecoveryCode'
];
validatorsWithHandler.forEach(validatorName => {
const validator = validationModule[validatorName];
expect(validator[validator.length - 1]).toBe(validationModule.handleValidationErrors);
});
});
});
});