2061 lines
69 KiB
JavaScript
2061 lines
69 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()
|
|
})),
|
|
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
|
|
} = 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',
|
|
'<script>alert("XSS")</script>',
|
|
'\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:alert("XSS")',
|
|
'javascript: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 = [
|
|
'<script>alert("XSS")</script>',
|
|
'<script>alert("XSS")</script>',
|
|
'<script>alert("XSS")</script>',
|
|
'&lt;script&gt;alert("XSS")&lt;/script&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 }]
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
}); |