seo-generator-server/tests/unit/api-controller.test.js
StillHammer 5f9ff4941d Complete API system implementation with comprehensive testing
- APIController.js: Full RESTful API with articles, projects, templates endpoints
- Real HTTP integration tests with live server validation
- Unit tests with proper mocking and error handling
- API documentation with examples and usage patterns
- Enhanced audit tool supporting HTML, npm scripts, dynamic imports
- Cleaned 28 dead files identified by enhanced audit analysis
- Google Sheets integration fully validated in test environment
2025-09-16 11:10:46 +08:00

479 lines
15 KiB
JavaScript

/**
* 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: '<?xml version="1.0"?><template></template>',
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: '<template></template>'
}
};
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: '<template></template>'
}
};
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: '<p>Contenu HTML</p>',
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');