WIP
This commit is contained in:
529
backend/tests/test-error-handling.js
Normal file
529
backend/tests/test-error-handling.js
Normal file
@ -0,0 +1,529 @@
|
||||
/**
|
||||
* Test script for comprehensive error handling and logging system
|
||||
* Tests all components of the error handling implementation
|
||||
*/
|
||||
|
||||
const axios = require('axios');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const BASE_URL = 'http://localhost:3000';
|
||||
const API_BASE = `${BASE_URL}/api`;
|
||||
|
||||
class ErrorHandlingTester {
|
||||
constructor() {
|
||||
this.testResults = [];
|
||||
this.logDir = path.join(__dirname, 'logs');
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all error handling tests
|
||||
*/
|
||||
async runAllTests() {
|
||||
console.log('🧪 Starting Error Handling and Logging Tests...\n');
|
||||
|
||||
try {
|
||||
// Test 1: Database Error Handling
|
||||
await this.testDatabaseErrors();
|
||||
|
||||
// Test 2: Authentication Error Handling
|
||||
await this.testAuthenticationErrors();
|
||||
|
||||
// Test 3: Validation Error Handling
|
||||
await this.testValidationErrors();
|
||||
|
||||
// Test 4: Rate Limiting Error Handling
|
||||
await this.testRateLimitingErrors();
|
||||
|
||||
// Test 5: Logging System
|
||||
await this.testLoggingSystem();
|
||||
|
||||
// Test 6: API Error Responses
|
||||
await this.testAPIErrorResponses();
|
||||
|
||||
// Test 7: Security Event Logging
|
||||
await this.testSecurityEventLogging();
|
||||
|
||||
// Generate test report
|
||||
this.generateTestReport();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Test suite failed:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test database error handling
|
||||
*/
|
||||
async testDatabaseErrors() {
|
||||
console.log('📊 Testing Database Error Handling...');
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: 'Duplicate email registration',
|
||||
test: async () => {
|
||||
// First registration
|
||||
await this.makeRequest('POST', '/auth/register', {
|
||||
email: 'test@example.com',
|
||||
password: 'TestPassword123!'
|
||||
});
|
||||
|
||||
// Duplicate registration
|
||||
const response = await this.makeRequest('POST', '/auth/register', {
|
||||
email: 'test@example.com',
|
||||
password: 'AnotherPassword123!'
|
||||
}, false);
|
||||
|
||||
return response.status === 409 && response.data.code === 'EMAIL_EXISTS';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Invalid user ID in bookmark creation',
|
||||
test: async () => {
|
||||
const response = await this.makeRequest('POST', '/bookmarks', {
|
||||
title: 'Test Bookmark',
|
||||
url: 'https://example.com'
|
||||
}, false, 'invalid-user-token');
|
||||
|
||||
return response.status === 401 || response.status === 400;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
await this.runTestGroup('Database Errors', tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test authentication error handling
|
||||
*/
|
||||
async testAuthenticationErrors() {
|
||||
console.log('🔐 Testing Authentication Error Handling...');
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: 'Invalid credentials login',
|
||||
test: async () => {
|
||||
const response = await this.makeRequest('POST', '/auth/login', {
|
||||
email: 'nonexistent@example.com',
|
||||
password: 'wrongpassword'
|
||||
}, false);
|
||||
|
||||
return response.status === 401 && response.data.code === 'INVALID_CREDENTIALS';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Expired token access',
|
||||
test: async () => {
|
||||
const response = await this.makeRequest('GET', '/user/profile', {}, false, 'expired.token.here');
|
||||
|
||||
return response.status === 401 && response.data.code === 'TOKEN_EXPIRED';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Invalid token format',
|
||||
test: async () => {
|
||||
const response = await this.makeRequest('GET', '/user/profile', {}, false, 'invalid-token');
|
||||
|
||||
return response.status === 401 && response.data.code === 'INVALID_TOKEN';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Missing authentication token',
|
||||
test: async () => {
|
||||
const response = await this.makeRequest('GET', '/user/profile', {}, false);
|
||||
|
||||
return response.status === 401;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
await this.runTestGroup('Authentication Errors', tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test validation error handling
|
||||
*/
|
||||
async testValidationErrors() {
|
||||
console.log('✅ Testing Validation Error Handling...');
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: 'Invalid email format registration',
|
||||
test: async () => {
|
||||
const response = await this.makeRequest('POST', '/auth/register', {
|
||||
email: 'invalid-email',
|
||||
password: 'TestPassword123!'
|
||||
}, false);
|
||||
|
||||
return response.status === 400 && response.data.code === 'VALIDATION_ERROR';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Weak password registration',
|
||||
test: async () => {
|
||||
const response = await this.makeRequest('POST', '/auth/register', {
|
||||
email: 'test2@example.com',
|
||||
password: '123'
|
||||
}, false);
|
||||
|
||||
return response.status === 400 && response.data.code === 'VALIDATION_ERROR';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Missing required fields in bookmark creation',
|
||||
test: async () => {
|
||||
const validToken = await this.getValidToken();
|
||||
const response = await this.makeRequest('POST', '/bookmarks', {
|
||||
title: '' // Missing title and URL
|
||||
}, false, validToken);
|
||||
|
||||
return response.status === 400 && response.data.code === 'MISSING_REQUIRED_FIELDS';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Invalid URL format in bookmark',
|
||||
test: async () => {
|
||||
const validToken = await this.getValidToken();
|
||||
const response = await this.makeRequest('POST', '/bookmarks', {
|
||||
title: 'Test Bookmark',
|
||||
url: 'not-a-valid-url'
|
||||
}, false, validToken);
|
||||
|
||||
return response.status === 400 && response.data.code === 'VALIDATION_ERROR';
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
await this.runTestGroup('Validation Errors', tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test rate limiting error handling
|
||||
*/
|
||||
async testRateLimitingErrors() {
|
||||
console.log('🚦 Testing Rate Limiting Error Handling...');
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: 'Authentication rate limiting',
|
||||
test: async () => {
|
||||
// Make multiple rapid login attempts
|
||||
const promises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
promises.push(
|
||||
this.makeRequest('POST', '/auth/login', {
|
||||
email: 'test@example.com',
|
||||
password: 'wrongpassword'
|
||||
}, false)
|
||||
);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(promises);
|
||||
const rateLimitedResponse = responses.find(r => r.status === 429);
|
||||
|
||||
return rateLimitedResponse && rateLimitedResponse.data.code === 'RATE_LIMIT_EXCEEDED';
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
await this.runTestGroup('Rate Limiting Errors', tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test logging system
|
||||
*/
|
||||
async testLoggingSystem() {
|
||||
console.log('📝 Testing Logging System...');
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: 'Log files creation',
|
||||
test: async () => {
|
||||
// Check if log files are created
|
||||
const logFiles = ['app', 'auth', 'database', 'api', 'security'];
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
|
||||
let allFilesExist = true;
|
||||
for (const logType of logFiles) {
|
||||
const logFile = path.join(this.logDir, `${logType}-${today}.log`);
|
||||
if (!fs.existsSync(logFile)) {
|
||||
console.log(`⚠️ Log file not found: ${logFile}`);
|
||||
allFilesExist = false;
|
||||
}
|
||||
}
|
||||
|
||||
return allFilesExist;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Authentication failure logging',
|
||||
test: async () => {
|
||||
// Generate an authentication failure
|
||||
await this.makeRequest('POST', '/auth/login', {
|
||||
email: 'test@example.com',
|
||||
password: 'wrongpassword'
|
||||
}, false);
|
||||
|
||||
// Check if it was logged
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const authLogFile = path.join(this.logDir, `auth-${today}.log`);
|
||||
|
||||
if (fs.existsSync(authLogFile)) {
|
||||
const logContent = fs.readFileSync(authLogFile, 'utf8');
|
||||
return logContent.includes('Authentication failure');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'API request logging',
|
||||
test: async () => {
|
||||
// Make an API request
|
||||
await this.makeRequest('GET', '/health', {}, false);
|
||||
|
||||
// Check if it was logged
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const apiLogFile = path.join(this.logDir, `api-${today}.log`);
|
||||
|
||||
if (fs.existsSync(apiLogFile)) {
|
||||
const logContent = fs.readFileSync(apiLogFile, 'utf8');
|
||||
return logContent.includes('API request: GET /health');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
await this.runTestGroup('Logging System', tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test API error responses
|
||||
*/
|
||||
async testAPIErrorResponses() {
|
||||
console.log('🌐 Testing API Error Responses...');
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: 'Consistent error response format',
|
||||
test: async () => {
|
||||
const response = await this.makeRequest('POST', '/auth/login', {
|
||||
email: 'invalid@example.com',
|
||||
password: 'wrongpassword'
|
||||
}, false);
|
||||
|
||||
const hasRequiredFields = response.data.error &&
|
||||
response.data.code &&
|
||||
response.data.timestamp;
|
||||
|
||||
return response.status === 401 && hasRequiredFields;
|
||||
}
|
||||
},
|
||||
{
|
||||
name: '404 error handling',
|
||||
test: async () => {
|
||||
const response = await this.makeRequest('GET', '/nonexistent-endpoint', {}, false);
|
||||
|
||||
return response.status === 404 && response.data.code === 'ROUTE_NOT_FOUND';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Error response security (no stack traces in production)',
|
||||
test: async () => {
|
||||
const response = await this.makeRequest('POST', '/auth/register', {
|
||||
email: 'invalid-email',
|
||||
password: 'weak'
|
||||
}, false);
|
||||
|
||||
// In production, stack traces should not be exposed
|
||||
const hasStackTrace = response.data.stack !== undefined;
|
||||
const isProduction = process.env.NODE_ENV === 'production';
|
||||
|
||||
return !isProduction || !hasStackTrace;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
await this.runTestGroup('API Error Responses', tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test security event logging
|
||||
*/
|
||||
async testSecurityEventLogging() {
|
||||
console.log('🔒 Testing Security Event Logging...');
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: 'Rate limit security logging',
|
||||
test: async () => {
|
||||
// Trigger rate limiting
|
||||
const promises = [];
|
||||
for (let i = 0; i < 15; i++) {
|
||||
promises.push(
|
||||
this.makeRequest('POST', '/auth/login', {
|
||||
email: 'test@example.com',
|
||||
password: 'wrongpassword'
|
||||
}, false)
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Check security log
|
||||
const today = new Date().toISOString().split('T')[0];
|
||||
const securityLogFile = path.join(this.logDir, `security-${today}.log`);
|
||||
|
||||
if (fs.existsSync(securityLogFile)) {
|
||||
const logContent = fs.readFileSync(securityLogFile, 'utf8');
|
||||
return logContent.includes('Security event');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
await this.runTestGroup('Security Event Logging', tests);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a group of tests
|
||||
*/
|
||||
async runTestGroup(groupName, tests) {
|
||||
console.log(`\n--- ${groupName} ---`);
|
||||
|
||||
for (const test of tests) {
|
||||
try {
|
||||
const result = await test.test();
|
||||
const status = result ? '✅ PASS' : '❌ FAIL';
|
||||
console.log(`${status}: ${test.name}`);
|
||||
|
||||
this.testResults.push({
|
||||
group: groupName,
|
||||
name: test.name,
|
||||
passed: result
|
||||
});
|
||||
} catch (error) {
|
||||
console.log(`❌ ERROR: ${test.name} - ${error.message}`);
|
||||
this.testResults.push({
|
||||
group: groupName,
|
||||
name: test.name,
|
||||
passed: false,
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make HTTP request with error handling
|
||||
*/
|
||||
async makeRequest(method, endpoint, data = {}, expectSuccess = true, token = null) {
|
||||
const config = {
|
||||
method,
|
||||
url: `${API_BASE}${endpoint}`,
|
||||
data,
|
||||
validateStatus: () => true, // Don't throw on HTTP errors
|
||||
timeout: 10000
|
||||
};
|
||||
|
||||
if (token) {
|
||||
config.headers = {
|
||||
'Authorization': `Bearer ${token}`
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await axios(config);
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (expectSuccess) {
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
status: error.response?.status || 500,
|
||||
data: error.response?.data || { error: error.message }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a valid authentication token for testing
|
||||
*/
|
||||
async getValidToken() {
|
||||
try {
|
||||
// Register a test user
|
||||
await this.makeRequest('POST', '/auth/register', {
|
||||
email: 'testuser@example.com',
|
||||
password: 'TestPassword123!'
|
||||
});
|
||||
|
||||
// Login to get token
|
||||
const response = await this.makeRequest('POST', '/auth/login', {
|
||||
email: 'testuser@example.com',
|
||||
password: 'TestPassword123!'
|
||||
});
|
||||
|
||||
// Extract token from cookie or response
|
||||
return 'valid-token-placeholder'; // This would need to be implemented based on your auth system
|
||||
} catch (error) {
|
||||
console.warn('Could not get valid token for testing:', error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate test report
|
||||
*/
|
||||
generateTestReport() {
|
||||
console.log('\n📊 Test Report');
|
||||
console.log('================');
|
||||
|
||||
const totalTests = this.testResults.length;
|
||||
const passedTests = this.testResults.filter(t => t.passed).length;
|
||||
const failedTests = totalTests - passedTests;
|
||||
|
||||
console.log(`Total Tests: ${totalTests}`);
|
||||
console.log(`Passed: ${passedTests} ✅`);
|
||||
console.log(`Failed: ${failedTests} ❌`);
|
||||
console.log(`Success Rate: ${((passedTests / totalTests) * 100).toFixed(1)}%`);
|
||||
|
||||
if (failedTests > 0) {
|
||||
console.log('\n❌ Failed Tests:');
|
||||
this.testResults
|
||||
.filter(t => !t.passed)
|
||||
.forEach(t => {
|
||||
console.log(` - ${t.group}: ${t.name}`);
|
||||
if (t.error) {
|
||||
console.log(` Error: ${t.error}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save report to file
|
||||
const reportPath = path.join(__dirname, 'error-handling-test-report.json');
|
||||
fs.writeFileSync(reportPath, JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
summary: {
|
||||
total: totalTests,
|
||||
passed: passedTests,
|
||||
failed: failedTests,
|
||||
successRate: (passedTests / totalTests) * 100
|
||||
},
|
||||
results: this.testResults
|
||||
}, null, 2));
|
||||
|
||||
console.log(`\n📄 Detailed report saved to: ${reportPath}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Run tests if this script is executed directly
|
||||
if (require.main === module) {
|
||||
const tester = new ErrorHandlingTester();
|
||||
tester.runAllTests().catch(console.error);
|
||||
}
|
||||
|
||||
module.exports = ErrorHandlingTester;
|
||||
Reference in New Issue
Block a user