sourcefinder/tests/performance/load.test.js
Alexis Trouvé a7bd6115b7
Some checks failed
SourceFinder CI/CD Pipeline / Code Quality & Linting (push) Has been cancelled
SourceFinder CI/CD Pipeline / Unit Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Security Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Integration Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Performance Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Code Coverage Report (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (16.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (18.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Build & Deployment Validation (20.x) (push) Has been cancelled
SourceFinder CI/CD Pipeline / Regression Tests (push) Has been cancelled
SourceFinder CI/CD Pipeline / Security Audit (push) Has been cancelled
SourceFinder CI/CD Pipeline / Notify Results (push) Has been cancelled
feat: Implémentation complète du système SourceFinder avec tests
- Architecture modulaire avec injection de dépendances
- Système de scoring intelligent multi-facteurs (spécificité, fraîcheur, qualité, réutilisation)
- Moteur anti-injection 4 couches (preprocessing, patterns, sémantique, pénalités)
- API REST complète avec validation et rate limiting
- Repository JSON avec index mémoire et backup automatique
- Provider LLM modulaire pour génération de contenu
- Suite de tests complète (Jest) :
  * Tests unitaires pour sécurité et scoring
  * Tests d'intégration API end-to-end
  * Tests de sécurité avec simulation d'attaques
  * Tests de performance et charge
- Pipeline CI/CD avec GitHub Actions
- Logging structuré et monitoring
- Configuration ESLint et environnement de test

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-15 23:06:10 +08:00

455 lines
15 KiB
JavaScript

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