Files
url_tracker_tool/test-phase-7.js
Andrei c34de838f4 feat(phase-7): Advanced rate limiting with Redis and header redaction
- Implement tier-based rate limiting with rate-limiter-flexible
- Add Redis-backed rate limiters for different user tiers (free/pro/enterprise)
- Create comprehensive header redaction service for security
- Implement burst protection with per-minute limits
- Add organization and project-based rate limiting keys
- Create rate limiting middleware with proper error handling
- Integrate rate limits with tracking, bulk, and export endpoints
- Add header redaction to redirect tracking service
- Implement request logging with redacted sensitive headers
- Add comprehensive rate limit headers (limit, remaining, reset, tier)
- Support for anonymous vs authenticated rate limits
- Legacy endpoint rate limiting preserved for backward compatibility
- Admin functions for rate limit management and statistics
- Comprehensive test suite for all rate limiting scenarios

Security improvements:
- Sensitive header redaction (auth tokens, cookies, secrets)
- Partial redaction for debugging (admin mode)
- URL parameter redaction for sensitive data
- Request/response body redaction
- Configurable redaction levels

Backward compatibility: Maintained 100/hr rate limit for legacy endpoints
2025-08-18 14:40:31 +00:00

456 lines
14 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Test script for Phase 7: Advanced Rate Limiting + Header Redaction
* Tests the new rate limiting system and header redaction functionality
*/
const http = require('http');
const BASE_URL = 'http://localhost:3333';
// Helper function to make HTTP requests
function makeRequest(options, data = null) {
return new Promise((resolve, reject) => {
const req = http.request(options, (res) => {
let body = '';
res.on('data', (chunk) => body += chunk);
res.on('end', () => {
try {
const result = {
statusCode: res.statusCode,
headers: res.headers,
body: res.headers['content-type'] && res.headers['content-type'].includes('application/json')
? JSON.parse(body)
: body
};
resolve(result);
} catch (error) {
resolve({
statusCode: res.statusCode,
headers: res.headers,
body: body
});
}
});
});
req.on('error', reject);
if (data) {
if (typeof data === 'string') {
req.write(data);
} else {
req.write(JSON.stringify(data));
}
}
req.end();
});
}
// Helper function to sleep
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
async function runTests() {
console.log('🧪 Starting Phase 7: Advanced Rate Limiting + Header Redaction Tests\n');
let authToken = null;
// Test 1: User Registration/Login
console.log('1⃣ Testing user authentication...');
try {
const loginResult = await makeRequest({
hostname: 'localhost',
port: 3333,
path: '/api/v1/auth/login',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}, {
email: 'rate-test@example.com',
password: 'ratetest123'
});
if (loginResult.statusCode === 200 && loginResult.body.success) {
authToken = loginResult.body.data.token;
console.log('✅ User login successful');
} else {
// Try to register if login fails
const registerResult = await makeRequest({
hostname: 'localhost',
port: 3333,
path: '/api/v1/auth/register',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}, {
email: 'rate-test@example.com',
name: 'Rate Test User',
password: 'ratetest123',
organizationName: 'Rate Test Org'
});
if (registerResult.statusCode === 201 && registerResult.body.success) {
authToken = registerResult.body.data.token;
console.log('✅ User registration successful');
} else {
console.log('❌ Authentication failed:', registerResult.body);
return;
}
}
} catch (error) {
console.log('❌ Authentication error:', error.message);
return;
}
// Test 2: Rate Limit Headers
console.log('\n2⃣ Testing rate limit headers...');
try {
const trackResult = await makeRequest({
hostname: 'localhost',
port: 3333,
path: '/api/v2/track',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
},
}, {
url: 'https://example.com'
});
console.log('Status Code:', trackResult.statusCode);
console.log('Rate Limit Headers:');
console.log(' X-RateLimit-Limit:', trackResult.headers['x-ratelimit-limit']);
console.log(' X-RateLimit-Remaining:', trackResult.headers['x-ratelimit-remaining']);
console.log(' X-RateLimit-Reset:', trackResult.headers['x-ratelimit-reset']);
console.log(' X-RateLimit-Tier:', trackResult.headers['x-ratelimit-tier']);
if (trackResult.headers['x-ratelimit-limit']) {
console.log('✅ Rate limit headers present');
} else {
console.log('⚠️ Rate limit headers missing');
}
} catch (error) {
console.log('❌ Rate limit headers test error:', error.message);
}
// Test 3: Anonymous vs Authenticated Rate Limits
console.log('\n3⃣ Testing anonymous vs authenticated rate limits...');
try {
// Anonymous request
const anonResult = await makeRequest({
hostname: 'localhost',
port: 3333,
path: '/api/v2/track',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}, {
url: 'https://httpbin.org/get'
});
console.log('Anonymous Request:');
console.log(' Limit:', anonResult.headers['x-ratelimit-limit']);
console.log(' Tier:', anonResult.headers['x-ratelimit-tier']);
// Authenticated request
const authResult = await makeRequest({
hostname: 'localhost',
port: 3333,
path: '/api/v2/track',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
},
}, {
url: 'https://httpbin.org/get'
});
console.log('Authenticated Request:');
console.log(' Limit:', authResult.headers['x-ratelimit-limit']);
console.log(' Tier:', authResult.headers['x-ratelimit-tier']);
const anonLimit = parseInt(anonResult.headers['x-ratelimit-limit'] || '0');
const authLimit = parseInt(authResult.headers['x-ratelimit-limit'] || '0');
if (authLimit > anonLimit) {
console.log('✅ Authenticated users have higher rate limits');
} else {
console.log('⚠️ Rate limit difference not detected');
}
} catch (error) {
console.log('❌ Rate limit comparison error:', error.message);
}
// Test 4: Legacy Endpoint Rate Limiting
console.log('\n4⃣ Testing legacy endpoint rate limiting...');
try {
const legacyResult = await makeRequest({
hostname: 'localhost',
port: 3333,
path: '/api/track',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}, {
url: 'https://example.com'
});
console.log('Legacy endpoint status:', legacyResult.statusCode);
console.log('Legacy rate limit:', legacyResult.headers['x-ratelimit-limit']);
if (legacyResult.headers['x-ratelimit-limit']) {
console.log('✅ Legacy endpoints have rate limiting');
} else {
console.log('⚠️ Legacy endpoints missing rate limiting');
}
} catch (error) {
console.log('❌ Legacy rate limiting test error:', error.message);
}
// Test 5: Burst Protection
console.log('\n5⃣ Testing burst protection...');
try {
console.log('Making rapid authenticated requests...');
const requests = [];
for (let i = 0; i < 15; i++) {
requests.push(
makeRequest({
hostname: 'localhost',
port: 3333,
path: '/api/v2/track',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
},
}, {
url: `https://httpbin.org/delay/0`,
timeout: 5000
})
);
}
const results = await Promise.allSettled(requests);
const statusCodes = results.map(r => r.status === 'fulfilled' ? r.value.statusCode : 'error');
const rateLimited = statusCodes.filter(code => code === 429).length;
const successful = statusCodes.filter(code => code === 200 || code === 201).length;
console.log(`Results: ${successful} successful, ${rateLimited} rate limited`);
if (rateLimited > 0) {
console.log('✅ Burst protection is working');
} else {
console.log('⚠️ Burst protection may not be active (or limits are very high)');
}
} catch (error) {
console.log('❌ Burst protection test error:', error.message);
}
// Test 6: Header Redaction (indirect test via response logs)
console.log('\n6⃣ Testing header redaction...');
try {
const sensitiveHeadersResult = await makeRequest({
hostname: 'localhost',
port: 3333,
path: '/api/v2/track',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
'X-Secret-Key': 'secret123',
'Cookie': 'session=abc123',
'X-API-Key': 'api_key_12345',
'User-Agent': 'RedirectIntelligence Test Client v1.0',
},
}, {
url: 'https://httpbin.org/headers'
});
console.log('Request with sensitive headers sent');
console.log('Status:', sensitiveHeadersResult.statusCode);
// Check if the request was processed (indicating headers were handled properly)
if (sensitiveHeadersResult.statusCode === 200 || sensitiveHeadersResult.statusCode === 201) {
console.log('✅ Request with sensitive headers processed successfully');
console.log(' (Header redaction occurs server-side in logs/storage)');
} else {
console.log('⚠️ Request with sensitive headers failed');
}
} catch (error) {
console.log('❌ Header redaction test error:', error.message);
}
// Test 7: Bulk Rate Limiting
console.log('\n7⃣ Testing bulk endpoint rate limiting...');
try {
const bulkResult = await makeRequest({
hostname: 'localhost',
port: 3333,
path: '/api/v2/bulk/jobs',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
},
}, {
urls: [
{ url: 'https://example.com', label: 'Test 1' }
]
});
console.log('Bulk endpoint status:', bulkResult.statusCode);
console.log('Bulk rate limit tier:', bulkResult.headers['x-ratelimit-tier']);
console.log('Bulk rate limit:', bulkResult.headers['x-ratelimit-limit']);
if (bulkResult.headers['x-ratelimit-tier']) {
console.log('✅ Bulk endpoints have tier-based rate limiting');
} else {
console.log('⚠️ Bulk rate limiting not detected');
}
} catch (error) {
console.log('❌ Bulk rate limiting test error:', error.message);
}
// Test 8: Rate Limit Exceeded Response
console.log('\n8⃣ Testing rate limit exceeded response...');
try {
console.log('Making requests to approach rate limit...');
let lastResponse = null;
// Make many requests quickly to trigger rate limiting
for (let i = 0; i < 60; i++) {
try {
const response = await makeRequest({
hostname: 'localhost',
port: 3333,
path: '/api/track', // Use legacy endpoint for predictable low limits
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
}, {
url: 'https://httpbin.org/get'
});
lastResponse = response;
if (response.statusCode === 429) {
console.log('✅ Rate limit exceeded (429) response received');
console.log('Response body:', response.body);
console.log('Retry-After header:', response.headers['retry-after']);
console.log('X-RateLimit headers:', {
limit: response.headers['x-ratelimit-limit'],
remaining: response.headers['x-ratelimit-remaining'],
reset: response.headers['x-ratelimit-reset'],
});
break;
}
// Small delay to avoid overwhelming the server
await sleep(50);
} catch (error) {
console.log('Request error (expected for rate limiting):', error.message);
}
}
if (!lastResponse || lastResponse.statusCode !== 429) {
console.log('⚠️ Rate limit not triggered (limits may be too high for testing)');
}
} catch (error) {
console.log('❌ Rate limit exceeded test error:', error.message);
}
// Test 9: Different User Agent Handling
console.log('\n9⃣ Testing different user agent handling...');
try {
const userAgents = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'curl/7.68.0',
'RedirectIntelligence/2.0 TestClient',
];
for (const ua of userAgents) {
const result = await makeRequest({
hostname: 'localhost',
port: 3333,
path: '/api/v2/track',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`,
'User-Agent': ua,
},
}, {
url: 'https://httpbin.org/user-agent'
});
console.log(`User-Agent: ${ua.substring(0, 30)}... -> Status: ${result.statusCode}`);
}
console.log('✅ User-Agent handling test completed');
} catch (error) {
console.log('❌ User-Agent test error:', error.message);
}
// Test 10: Health Check (should not be rate limited)
console.log('\n🔟 Testing health check endpoint...');
try {
const healthResult = await makeRequest({
hostname: 'localhost',
port: 3333,
path: '/health',
method: 'GET',
});
console.log('Health check status:', healthResult.statusCode);
console.log('Health check rate limited:', !!healthResult.headers['x-ratelimit-limit']);
if (healthResult.statusCode === 200 && !healthResult.headers['x-ratelimit-limit']) {
console.log('✅ Health check endpoint is not rate limited');
} else {
console.log('⚠️ Health check endpoint may have rate limiting');
}
} catch (error) {
console.log('❌ Health check test error:', error.message);
}
console.log('\n🎉 Phase 7 testing completed!');
console.log('\nKey features tested:');
console.log('✓ Advanced rate limiting with user tiers');
console.log('✓ Rate limit headers in responses');
console.log('✓ Anonymous vs authenticated rate limits');
console.log('✓ Legacy endpoint rate limiting');
console.log('✓ Burst protection for rapid requests');
console.log('✓ Header redaction (server-side)');
console.log('✓ Bulk endpoint tier-based limiting');
console.log('✓ Rate limit exceeded responses');
console.log('✓ User-Agent handling');
console.log('✓ Health check endpoint exclusion');
}
// Error handling
process.on('uncaughtException', (error) => {
console.log('\n💥 Uncaught Exception:', error.message);
process.exit(1);
});
process.on('unhandledRejection', (reason) => {
console.log('\n💥 Unhandled Rejection:', reason);
process.exit(1);
});
// Run tests
runTests().catch(error => {
console.log('\n💥 Test execution failed:', error.message);
process.exit(1);
});