/** * TESTS UNITAIRES COMPLETS - APIController * Tests isolés avec mocks pour valider chaque méthode */ const { describe, it, beforeEach, afterEach, mock } = require('node:test'); const assert = require('node:assert'); const { APIController } = require('../../lib/APIController'); // Mock des dépendances const mockGetPersonalities = mock.fn(); const mockReadInstructionsData = mock.fn(); const mockGetStoredArticle = mock.fn(); const mockGetRecentArticles = mock.fn(); const mockHandleFullWorkflow = mock.fn(); // Patch des modules mock.module('../../lib/BrainConfig', () => ({ getPersonalities: mockGetPersonalities, readInstructionsData: mockReadInstructionsData })); mock.module('../../lib/ArticleStorage', () => ({ getStoredArticle: mockGetStoredArticle, getRecentArticles: mockGetRecentArticles })); mock.module('../../lib/Main', () => ({ handleFullWorkflow: mockHandleFullWorkflow })); describe('APIController - Tests Unitaires Complets', () => { let apiController; let mockReq, mockRes; beforeEach(() => { apiController = new APIController(); // Mock response object complet mockRes = { data: null, statusCode: 200, headers: {}, json: mock.fn((data) => { mockRes.data = data; return mockRes; }), status: mock.fn((code) => { mockRes.statusCode = code; return mockRes; }), setHeader: mock.fn((key, value) => { mockRes.headers[key] = value; return mockRes; }), send: mock.fn((data) => { mockRes.sentData = data; return mockRes; }) }; // Reset mocks mockGetPersonalities.mock.resetCalls(); mockReadInstructionsData.mock.resetCalls(); mockGetStoredArticle.mock.resetCalls(); mockGetRecentArticles.mock.resetCalls(); mockHandleFullWorkflow.mock.resetCalls(); }); describe('🏥 Health Check', () => { it('should return healthy status with all required fields', async () => { mockReq = {}; await apiController.getHealth(mockReq, mockRes); assert.strictEqual(mockRes.json.mock.callCount(), 1); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, true); assert.strictEqual(response.data.status, 'healthy'); assert.ok(response.data.timestamp); assert.ok(response.data.version); assert.ok(typeof response.data.uptime === 'number'); assert.ok(response.data.memory); assert.ok(response.data.environment); }); it('should handle health check errors gracefully', async () => { // Forcer une erreur en cassant process.uptime const originalUptime = process.uptime; process.uptime = () => { throw new Error('Process error'); }; await apiController.getHealth(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.callCount(), 1); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 500); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, false); assert.ok(response.error); // Restaurer process.uptime = originalUptime; }); }); describe('📊 Metrics', () => { it('should return complete system metrics', async () => { mockReq = {}; await apiController.getMetrics(mockReq, mockRes); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, true); assert.ok(response.data.articles); assert.ok(response.data.projects); assert.ok(response.data.templates); assert.ok(response.data.system); // Vérifier structure des métriques assert.ok(typeof response.data.articles.total === 'number'); assert.ok(typeof response.data.projects.total === 'number'); assert.ok(typeof response.data.templates.total === 'number'); assert.ok(response.data.system.uptime !== undefined); assert.ok(response.data.system.memory); assert.ok(response.data.system.platform); assert.ok(response.data.system.nodeVersion); }); }); describe('📁 Gestion Projets', () => { it('should create project with valid data', async () => { mockReq = { body: { name: 'Test Project', description: 'Description test', config: { option: 'value' } } }; await apiController.createProject(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.callCount(), 1); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 201); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, true); assert.strictEqual(response.data.name, 'Test Project'); assert.strictEqual(response.data.description, 'Description test'); assert.ok(response.data.id); assert.ok(response.data.createdAt); assert.strictEqual(response.data.articlesCount, 0); }); it('should reject project creation without name', async () => { mockReq = { body: { description: 'Sans nom' } }; await apiController.createProject(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 400); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, false); assert.ok(response.error.includes('Nom du projet requis')); }); it('should return empty project list initially', async () => { mockReq = {}; await apiController.getProjects(mockReq, mockRes); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, true); assert.ok(Array.isArray(response.data.projects)); assert.strictEqual(response.data.total, 0); }); it('should return projects after creation', async () => { // Créer un projet d'abord await apiController.createProject({ body: { name: 'Project 1', description: 'Desc 1' } }, mockRes); // Puis récupérer la liste mockReq = {}; await apiController.getProjects(mockReq, mockRes); const response = mockRes.json.mock.calls[1].arguments[0]; // Deuxième appel assert.strictEqual(response.success, true); assert.strictEqual(response.data.projects.length, 1); assert.strictEqual(response.data.total, 1); assert.strictEqual(response.data.projects[0].name, 'Project 1'); }); }); describe('📋 Gestion Templates', () => { it('should create template with complete data', async () => { mockReq = { body: { name: 'Template Test', content: '', description: 'Template de test', category: 'test' } }; await apiController.createTemplate(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 201); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, true); assert.strictEqual(response.data.name, 'Template Test'); assert.strictEqual(response.data.category, 'test'); assert.ok(response.data.id); assert.ok(response.data.createdAt); }); it('should reject template without name', async () => { mockReq = { body: { content: '' } }; await apiController.createTemplate(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 400); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, false); assert.ok(response.error.includes('Nom et contenu du template requis')); }); it('should reject template without content', async () => { mockReq = { body: { name: 'Template sans contenu' } }; await apiController.createTemplate(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 400); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, false); assert.ok(response.error.includes('Nom et contenu du template requis')); }); it('should use default category when not provided', async () => { mockReq = { body: { name: 'Template sans catégorie', content: '' } }; await apiController.createTemplate(mockReq, mockRes); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.data.category, 'custom'); }); }); describe('📝 Gestion Articles', () => { it('should validate article creation input', async () => { mockReq = { body: {} }; await apiController.createArticle(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 400); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, false); assert.ok(response.error.includes('Mot-clé ou numéro de ligne requis')); }); it('should create article with keyword', async () => { mockHandleFullWorkflow.mock.mockImplementationOnce(async () => ({ id: 'article_123', slug: 'test-article', content: 'Contenu généré' })); mockReq = { body: { keyword: 'test keyword', project: 'test-project', config: { selectiveStack: 'standardEnhancement' } } }; await apiController.createArticle(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 201); assert.strictEqual(mockHandleFullWorkflow.mock.callCount(), 1); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, true); assert.ok(response.data.id); assert.ok(response.data.article); }); it('should create article with row number', async () => { mockHandleFullWorkflow.mock.mockImplementationOnce(async () => ({ id: 'article_456', content: 'Contenu de la ligne 5' })); mockReq = { body: { rowNumber: 5, project: 'sheets-project' } }; await apiController.createArticle(mockReq, mockRes); assert.strictEqual(mockHandleFullWorkflow.mock.callCount(), 1); const workflowArgs = mockHandleFullWorkflow.mock.calls[0].arguments[0]; assert.strictEqual(workflowArgs.rowNumber, 5); assert.strictEqual(workflowArgs.project, 'sheets-project'); assert.strictEqual(workflowArgs.source, 'api'); }); it('should handle article creation errors', async () => { mockHandleFullWorkflow.mock.mockImplementationOnce(async () => { throw new Error('Workflow failed'); }); mockReq = { body: { keyword: 'test fail' } }; await apiController.createArticle(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 500); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, false); assert.ok(response.error); }); it('should get articles with default pagination', async () => { mockGetRecentArticles.mock.mockImplementationOnce(async () => [ { id: '1', title: 'Article 1' }, { id: '2', title: 'Article 2' } ]); mockReq = { query: {} }; await apiController.getArticles(mockReq, mockRes); assert.strictEqual(mockGetRecentArticles.mock.callCount(), 1); assert.strictEqual(mockGetRecentArticles.mock.calls[0].arguments[0], 50); // limit par défaut const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, true); assert.strictEqual(response.data.articles.length, 2); assert.strictEqual(response.data.total, 2); assert.strictEqual(response.data.limit, 50); assert.strictEqual(response.data.offset, 0); }); it('should get articles with custom pagination', async () => { mockGetRecentArticles.mock.mockImplementationOnce(async () => [ { id: '1' }, { id: '2' }, { id: '3' }, { id: '4' }, { id: '5' } ]); mockReq = { query: { limit: '3', offset: '1' } }; await apiController.getArticles(mockReq, mockRes); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.data.limit, 3); assert.strictEqual(response.data.offset, 1); assert.strictEqual(response.data.articles.length, 3); }); it('should handle article retrieval error', async () => { mockGetStoredArticle.mock.mockImplementationOnce(async () => { throw new Error('Google Sheets error'); }); mockReq = { params: { id: 'test_id' }, query: {} }; await apiController.getArticle(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 500); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, false); }); }); describe('⚙️ Configuration', () => { it('should get personalities config', async () => { mockGetPersonalities.mock.mockImplementationOnce(async () => [ { nom: 'Marc', style: 'professionnel' }, { nom: 'Sophie', style: 'familier' } ]); mockReq = {}; await apiController.getPersonalitiesConfig(mockReq, mockRes); assert.strictEqual(mockGetPersonalities.mock.callCount(), 1); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, true); assert.strictEqual(response.data.personalities.length, 2); assert.strictEqual(response.data.total, 2); }); it('should handle personalities config error', async () => { mockGetPersonalities.mock.mockImplementationOnce(async () => { throw new Error('Google Sheets unavailable'); }); mockReq = {}; await apiController.getPersonalitiesConfig(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 500); const response = mockRes.json.mock.calls[0].arguments[0]; assert.strictEqual(response.success, false); }); }); describe('🔍 Edge Cases et Validation', () => { it('should handle missing query parameters gracefully', async () => { mockReq = { // Pas de query }; await apiController.getHealth(mockReq, mockRes); // Ne devrait pas planter assert.strictEqual(mockRes.json.mock.callCount(), 1); }); it('should handle malformed request bodies', async () => { mockReq = { body: null }; await apiController.createProject(mockReq, mockRes); assert.strictEqual(mockRes.status.mock.calls[0].arguments[0], 400); }); it('should validate article format parameter', async () => { mockGetStoredArticle.mock.mockImplementationOnce(async () => ({ id: 'test', content: 'Contenu test', htmlContent: '

Contenu HTML

', textContent: 'Contenu texte' })); // Test format HTML mockReq = { params: { id: 'test' }, query: { format: 'html' } }; await apiController.getArticle(mockReq, mockRes); assert.strictEqual(mockRes.setHeader.mock.callCount(), 1); assert.strictEqual(mockRes.setHeader.mock.calls[0].arguments[0], 'Content-Type'); assert.strictEqual(mockRes.send.mock.callCount(), 1); }); }); }); console.log('🧪 Tests Unitaires APIController - Validation complète des méthodes');