seo-generator-server/tests/integration/api-server.test.js
StillHammer 4f60de68d6 Fix BatchProcessor initialization and add comprehensive test suite
- Fix BatchProcessor constructor to avoid server blocking during startup
- Add comprehensive integration tests for all modular combinations
- Enhance CLAUDE.md documentation with new test commands
- Update SelectiveLayers configuration for better LLM allocation
- Add AutoReporter system for test automation
- Include production workflow validation tests

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 14:17:49 +08:00

475 lines
15 KiB
JavaScript

import { AutoReporter } from '../reporters/AutoReporter.js';
/**
* TESTS D'INTÉGRATION COMPLETS - API Server
* Tests avec serveur HTTP réel et requêtes HTTP authentiques
*/
import { describe, it, before, after } from 'node:test';
import assert from 'node:assert';
import http from 'node:http';
import { requireCommonJS } from '../_helpers/commonjs-bridge.js';
const { ManualServer } = requireCommonJS('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();
});
}
// Auto-Reporter Configuration
const autoReporter = new AutoReporter();
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: '<?xml version="1.0" encoding="UTF-8"?><template><title>{{TITLE}}</title><content>{{CONTENT}}</content></template>',
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');