seo-generator-server/tests/edge-cases/api-parameters.test.js
StillHammer 4f60de68d6 Fix BatchProcessor initialization and add comprehensive test suite
- 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>
2025-09-19 14:17:49 +08:00

455 lines
15 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { AutoReporter } from '../reporters/AutoReporter.js';
/**
* TESTS PARAMÈTRES EDGE CASES - API Controller
* Tests des limites de paramètres et pagination
*/
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();
});
}
// Auto-Reporter Configuration
const autoReporter = new AutoReporter();
describe('API Parameters Edge Cases', () => {
let server;
let baseUrl;
const testPort = 3096;
before(async () => {
server = new ManualServer({ port: testPort, wsPort: 8096 });
await server.start();
baseUrl = `http://localhost:${testPort}`;
console.log(`📊 Serveur paramètres démarré sur ${baseUrl}`);
});
after(async () => {
if (server) {
await server.stop();
console.log('🛑 Serveur paramètres arrêté');
}
});
describe('📄 Pagination Edge Cases', () => {
it('should handle extreme pagination values', async () => {
const testCases = [
{ limit: -1, offset: 0, expected: 'normalize negative limit' },
{ limit: 999999, offset: 0, expected: 'cap very large limit' },
{ limit: 50, offset: -1, expected: 'normalize negative offset' },
{ limit: 0, offset: 0, expected: 'handle zero limit' },
{ limit: 50, offset: 999999, expected: 'handle large offset' }
];
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 normaliser ou échouer gracieusement
assert.ok([200, 400, 500].includes(response.statusCode),
`Test case: ${testCase.expected}`);
if (response.statusCode === 200) {
assert.ok(response.data.data.limit >= 0, 'Limit should be normalized to >= 0');
assert.ok(response.data.data.offset >= 0, 'Offset should be normalized to >= 0');
}
}
});
it('should handle invalid pagination types', async () => {
const invalidTypes = [
{ limit: 'abc', offset: 0 },
{ limit: 50, offset: 'def' },
{ limit: 'null', offset: 'undefined' },
{ limit: '[]', offset: '{}' },
{ limit: 'true', offset: 'false' }
];
for (const testCase of invalidTypes) {
const response = await makeRequest({
hostname: 'localhost',
port: testPort,
path: `/api/articles?limit=${testCase.limit}&offset=${testCase.offset}`,
method: 'GET'
});
// L'API peut convertir ou échouer
assert.ok([200, 400].includes(response.statusCode));
if (response.statusCode === 200) {
// Vérifier que les valeurs sont normalisées
assert.ok(typeof response.data.data.limit === 'number');
assert.ok(typeof response.data.data.offset === 'number');
}
}
});
it('should handle query parameters with special characters', async () => {
const specialParams = [
'project=%3Cscript%3E', // <script> encodé
'status=%22admin%22%20OR%201%3D1', // "admin" OR 1=1 encodé
'filter=%27%3B%20DROP%20TABLE%3B%20--', // '; DROP TABLE; -- encodé
'search=%E2%9C%93%F0%9F%92%A9', // Unicode émojis
];
for (const param of specialParams) {
const response = await makeRequest({
hostname: 'localhost',
port: testPort,
path: `/api/articles?${param}`,
method: 'GET'
});
// L'API doit gérer les paramètres décodés sans problème
assert.ok([200, 400, 500].includes(response.statusCode));
}
});
});
describe('🔢 Limites de Taille', () => {
it('should handle very long strings in fields', async () => {
const lengths = [1000, 10000, 100000]; // 1KB, 10KB, 100KB
for (const length of lengths) {
const longString = 'A'.repeat(length);
const response = await makeRequest({
hostname: 'localhost',
port: testPort,
path: '/api/projects',
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, {
name: `Test ${length}`,
description: longString,
config: { note: `String de ${length} caractères` }
});
assert.ok([201, 400, 413].includes(response.statusCode),
`Failed for ${length} chars`);
if (response.statusCode === 201) {
assert.strictEqual(response.data.data.description.length, length);
}
}
});
it('should handle deeply nested objects', async () => {
// Objet avec 10 niveaux de profondeur
let deepObject = { value: 'deep' };
for (let i = 0; i < 10; i++) {
deepObject = { level: i, nested: deepObject };
}
const response = await makeRequest({
hostname: 'localhost',
port: testPort,
path: '/api/projects',
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, {
name: 'Deep Object Test',
description: 'Test objet profond',
config: deepObject
});
assert.ok([201, 400].includes(response.statusCode));
if (response.statusCode === 201) {
// Vérifier que la structure est préservée
assert.ok(response.data.data.config.nested);
}
});
it('should handle arrays with many elements', async () => {
// Array avec 1000 éléments
const largeArray = Array(1000).fill().map((_, i) => `item_${i}`);
const response = await makeRequest({
hostname: 'localhost',
port: testPort,
path: '/api/templates',
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, {
name: 'Large Array Test',
content: '<template></template>',
description: 'Test array volumineux',
metadata: {
tags: largeArray,
count: largeArray.length
}
});
assert.ok([201, 400, 413].includes(response.statusCode));
if (response.statusCode === 201) {
assert.strictEqual(response.data.data.metadata.tags.length, 1000);
}
});
});
describe('🎯 Types de Données Extrêmes', () => {
it('should handle all JSON primitive types', async () => {
const primitiveTests = [
{ name: null, type: 'null' },
{ name: undefined, type: 'undefined' },
{ name: true, type: 'boolean true' },
{ name: false, type: 'boolean false' },
{ name: 0, type: 'zero number' },
{ name: -1, type: 'negative number' },
{ name: 1.5, type: 'float number' },
{ name: Number.MAX_SAFE_INTEGER, type: 'max safe integer' },
{ name: '', type: 'empty string' }
];
for (const test of primitiveTests) {
const response = await makeRequest({
hostname: 'localhost',
port: testPort,
path: '/api/projects',
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, {
name: test.name,
description: `Test type: ${test.type}`
});
// L'API peut convertir ou valider
assert.ok([201, 400].includes(response.statusCode),
`Failed for type: ${test.type}`);
if (response.statusCode === 201) {
// Si accepté, vérifier conversion string
assert.strictEqual(typeof response.data.data.name, 'string');
}
}
});
it('should handle Unicode and special characters', async () => {
const unicodeTests = [
{ name: '🚀 Émojis 中文 русский', desc: 'Unicode mixing' },
{ name: 'Ñoño café naïve', desc: 'Accented characters' },
{ name: '\\n\\t\\r\\0', desc: 'Escape sequences' },
{ name: '\u0001\u0002\u0003', desc: 'Control characters' },
{ name: '𝕳𝖊𝖑𝖑𝖔', desc: 'Mathematical symbols' },
{ name: '♠♥♦♣', desc: 'Special symbols' }
];
for (const test of unicodeTests) {
const response = await makeRequest({
hostname: 'localhost',
port: testPort,
path: '/api/projects',
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, {
name: test.name,
description: test.desc
});
assert.ok([201, 400].includes(response.statusCode),
`Failed for: ${test.desc}`);
if (response.statusCode === 201) {
assert.strictEqual(response.data.data.name, test.name);
}
}
});
it('should handle binary and encoded data', async () => {
// Test avec données binaires encodées
const binaryData = Buffer.from('Hello World').toString('base64');
const hexData = Buffer.from('Test Data').toString('hex');
const tests = [
{ content: binaryData, encoding: 'base64' },
{ content: hexData, encoding: 'hex' },
{ content: encodeURIComponent('Test with spaces & symbols'), encoding: 'urlencoded' }
];
for (const test of tests) {
const response = await makeRequest({
hostname: 'localhost',
port: testPort,
path: '/api/templates',
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, {
name: `Binary Test ${test.encoding}`,
content: test.content,
description: `Test encoding ${test.encoding}`
});
assert.ok([201, 400].includes(response.statusCode));
if (response.statusCode === 201) {
assert.strictEqual(response.data.data.content, test.content);
}
}
});
});
describe('⚡ Performance et Charge', () => {
it('should handle rapid sequential requests', async () => {
const startTime = Date.now();
const results = [];
// 50 requêtes séquentielles
for (let i = 0; i < 50; i++) {
try {
const response = await makeRequest({
hostname: 'localhost',
port: testPort,
path: '/api/health',
method: 'GET'
});
results.push(response.statusCode);
} catch (error) {
results.push('ERROR');
}
}
const duration = Date.now() - startTime;
const successCount = results.filter(code => code === 200).length;
console.log(`🏃 Sequential: ${successCount}/50 in ${duration}ms`);
// Au moins 90% de succès
assert.ok(successCount >= 45, `Too many failures: ${successCount}/50`);
// Performance raisonnable (< 10s pour 50 requêtes)
assert.ok(duration < 10000, `Too slow: ${duration}ms`);
});
it('should maintain response consistency under load', async () => {
// Créer plusieurs projets en parallèle puis vérifier cohérence
const createPromises = Array(10).fill().map((_, i) =>
makeRequest({
hostname: 'localhost',
port: testPort,
path: '/api/projects',
method: 'POST',
headers: { 'Content-Type': 'application/json' }
}, {
name: `Load Test Project ${i}`,
description: `Project ${i} for load testing`
})
);
const createResults = await Promise.allSettled(createPromises);
// Vérifier que tous les projets créés ont des IDs uniques
const successfulCreations = createResults
.filter(r => r.status === 'fulfilled' && r.value.statusCode === 201)
.map(r => r.value.data.data);
const ids = successfulCreations.map(p => p.id);
const uniqueIds = new Set(ids);
assert.strictEqual(ids.length, uniqueIds.size, 'All IDs should be unique');
assert.ok(successfulCreations.length >= 8, 'Most projects should be created successfully');
// Vérifier que la liste des projets est cohérente
const listResponse = await makeRequest({
hostname: 'localhost',
port: testPort,
path: '/api/projects',
method: 'GET'
});
assert.strictEqual(listResponse.statusCode, 200);
assert.ok(listResponse.data.data.projects.length >= successfulCreations.length);
});
it('should handle mixed operation types concurrently', async () => {
// Mix de différentes opérations en parallèle
const operations = [
// Health checks
...Array(5).fill().map(() => ({ type: 'health', path: '/api/health', method: 'GET' })),
// Metrics
...Array(3).fill().map(() => ({ type: 'metrics', path: '/api/metrics', method: 'GET' })),
// Project listings
...Array(3).fill().map(() => ({ type: 'list', path: '/api/projects', method: 'GET' })),
// Project creation
...Array(5).fill().map((_, i) => ({
type: 'create',
path: '/api/projects',
method: 'POST',
data: { name: `Mixed Op Project ${i}`, description: 'Mixed operations test' }
}))
];
const promises = operations.map(op =>
makeRequest({
hostname: 'localhost',
port: testPort,
path: op.path,
method: op.method,
headers: op.data ? { 'Content-Type': 'application/json' } : {}
}, op.data)
);
const results = await Promise.allSettled(promises);
// Analyser les résultats par type
const resultsByType = {};
results.forEach((result, index) => {
const type = operations[index].type;
if (!resultsByType[type]) resultsByType[type] = { success: 0, total: 0 };
resultsByType[type].total++;
if (result.status === 'fulfilled' && [200, 201].includes(result.value.statusCode)) {
resultsByType[type].success++;
}
});
// Vérifier que chaque type d'opération a un bon taux de succès
Object.entries(resultsByType).forEach(([type, stats]) => {
const successRate = (stats.success / stats.total) * 100;
console.log(`📊 ${type}: ${successRate.toFixed(1)}% success (${stats.success}/${stats.total})`);
assert.ok(successRate >= 80, `${type} success rate too low: ${successRate}%`);
});
});
});
});
console.log('📊 Tests Paramètres Edge Cases - Validation limites et performance');