- 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
479 lines
15 KiB
JavaScript
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'); |