Major Changes: - Moved legacy system to Legacy/ folder for archival - Built new modular architecture with strict separation of concerns - Created core system: Module, EventBus, ModuleLoader, Router - Added Application bootstrap with auto-start functionality - Implemented development server with ES6 modules support - Created comprehensive documentation and project context - Converted SBS-7-8 content to JSON format - Copied all legacy games and content to new structure New Architecture Features: - Sealed modules with WeakMap private data - Strict dependency injection system - Event-driven communication only - Inviolable responsibility patterns - Auto-initialization without commands - Component-based UI foundation ready Technical Stack: - Vanilla JS/HTML/CSS only - ES6 modules with proper imports/exports - HTTP development server (no file:// protocol) - Modular CSS with component scoping - Comprehensive error handling and debugging Ready for Phase 2: Converting legacy modules to new architecture 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
443 lines
18 KiB
JavaScript
443 lines
18 KiB
JavaScript
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { createMockDOM, cleanupMockDOM, createLogCapture, delay } from '../utils/test-helpers.js';
|
|
|
|
// Tests de stress et edge cases d'intégration
|
|
describe('Tests de Stress et Edge Cases d\'Intégration', () => {
|
|
let logCapture;
|
|
|
|
// Helper pour faire des requêtes HTTP réelles
|
|
async function makeRequest(path, options = {}) {
|
|
const fetch = (await import('node-fetch')).default;
|
|
const url = `http://localhost:8083${path}`;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
timeout: options.timeout || 5000,
|
|
...options
|
|
});
|
|
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
data: response.ok ? await response.text() : await response.text(),
|
|
headers: Object.fromEntries(response.headers.entries())
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
ok: false,
|
|
error: error.message,
|
|
timeout: error.code === 'TIMEOUT'
|
|
};
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
createMockDOM();
|
|
logCapture = createLogCapture();
|
|
});
|
|
|
|
afterEach(() => {
|
|
logCapture.restore();
|
|
cleanupMockDOM();
|
|
});
|
|
|
|
describe('Tests de Charge du Proxy', () => {
|
|
test('devrait gérer 100 requêtes simultanées', async () => {
|
|
const concurrentRequests = 100;
|
|
const promises = [];
|
|
|
|
console.log(`🔥 Lancement de ${concurrentRequests} requêtes simultanées...`);
|
|
|
|
for (let i = 0; i < concurrentRequests; i++) {
|
|
promises.push(makeRequest('/do-proxy/sbs-level-7-8-new.json', {
|
|
timeout: 10000 // Timeout plus long pour la charge
|
|
}));
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
const results = await Promise.allSettled(promises);
|
|
const endTime = Date.now();
|
|
|
|
const successful = results.filter(r =>
|
|
r.status === 'fulfilled' && r.value.ok
|
|
).length;
|
|
|
|
const failed = results.filter(r =>
|
|
r.status === 'rejected' || !r.value.ok
|
|
).length;
|
|
|
|
const timeouts = results.filter(r =>
|
|
r.status === 'fulfilled' && r.value.timeout
|
|
).length;
|
|
|
|
console.log(`✅ Résultats: ${successful} succès, ${failed} échecs, ${timeouts} timeouts`);
|
|
console.log(`⏱️ Temps total: ${endTime - startTime}ms`);
|
|
console.log(`📊 Moyenne: ${(endTime - startTime) / concurrentRequests}ms par requête`);
|
|
|
|
// Au moins 80% de succès attendu
|
|
assert.ok(successful >= concurrentRequests * 0.8,
|
|
`Taux de succès trop faible: ${successful}/${concurrentRequests}`);
|
|
|
|
// Temps total raisonnable (moins de 30 secondes)
|
|
assert.ok(endTime - startTime < 30000,
|
|
`Temps total trop long: ${endTime - startTime}ms`);
|
|
});
|
|
|
|
test('devrait gérer les requêtes avec différentes tailles de payload', async () => {
|
|
const requests = [
|
|
'/do-proxy/sbs-level-7-8-new.json', // ~9KB
|
|
'/do-proxy/english-class-demo.json', // ~12KB
|
|
'/do-proxy/nonexistent-small.json', // 404
|
|
'/do-proxy/nonexistent-large.json' // 404
|
|
];
|
|
|
|
const results = await Promise.all(
|
|
requests.map(path => makeRequest(path))
|
|
);
|
|
|
|
// Vérifier que les différentes tailles sont gérées
|
|
const validResponses = results.filter(r => r.ok);
|
|
assert.ok(validResponses.length >= 2, 'Should handle multiple payload sizes');
|
|
|
|
// Vérifier que les 404 sont correctement gérées
|
|
const notFoundResponses = results.filter(r => !r.ok && r.status === 404);
|
|
assert.ok(notFoundResponses.length >= 0, 'Should handle 404s gracefully');
|
|
});
|
|
|
|
test('devrait maintenir les performances sous charge continue', async () => {
|
|
const duration = 10000; // 10 secondes
|
|
const requestInterval = 100; // Une requête toutes les 100ms
|
|
|
|
console.log('🔄 Test de charge continue pendant 10 secondes...');
|
|
|
|
const startTime = Date.now();
|
|
const results = [];
|
|
let requestCount = 0;
|
|
|
|
while (Date.now() - startTime < duration) {
|
|
const requestStart = Date.now();
|
|
|
|
try {
|
|
const result = await makeRequest('/do-proxy/sbs-level-7-8-new.json', {
|
|
timeout: 2000
|
|
});
|
|
|
|
const requestTime = Date.now() - requestStart;
|
|
results.push({
|
|
success: result.ok,
|
|
time: requestTime,
|
|
requestNumber: requestCount
|
|
});
|
|
|
|
} catch (error) {
|
|
results.push({
|
|
success: false,
|
|
time: Date.now() - requestStart,
|
|
error: error.message,
|
|
requestNumber: requestCount
|
|
});
|
|
}
|
|
|
|
requestCount++;
|
|
await delay(requestInterval);
|
|
}
|
|
|
|
const totalTime = Date.now() - startTime;
|
|
const successfulRequests = results.filter(r => r.success).length;
|
|
const avgResponseTime = results
|
|
.filter(r => r.success)
|
|
.reduce((sum, r) => sum + r.time, 0) / successfulRequests;
|
|
|
|
console.log(`📊 Résultats charge continue:`);
|
|
console.log(` • Durée totale: ${totalTime}ms`);
|
|
console.log(` • Requêtes totales: ${requestCount}`);
|
|
console.log(` • Requêtes réussies: ${successfulRequests}`);
|
|
console.log(` • Taux de succès: ${(successfulRequests/requestCount*100).toFixed(1)}%`);
|
|
console.log(` • Temps de réponse moyen: ${avgResponseTime.toFixed(1)}ms`);
|
|
|
|
// Assertions de performance
|
|
assert.ok(successfulRequests / requestCount >= 0.9, 'Au moins 90% de succès');
|
|
assert.ok(avgResponseTime < 1000, 'Temps de réponse moyen < 1s');
|
|
});
|
|
});
|
|
|
|
describe('Tests de Robustesse Réseau', () => {
|
|
test('devrait gérer les interruptions de connexion', async () => {
|
|
// Simuler des requêtes rapides qui pourraient être interrompues
|
|
const rapidRequests = Array(20).fill().map((_, i) =>
|
|
makeRequest('/do-proxy/sbs-level-7-8-new.json', {
|
|
timeout: 100 + i * 10 // Timeouts variables
|
|
})
|
|
);
|
|
|
|
const results = await Promise.allSettled(rapidRequests);
|
|
|
|
// Compter les différents types de résultats
|
|
const successful = results.filter(r =>
|
|
r.status === 'fulfilled' && r.value.ok
|
|
).length;
|
|
|
|
const timeouts = results.filter(r =>
|
|
r.status === 'fulfilled' && r.value.timeout
|
|
).length;
|
|
|
|
console.log(`🌐 Requêtes rapides: ${successful} succès, ${timeouts} timeouts`);
|
|
|
|
// Même avec des timeouts courts, certaines requêtes devraient passer
|
|
assert.ok(successful > 0, 'Au moins quelques requêtes devraient réussir');
|
|
});
|
|
|
|
test('devrait récupérer après des erreurs temporaires', async () => {
|
|
// Tester la récupération en faisant plusieurs tentatives
|
|
let consecutiveSuccesses = 0;
|
|
const maxAttempts = 10;
|
|
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
const result = await makeRequest('/do-proxy/sbs-level-7-8-new.json');
|
|
|
|
if (result.ok) {
|
|
consecutiveSuccesses++;
|
|
} else {
|
|
consecutiveSuccesses = 0;
|
|
console.log(`⚠️ Échec temporaire (tentative ${i + 1}): ${result.error || result.status}`);
|
|
}
|
|
|
|
// Si on a 3 succès consécutifs, le système s'est récupéré
|
|
if (consecutiveSuccesses >= 3) {
|
|
console.log(`✅ Récupération réussie après ${i + 1} tentatives`);
|
|
break;
|
|
}
|
|
|
|
await delay(500); // Attendre entre les tentatives
|
|
}
|
|
|
|
assert.ok(consecutiveSuccesses >= 3, 'Le système devrait pouvoir se récupérer');
|
|
});
|
|
});
|
|
|
|
describe('Tests de Limites Mémoire', () => {
|
|
test('devrait gérer des réponses très volumineuses', async () => {
|
|
// Tester avec un fichier qui pourrait être volumineux
|
|
const result = await makeRequest('/do-proxy/sbs-level-7-8-new.json');
|
|
|
|
if (result.ok) {
|
|
const dataSize = result.data.length;
|
|
console.log(`📏 Taille des données reçues: ${(dataSize/1024).toFixed(1)}KB`);
|
|
|
|
// Vérifier que même de gros fichiers sont gérés
|
|
assert.ok(dataSize > 0, 'Should receive data');
|
|
assert.ok(dataSize < 10 * 1024 * 1024, 'Should not exceed 10MB'); // Limite raisonnable
|
|
|
|
// Vérifier que c'est du JSON valide
|
|
try {
|
|
JSON.parse(result.data);
|
|
} catch (error) {
|
|
assert.fail('Les données devraient être du JSON valide');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('devrait nettoyer la mémoire entre les requêtes', async () => {
|
|
// Faire plusieurs requêtes séquentielles pour tester le nettoyage
|
|
const requests = 20;
|
|
let maxMemoryUsage = 0;
|
|
|
|
for (let i = 0; i < requests; i++) {
|
|
const result = await makeRequest('/do-proxy/sbs-level-7-8-new.json');
|
|
|
|
if (result.ok) {
|
|
const currentUsage = process.memoryUsage().heapUsed;
|
|
maxMemoryUsage = Math.max(maxMemoryUsage, currentUsage);
|
|
}
|
|
|
|
// Petite pause pour permettre le garbage collection
|
|
if (i % 5 === 0) {
|
|
await delay(10);
|
|
}
|
|
}
|
|
|
|
console.log(`🧠 Utilisation mémoire maximale: ${(maxMemoryUsage/1024/1024).toFixed(1)}MB`);
|
|
|
|
// La mémoire ne devrait pas exploser (limite arbitraire de 500MB)
|
|
assert.ok(maxMemoryUsage < 500 * 1024 * 1024, 'Memory usage should stay reasonable');
|
|
});
|
|
});
|
|
|
|
describe('Tests de Sécurité Edge Cases', () => {
|
|
test('devrait rejeter les tentatives d\'injection de path', async () => {
|
|
const maliciousPaths = [
|
|
'/do-proxy/../../../etc/passwd',
|
|
'/do-proxy/..\\\\..\\\\..\\\\windows\\\\system32\\\\config\\\\sam',
|
|
'/do-proxy/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd',
|
|
'/do-proxy/....//....//....//etc//passwd',
|
|
'/do-proxy/\\x2e\\x2e\\x2f\\x2e\\x2e\\x2f\\x2e\\x2e\\x2fetc\\x2fpasswd'
|
|
];
|
|
|
|
for (const path of maliciousPaths) {
|
|
const result = await makeRequest(path);
|
|
|
|
// Ces requêtes devraient échouer (404 ou 403)
|
|
assert.ok(!result.ok || result.status === 404 || result.status === 403,
|
|
`Path injection should be blocked: ${path}`);
|
|
|
|
// Ne devrait pas retourner de contenu système
|
|
if (result.data) {
|
|
assert.ok(!result.data.includes('root:'), 'Should not expose system files');
|
|
assert.ok(!result.data.includes('Administrator'), 'Should not expose system files');
|
|
}
|
|
}
|
|
});
|
|
|
|
test('devrait limiter la taille des requêtes', async () => {
|
|
// Tenter d'envoyer une requête avec des headers très longs
|
|
const longHeader = 'x'.repeat(10000);
|
|
|
|
try {
|
|
const result = await makeRequest('/do-proxy/test.json', {
|
|
headers: {
|
|
'X-Very-Long-Header': longHeader
|
|
}
|
|
});
|
|
|
|
// La requête peut échouer (ce qui est bien) ou réussir en ignorant l'header
|
|
if (!result.ok) {
|
|
assert.ok([400, 413, 431].includes(result.status),
|
|
'Should reject oversized headers appropriately');
|
|
}
|
|
} catch (error) {
|
|
// C'est acceptable que ça lève une exception
|
|
assert.ok(error.message.includes('header') ||
|
|
error.message.includes('too large') ||
|
|
error.message.includes('ECONNRESET'));
|
|
}
|
|
});
|
|
|
|
test('devrait gérer les caractères spéciaux dans les URLs', async () => {
|
|
const specialCharPaths = [
|
|
'/do-proxy/file with spaces.json',
|
|
'/do-proxy/file%20with%20encoded%20spaces.json',
|
|
'/do-proxy/file+with+plus.json',
|
|
'/do-proxy/файл-на-русском.json',
|
|
'/do-proxy/文件中文.json',
|
|
'/do-proxy/file&with&ersands.json',
|
|
'/do-proxy/file?with?questions.json'
|
|
];
|
|
|
|
for (const path of specialCharPaths) {
|
|
const result = await makeRequest(path);
|
|
|
|
// Ces requêtes peuvent échouer (404) mais ne devraient pas crasher le serveur
|
|
assert.ok(typeof result.status === 'number',
|
|
`Should handle special characters gracefully: ${path}`);
|
|
|
|
// Le serveur devrait toujours répondre
|
|
assert.ok(result.status >= 200 && result.status < 600,
|
|
'Should return valid HTTP status');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Tests de Compatibilité', () => {
|
|
test('devrait fonctionner avec différents User-Agents', async () => {
|
|
const userAgents = [
|
|
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/91.0.4472.124',
|
|
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Safari/537.36',
|
|
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 Firefox/89.0',
|
|
'Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) Mobile/15E148',
|
|
'curl/7.68.0',
|
|
'node-fetch/1.0 (+https://github.com/bitinn/node-fetch)',
|
|
'PostmanRuntime/7.28.0'
|
|
];
|
|
|
|
for (const userAgent of userAgents) {
|
|
const result = await makeRequest('/do-proxy/sbs-level-7-8-new.json', {
|
|
headers: {
|
|
'User-Agent': userAgent
|
|
}
|
|
});
|
|
|
|
// Toutes les requêtes valides devraient fonctionner indépendamment du User-Agent
|
|
if (result.ok) {
|
|
assert.ok(result.data.length > 0,
|
|
`Should work with User-Agent: ${userAgent.substring(0, 50)}...`);
|
|
}
|
|
}
|
|
});
|
|
|
|
test('devrait gérer différents types de Accept headers', async () => {
|
|
const acceptHeaders = [
|
|
'application/json',
|
|
'application/json, text/plain, */*',
|
|
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
'*/*',
|
|
'application/json;charset=utf-8',
|
|
'text/plain'
|
|
];
|
|
|
|
for (const accept of acceptHeaders) {
|
|
const result = await makeRequest('/do-proxy/sbs-level-7-8-new.json', {
|
|
headers: {
|
|
'Accept': accept
|
|
}
|
|
});
|
|
|
|
// Les requêtes JSON devraient fonctionner avec tous les Accept headers raisonnables
|
|
if (result.ok) {
|
|
try {
|
|
JSON.parse(result.data);
|
|
} catch (error) {
|
|
assert.fail(`Should return valid JSON regardless of Accept header: ${accept}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Tests de Récupération d\'Erreurs', () => {
|
|
test('devrait récupérer après des erreurs en série', async () => {
|
|
// Faire plusieurs requêtes vers des ressources inexistantes
|
|
const badRequests = Array(5).fill().map((_, i) =>
|
|
makeRequest(`/do-proxy/nonexistent-${i}.json`)
|
|
);
|
|
|
|
const badResults = await Promise.all(badRequests);
|
|
|
|
// Toutes devraient échouer
|
|
assert.ok(badResults.every(r => !r.ok), 'Bad requests should fail');
|
|
|
|
// Puis tester qu'une bonne requête fonctionne encore
|
|
await delay(100); // Petite pause
|
|
|
|
const goodResult = await makeRequest('/do-proxy/sbs-level-7-8-new.json');
|
|
|
|
if (goodResult.ok) {
|
|
assert.ok(goodResult.data.length > 0,
|
|
'Le serveur devrait récupérer après des erreurs');
|
|
} else {
|
|
console.log('⚠️ Le serveur peut être temporairement indisponible');
|
|
}
|
|
});
|
|
|
|
test('devrait maintenir les connexions après des timeouts', async () => {
|
|
// Faire des requêtes avec des timeouts très courts
|
|
const shortTimeoutRequests = Array(3).fill().map(() =>
|
|
makeRequest('/do-proxy/sbs-level-7-8-new.json', {
|
|
timeout: 1 // 1ms - presque garanti de timeout
|
|
})
|
|
);
|
|
|
|
await Promise.allSettled(shortTimeoutRequests);
|
|
|
|
// Puis tester avec un timeout normal
|
|
const normalResult = await makeRequest('/do-proxy/sbs-level-7-8-new.json', {
|
|
timeout: 5000
|
|
});
|
|
|
|
if (normalResult.ok) {
|
|
assert.ok(normalResult.data.length > 0,
|
|
'Les connexions devraient être maintenues après des timeouts');
|
|
}
|
|
});
|
|
});
|
|
}); |