Files
bookmarksite/backend/tests/test-error-handling.js
2025-07-20 20:43:06 +02:00

529 lines
18 KiB
JavaScript

/**
* 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;