/** * Tests de performance et charge * Validation des KPIs selon CDC: < 5s response time, > 99.5% uptime */ const request = require('supertest'); const SourceFinderApp = require('../../src/app'); describe('Performance & Load Tests', () => { let app; let server; beforeAll(async () => { const sourceFinderApp = new SourceFinderApp(); app = await sourceFinderApp.initialize(); server = app.listen(0); }); afterAll(async () => { if (server) { await new Promise(resolve => server.close(resolve)); } }); describe('Response Time Requirements (CDC: < 5s)', () => { test('API search devrait répondre en moins de 5 secondes', async () => { const startTime = Date.now(); const response = await request(app) .post('/api/v1/news/search') .send({ race_code: '352-1', max_results: 3, min_score: 40, client_id: 'perf-test-1' }) .timeout(6000); const responseTime = Date.now() - startTime; expect([200, 202]).toContain(response.status); expect(responseTime).toBeLessThan(5000); // CDC requirement if (response.status === 200) { expect(response.body.metadata.processingTime).toBeLessThan(5000); } console.log(`✓ Response time: ${responseTime}ms (target: <5000ms)`); }, 10000); test('Health check devrait répondre en moins de 1 seconde', async () => { const times = []; for (let i = 0; i < 5; i++) { const startTime = Date.now(); await request(app) .get('/api/v1/health') .expect(200); times.push(Date.now() - startTime); } const avgTime = times.reduce((a, b) => a + b, 0) / times.length; const maxTime = Math.max(...times); expect(maxTime).toBeLessThan(1000); expect(avgTime).toBeLessThan(500); console.log(`✓ Health check avg: ${avgTime}ms, max: ${maxTime}ms`); }); test('Metrics endpoint devrait être rapide', async () => { const startTime = Date.now(); const response = await request(app) .get('/api/v1/metrics') .expect(200); const responseTime = Date.now() - startTime; expect(responseTime).toBeLessThan(1000); expect(response.body.metrics).toBeDefined(); console.log(`✓ Metrics response time: ${responseTime}ms`); }); }); describe('Concurrent Request Handling', () => { test('devrait gérer 10 requêtes simultanées', async () => { const concurrentRequests = 10; const requests = Array(concurrentRequests).fill().map((_, i) => request(app) .post('/api/v1/news/search') .send({ race_code: '352-1', max_results: 1, client_id: `concurrent-test-${i}` }) .timeout(10000) ); const startTime = Date.now(); const responses = await Promise.allSettled(requests); const totalTime = Date.now() - startTime; const successful = responses.filter(r => r.status === 'fulfilled' && [200, 202].includes(r.value.status) ); // Au moins 80% des requêtes doivent réussir expect(successful.length).toBeGreaterThanOrEqual(concurrentRequests * 0.8); // Temps total ne doit pas dépasser 15 secondes expect(totalTime).toBeLessThan(15000); console.log(`✓ ${successful.length}/${concurrentRequests} requests succeeded in ${totalTime}ms`); }, 20000); test('devrait maintenir performance sous charge soutenue', async () => { const rounds = 3; const requestsPerRound = 5; const results = []; for (let round = 0; round < rounds; round++) { const roundStart = Date.now(); const roundRequests = Array(requestsPerRound).fill().map((_, i) => request(app) .get('/api/v1/health') .timeout(2000) ); const responses = await Promise.allSettled(roundRequests); const successful = responses.filter(r => r.status === 'fulfilled' && r.value.status === 200 ); const roundTime = Date.now() - roundStart; results.push({ round: round + 1, successful: successful.length, total: requestsPerRound, time: roundTime }); // Petite pause entre les rounds await new Promise(resolve => setTimeout(resolve, 1000)); } // Vérifier que la performance reste stable results.forEach((result, index) => { expect(result.successful).toBeGreaterThanOrEqual(requestsPerRound * 0.9); expect(result.time).toBeLessThan(3000); console.log(`✓ Round ${result.round}: ${result.successful}/${result.total} in ${result.time}ms`); }); // Vérifier que le système ne se dégrade pas const firstRoundTime = results[0].time; const lastRoundTime = results[results.length - 1].time; expect(lastRoundTime).toBeLessThan(firstRoundTime * 1.5); // Max 50% de dégradation }, 25000); }); describe('Memory and Resource Management', () => { test('devrait maintenir usage mémoire stable', async () => { const initialMemory = process.memoryUsage(); // Effectuer plusieurs requêtes pour charger le système for (let i = 0; i < 10; i++) { await request(app) .get('/api/v1/metrics') .timeout(2000); } const afterRequestsMemory = process.memoryUsage(); // La mémoire ne devrait pas augmenter de manière excessive const memoryIncrease = afterRequestsMemory.heapUsed - initialMemory.heapUsed; const memoryIncreasePercent = (memoryIncrease / initialMemory.heapUsed) * 100; expect(memoryIncreasePercent).toBeLessThan(50); // Max 50% d'augmentation console.log(`✓ Memory increase: ${(memoryIncrease / 1024 / 1024).toFixed(2)}MB (${memoryIncreasePercent.toFixed(1)}%)`); // Forcer garbage collection si disponible if (global.gc) { global.gc(); } }); test('devrait gérer nettoyage des ressources temporaires', async () => { // Vérifier que les caches se nettoient correctement const metricsResponse = await request(app) .get('/api/v1/metrics') .expect(200); const { metrics } = metricsResponse.body; // Les caches ne devraient pas grandir indéfiniment if (metrics.search && metrics.search.cacheSize !== undefined) { expect(metrics.search.cacheSize).toBeLessThan(1000); } if (metrics.security && metrics.security.cache) { expect(metrics.security.cache.size).toBeLessThan(500); } }); }); describe('Stress Testing', () => { test('devrait résister à une charge élevée ponctuelle', async () => { const highLoad = 20; const timeout = 15000; const startTime = Date.now(); const stressRequests = Array(highLoad).fill().map((_, i) => { // Mélanger différents types de requêtes if (i % 3 === 0) { return request(app).get('/api/v1/health').timeout(timeout); } else if (i % 3 === 1) { return request(app).get('/api/v1/metrics').timeout(timeout); } else { return request(app) .post('/api/v1/news/search') .send({ race_code: '352-1', max_results: 1, client_id: `stress-${i}` }) .timeout(timeout); } }); const responses = await Promise.allSettled(stressRequests); const totalTime = Date.now() - startTime; const successful = responses.filter(r => r.status === 'fulfilled' && [200, 202].includes(r.value.status) ); const failed = responses.filter(r => r.status === 'rejected'); const errorRate = failed.length / responses.length; // Critères de réussite du stress test expect(successful.length).toBeGreaterThanOrEqual(highLoad * 0.7); // 70% de réussite minimum expect(errorRate).toBeLessThan(0.3); // Moins de 30% d'erreurs expect(totalTime).toBeLessThan(20000); // Moins de 20 secondes console.log(`✓ Stress test: ${successful.length}/${highLoad} successful, ${(errorRate * 100).toFixed(1)}% error rate, ${totalTime}ms total`); // Vérifier que le système récupère après le stress await new Promise(resolve => setTimeout(resolve, 2000)); const recoveryResponse = await request(app) .get('/api/v1/health') .expect(200); expect(recoveryResponse.body.status).toMatch(/healthy|degraded/); }, 30000); test('devrait gérer requêtes avec payload volumineux', async () => { const largePayload = { race_code: '352-1', product_context: 'A'.repeat(5000), // 5KB de contexte content_type: 'education', max_results: 1, client_id: 'large-payload-test' }; const startTime = Date.now(); const response = await request(app) .post('/api/v1/news/search') .send(largePayload) .timeout(10000); const responseTime = Date.now() - startTime; expect([200, 202, 400]).toContain(response.status); // 400 si payload trop grand expect(responseTime).toBeLessThan(8000); if (response.status === 400) { expect(response.body.error).toBeDefined(); console.log('✓ Large payload correctly rejected'); } else { console.log(`✓ Large payload processed in ${responseTime}ms`); } }); }); describe('Performance Regression Testing', () => { test('devrait maintenir temps de réponse baseline', async () => { // Baseline pour différents types de requêtes const baselines = [ { name: 'Health check', request: () => request(app).get('/api/v1/health'), maxTime: 500 }, { name: 'Metrics', request: () => request(app).get('/api/v1/metrics'), maxTime: 1000 }, { name: 'Stock status', request: () => request(app).get('/api/v1/stock/status'), maxTime: 2000 } ]; for (const baseline of baselines) { const measurements = []; // Effectuer 5 mesures for (let i = 0; i < 5; i++) { const startTime = Date.now(); const response = await baseline.request().timeout(baseline.maxTime + 1000); const responseTime = Date.now() - startTime; measurements.push(responseTime); expect(response.status).toBe(200); } const avgTime = measurements.reduce((a, b) => a + b, 0) / measurements.length; const maxTime = Math.max(...measurements); expect(avgTime).toBeLessThan(baseline.maxTime); expect(maxTime).toBeLessThan(baseline.maxTime * 1.5); console.log(`✓ ${baseline.name}: avg=${avgTime}ms, max=${maxTime}ms (baseline=${baseline.maxTime}ms)`); } }); test('devrait détecter les fuites mémoire potentielles', async () => { const iterations = 50; const memorySnapshots = []; for (let i = 0; i < iterations; i++) { await request(app) .get('/api/v1/health') .timeout(2000); if (i % 10 === 0) { memorySnapshots.push(process.memoryUsage().heapUsed); } } // Vérifier que la mémoire ne croît pas linéairement const memoryGrowth = memorySnapshots.map((current, index) => { if (index === 0) return 0; return current - memorySnapshots[index - 1]; }).slice(1); const avgGrowth = memoryGrowth.reduce((a, b) => a + b, 0) / memoryGrowth.length; // La croissance moyenne ne devrait pas dépasser 1MB par tranche expect(Math.abs(avgGrowth)).toBeLessThan(1024 * 1024); console.log(`✓ Memory growth check: avg=${(avgGrowth / 1024).toFixed(2)}KB per 10 requests`); }, 20000); }); describe('Database and I/O Performance', () => { test('devrait maintenir performance des opérations de stock', async () => { const operations = [ () => request(app).get('/api/v1/stock/status'), () => request(app).get('/api/v1/stock/status?race_code=352-1'), () => request(app).post('/api/v1/stock/refresh').send({ race_code: '352-1' }) ]; for (const operation of operations) { const startTime = Date.now(); const response = await operation().timeout(5000); const responseTime = Date.now() - startTime; expect([200, 202]).toContain(response.status); expect(responseTime).toBeLessThan(3000); // Opérations de stock < 3s console.log(`✓ Stock operation completed in ${responseTime}ms`); } }); test('devrait gérer opérations concurrentes sur le stock', async () => { const concurrentStockOps = Array(5).fill().map(() => request(app) .get('/api/v1/stock/status') .timeout(5000) ); const startTime = Date.now(); const responses = await Promise.allSettled(concurrentStockOps); const totalTime = Date.now() - startTime; const successful = responses.filter(r => r.status === 'fulfilled' && r.value.status === 200 ); expect(successful.length).toBe(5); // Toutes les lectures doivent réussir expect(totalTime).toBeLessThan(8000); // Moins de 8 secondes au total console.log(`✓ Concurrent stock operations: ${successful.length}/5 in ${totalTime}ms`); }); }); describe('Performance Monitoring', () => { test('devrait exposer métriques de performance détaillées', async () => { const response = await request(app) .get('/api/v1/metrics') .expect(200); const { metrics } = response.body; // Vérifier présence des métriques de performance expect(metrics.search).toHaveProperty('averageResponseTime'); expect(metrics.search).toHaveProperty('totalSearches'); expect(metrics.system).toHaveProperty('uptime'); expect(metrics.system).toHaveProperty('memory'); // Les métriques doivent être numériques et positives expect(typeof metrics.search.averageResponseTime).toBe('number'); expect(metrics.search.averageResponseTime).toBeGreaterThanOrEqual(0); expect(typeof metrics.system.uptime).toBe('number'); expect(metrics.system.uptime).toBeGreaterThan(0); console.log('✓ Performance metrics available and valid'); }); test('devrait permettre monitoring continu', async () => { // Effectuer quelques opérations pour générer des métriques await request(app) .post('/api/v1/news/search') .send({ race_code: '352-1', max_results: 1, client_id: 'monitoring-test' }) .timeout(10000); const metricsAfter = await request(app) .get('/api/v1/metrics') .expect(200); // Les compteurs doivent avoir été incrémentés expect(metricsAfter.body.metrics.search.totalSearches).toBeGreaterThanOrEqual(1); console.log('✓ Continuous monitoring functional'); }, 15000); }); });