- Security tests: SQL injection, XSS, path traversal validation - Parameters tests: Unicode, large payloads, extreme values handling - Performance tests: Concurrency, rapid requests, mixed operations - All edge cases pass with robust error handling and data validation - API demonstrates production-ready security and stability
477 lines
15 KiB
JavaScript
477 lines
15 KiB
JavaScript
/**
|
|
* TESTS EDGE CASES - API Controller
|
|
* Tests des cas limites, erreurs et comportements extrêmes
|
|
*/
|
|
|
|
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(10000, () => {
|
|
req.destroy();
|
|
reject(new Error('Request timeout'));
|
|
});
|
|
|
|
if (postData) {
|
|
req.write(typeof postData === 'object' ? JSON.stringify(postData) : postData);
|
|
}
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
describe('API Edge Cases - Tests des Cas Limites', () => {
|
|
let server;
|
|
let baseUrl;
|
|
const testPort = 3098; // Port différent pour éviter conflits
|
|
|
|
before(async () => {
|
|
server = new ManualServer({ port: testPort, wsPort: 8098 });
|
|
await server.start();
|
|
baseUrl = `http://localhost:${testPort}`;
|
|
console.log(`🧪 Serveur edge cases démarré sur ${baseUrl}`);
|
|
});
|
|
|
|
after(async () => {
|
|
if (server) {
|
|
await server.stop();
|
|
console.log('🛑 Serveur edge cases arrêté');
|
|
}
|
|
});
|
|
|
|
describe('🔥 Paramètres Extrêmes', () => {
|
|
it('should handle very long project names', async () => {
|
|
const longName = 'A'.repeat(10000); // 10KB de nom
|
|
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, {
|
|
name: longName,
|
|
description: 'Test nom très long'
|
|
});
|
|
|
|
// L'API doit gérer les noms longs gracieusement
|
|
assert.ok([201, 400].includes(response.statusCode));
|
|
|
|
if (response.statusCode === 201) {
|
|
assert.strictEqual(response.data.success, true);
|
|
assert.strictEqual(response.data.data.name, longName);
|
|
} else {
|
|
assert.strictEqual(response.data.success, false);
|
|
}
|
|
});
|
|
|
|
it('should handle pagination with extreme values', async () => {
|
|
// Test avec des valeurs de pagination extrêmes
|
|
const testCases = [
|
|
{ limit: -1, offset: 0 },
|
|
{ limit: 999999, offset: 0 },
|
|
{ limit: 50, offset: -1 },
|
|
{ limit: 'invalid', offset: 'invalid' },
|
|
{ limit: 0, offset: 999999 }
|
|
];
|
|
|
|
for (const testCase of testCases) {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: `/api/articles?limit=${testCase.limit}&offset=${testCase.offset}`,
|
|
method: 'GET'
|
|
});
|
|
|
|
// L'API doit soit réussir avec des valeurs normalisées, soit échouer gracieusement
|
|
assert.ok([200, 400, 500].includes(response.statusCode));
|
|
|
|
if (response.statusCode === 200) {
|
|
assert.ok(response.data.data.limit >= 0);
|
|
assert.ok(response.data.data.offset >= 0);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should handle special characters in all fields', async () => {
|
|
const specialChars = {
|
|
name: '🚀 Test "Special" <script>alert(1)</script> & émojis 中文',
|
|
description: 'Description avec \n\t\r caractères spéciaux \\n "quotes" \'single\' & symbols €£¥',
|
|
config: {
|
|
special: '<>&"\'{}[]()=+-*/\\',
|
|
unicode: '🎯🔥💯✨🌟⭐',
|
|
mixed: 'Normal text avec 中文 и русский'
|
|
}
|
|
};
|
|
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, specialChars);
|
|
|
|
assert.ok([201, 400].includes(response.statusCode));
|
|
|
|
if (response.statusCode === 201) {
|
|
assert.strictEqual(response.data.data.name, specialChars.name);
|
|
assert.strictEqual(response.data.data.description, specialChars.description);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('🛡️ Sécurité et Injection', () => {
|
|
it('should reject SQL injection attempts', async () => {
|
|
const sqlInjections = [
|
|
"'; DROP TABLE projects; --",
|
|
"1' OR '1'='1",
|
|
"admin'/*",
|
|
"1; SELECT * FROM users; --"
|
|
];
|
|
|
|
for (const injection of sqlInjections) {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, {
|
|
name: injection,
|
|
description: 'Test injection SQL'
|
|
});
|
|
|
|
// L'API doit traiter cela comme une chaîne normale, pas d'injection
|
|
assert.ok([201, 400].includes(response.statusCode));
|
|
|
|
if (response.statusCode === 201) {
|
|
assert.strictEqual(response.data.data.name, injection); // Stocké tel quel
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should handle XSS attempts gracefully', async () => {
|
|
const xssPayloads = [
|
|
'<script>alert("XSS")</script>',
|
|
'javascript:alert(1)',
|
|
'<img src=x onerror=alert(1)>',
|
|
'<svg onload=alert(1)>'
|
|
];
|
|
|
|
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 côté serveur)
|
|
assert.strictEqual(response.data.data.content, payload);
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should validate content-type header requirements', async () => {
|
|
// Test sans Content-Type
|
|
const response1 = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST'
|
|
}, '{"name":"Test"}');
|
|
|
|
// Test avec mauvais Content-Type
|
|
const response2 = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'text/plain' }
|
|
}, '{"name":"Test"}');
|
|
|
|
// Express devrait gérer ces cas
|
|
assert.ok([400, 415, 500].includes(response1.statusCode));
|
|
assert.ok([400, 415, 500].includes(response2.statusCode));
|
|
});
|
|
});
|
|
|
|
describe('💣 Cas d\'Erreur Extrêmes', () => {
|
|
it('should handle malformed JSON gracefully', async () => {
|
|
const malformedJsons = [
|
|
'{"name":}',
|
|
'{"name":"test",}',
|
|
'{name:"test"}',
|
|
'{"name":"test"',
|
|
'null',
|
|
'undefined',
|
|
'',
|
|
'[]',
|
|
'true'
|
|
];
|
|
|
|
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 very large payloads', async () => {
|
|
// Payload de 1MB
|
|
const largePayload = {
|
|
name: 'Large Test',
|
|
description: 'X'.repeat(1024 * 1024), // 1MB de description
|
|
config: {
|
|
data: 'Y'.repeat(100000) // 100KB supplémentaires
|
|
}
|
|
};
|
|
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, largePayload);
|
|
|
|
// L'API peut soit accepter, soit rejeter selon les limites configurées
|
|
assert.ok([201, 400, 413].includes(response.statusCode));
|
|
});
|
|
|
|
it('should handle concurrent requests without corruption', async () => {
|
|
// 20 requêtes simultanées
|
|
const promises = Array(20).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);
|
|
|
|
// Compter les succès
|
|
const successes = results.filter(r =>
|
|
r.status === 'fulfilled' && r.value.statusCode === 201
|
|
);
|
|
|
|
// Au moins quelques requêtes doivent réussir
|
|
assert.ok(successes.length > 0);
|
|
|
|
// Vérifier que les IDs sont uniques
|
|
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');
|
|
});
|
|
});
|
|
|
|
describe('🌐 Protocole HTTP Edge Cases', () => {
|
|
it('should handle unsupported HTTP methods', async () => {
|
|
const methods = ['PATCH', 'DELETE', 'HEAD', 'TRACE', 'CONNECT'];
|
|
|
|
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 and paths', async () => {
|
|
const invalidPaths = [
|
|
'/api/projects/../../../etc/passwd',
|
|
'/api/projects/%2e%2e%2f%2e%2e%2f',
|
|
'/api/projects/\\..\\..\\windows\\system32',
|
|
'/api/projects?' + 'x'.repeat(10000), // Query string très longue
|
|
'/api/projects#fragment',
|
|
'/api/projects with spaces',
|
|
'/api/проекты', // URL avec caractères non-ASCII
|
|
];
|
|
|
|
for (const path of invalidPaths) {
|
|
try {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path,
|
|
method: 'GET'
|
|
});
|
|
|
|
// Doit retourner 404 ou 400, pas d'erreur de sécurité
|
|
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'));
|
|
}
|
|
}
|
|
});
|
|
|
|
it('should handle connection timeouts gracefully', async () => {
|
|
// Test avec un article qui va prendre du temps (workflow LLM)
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
const response = await makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/articles',
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' }
|
|
}, {
|
|
keyword: 'test timeout edge case',
|
|
project: 'timeout-test'
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
// Si ça réussit, vérifier que c'est raisonnable
|
|
if (response.statusCode === 201) {
|
|
assert.ok(duration < 120000, `Article generation took ${duration}ms`);
|
|
assert.strictEqual(response.data.success, true);
|
|
} else {
|
|
// Erreur attendue (timeout LLM, etc.)
|
|
assert.strictEqual(response.data.success, false);
|
|
}
|
|
|
|
} catch (error) {
|
|
// Timeout de requête attendu
|
|
assert.ok(error.message.includes('timeout') || error.message.includes('Timeout'));
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('📊 Performance Edge Cases', () => {
|
|
it('should handle rapid successive requests', async () => {
|
|
const requests = [];
|
|
const startTime = Date.now();
|
|
|
|
// 50 requêtes health check rapides
|
|
for (let i = 0; i < 50; i++) {
|
|
requests.push(
|
|
makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: '/api/health',
|
|
method: 'GET'
|
|
})
|
|
);
|
|
}
|
|
|
|
const results = await Promise.allSettled(requests);
|
|
const duration = Date.now() - startTime;
|
|
|
|
const successes = results.filter(r =>
|
|
r.status === 'fulfilled' && r.value.statusCode === 200
|
|
);
|
|
|
|
// La plupart des requêtes doivent réussir
|
|
assert.ok(successes.length >= 45, `Only ${successes.length}/50 requests succeeded`);
|
|
|
|
// Performance raisonnable
|
|
assert.ok(duration < 5000, `50 health checks took ${duration}ms`);
|
|
|
|
console.log(`📈 Performance: ${successes.length}/50 requests in ${duration}ms`);
|
|
});
|
|
|
|
it('should maintain stability under stress', async () => {
|
|
// Mix de différents types de requêtes
|
|
const stressRequests = [
|
|
// Health checks
|
|
...Array(10).fill().map(() => ({ path: '/api/health', method: 'GET' })),
|
|
|
|
// Metrics
|
|
...Array(5).fill().map(() => ({ path: '/api/metrics', method: 'GET' })),
|
|
|
|
// Project creation
|
|
...Array(5).fill().map((_, i) => ({
|
|
path: '/api/projects',
|
|
method: 'POST',
|
|
data: { name: `Stress Project ${i}`, description: 'Stress test' }
|
|
})),
|
|
|
|
// Template creation
|
|
...Array(3).fill().map((_, i) => ({
|
|
path: '/api/templates',
|
|
method: 'POST',
|
|
data: { name: `Stress Template ${i}`, content: '<template></template>' }
|
|
}))
|
|
];
|
|
|
|
const promises = stressRequests.map(req =>
|
|
makeRequest({
|
|
hostname: 'localhost',
|
|
port: testPort,
|
|
path: req.path,
|
|
method: req.method,
|
|
headers: req.data ? { 'Content-Type': 'application/json' } : {}
|
|
}, req.data)
|
|
);
|
|
|
|
const results = await Promise.allSettled(promises);
|
|
|
|
// Calculer taux de succès
|
|
const successes = results.filter(r =>
|
|
r.status === 'fulfilled' && [200, 201].includes(r.value.statusCode)
|
|
);
|
|
|
|
const successRate = (successes.length / results.length) * 100;
|
|
console.log(`🎯 Stress test: ${successRate.toFixed(1)}% success rate`);
|
|
|
|
// Au moins 80% de succès attendu
|
|
assert.ok(successRate >= 80, `Success rate too low: ${successRate}%`);
|
|
});
|
|
});
|
|
});
|
|
|
|
console.log('🧪 Tests Edge Cases API - Validation des cas limites et sécurité'); |