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
- 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>
455 lines
15 KiB
JavaScript
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);
|
|
});
|
|
}); |