- 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>
436 lines
13 KiB
JavaScript
436 lines
13 KiB
JavaScript
import { AutoReporter } from '../reporters/AutoReporter.js';
|
|
/**
|
|
* TESTS SÉCURITÉ - API Controller
|
|
* Tests de sécurité, injection, et validation
|
|
*/
|
|
|
|
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 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);
|
|
req.setTimeout(5000, () => {
|
|
req.destroy();
|
|
reject(new Error('Request timeout'));
|
|
});
|
|
|
|
if (postData) {
|
|
req.write(typeof postData === 'object' ? JSON.stringify(postData) : postData);
|
|
}
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
|
|
// Auto-Reporter Configuration
|
|
const autoReporter = new AutoReporter();
|
|
|
|
describe('API Security Tests - Tests de Sécurité', () => {
|
|
let server;
|
|
let baseUrl;
|
|
const testPort = 3097;
|
|
|
|
before(async () => {
|
|
server = new ManualServer({ port: testPort, wsPort: 8097 });
|
|
await server.start();
|
|
baseUrl = `http://localhost:${testPort}`;
|
|
console.log(`🔒 Serveur sécurité démarré sur ${baseUrl}`);
|
|
});
|
|
|
|
after(async () => {
|
|
if (server) {
|
|
await server.stop();
|
|
console.log('🛑 Serveur sécurité arrêté');
|
|
}
|
|
});
|
|
|
|
describe('🛡️ Injection et XSS', () => {
|
|
it('should handle SQL injection attempts safely', async () => {
|
|
const sqlPayloads = [
|
|
"'; DROP TABLE projects; --",
|
|
"1' OR '1'='1",
|
|
"admin'/*",
|
|
"1; SELECT * FROM users; --",
|
|
"' UNION SELECT * FROM passwords --"
|
|
];
|
|
|
|
for (const payload of sqlPayloads) {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, {
|
|
name: payload,
|
|
description: 'Test injection SQL'
|
|
});
|
|
|
|
// Doit traiter comme chaîne normale
|
|
assert.ok([201, 400].includes(response.statusCode));
|
|
|
|
if (response.statusCode === 201) {
|
|
assert.strictEqual(response.data.data.name, payload);
|
|
assert.strictEqual(response.data.success, true);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should handle XSS payloads safely', async () => {
|
|
const xssPayloads = [
|
|
'<script>alert("XSS")</script>',
|
|
'javascript:alert(1)',
|
|
'<img src=x onerror=alert(1)>',
|
|
'<svg onload=alert(1)>',
|
|
'"><script>alert(document.cookie)</script>',
|
|
"';alert(String.fromCharCode(88,83,83))//';alert(String.fromCharCode(88,83,83))//",
|
|
'"><img src=x onerror=alert(1)>',
|
|
'<iframe src="javascript:alert(1)"></iframe>'
|
|
];
|
|
|
|
for (const payload of xssPayloads) {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/templates',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, {
|
|
name: 'Test XSS',
|
|
content: payload,
|
|
description: 'Template avec payload XSS'
|
|
});
|
|
|
|
assert.ok([201, 400].includes(response.statusCode));
|
|
|
|
if (response.statusCode === 201) {
|
|
// Le payload doit être stocké tel quel, pas d'exécution
|
|
assert.strictEqual(response.data.data.content, payload);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should handle path traversal attempts', async () => {
|
|
const pathTraversals = [
|
|
'../../../etc/passwd',
|
|
'..\\..\\..\\windows\\system32\\config',
|
|
'%2e%2e%2f%2e%2e%2f%2e%2e%2f',
|
|
'....//....//....//etc/passwd',
|
|
'..%2F..%2F..%2Fetc%2Fpasswd'
|
|
];
|
|
|
|
for (const path of pathTraversals) {
|
|
try {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: `/api/articles/${path}`,
|
|
method: 'GET'
|
|
});
|
|
|
|
// Doit retourner 404 ou 500, pas accéder aux fichiers système
|
|
assert.ok([404, 500].includes(response.statusCode));
|
|
|
|
} catch (error) {
|
|
// Erreurs d'URL malformée acceptables
|
|
assert.ok(error.message.includes('Invalid') || error.message.includes('URI'));
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('🔍 Validation des Données', () => {
|
|
it('should validate required fields strictly', async () => {
|
|
const invalidPayloads = [
|
|
null,
|
|
undefined,
|
|
{},
|
|
{ name: '' },
|
|
{ name: null },
|
|
{ name: undefined },
|
|
{ description: 'Sans nom' },
|
|
{ name: ' ' }, // Espaces seulement
|
|
];
|
|
|
|
for (const payload of invalidPayloads) {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, payload);
|
|
|
|
assert.strictEqual(response.statusCode, 400);
|
|
assert.strictEqual(response.data.success, false);
|
|
assert.ok(response.data.error);
|
|
}
|
|
});
|
|
|
|
it('should handle malformed JSON gracefully', async () => {
|
|
const malformedJsons = [
|
|
'{"name":}',
|
|
'{"name":"test",}',
|
|
'{name:"test"}', // Clés sans guillemets
|
|
'{"name":"test"', // JSON incomplet
|
|
'null',
|
|
'undefined',
|
|
'',
|
|
'[]', // Array au lieu d'objet
|
|
'true', // Boolean au lieu d'objet
|
|
'{"name":"test","config":{invalid}}' // Objet imbriqué malformé
|
|
];
|
|
|
|
for (const json of malformedJsons) {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, json);
|
|
|
|
assert.strictEqual(response.statusCode, 400);
|
|
assert.strictEqual(response.data.success, false);
|
|
}
|
|
});
|
|
|
|
it('should handle extreme data types', async () => {
|
|
const extremePayloads = [
|
|
{ name: 123 }, // Number au lieu de string
|
|
{ name: true }, // Boolean au lieu de string
|
|
{ name: [] }, // Array au lieu de string
|
|
{ name: {} }, // Object au lieu de string
|
|
{ name: 'Test', description: 123 }, // Number en description
|
|
{ name: 'Test', config: 'not an object' } // String au lieu d'object
|
|
];
|
|
|
|
for (const payload of extremePayloads) {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, payload);
|
|
|
|
// L'API peut soit accepter (conversion automatique) soit rejeter
|
|
assert.ok([201, 400].includes(response.statusCode));
|
|
|
|
if (response.statusCode === 201) {
|
|
// Vérifier que les types sont convertis proprement
|
|
assert.strictEqual(typeof response.data.data.name, 'string');
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('🌐 Protocole HTTP', () => {
|
|
it('should handle missing Content-Type header', async () => {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST'
|
|
// Pas de Content-Type
|
|
}, '{"name":"Test"}');
|
|
|
|
// Express devrait gérer gracieusement
|
|
assert.ok([400, 415, 500].includes(response.statusCode));
|
|
});
|
|
|
|
it('should handle wrong Content-Type header', async () => {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
}, '{"name":"Test"}');
|
|
|
|
assert.ok([400, 415, 500].includes(response.statusCode));
|
|
});
|
|
|
|
it('should handle unsupported HTTP methods', async () => {
|
|
const methods = ['PATCH', 'DELETE', 'HEAD', 'TRACE'];
|
|
|
|
for (const method of methods) {
|
|
try {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method
|
|
});
|
|
|
|
// 405 Method Not Allowed ou 404
|
|
assert.ok([404, 405].includes(response.statusCode));
|
|
} catch (error) {
|
|
// Certaines méthodes peuvent être rejetées par Node.js
|
|
assert.ok(error.message.includes('Method') || error.message.includes('method'));
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should handle invalid URLs', async () => {
|
|
const invalidPaths = [
|
|
'/api/projects with spaces',
|
|
'/api/проекты', // Caractères non-ASCII
|
|
'/api/projects?' + 'x'.repeat(1000), // Query string très longue
|
|
'/api/projects#fragment'
|
|
];
|
|
|
|
for (const path of invalidPaths) {
|
|
try {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path,
|
|
method: 'GET'
|
|
});
|
|
|
|
assert.ok([400, 404].includes(response.statusCode));
|
|
} catch (error) {
|
|
// Erreurs de parsing d'URL attendues
|
|
assert.ok(error.message.includes('Invalid') || error.message.includes('URI'));
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('📊 Limites et Performance', () => {
|
|
it('should handle very large payloads', async () => {
|
|
// Payload de 100KB
|
|
const largePayload = {
|
|
name: 'Large Test',
|
|
description: 'X'.repeat(100 * 1024), // 100KB
|
|
config: {
|
|
data: 'Y'.repeat(10000)
|
|
}
|
|
};
|
|
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, largePayload);
|
|
|
|
// Peut accepter ou rejeter selon les limites
|
|
assert.ok([201, 400, 413].includes(response.statusCode));
|
|
});
|
|
|
|
it('should handle concurrent requests safely', async () => {
|
|
// 10 requêtes simultanées
|
|
const promises = Array(10).fill().map((_, i) =>
|
|
makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, {
|
|
name: `Concurrent Project ${i}`,
|
|
description: `Test concurrence ${i}`
|
|
})
|
|
);
|
|
|
|
const results = await Promise.allSettled(promises);
|
|
|
|
// Vérifier succès
|
|
const successes = results.filter(r =>
|
|
r.status === 'fulfilled' && r.value.statusCode === 201
|
|
);
|
|
|
|
assert.ok(successes.length > 0, 'Au moins une requête doit réussir');
|
|
|
|
// Vérifier unicité des IDs
|
|
const ids = successes.map(r => r.value.data.data.id);
|
|
const uniqueIds = new Set(ids);
|
|
assert.strictEqual(ids.length, uniqueIds.size, 'IDs doivent être uniques');
|
|
});
|
|
|
|
it('should maintain stability under rapid requests', async () => {
|
|
const requests = [];
|
|
|
|
// 20 requêtes health check rapides
|
|
for (let i = 0; i < 20; i++) {
|
|
requests.push(
|
|
makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/health',
|
|
method: 'GET'
|
|
})
|
|
);
|
|
}
|
|
|
|
const results = await Promise.allSettled(requests);
|
|
|
|
const successes = results.filter(r =>
|
|
r.status === 'fulfilled' && r.value.statusCode === 200
|
|
);
|
|
|
|
// Au moins 90% doivent réussir
|
|
const successRate = (successes.length / results.length) * 100;
|
|
assert.ok(successRate >= 90, `Success rate too low: ${successRate}%`);
|
|
});
|
|
});
|
|
|
|
describe('🔒 Headers et CORS', () => {
|
|
it('should handle CORS preflight requests', async () => {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/health',
|
|
method: 'OPTIONS',
|
|
headers: {
|
|
'Origin': 'http://localhost:3000',
|
|
'Access-Control-Request-Method': 'POST'
|
|
}
|
|
});
|
|
|
|
// CORS devrait être géré
|
|
assert.ok([200, 204].includes(response.statusCode));
|
|
});
|
|
|
|
it('should set appropriate security headers', async () => {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/health',
|
|
method: 'GET'
|
|
});
|
|
|
|
assert.strictEqual(response.statusCode, 200);
|
|
|
|
// Vérifier headers de sécurité de base
|
|
assert.ok(response.headers['content-type']);
|
|
|
|
// Les headers de sécurité peuvent être ajoutés par Express/middleware
|
|
console.log('📋 Headers reçus:', Object.keys(response.headers));
|
|
});
|
|
});
|
|
});
|
|
|
|
console.log('🔒 Tests Sécurité API - Validation injection, XSS et limites'); |