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>
282 lines
11 KiB
JavaScript
282 lines
11 KiB
JavaScript
import { test, describe, beforeEach, afterEach } from 'node:test';
|
|
import { strict as assert } from 'node:assert';
|
|
import { spawn } from 'child_process';
|
|
import { readFileSync } from 'fs';
|
|
import path from 'path';
|
|
|
|
// Test d'intégration pour le proxy DigitalOcean
|
|
describe('Proxy DigitalOcean - Tests d\'Intégration', () => {
|
|
let proxyProcess;
|
|
const proxyPort = 8083;
|
|
const proxyUrl = `http://localhost:${proxyPort}`;
|
|
|
|
// Helper pour faire des requêtes HTTP
|
|
async function makeRequest(path, options = {}) {
|
|
const fetch = (await import('node-fetch')).default;
|
|
const url = `${proxyUrl}${path}`;
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
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
|
|
};
|
|
}
|
|
}
|
|
|
|
// Helper pour attendre que le serveur soit prêt
|
|
async function waitForServer(maxAttempts = 10, delay = 1000) {
|
|
for (let i = 0; i < maxAttempts; i++) {
|
|
try {
|
|
const response = await makeRequest('/do-proxy/_list');
|
|
if (response.status) {
|
|
return true;
|
|
}
|
|
} catch (error) {
|
|
// Ignorer les erreurs de connexion
|
|
}
|
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
// Démarrer le serveur proxy si pas déjà actif
|
|
try {
|
|
const testResponse = await makeRequest('/do-proxy/_list');
|
|
if (testResponse.status) {
|
|
console.log('🟢 Serveur proxy déjà actif');
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
// Server not running, start it
|
|
}
|
|
|
|
console.log('🚀 Démarrage du serveur proxy pour les tests...');
|
|
|
|
const serverPath = path.resolve(process.cwd(), 'export_logger/websocket-server.js');
|
|
proxyProcess = spawn('node', [serverPath], {
|
|
cwd: path.dirname(serverPath),
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
detached: false
|
|
});
|
|
|
|
// Attendre que le serveur soit prêt
|
|
const isReady = await waitForServer();
|
|
if (!isReady) {
|
|
throw new Error('Impossible de démarrer le serveur proxy pour les tests');
|
|
}
|
|
|
|
console.log('✅ Serveur proxy prêt');
|
|
});
|
|
|
|
afterEach(() => {
|
|
// Ne pas arrêter le serveur car il peut être utilisé par d'autres tests
|
|
// ou l'application principale
|
|
if (proxyProcess && !proxyProcess.killed) {
|
|
// proxyProcess.kill();
|
|
}
|
|
});
|
|
|
|
describe('Endpoints du proxy', () => {
|
|
test('GET /do-proxy/sbs-level-7-8-new.json devrait retourner du JSON valide', async () => {
|
|
const response = await makeRequest('/do-proxy/sbs-level-7-8-new.json');
|
|
|
|
assert.equal(response.ok, true, `Request failed: ${response.error || response.data}`);
|
|
assert.equal(response.status, 200);
|
|
|
|
// Vérifier que c'est du JSON valide
|
|
let jsonData;
|
|
try {
|
|
jsonData = JSON.parse(response.data);
|
|
} catch (error) {
|
|
assert.fail(`Response is not valid JSON: ${error.message}`);
|
|
}
|
|
|
|
// Vérifier la structure du contenu
|
|
assert.ok(jsonData.name, 'JSON should have a name field');
|
|
assert.ok(jsonData.vocabulary, 'JSON should have a vocabulary field');
|
|
assert.equal(typeof jsonData.vocabulary, 'object');
|
|
});
|
|
|
|
test('GET /do-proxy/english-class-demo.json devrait retourner du contenu', async () => {
|
|
const response = await makeRequest('/do-proxy/english-class-demo.json');
|
|
|
|
assert.equal(response.ok, true, `Request failed: ${response.error || response.data}`);
|
|
assert.equal(response.status, 200);
|
|
|
|
let jsonData;
|
|
try {
|
|
jsonData = JSON.parse(response.data);
|
|
} catch (error) {
|
|
assert.fail(`Response is not valid JSON: ${error.message}`);
|
|
}
|
|
|
|
assert.ok(jsonData.name || jsonData.vocabulary, 'JSON should have content');
|
|
});
|
|
|
|
test('HEAD /do-proxy/sbs-level-7-8-new.json devrait retourner headers sans body', async () => {
|
|
const response = await makeRequest('/do-proxy/sbs-level-7-8-new.json', {
|
|
method: 'HEAD'
|
|
});
|
|
|
|
assert.equal(response.ok, true);
|
|
assert.equal(response.status, 200);
|
|
// HEAD request should have empty or minimal body
|
|
assert.ok(response.data.length <= 100, 'HEAD response should have minimal content');
|
|
});
|
|
|
|
test('GET /do-proxy/nonexistent.json devrait retourner 404', async () => {
|
|
const response = await makeRequest('/do-proxy/nonexistent-file-12345.json');
|
|
|
|
assert.equal(response.ok, false);
|
|
// Should be either 404 (not found) or 403 (forbidden)
|
|
assert.ok([403, 404].includes(response.status));
|
|
});
|
|
});
|
|
|
|
describe('CORS et headers', () => {
|
|
test('les réponses devrait inclure les headers CORS', async () => {
|
|
const response = await makeRequest('/do-proxy/sbs-level-7-8-new.json');
|
|
|
|
assert.equal(response.ok, true);
|
|
assert.ok(response.headers['access-control-allow-origin']);
|
|
assert.equal(response.headers['access-control-allow-origin'], '*');
|
|
});
|
|
|
|
test('OPTIONS request devrait être supportée', async () => {
|
|
const response = await makeRequest('/do-proxy/sbs-level-7-8-new.json', {
|
|
method: 'OPTIONS'
|
|
});
|
|
|
|
assert.equal(response.status, 200);
|
|
assert.ok(response.headers['access-control-allow-origin']);
|
|
assert.ok(response.headers['access-control-allow-methods']);
|
|
});
|
|
});
|
|
|
|
describe('Listing des fichiers', () => {
|
|
test('GET /do-proxy/_list devrait retourner une liste de fichiers ou une erreur 403', async () => {
|
|
const response = await makeRequest('/do-proxy/_list');
|
|
|
|
// Le listing peut échouer avec 403 (permissions insuffisantes) ce qui est attendu
|
|
if (response.status === 403) {
|
|
let jsonData;
|
|
try {
|
|
jsonData = JSON.parse(response.data);
|
|
} catch (error) {
|
|
assert.fail('403 response should be valid JSON');
|
|
}
|
|
|
|
assert.ok(jsonData.error, 'Should contain error message');
|
|
assert.ok(jsonData.knownFiles, 'Should contain known files list');
|
|
assert.ok(Array.isArray(jsonData.knownFiles));
|
|
} else if (response.status === 200) {
|
|
let jsonData;
|
|
try {
|
|
jsonData = JSON.parse(response.data);
|
|
} catch (error) {
|
|
assert.fail('200 response should be valid JSON');
|
|
}
|
|
|
|
assert.ok(jsonData.files, 'Should contain files array');
|
|
assert.ok(Array.isArray(jsonData.files));
|
|
} else {
|
|
assert.fail(`Unexpected status: ${response.status}`);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Performance et timeouts', () => {
|
|
test('les requêtes devrait répondre dans un délai raisonnable', async () => {
|
|
const startTime = Date.now();
|
|
const response = await makeRequest('/do-proxy/sbs-level-7-8-new.json');
|
|
const endTime = Date.now();
|
|
|
|
assert.equal(response.ok, true);
|
|
|
|
const responseTime = endTime - startTime;
|
|
assert.ok(responseTime < 10000, `Response time too slow: ${responseTime}ms`);
|
|
});
|
|
|
|
test('les requêtes multiples simultanées devrait fonctionner', async () => {
|
|
const promises = [
|
|
makeRequest('/do-proxy/sbs-level-7-8-new.json'),
|
|
makeRequest('/do-proxy/english-class-demo.json'),
|
|
makeRequest('/do-proxy/sbs-level-7-8-new.json') // Duplicate to test caching
|
|
];
|
|
|
|
const responses = await Promise.all(promises);
|
|
|
|
responses.forEach((response, index) => {
|
|
assert.equal(response.ok, true, `Request ${index} failed: ${response.error}`);
|
|
assert.equal(response.status, 200);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('Authentification AWS', () => {
|
|
test('les requêtes devrait inclure les headers d\'authentification AWS', async () => {
|
|
// Ce test vérifie indirectement l'authentification en s'attendant à ce que
|
|
// les requêtes réussissent, ce qui nécessite une authentification correcte
|
|
const response = await makeRequest('/do-proxy/sbs-level-7-8-new.json');
|
|
|
|
assert.equal(response.ok, true, 'Authentication should work for valid files');
|
|
assert.equal(response.status, 200);
|
|
|
|
// Si l'authentification échoue, on obtiendrait typiquement 403 Forbidden
|
|
// Le fait que nous obtenons 200 indique que l'authentification fonctionne
|
|
});
|
|
});
|
|
|
|
describe('Gestion des erreurs', () => {
|
|
test('les routes invalides devrait retourner 404', async () => {
|
|
const response = await makeRequest('/invalid-route');
|
|
|
|
assert.equal(response.ok, false);
|
|
assert.equal(response.status, 404);
|
|
});
|
|
|
|
test('les méthodes non supportées devrait retourner une erreur', async () => {
|
|
const response = await makeRequest('/do-proxy/sbs-level-7-8-new.json', {
|
|
method: 'PUT'
|
|
});
|
|
|
|
// PUT n'est pas supporté, devrait retourner une erreur
|
|
assert.equal(response.ok, false);
|
|
});
|
|
});
|
|
|
|
describe('Intégration avec DigitalOcean Spaces', () => {
|
|
test('devrait pouvoir récupérer des fichiers réels depuis DigitalOcean', async () => {
|
|
const response = await makeRequest('/do-proxy/sbs-level-7-8-new.json');
|
|
|
|
if (response.ok) {
|
|
const jsonData = JSON.parse(response.data);
|
|
|
|
// Vérifier que le contenu a la structure attendue d'un fichier de contenu
|
|
assert.ok(jsonData.name || jsonData.vocabulary, 'Should contain educational content');
|
|
|
|
if (jsonData.vocabulary) {
|
|
assert.equal(typeof jsonData.vocabulary, 'object');
|
|
assert.ok(Object.keys(jsonData.vocabulary).length > 0, 'Vocabulary should not be empty');
|
|
}
|
|
} else {
|
|
// Si la connexion à DigitalOcean échoue, au moins vérifier que l'erreur est appropriée
|
|
assert.ok([403, 404, 500].includes(response.status),
|
|
`Unexpected error status: ${response.status}`);
|
|
}
|
|
});
|
|
});
|
|
}); |