/** * TESTS D'INTÉGRATION COMPLETS - API Server * Tests avec serveur HTTP réel et requêtes HTTP authentiques */ 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 faire des 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); if (postData) { req.write(typeof postData === 'object' ? JSON.stringify(postData) : postData); } req.end(); }); } describe('API Server - Tests d\'Intégration Complets', () => { let server; let baseUrl; const testPort = 3099; // Port spécifique pour les tests before(async () => { // Démarrer serveur de test server = new ManualServer({ port: testPort, wsPort: 8099 }); await server.start(); baseUrl = `http://localhost:${testPort}`; console.log(`🚀 Serveur de test démarré sur ${baseUrl}`); }); after(async () => { if (server) { await server.stop(); console.log('🛑 Serveur de test arrêté'); } }); describe('🏥 Health Check Integration', () => { it('should respond to health check with real HTTP', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/health', method: 'GET', headers: { 'Accept': 'application/json' } }); assert.strictEqual(response.statusCode, 200); assert.strictEqual(response.data.success, true); assert.strictEqual(response.data.data.status, 'healthy'); assert.ok(response.data.data.version); assert.ok(typeof response.data.data.uptime === 'number'); }); it('should include correct headers in health response', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/health', method: 'GET' }); assert.ok(response.headers['content-type'].includes('application/json')); assert.strictEqual(response.statusCode, 200); }); }); describe('📊 Metrics Integration', () => { it('should return system metrics via real HTTP', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/metrics', method: 'GET', headers: { 'Accept': 'application/json' } }); assert.strictEqual(response.statusCode, 200); assert.strictEqual(response.data.success, true); assert.ok(response.data.data.articles); assert.ok(response.data.data.projects); assert.ok(response.data.data.templates); assert.ok(response.data.data.system); // Vérifier types assert.strictEqual(typeof response.data.data.articles.total, 'number'); assert.strictEqual(typeof response.data.data.system.uptime, 'number'); }); }); describe('📁 Projects Integration', () => { let createdProjectId; it('should create project via POST request', async () => { const projectData = { name: 'Test Integration Project', description: 'Projet créé via test d\'intégration', config: { defaultPersonality: 'Marc', selectiveStack: 'standardEnhancement' } }; const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }, projectData); assert.strictEqual(response.statusCode, 201); assert.strictEqual(response.data.success, true); assert.strictEqual(response.data.data.name, projectData.name); assert.strictEqual(response.data.data.description, projectData.description); assert.ok(response.data.data.id); assert.ok(response.data.data.createdAt); createdProjectId = response.data.data.id; }); it('should return 400 for invalid project data', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, { description: 'Sans nom' }); assert.strictEqual(response.statusCode, 400); assert.strictEqual(response.data.success, false); assert.ok(response.data.error.includes('Nom du projet requis')); }); it('should retrieve projects list including created project', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'GET', headers: { 'Accept': 'application/json' } }); assert.strictEqual(response.statusCode, 200); assert.strictEqual(response.data.success, true); assert.ok(Array.isArray(response.data.data.projects)); assert.ok(response.data.data.projects.length >= 1); // Vérifier que notre projet créé est présent const createdProject = response.data.data.projects.find(p => p.id === createdProjectId); assert.ok(createdProject); assert.strictEqual(createdProject.name, 'Test Integration Project'); }); }); describe('📋 Templates Integration', () => { let createdTemplateId; it('should create template via POST request', async () => { const templateData = { name: 'Template Integration Test', content: '', description: 'Template créé via test d\'intégration', category: 'integration-test' }; const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/templates', method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }, templateData); assert.strictEqual(response.statusCode, 201); assert.strictEqual(response.data.success, true); assert.strictEqual(response.data.data.name, templateData.name); assert.strictEqual(response.data.data.content, templateData.content); assert.strictEqual(response.data.data.category, templateData.category); createdTemplateId = response.data.data.id; }); it('should retrieve templates list', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/templates', method: 'GET' }); assert.strictEqual(response.statusCode, 200); assert.ok(Array.isArray(response.data.data.templates)); const createdTemplate = response.data.data.templates.find(t => t.id === createdTemplateId); assert.ok(createdTemplate); }); }); describe('📝 Articles Integration', () => { it('should validate article creation input', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/articles', method: 'POST', headers: { 'Content-Type': 'application/json' } }, {}); assert.strictEqual(response.statusCode, 400); assert.strictEqual(response.data.success, false); assert.ok(response.data.error.includes('Mot-clé ou numéro de ligne requis')); }); it('should accept valid article creation request', async () => { const articleData = { keyword: 'test intégration keyword', project: 'integration-test', config: { selectiveStack: 'lightEnhancement', adversarialMode: 'none' } }; // Note: Ce test peut prendre du temps car il fait appel aux LLMs const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/articles', method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }, articleData); // Peut être 201 (succès) ou 500 (erreur LLM/Google Sheets) assert.ok([201, 500].includes(response.statusCode)); if (response.statusCode === 201) { assert.strictEqual(response.data.success, true); assert.ok(response.data.data.id || response.data.data.article); } else { // Erreur attendue si pas d'accès LLM/Sheets assert.strictEqual(response.data.success, false); } }); it('should retrieve articles list', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/articles', method: 'GET', headers: { 'Accept': 'application/json' } }); // Peut être 200 (succès) ou 500 (erreur Google Sheets) assert.ok([200, 500].includes(response.statusCode)); if (response.statusCode === 200) { assert.ok(Array.isArray(response.data.data.articles)); assert.ok(typeof response.data.data.total === 'number'); } }); it('should handle pagination parameters', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/articles?limit=10&offset=5', method: 'GET' }); // Même si ça échoue côté Google Sheets, la structure doit être correcte if (response.statusCode === 200) { assert.strictEqual(response.data.data.limit, 10); assert.strictEqual(response.data.data.offset, 5); } }); }); describe('⚙️ Configuration Integration', () => { it('should retrieve personalities configuration', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/config/personalities', method: 'GET' }); // Peut échouer si Google Sheets non accessible assert.ok([200, 500].includes(response.statusCode)); if (response.statusCode === 200) { assert.strictEqual(response.data.success, true); assert.ok(Array.isArray(response.data.data.personalities)); assert.ok(typeof response.data.data.total === 'number'); } }); }); describe('🌐 HTTP Protocol Compliance', () => { it('should handle OPTIONS requests (CORS preflight)', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/health', method: 'OPTIONS' }); // Express + CORS devrait gérer OPTIONS assert.ok([200, 204].includes(response.statusCode)); }); it('should return 404 for non-existent endpoints', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/nonexistent', method: 'GET' }); assert.strictEqual(response.statusCode, 404); }); it('should handle malformed JSON gracefully', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, '{ invalid json }'); assert.strictEqual(response.statusCode, 400); }); it('should set correct content-type headers', async () => { const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/health', method: 'GET' }); assert.ok(response.headers['content-type'].includes('application/json')); }); }); describe('🔒 Error Handling Integration', () => { it('should handle server errors gracefully', async () => { // Tenter de récupérer un article avec ID invalide const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/articles/invalid_id_format', method: 'GET' }); assert.strictEqual(response.statusCode, 500); assert.strictEqual(response.data.success, false); assert.ok(response.data.error); assert.ok(response.data.message); }); it('should maintain consistent error format across endpoints', async () => { const responses = await Promise.all([ makeRequest({ hostname: 'localhost', port: testPort, path: '/api/projects', method: 'POST', headers: { 'Content-Type': 'application/json' } }, {}), makeRequest({ hostname: 'localhost', port: testPort, path: '/api/templates', method: 'POST', headers: { 'Content-Type': 'application/json' } }, {}), makeRequest({ hostname: 'localhost', port: testPort, path: '/api/articles', method: 'POST', headers: { 'Content-Type': 'application/json' } }, {}) ]); responses.forEach(response => { assert.strictEqual(response.statusCode, 400); assert.strictEqual(response.data.success, false); assert.ok(response.data.error); assert.ok(typeof response.data.error === 'string'); }); }); }); describe('📈 Performance Integration', () => { it('should respond to health check within reasonable time', async () => { const start = Date.now(); const response = await makeRequest({ hostname: 'localhost', port: testPort, path: '/api/health', method: 'GET' }); const duration = Date.now() - start; assert.strictEqual(response.statusCode, 200); assert.ok(duration < 1000, `Health check took ${duration}ms, should be < 1000ms`); }); it('should handle concurrent requests', async () => { const concurrentRequests = Array(5).fill().map(() => makeRequest({ hostname: 'localhost', port: testPort, path: '/api/metrics', method: 'GET' }) ); const responses = await Promise.all(concurrentRequests); responses.forEach(response => { assert.strictEqual(response.statusCode, 200); assert.strictEqual(response.data.success, true); }); }); }); }); console.log('🔥 Tests d\'Intégration API Server - Validation HTTP complète');