import { AutoReporter } from '../reporters/AutoReporter.js'; /** * TESTS EDGE CASES - API Controller * Tests des cas limites, erreurs et comportements extrêmes */ const { describe, it, before, after } = require('node:test'); const assert = require('node:assert'); const http = require('node:http'); const { ManualServer } = require('../../lib/modes/ManualServer'); // Helper pour requêtes HTTP function makeRequest(options, postData = null) { return new Promise((resolve, reject) => { const req = http.request(options, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { try { const parsed = res.headers['content-type']?.includes('application/json') ? JSON.parse(data) : data; resolve({ statusCode: res.statusCode, headers: res.headers, data: parsed }); } catch (e) { resolve({ statusCode: res.statusCode, headers: res.headers, data }); } }); }); req.on('error', reject); req.setTimeout(10000, () => { req.destroy(); reject(new Error('Request timeout')); }); if (postData) { req.write(typeof postData === 'object' ? JSON.stringify(postData) : postData); } req.end(); }); } // Auto-Reporter Configuration const autoReporter = new AutoReporter(); describe('API Edge Cases - Tests des Cas Limites', () => { let server; let baseUrl; const testPort = 3098; // Port différent pour éviter conflits before(async () => { server = new ManualServer({ port: testPort, wsPort: 8098 }); await server.start(); baseUrl = `http://localhost:${testPort}`; console.log(`🧪 Serveur edge cases démarré sur ${baseUrl}`); }); after(async () => { if (server) { await server.stop(); console.log('🛑 Serveur edge cases arrêté'); } }); describe('🔥 Paramètres Extrêmes', () => { it('should handle very long project names', async () => { const longName = 'A'.repeat(10000); // 10KB de nom const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, { name: longName, description: 'Test nom très long' }); // L'API doit gérer les noms longs gracieusement assert.ok([201, 400].includes(response.statusCode)); if (response.statusCode === 201) { assert.strictEqual(response.data.success, true); assert.strictEqual(response.data.data.name, longName); } else { assert.strictEqual(response.data.success, false); } }); it('should handle pagination with extreme values', async () => { // Test avec des valeurs de pagination extrêmes const testCases = [ { limit: -1, offset: 0 }, { limit: 999999, offset: 0 }, { limit: 50, offset: -1 }, { limit: 'invalid', offset: 'invalid' }, { limit: 0, offset: 999999 } ]; for (const testCase of testCases) { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: `/api/articles?limit=${testCase.limit}&offset=${testCase.offset}`, method: 'GET' }); // L'API doit soit réussir avec des valeurs normalisées, soit échouer gracieusement assert.ok([200, 400, 500].includes(response.statusCode)); if (response.statusCode === 200) { assert.ok(response.data.data.limit >= 0); assert.ok(response.data.data.offset >= 0); } } }); it('should handle special characters in all fields', async () => { const specialChars = { name: '🚀 Test "Special" & émojis 中文', description: 'Description avec \n\t\r caractères spéciaux \\n "quotes" \'single\' & symbols €£¥', config: { special: '<>&"\'{}[]()=+-*/\\', unicode: '🎯🔥💯✨🌟⭐', mixed: 'Normal text avec 中文 и русский' } }; const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, specialChars); assert.ok([201, 400].includes(response.statusCode)); if (response.statusCode === 201) { assert.strictEqual(response.data.data.name, specialChars.name); assert.strictEqual(response.data.data.description, specialChars.description); } }); }); describe('🛡️ Sécurité et Injection', () => { it('should reject SQL injection attempts', async () => { const sqlInjections = [ "'; DROP TABLE projects; --", "1' OR '1'='1", "admin'/*", "1; SELECT * FROM users; --" ]; for (const injection of sqlInjections) { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, { name: injection, description: 'Test injection SQL' }); // L'API doit traiter cela comme une chaîne normale, pas d'injection assert.ok([201, 400].includes(response.statusCode)); if (response.statusCode === 201) { assert.strictEqual(response.data.data.name, injection); // Stocké tel quel } } }); it('should handle XSS attempts gracefully', async () => { const xssPayloads = [ '', 'javascript:alert(1)', '', '' ]; for (const payload of xssPayloads) { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/templates', method: 'POST', headers: { 'Content-Type': 'application/json' } }, { name: 'Test XSS', content: payload, description: 'Template avec payload XSS' }); assert.ok([201, 400].includes(response.statusCode)); if (response.statusCode === 201) { // Le payload doit être stocké tel quel (pas d'exécution côté serveur) assert.strictEqual(response.data.data.content, payload); } } }); it('should validate content-type header requirements', async () => { // Test sans Content-Type const response1 = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST' }, '{"name":"Test"}'); // Test avec mauvais Content-Type const response2 = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'text/plain' } }, '{"name":"Test"}'); // Express devrait gérer ces cas assert.ok([400, 415, 500].includes(response1.statusCode)); assert.ok([400, 415, 500].includes(response2.statusCode)); }); }); describe('💣 Cas d\'Erreur Extrêmes', () => { it('should handle malformed JSON gracefully', async () => { const malformedJsons = [ '{"name":}', '{"name":"test",}', '{name:"test"}', '{"name":"test"', 'null', 'undefined', '', '[]', 'true' ]; for (const json of malformedJsons) { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, json); assert.strictEqual(response.statusCode, 400); assert.strictEqual(response.data.success, false); } }); it('should handle very large payloads', async () => { // Payload de 1MB const largePayload = { name: 'Large Test', description: 'X'.repeat(1024 * 1024), // 1MB de description config: { data: 'Y'.repeat(100000) // 100KB supplémentaires } }; const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, largePayload); // L'API peut soit accepter, soit rejeter selon les limites configurées assert.ok([201, 400, 413].includes(response.statusCode)); }); it('should handle concurrent requests without corruption', async () => { // 20 requêtes simultanées const promises = Array(20).fill().map((_, i) => makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, { name: `Concurrent Project ${i}`, description: `Test concurrence ${i}` }) ); const results = await Promise.allSettled(promises); // Compter les succès const successes = results.filter(r => r.status === 'fulfilled' && r.value.statusCode === 201 ); // Au moins quelques requêtes doivent réussir assert.ok(successes.length > 0); // Vérifier que les IDs sont uniques const ids = successes.map(r => r.value.data.data.id); const uniqueIds = new Set(ids); assert.strictEqual(ids.length, uniqueIds.size, 'IDs doivent être uniques'); }); }); describe('🌐 Protocole HTTP Edge Cases', () => { it('should handle unsupported HTTP methods', async () => { const methods = ['PATCH', 'DELETE', 'HEAD', 'TRACE', 'CONNECT']; for (const method of methods) { try { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method }); // 405 Method Not Allowed ou 404 assert.ok([404, 405].includes(response.statusCode)); } catch (error) { // Certaines méthodes peuvent être rejetées par Node.js assert.ok(error.message.includes('Method') || error.message.includes('method')); } } }); it('should handle invalid URLs and paths', async () => { const invalidPaths = [ '/api/projects/../../../etc/passwd', '/api/projects/%2e%2e%2f%2e%2e%2f', '/api/projects/\\..\\..\\windows\\system32', '/api/projects?' + 'x'.repeat(10000), // Query string très longue '/api/projects#fragment', '/api/projects with spaces', '/api/проекты', // URL avec caractères non-ASCII ]; for (const path of invalidPaths) { try { const response = await makeRequest({ hostname: 'localhost', port: testPort, path, method: 'GET' }); // Doit retourner 404 ou 400, pas d'erreur de sécurité assert.ok([400, 404].includes(response.statusCode)); } catch (error) { // Erreurs de parsing d'URL attendues assert.ok(error.message.includes('Invalid') || error.message.includes('URI')); } } }); it('should handle connection timeouts gracefully', async () => { // Test avec un article qui va prendre du temps (workflow LLM) const startTime = Date.now(); try { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/articles', method: 'POST', headers: { 'Content-Type': 'application/json' } }, { keyword: 'test timeout edge case', project: 'timeout-test' }); const duration = Date.now() - startTime; // Si ça réussit, vérifier que c'est raisonnable if (response.statusCode === 201) { assert.ok(duration < 120000, `Article generation took ${duration}ms`); assert.strictEqual(response.data.success, true); } else { // Erreur attendue (timeout LLM, etc.) assert.strictEqual(response.data.success, false); } } catch (error) { // Timeout de requête attendu assert.ok(error.message.includes('timeout') || error.message.includes('Timeout')); } }); }); describe('📊 Performance Edge Cases', () => { it('should handle rapid successive requests', async () => { const requests = []; const startTime = Date.now(); // 50 requêtes health check rapides for (let i = 0; i < 50; i++) { requests.push( makeRequest({ hostname: 'localhost', port: testPort, path: '/api/health', method: 'GET' }) ); } const results = await Promise.allSettled(requests); const duration = Date.now() - startTime; const successes = results.filter(r => r.status === 'fulfilled' && r.value.statusCode === 200 ); // La plupart des requêtes doivent réussir assert.ok(successes.length >= 45, `Only ${successes.length}/50 requests succeeded`); // Performance raisonnable assert.ok(duration < 5000, `50 health checks took ${duration}ms`); console.log(`📈 Performance: ${successes.length}/50 requests in ${duration}ms`); }); it('should maintain stability under stress', async () => { // Mix de différents types de requêtes const stressRequests = [ // Health checks ...Array(10).fill().map(() => ({ path: '/api/health', method: 'GET' })), // Metrics ...Array(5).fill().map(() => ({ path: '/api/metrics', method: 'GET' })), // Project creation ...Array(5).fill().map((_, i) => ({ path: '/api/projects', method: 'POST', data: { name: `Stress Project ${i}`, description: 'Stress test' } })), // Template creation ...Array(3).fill().map((_, i) => ({ path: '/api/templates', method: 'POST', data: { name: `Stress Template ${i}`, content: '' } })) ]; const promises = stressRequests.map(req => makeRequest({ hostname: 'localhost', port: testPort, path: req.path, method: req.method, headers: req.data ? { 'Content-Type': 'application/json' } : {} }, req.data) ); const results = await Promise.allSettled(promises); // Calculer taux de succès const successes = results.filter(r => r.status === 'fulfilled' && [200, 201].includes(r.value.statusCode) ); const successRate = (successes.length / results.length) * 100; console.log(`🎯 Stress test: ${successRate.toFixed(1)}% success rate`); // Au moins 80% de succès attendu assert.ok(successRate >= 80, `Success rate too low: ${successRate}%`); }); }); }); console.log('🧪 Tests Edge Cases API - Validation des cas limites et sécurité');