From 93315dc5a1e58b9455b10fdbd36d1b1251440f89 Mon Sep 17 00:00:00 2001 From: Steph Ponzo Date: Mon, 23 Feb 2026 18:04:20 +0100 Subject: [PATCH] Alertes_Crud_socketIO_ok ! --- .../modules/alerts/adapters/mysql.adapter.js | 149 +++++- server/modules/alerts/alerts.controller.js | 165 ++++++- server/modules/alerts/alerts.repo.js | 48 +- server/modules/alerts/alerts.router.js | 12 +- server/modules/alerts/alerts.service.js | 30 +- server/modules/alerts/index.js | 2 +- server/modules/alerts/init-alerts.js | 159 ------- server/modules/alerts/socketManager.js | 60 +-- server/modules/init-alerts.js | 29 +- server/package-lock.json | 249 ++++++++++ server/package.json | 11 +- server/test-module-complet.js | 445 ++++++++++++++++++ server/test-server.js | 121 +++++ 13 files changed, 1241 insertions(+), 239 deletions(-) delete mode 100644 server/modules/alerts/init-alerts.js create mode 100644 server/test-module-complet.js create mode 100644 server/test-server.js diff --git a/server/modules/alerts/adapters/mysql.adapter.js b/server/modules/alerts/adapters/mysql.adapter.js index a6a1e14..7236df1 100644 --- a/server/modules/alerts/adapters/mysql.adapter.js +++ b/server/modules/alerts/adapters/mysql.adapter.js @@ -171,10 +171,18 @@ export function createMySQLAdapter(dbConnection, options = {}) { }, // ===================================================== - // 5. CRÉER UNE NOUVELLE RÈGLE D'ALERTE + // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create) // ===================================================== - async createAlertRule(ruleData) { + /** + * Crée une nouvelle règle d'alerte + * @param {Object} ruleData - Données de la règle + * @returns {Promise} Règle créée avec son ID + */ + async createRule(ruleData) { try { + const ruleId = ruleData.ruleId || uuidv4(); + const now = Date.now(); + const sql = ` INSERT INTO ${tables.alerts} ( rule_id, @@ -193,28 +201,141 @@ export function createMySQLAdapter(dbConnection, options = {}) { `; const values = [ - ruleData.rule_id || uuidv4(), - ruleData.user_id, - ruleData.pair_id || null, + ruleId, + ruleData.userId, + ruleData.pairId || null, ruleData.enabled !== undefined ? ruleData.enabled : 1, - ruleData.rule_type, + ruleData.ruleType || 'SIGNAL_THRESHOLD', ruleData.severity || 'INFO', - ruleData.min_confidence || 0.75, - ruleData.channel || 'EMAIL', + ruleData.minConfidence || 0.7, + ruleData.channel || 'CONSOLE', JSON.stringify(ruleData.params || {}), - ruleData.cooldown_ms || 60000, - Date.now(), - Date.now() + ruleData.cooldownMs || 60000, + now, + now ]; await dbConnection.execute(sql, values); - console.log(`[MySQL] Règle d'alerte créée : ${ruleData.rule_type}`); + console.log(`[MySQL] Règle créée : ${ruleId}`); + + return { ruleId, ...ruleData, createdAt: now }; + + } catch (error) { + console.error('[MySQL] Erreur dans createRule:', error); + throw error; + } + }, + + // ===================================================== + // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update) + // ===================================================== + /** + * Met à jour une règle existante + * @param {string} ruleId - ID de la règle + * @param {Object} updates - Champs à mettre à jour + * @returns {Promise} true si modifié + */ + async updateRule(ruleId, updates) { + try { + // Construire la requête dynamiquement + const fields = []; + const values = []; + + if (updates.minConfidence !== undefined) { + fields.push('min_confidence = ?'); + values.push(updates.minConfidence); + } + if (updates.enabled !== undefined) { + fields.push('enabled = ?'); + values.push(updates.enabled); + } + if (updates.channel !== undefined) { + fields.push('channel = ?'); + values.push(updates.channel); + } + if (updates.severity !== undefined) { + fields.push('severity = ?'); + values.push(updates.severity); + } + if (updates.cooldownMs !== undefined) { + fields.push('cooldown_ms = ?'); + values.push(updates.cooldownMs); + } + if (updates.pairId !== undefined) { + fields.push('pair_id = ?'); + values.push(updates.pairId); + } + + // Toujours mettre à jour le timestamp + fields.push('updated_at_ms = ?'); + values.push(Date.now()); + + // Ajouter le ruleId à la fin pour le WHERE + values.push(ruleId); + + const sql = ` + UPDATE ${tables.alerts} + SET ${fields.join(', ')} + WHERE rule_id = ? + `; + + const [result] = await dbConnection.execute(sql, values); + console.log(`[MySQL] Règle ${ruleId} mise à jour`); + + return result.affectedRows > 0; } catch (error) { - console.error('[MySQL] Erreur dans createAlertRule:', error); + console.error('[MySQL] Erreur dans updateRule:', error); throw error; } - } + }, + + // ===================================================== + // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete) + // ===================================================== + /** + * Supprime une règle + * @param {string} ruleId - ID de la règle + * @returns {Promise} true si supprimé + */ + async deleteRule(ruleId) { + try { + const sql = `DELETE FROM ${tables.alerts} WHERE rule_id = ?`; + + const [result] = await dbConnection.execute(sql, [ruleId]); + console.log(`[MySQL] Règle ${ruleId} supprimée`); + + return result.affectedRows > 0; + + } catch (error) { + console.error('[MySQL] Erreur dans deleteRule:', error); + throw error; + } + }, + + // ===================================================== + // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read) + // ===================================================== + /** + * Récupère une règle par son ID + * @param {string} ruleId - ID de la règle + * @returns {Promise} La règle ou null + */ + async findRuleById(ruleId) { + try { + const sql = ` + SELECT * FROM ${tables.alerts} + WHERE rule_id = ? + `; + + const [rows] = await dbConnection.execute(sql, [ruleId]); + return rows.length > 0 ? rows[0] : null; + + } catch (error) { + console.error('[MySQL] Erreur dans findRuleById:', error); + throw error; + } + }, }; } diff --git a/server/modules/alerts/alerts.controller.js b/server/modules/alerts/alerts.controller.js index c2a495e..ea02247 100644 --- a/server/modules/alerts/alerts.controller.js +++ b/server/modules/alerts/alerts.controller.js @@ -114,7 +114,7 @@ export function createAlertsController(alertsService, alertsRepo) { // ================================================================================= // GESTION DES ERREURS // ================================================================================= - console.error('❌ Erreur dans processSignal controller:', error); + console.error(' Erreur dans processSignal controller:', error); res.status(500).json({ success: false, @@ -198,6 +198,169 @@ export function createAlertsController(alertsService, alertsRepo) { error: 'Erreur serveur' }); } + }, + + // ========================================================================================= + // CRUD + //========================================================================================== + + + // ===================================================== + // POST /api/alerts/rules - CRÉER UNE RÈGLE + // ===================================================== + /** + * Crée une nouvelle règle d'alerte + * + * Body attendu : + * { + * "userId": "uuid", + * "pairId": 1, + * "channel": "EMAIL", + * "minConfidence": 0.8, + * "severity": "WARNING", + * "cooldownMs": 60000 + * } + */ + async createRule(req, res) { + try { + const ruleData = req.body; + + // Validation des champs obligatoires + if (!ruleData.userId || !ruleData.channel) { + return res.status(400).json({ + success: false, + error: 'Champs manquants (userId, channel requis)' + }); + } + + // Appeler le repo (qui appelle l'adapter) + const newRule = await alertsRepo.createRule(ruleData); + + res.status(201).json({ + success: true, + rule: newRule, + message: 'Règle créée avec succès' + }); + + } catch (error) { + console.error('❌ Erreur createRule:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } + }, + + // ===================================================== + // PUT /api/alerts/rules/:id - MODIFIER UNE RÈGLE + // ===================================================== + /** + * Met à jour une règle existante + * + * Body possible : + * { + * "minConfidence": 0.9, + * "enabled": true, + * "channel": "CONSOLE", + * "severity": "CRITICAL" + * } + */ + async updateRule(req, res) { + try { + const { id } = req.params; + const updates = req.body; + + // Vérifier qu'il y a quelque chose à mettre à jour + if (Object.keys(updates).length === 0) { + return res.status(400).json({ + success: false, + error: 'Aucune donnée à mettre à jour' + }); + } + + // Appeler le repo (qui appelle l'adapter) + const updated = await alertsRepo.updateRule(id, updates); + + if (!updated) { + return res.status(404).json({ + success: false, + error: 'Règle non trouvée' + }); + } + + res.json({ + success: true, + message: 'Règle mise à jour' + }); + + } catch (error) { + console.error('❌ Erreur updateRule:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } + }, + + // ===================================================== + // DELETE /api/alerts/rules/:id - SUPPRIMER UNE RÈGLE + // ===================================================== + async deleteRule(req, res) { + try { + const { id } = req.params; + + // Appeler le repo (qui appelle l'adapter) + const deleted = await alertsRepo.deleteRule(id); + + if (!deleted) { + return res.status(404).json({ + success: false, + error: 'Règle non trouvée' + }); + } + + res.json({ + success: true, + message: 'Règle supprimée' + }); + + } catch (error) { + console.error('❌ Erreur deleteRule:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } + }, + + // ===================================================== + // GET /api/alerts/rules/detail/:id - DÉTAIL D'UNE RÈGLE + // ===================================================== + async getRuleById(req, res) { + try { + const { id } = req.params; + + const rule = await alertsRepo.findRuleById(id); + + if (!rule) { + return res.status(404).json({ + success: false, + error: 'Règle non trouvée' + }); + } + + res.json({ + success: true, + rule + }); + + } catch (error) { + console.error('❌ Erreur getRuleById:', error); + res.status(500).json({ + success: false, + error: error.message + }); + } } }; } diff --git a/server/modules/alerts/alerts.repo.js b/server/modules/alerts/alerts.repo.js index bf26e2a..8c0e288 100644 --- a/server/modules/alerts/alerts.repo.js +++ b/server/modules/alerts/alerts.repo.js @@ -90,14 +90,52 @@ export function createAlertsRepo(adapter) { }, // ===================================================== - // 5. CRÉER UNE NOUVELLE RÈGLE D'ALERTE + // 5. CRÉER UNE RÈGLE D'ALERTE (CRUD - Create) // ===================================================== /** - * Crée une règle d'alerte + * Crée une nouvelle règle d'alerte * @param {Object} ruleData - Données de la règle + * @returns {Promise} Règle créée */ - async createAlertRule(ruleData) { - return adapter.createAlertRule(ruleData); - } + async createRule(ruleData) { + return adapter.createRule(ruleData); + }, + + // ===================================================== + // 6. MODIFIER UNE RÈGLE D'ALERTE (CRUD - Update) + // ===================================================== + /** + * Met à jour une règle existante + * @param {string} ruleId - ID de la règle + * @param {Object} updates - Champs à mettre à jour + * @returns {Promise} true si modifié + */ + async updateRule(ruleId, updates) { + return adapter.updateRule(ruleId, updates); + }, + + // ===================================================== + // 7. SUPPRIMER UNE RÈGLE D'ALERTE (CRUD - Delete) + // ===================================================== + /** + * Supprime une règle + * @param {string} ruleId - ID de la règle + * @returns {Promise} true si supprimé + */ + async deleteRule(ruleId) { + return adapter.deleteRule(ruleId); + }, + + // ===================================================== + // 8. RÉCUPÉRER UNE RÈGLE PAR ID (CRUD - Read) + // ===================================================== + /** + * Récupère une règle par son ID + * @param {string} ruleId - ID de la règle + * @returns {Promise} La règle ou null + */ + async findRuleById(ruleId) { + return adapter.findRuleById(ruleId); + }, }; } diff --git a/server/modules/alerts/alerts.router.js b/server/modules/alerts/alerts.router.js index 8bfb49b..8280d86 100644 --- a/server/modules/alerts/alerts.router.js +++ b/server/modules/alerts/alerts.router.js @@ -26,10 +26,8 @@ export function createAlertsRouter(alertsController) { // ========================================================= // DÉFINITION DES ROUTES // ========================================================= - -// ============================================================================= // POST /api/alerts/process-signal -// ============================================================================= +// ========================================================= /** * Traiter un signal crypto et envoyer les alertes * @@ -69,6 +67,14 @@ export function createAlertsRouter(alertsController) { */ router.get('/history/:userId', alertsController.getHistory); +// ============================================================================= +// CRUD +// ============================================================================= + router.post('/rules', alertsController.createRule); + router.get('/rules/detail/:id', alertsController.getRuleById); + router.put('/rules/:id', alertsController.updateRule); + router.delete('/rules/:id', alertsController.deleteRule); + return router; } diff --git a/server/modules/alerts/alerts.service.js b/server/modules/alerts/alerts.service.js index c8d541e..4280614 100644 --- a/server/modules/alerts/alerts.service.js +++ b/server/modules/alerts/alerts.service.js @@ -69,7 +69,7 @@ export function createAlertsService(alertsRepo, options = {}) { * @returns {boolean} true si l'alerte doit être envoyée */ shouldSendAlert(rule, signal) { - console.log(`🔍 Vérification règle ${rule.rule_id}...`); + console.log(`Vérification règle ${rule.rule_id}...`); // ───────────────────────────────────────────────── // CRITÈRE 1 : Vérifier la confidence minimum @@ -114,7 +114,7 @@ export function createAlertsService(alertsRepo, options = {}) { // ───────────────────────────────────────────────── // TOUS LES CRITÈRES SONT OK ! // ───────────────────────────────────────────────── - console.log(` ✅ Tous les critères sont remplis`); + console.log(` Tous les critères sont remplis`); return true; }, @@ -134,7 +134,7 @@ export function createAlertsService(alertsRepo, options = {}) { * @returns {Promise} 'SENT' ou 'FAILED' */ async sendViaChannel(rule, signal) { - console.log(`📡 Envoi via ${rule.channel}...`); + console.log(`Envoi via ${rule.channel}...`); let status = 'SENT'; @@ -159,7 +159,7 @@ export function createAlertsService(alertsRepo, options = {}) { const chatId = params.chatId; if (!chatId) { - console.error('❌ Pas de chatId dans params'); + console.error(' Pas de chatId dans params'); status = 'FAILED'; break; } @@ -173,7 +173,7 @@ export function createAlertsService(alertsRepo, options = {}) { const webhookUrl = discordParams.webhookUrl; if (!webhookUrl) { - console.error('❌ Pas de webhookUrl dans params'); + console.error(' Pas de webhookUrl dans params'); status = 'FAILED'; break; } @@ -195,7 +195,7 @@ export function createAlertsService(alertsRepo, options = {}) { // ── CANAL INCONNU ──────────────────────── default: - console.warn(`⚠️ Canal inconnu: ${rule.channel}`); + console.warn(` Canal inconnu: ${rule.channel}`); status = 'FAILED'; } @@ -203,7 +203,7 @@ export function createAlertsService(alertsRepo, options = {}) { // ───────────────────────────────────────────── // GESTION DES ERREURS // ───────────────────────────────────────────── - console.error(`❌ Erreur lors de l'envoi via ${rule.channel}:`); + console.error(` Erreur lors de l'envoi via ${rule.channel}:`); console.error(` ${error.message}`); status = 'FAILED'; } @@ -235,7 +235,7 @@ export function createAlertsService(alertsRepo, options = {}) { */ async processSignal(signal) { console.log('\n' + '═'.repeat(80)); - console.log(`🔔 TRAITEMENT DU SIGNAL`); + console.log(` TRAITEMENT DU SIGNAL`); console.log(` Action : ${signal.action}`); console.log(` Paire : ${signal.pair}`); console.log(` User : ${signal.userId}`); @@ -252,12 +252,12 @@ export function createAlertsService(alertsRepo, options = {}) { ); if (rules.length === 0) { - console.log('⚠️ Aucune règle active trouvée pour ce signal'); + console.log(' Aucune règle active trouvée pour ce signal'); console.log(' → Vérifie que des règles sont configurées en DB\n'); return; } - console.log(`📋 ${rules.length} règle(s) trouvée(s)\n`); + console.log(` ${rules.length} règle(s) trouvée(s)\n`); // ───────────────────────────────────────────── // ÉTAPE 2 : Traiter chaque règle @@ -269,7 +269,7 @@ export function createAlertsService(alertsRepo, options = {}) { // Vérifier si on doit envoyer if (!this.shouldSendAlert(rule, signal)) { - console.log(`└─ ❌ Alerte NON envoyée\n`); + console.log(`└─ Alerte NON envoyée\n`); continue; } @@ -298,9 +298,9 @@ export function createAlertsService(alertsRepo, options = {}) { if (status === 'SENT') { await alertsRepo.updateLastNotified(rule.rule_id, Date.now()); sentCount++; - console.log(`└─ ✅ Alerte envoyée avec succès\n`); + console.log(`└─ Alerte envoyée avec succès\n`); } else { - console.log(`└─ ❌ Échec de l'envoi\n`); + console.log(`└─ Échec de l'envoi\n`); } } @@ -308,13 +308,13 @@ export function createAlertsService(alertsRepo, options = {}) { // RÉSUMÉ FINAL // ───────────────────────────────────────────── console.log('═'.repeat(80)); - console.log(`📊 RÉSUMÉ`); + console.log(` RÉSUMÉ`); console.log(` Règles vérifiées : ${rules.length}`); console.log(` Alertes envoyées : ${sentCount}`); console.log('═'.repeat(80) + '\n'); } catch (error) { - console.error('❌ ERREUR CRITIQUE dans processSignal:'); + console.error(' ERREUR CRITIQUE dans processSignal:'); console.error(error); throw error; // Remonter l'erreur pour que le controller puisse la gérer } diff --git a/server/modules/alerts/index.js b/server/modules/alerts/index.js index f9bc531..18f74ca 100644 --- a/server/modules/alerts/index.js +++ b/server/modules/alerts/index.js @@ -18,7 +18,7 @@ export { broadcastAlert, isUserConnected, getConnectedUsersCount, - getI + getIO } from './socketManager.js'; // Export des canaux (utiles si on veut les tester séparément) diff --git a/server/modules/alerts/init-alerts.js b/server/modules/alerts/init-alerts.js deleted file mode 100644 index 42407ac..0000000 --- a/server/modules/alerts/init-alerts.js +++ /dev/null @@ -1,159 +0,0 @@ -// ========================================================= -// INITIALISATION DU MODULE ALERTS - VERSION AVEC ADAPTER -// ========================================================= -// Ce fichier montre comment initialiser et utiliser le module d'alertes dans Wall-e-tte -// ========================================================= - -import db from '../config/db.js'; -import { createMySQLAdapter } from './alerts/adapters/mysql.adapter.js'; -import { - createAlertsRepo, - createAlertsService, - createAlertsController, - createAlertsRouter -} from './alerts/index.js'; - -// ========================================================= -// FONCTION D'INITIALISATION -// ========================================================= -/** - * Initialise tout le module d'alertes - * - * @param {Object} adapter - Adapter de base de données (MySQL, MongoDB, etc.) - * @param {Object} options - Options de configuration - * @returns {Object} { adapter, repo, service, controller, router } - * - * @example - * // Avec MySQL (défaut pour Wall-e-tte) - * const alerts = initializeAlertsModule(); - * - * @example - * // Avec un adapter personnalisé (autre projet) - * const mongoAdapter = createMongoAdapter(mongoClient); - * const alerts = initializeAlertsModule(mongoAdapter); - */ -export function initializeAlertsModule(adapter = null, options = {}) { - - console.log('\n' + '═'.repeat(80)); - console.log('INITIALISATION DU MODULE ALERTS (avec Adapter Pattern)'); - console.log('═'.repeat(80) + '\n'); - - // ───────────────────────────────────────────────────── - // ÉTAPE 1 : Créer l'Adapter (si non fourni) - // ───────────────────────────────────────────────────── - // Par défaut, on utilise MySQL pour Wall-e-tte - // Mais un autre projet peut injecter son propre adapter - if (!adapter) { - console.log("Création de l'adapter MySQL par défaut..."); - adapter = createMySQLAdapter(db, { - alertsTable: options.alertsTable || 'alert_rules', - usersTable: options.usersTable || 'users', - eventsTable: options.eventsTable || 'alert_events' - }); - } else { - console.log('📦 Utilisation de l\'adapter fourni'); - } - - // ───────────────────────────────────────────────────── - // ÉTAPE 2 : Créer le Repository - // ───────────────────────────────────────────────────── - // Le repository utilise l'adapter - const alertsRepo = createAlertsRepo(adapter); - - // ───────────────────────────────────────────────────── - // ÉTAPE 3 : Créer le Service - // ───────────────────────────────────────────────────── - // Le service contient la logique métier - const alertsService = createAlertsService(alertsRepo, { - defaultCooldown: options.defaultCooldown || 60000 - }); - - // ───────────────────────────────────────────────────── - // ÉTAPE 4 : Créer le Controller - // ───────────────────────────────────────────────────── - // Le controller gère les requêtes HTTP - const alertsController = createAlertsController(alertsService, alertsRepo); - - // ───────────────────────────────────────────────────── - // ÉTAPE 5 : Créer le Router - // ───────────────────────────────────────────────────── - // Le router définit les routes API - const alertsRouter = createAlertsRouter(alertsController); - - console.log('✅ Module Alerts complètement initialisé !'); - console.log('═'.repeat(80) + '\n'); - - // ───────────────────────────────────────────────────── - // RETOURNER TOUT - // ───────────────────────────────────────────────────── - return { - adapter: adapter, - repo: alertsRepo, - service: alertsService, - controller: alertsController, - router: alertsRouter - }; -} - -// ========================================================= -// EXPORT PAR DÉFAUT -// ========================================================= -// Pour Wall-e-tte, initialiser avec la DB MySQL -export default initializeAlertsModule(); - -// ========================================================= -// EXEMPLE D'UTILISATION -// ========================================================= -/* - ======================================================== - UTILISATION DANS WALL-E-TTE - ======================================================== - - // Option 1 : Import par défaut (MySQL automatique) - import alerts from './modules/init-alerts.js'; - - app.use('/api/alerts', alerts.router); - await alerts.service.processSignal(signal); - - - // Option 2 : Configuration personnalisée - import { initializeAlertsModule } from './modules/init-alerts.js'; - - const alerts = initializeAlertsModule(null, { - alertsTable: 'mes_alertes', - defaultCooldown: 120000 // 2 minutes - }); - - ========================================================= - UTILISATION DANS UN AUTRE PROJET (MySQL) - ========================================================= - - import theirDb from './their-db-config.js'; - import { createMySQLAdapter } from '@wall-e-tte/alerts/adapters/mysql.adapter.js'; - import { initializeAlertsModule } from '@wall-e-tte/alerts'; - - // Créer leur adapter avec leur DB - const adapter = createMySQLAdapter(theirDb, { - alertsTable: 'notifications', - usersTable: 'accounts' - }); - - // Initialiser avec leur adapter - const alerts = initializeAlertsModule(adapter); - - app.use('/api/notifications', alerts.router); - - ========================================================= - UTILISATION AVEC MONGODB (autre projet) - ========================================================= - - import { MongoClient } from 'mongodb'; - import { createMongoAdapter } from './adapters/mongo.adapter.js'; // À créer - import { initializeAlertsModule } from '@wall-e-tte/alerts'; - - const mongoClient = new MongoClient(uri); - const adapter = createMongoAdapter(mongoClient.db('alerts')); - - const alerts = initializeAlertsModule(adapter); - // aucune modification du repo/service/controller/router -*/ diff --git a/server/modules/alerts/socketManager.js b/server/modules/alerts/socketManager.js index 576feb3..ef6ff14 100644 --- a/server/modules/alerts/socketManager.js +++ b/server/modules/alerts/socketManager.js @@ -4,14 +4,14 @@ // RÔLE : Centraliser la gestion des connexions Socket.IO // pour le module d'alertes // ========================================================= -// UTILISÉ PAR : web.js (canal de notification web) +// UTILISÉ PAR : web.js // ========================================================= import { Server } from 'socket.io'; -// ───────────────────────────────────────────────────────── +// ========================================================= // STOCKAGE DES CONNEXIONS -// ───────────────────────────────────────────────────────── +// ========================================================= // Map : userId → socket // Permet d'envoyer une alerte à un utilisateur spécifique const userSockets = new Map(); @@ -19,26 +19,26 @@ const userSockets = new Map(); // Instance du serveur Socket.IO let io = null; -// ───────────────────────────────────────────────────────── +// ========================================================= // INITIALISATION DU SERVEUR SOCKET.IO -// ───────────────────────────────────────────────────────── +// ========================================================= /** * Initialise Socket.IO sur un serveur HTTP existant - * + * * @param {Object} httpServer - Serveur HTTP (Express) * @param {Object} options - Options Socket.IO (optionnel) * @returns {Server} Instance Socket.IO - * + * * @example - * // Dans le fichier principal du serveur (app.js ou index.js) + * // Dans le fichier principal du serveur (index.js) * import express from 'express'; * import { createServer } from 'http'; * import { initSocketIO } from './modules/alerts/socketManager.js'; - * + * * const app = express(); * const httpServer = createServer(app); * const io = initSocketIO(httpServer); - * + * * httpServer.listen(3000); */ export function initSocketIO(httpServer, options = {}) { @@ -56,7 +56,7 @@ export function initSocketIO(httpServer, options = {}) { // GESTION DES CONNEXIONS // ───────────────────────────────────────────────────── io.on('connection', (socket) => { - console.log(`🔌 [Socket.IO] Nouvelle connexion : ${socket.id}`); + console.log(` [Socket.IO] Nouvelle connexion : ${socket.id}`); // ───────────────────────────────────────────────── // EVENT : Authentification de l'utilisateur @@ -64,7 +64,7 @@ export function initSocketIO(httpServer, options = {}) { // Le client doit envoyer son userId après connexion socket.on('auth', (userId) => { if (!userId) { - console.log(`⚠️ [Socket.IO] Auth sans userId (socket: ${socket.id})`); + console.log(` [Socket.IO] Auth sans userId (socket: ${socket.id})`); return; } @@ -72,13 +72,13 @@ export function initSocketIO(httpServer, options = {}) { userSockets.set(userId, socket); socket.userId = userId; // Garder une référence sur le socket - console.log(`✅ [Socket.IO] Utilisateur authentifié : ${userId}`); + console.log(` [Socket.IO] Utilisateur authentifié : ${userId}`); console.log(` Utilisateurs connectés : ${userSockets.size}`); // Confirmer l'authentification au client - socket.emit('auth_success', { - userId, - message: 'Connecté au système d\'alertes' + socket.emit('auth_success', { + userId, + message: 'Connecté au système d\'alertes' }); }); @@ -97,24 +97,24 @@ export function initSocketIO(httpServer, options = {}) { // EVENT : Ping (test de connexion) // ───────────────────────────────────────────────── socket.on('ping_alerts', () => { - socket.emit('pong_alerts', { + socket.emit('pong_alerts', { timestamp: Date.now(), - status: 'ok' + status: 'ok' }); }); }); - console.log('✅ [Socket.IO] Serveur initialisé'); + console.log(' [Socket.IO] Serveur initialisé'); return io; } -// ───────────────────────────────────────────────────────── +// ========================================================= // FONCTIONS D'ENVOI D'ALERTES -// ───────────────────────────────────────────────────────── +// ========================================================= /** * Envoie une alerte à un utilisateur spécifique - * + * * @param {string} userId - ID de l'utilisateur cible * @param {Object} alertData - Données de l'alerte * @returns {boolean} true si envoyé, false si utilisateur non connecté @@ -127,23 +127,23 @@ export function sendAlertToUser(userId, alertData) { ...alertData, timestamp: Date.now() }); - console.log(`📤 [Socket.IO] Alerte envoyée à ${userId}`); + console.log(` [Socket.IO] Alerte envoyée à ${userId}`); return true; } - console.log(`⚠️ [Socket.IO] Utilisateur ${userId} non connecté`); + console.log(` [Socket.IO] Utilisateur ${userId} non connecté`); return false; } /** * Envoie une alerte à tous les utilisateurs connectés - * + * * @param {Object} alertData - Données de l'alerte * @returns {number} Nombre d'utilisateurs notifiés */ export function broadcastAlert(alertData) { if (!io) { - console.error('❌ [Socket.IO] Serveur non initialisé'); + console.error(' [Socket.IO] Serveur non initialisé'); return 0; } @@ -152,17 +152,17 @@ export function broadcastAlert(alertData) { timestamp: Date.now() }); - console.log(`📢 [Socket.IO] Alerte broadcast à ${userSockets.size} utilisateurs`); + console.log(` [Socket.IO] Alerte broadcast à ${userSockets.size} utilisateurs`); return userSockets.size; } -// ───────────────────────────────────────────────────────── +// ========================================================= // FONCTIONS UTILITAIRES -// ───────────────────────────────────────────────────────── +// ========================================================= /** * Vérifie si un utilisateur est connecté - * @param {string} userId + * @param {string} userId * @returns {boolean} */ export function isUserConnected(userId) { diff --git a/server/modules/init-alerts.js b/server/modules/init-alerts.js index 1956182..4f927b9 100644 --- a/server/modules/init-alerts.js +++ b/server/modules/init-alerts.js @@ -7,10 +7,11 @@ import db from '../config/db.js'; import { + createAlertsController, createAlertsRepo, + createAlertsRouter, createAlertsService, - createAlertsController, - createAlertsRouter + createMySQLAdapter } from './alerts/index.js'; // ========================================================= @@ -30,17 +31,27 @@ export function initializeAlertsModule(dbConnection = db, options = {}) { console.log('═'.repeat(80) + '\n'); // ───────────────────────────────────────────────────── - // ÉTAPE 1 : Créer le Repository + // ÉTAPE 1 : Créer l'Adapter MySQL + // ───────────────────────────────────────────────────── + // L'adapter contient les requêtes SQL spécifiques + const adapter = createMySQLAdapter(dbConnection, { + alertsTable: options.alertsTable || 'alert_rules', + usersTable: options.usersTable || 'users', + eventsTable: options.eventsTable || 'alert_events' + }); + + // ───────────────────────────────────────────────────── + // ÉTAPE 2 : Créer le Repository // ───────────────────────────────────────────────────── // Le repository gère les requêtes SQL - const alertsRepo = createAlertsRepo(dbConnection, { + const alertsRepo = createAlertsRepo(adapter, { alertsTable: options.alertsTable || 'alert_rules', usersTable: options.usersTable || 'users', eventsTable: options.eventsTable || 'alert_events' }); // ───────────────────────────────────────────────────── - // ÉTAPE 2 : Créer le Service + // ÉTAPE 3 : Créer le Service // ───────────────────────────────────────────────────── // Le service contient la logique métier const alertsService = createAlertsService(alertsRepo, { @@ -48,24 +59,26 @@ export function initializeAlertsModule(dbConnection = db, options = {}) { }); // ───────────────────────────────────────────────────── - // ÉTAPE 3 : Créer le Controller + // ÉTAPE 4 : Créer le Controller // ───────────────────────────────────────────────────── // Le controller gère les requêtes HTTP + // (Plus besoin de db car le CRUD passe par le repo) const alertsController = createAlertsController(alertsService, alertsRepo); // ───────────────────────────────────────────────────── - // ÉTAPE 4 : Créer le Router + // ÉTAPE 5 : Créer le Router // ───────────────────────────────────────────────────── // Le router définit les routes API const alertsRouter = createAlertsRouter(alertsController); - console.log('✅ Module Alerts complètement initialisé !'); + console.log('Module Alerts complètement initialisé !'); console.log('═'.repeat(80) + '\n'); // ───────────────────────────────────────────────────── // RETOURNER TOUT // ───────────────────────────────────────────────────── return { + adapter, repo: alertsRepo, service: alertsService, controller: alertsController, diff --git a/server/package-lock.json b/server/package-lock.json index d7d6718..ba0a0df 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -13,10 +13,35 @@ "express": "^4.18.2", "mysql2": "^3.6.5", "nodemailer": "^6.9.7", + "socket.io": "^4.8.3", "uuid": "^9.0.1" }, "devDependencies": {} }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -45,6 +70,15 @@ "node": ">= 6.0.0" } }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/body-parser": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", @@ -143,6 +177,23 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -221,6 +272,58 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.6.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz", + "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==", + "license": "MIT", + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.18.3" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -654,6 +757,15 @@ "node": ">=6.0.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", @@ -899,6 +1011,116 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/socket.io": { + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz", + "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.4.1", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz", + "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==", + "license": "MIT", + "dependencies": { + "debug": "~4.4.1", + "ws": "~8.18.3" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.5", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz", + "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.4.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/sql-escaper": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.1.tgz", @@ -945,6 +1167,12 @@ "node": ">= 0.6" } }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", @@ -984,6 +1212,27 @@ "engines": { "node": ">= 0.8" } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } } } } diff --git a/server/package.json b/server/package.json index 2c76723..59655e5 100644 --- a/server/package.json +++ b/server/package.json @@ -7,7 +7,12 @@ "scripts": { "test": "node test-alerts.js" }, - "keywords": ["crypto", "alerts", "notifications", "wall-e-tte"], + "keywords": [ + "crypto", + "alerts", + "notifications", + "wall-e-tte" + ], "author": "Stéphane", "license": "ISC", "dependencies": { @@ -15,7 +20,7 @@ "express": "^4.18.2", "mysql2": "^3.6.5", "nodemailer": "^6.9.7", + "socket.io": "^4.8.3", "uuid": "^9.0.1" - }, - "devDependencies": {} + } } diff --git a/server/test-module-complet.js b/server/test-module-complet.js new file mode 100644 index 0000000..a17d027 --- /dev/null +++ b/server/test-module-complet.js @@ -0,0 +1,445 @@ +// ========================================================= +// TEST COMPLET DU MODULE ALERTS +// ========================================================= +// Ce script teste : +// 1. Connexion à la base de données +// 2. CRUD des règles (Create, Read, Update, Delete) +// 3. Canaux de notification +// 4. Service processSignal +// ========================================================= +// USAGE : node test-module-complet.js +// ========================================================= + +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Charger les variables d'environnement +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +dotenv.config({ path: path.resolve(__dirname, '.env') }); + +// ========================================================= +// IMPORTS DU MODULE +// ========================================================= +import db from './config/db.js'; +import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js'; +import { createAlertsRepo } from './modules/alerts/alerts.repo.js'; +import { createAlertsService } from './modules/alerts/alerts.service.js'; + +// ========================================================= +// INITIALISATION +// ========================================================= +const adapter = createMySQLAdapter(db); +const repo = createAlertsRepo(adapter); +const service = createAlertsService(repo); + +// Variable pour stocker l'ID de la règle créée +let testRuleId = null; +let testUserId = null; + +// ========================================================= +// FONCTIONS UTILITAIRES +// ========================================================= +function printHeader(title) { + console.log('\n' + '─'.repeat(60)); + console.log(`📋 ${title}`); + console.log('─'.repeat(60)); +} + +function printSuccess(message) { + console.log(` ✅ ${message}`); +} + +function printError(message) { + console.log(` ❌ ${message}`); +} + +function printInfo(message) { + console.log(` ℹ️ ${message}`); +} + +// ========================================================= +// TEST 1 : CONNEXION DB +// ========================================================= +async function testConnection() { + printHeader('TEST 1 : Connexion à la base de données'); + + try { + const [rows] = await db.execute('SELECT 1 as test'); + if (rows[0].test === 1) { + printSuccess('Connexion DB OK'); + return true; + } + } catch (error) { + printError(`Connexion échouée : ${error.message}`); + return false; + } +} + +// ========================================================= +// TEST 1.5 : PRÉPARER UN UTILISATEUR DE TEST +// ========================================================= +async function prepareTestUser() { + printHeader('TEST 1.5 : Préparation utilisateur de test'); + + try { + // D'abord, chercher un utilisateur existant + const [existingUsers] = await db.execute('SELECT user_id FROM users LIMIT 1'); + + if (existingUsers.length > 0) { + testUserId = existingUsers[0].user_id; + printSuccess(`Utilisateur existant trouvé : ${testUserId}`); + return true; + } + + // Sinon, créer un utilisateur de test + printInfo('Aucun utilisateur trouvé, création d\'un utilisateur de test...'); + + const { v4: uuidv4 } = await import('uuid'); + testUserId = 'test-user-' + uuidv4().substring(0, 8); + + await db.execute(` + INSERT INTO users (user_id, email, password_hash, display_name, created_at_ms, updated_at_ms) + VALUES (?, ?, ?, ?, ?, ?) + `, [testUserId, 'test@test.com', 'hash123', 'Test User', Date.now(), Date.now()]); + + printSuccess(`Utilisateur de test créé : ${testUserId}`); + return true; + + } catch (error) { + printError(`Erreur préparation utilisateur : ${error.message}`); + printInfo('Conseil : Exécute le seed.sql pour créer des données de test'); + return false; + } +} + +// ========================================================= +// TEST 2 : CREATE - Créer une règle +// ========================================================= +async function testCreateRule() { + printHeader('TEST 2 : CRUD - Créer une règle (CREATE)'); + + if (!testUserId) { + printError('Pas d\'utilisateur de test disponible'); + return false; + } + + try { + const ruleData = { + userId: testUserId, // Utiliser l'utilisateur existant/créé + pairId: 1, + channel: 'CONSOLE', + minConfidence: 0.75, + severity: 'WARNING', + ruleType: 'SIGNAL_THRESHOLD', + cooldownMs: 60000 + }; + + printInfo(`Création d'une règle pour user: ${ruleData.userId}`); + + const result = await repo.createRule(ruleData); + + if (result && result.ruleId) { + testRuleId = result.ruleId; + printSuccess(`Règle créée avec ID: ${testRuleId}`); + return true; + } else { + printError('Règle créée mais pas d\'ID retourné'); + return false; + } + } catch (error) { + printError(`Erreur création : ${error.message}`); + return false; + } +} + +// ========================================================= +// TEST 3 : READ - Lire la règle créée +// ========================================================= +async function testReadRule() { + printHeader('TEST 3 : CRUD - Lire la règle (READ)'); + + if (!testRuleId) { + printError('Pas de règle à lire (test CREATE a échoué)'); + return false; + } + + try { + printInfo(`Lecture de la règle ID: ${testRuleId}`); + + const rule = await repo.findRuleById(testRuleId); + + if (rule) { + printSuccess(`Règle trouvée :`); + console.log(` - user_id: ${rule.user_id}`); + console.log(` - channel: ${rule.channel}`); + console.log(` - min_confidence: ${rule.min_confidence}`); + console.log(` - enabled: ${rule.enabled}`); + return true; + } else { + printError('Règle non trouvée'); + return false; + } + } catch (error) { + printError(`Erreur lecture : ${error.message}`); + return false; + } +} + +// ========================================================= +// TEST 4 : UPDATE - Modifier la règle +// ========================================================= +async function testUpdateRule() { + printHeader('TEST 4 : CRUD - Modifier la règle (UPDATE)'); + + if (!testRuleId) { + printError('Pas de règle à modifier (test CREATE a échoué)'); + return false; + } + + try { + const updates = { + minConfidence: 0.90, + channel: 'EMAIL', + severity: 'CRITICAL' + }; + + printInfo(`Modification de la règle ID: ${testRuleId}`); + printInfo(`Nouvelles valeurs: confidence=0.90, channel=EMAIL, severity=CRITICAL`); + + const updated = await repo.updateRule(testRuleId, updates); + + if (updated) { + // Vérifier la modification + const rule = await repo.findRuleById(testRuleId); + + // Debug : afficher les valeurs réelles + console.log(` [DEBUG] min_confidence: ${rule.min_confidence} (type: ${typeof rule.min_confidence})`); + console.log(` [DEBUG] channel: ${rule.channel}`); + console.log(` [DEBUG] severity: ${rule.severity}`); + + // Comparaison tolérante (float peut avoir des imprécisions) + const confidenceOk = Math.abs(parseFloat(rule.min_confidence) - 0.90) < 0.01; + const channelOk = rule.channel === 'EMAIL'; + + if (confidenceOk && channelOk) { + printSuccess('Règle modifiée et vérifiée'); + return true; + } else { + printError(`Modification non appliquée correctement`); + printError(` → confidence OK: ${confidenceOk}, channel OK: ${channelOk}`); + return false; + } + } else { + printError('Modification a retourné false'); + return false; + } + } catch (error) { + printError(`Erreur modification : ${error.message}`); + return false; + } +} + +// ========================================================= +// TEST 5 : DELETE - Supprimer la règle +// ========================================================= +async function testDeleteRule() { + printHeader('TEST 5 : CRUD - Supprimer la règle (DELETE)'); + + if (!testRuleId) { + printError('Pas de règle à supprimer (test CREATE a échoué)'); + return false; + } + + try { + printInfo(`Suppression de la règle ID: ${testRuleId}`); + + const deleted = await repo.deleteRule(testRuleId); + + if (deleted) { + // Vérifier la suppression + const rule = await repo.findRuleById(testRuleId); + + if (!rule) { + printSuccess('Règle supprimée et vérifiée'); + return true; + } else { + printError('Règle encore présente après suppression'); + return false; + } + } else { + printError('Suppression a retourné false'); + return false; + } + } catch (error) { + printError(`Erreur suppression : ${error.message}`); + return false; + } +} + +// ========================================================= +// TEST 6 : Canal CONSOLE +// ========================================================= +async function testConsoleChannel() { + printHeader('TEST 6 : Canal de notification CONSOLE'); + + try { + const { sendConsoleAlert } = await import('./modules/alerts/channels/console.js'); + + const testSignal = { + action: 'BUY', + pair: 'BTC/EUR', + confidence: 0.85, + criticality: 'WARNING', + reason: 'Test du canal console' + }; + + await sendConsoleAlert(testSignal); + printSuccess('Canal CONSOLE fonctionne'); + return true; + } catch (error) { + printError(`Erreur canal CONSOLE : ${error.message}`); + return false; + } +} + +// ========================================================= +// TEST 7 : Service processSignal (si règles existent) +// ========================================================= +async function testProcessSignal() { + printHeader('TEST 7 : Service processSignal'); + + if (!testUserId) { + printError('Pas d\'utilisateur de test disponible'); + return false; + } + + try { + // D'abord créer une règle pour le test + const ruleData = { + userId: testUserId, // Utiliser l'utilisateur existant + pairId: 1, + channel: 'CONSOLE', + minConfidence: 0.70, + severity: 'INFO' + }; + + printInfo('Création d\'une règle temporaire pour le test...'); + const rule = await repo.createRule(ruleData); + + // Simuler un signal + const testSignal = { + userId: testUserId, // Utiliser l'utilisateur existant + pairId: 1, + pair: 'BTC/EUR', + action: 'BUY', + confidence: 0.85, + criticality: 'WARNING', + reason: 'Test du service processSignal' + }; + + printInfo('Envoi du signal au service...'); + await service.processSignal(testSignal); + + printSuccess('Service processSignal exécuté sans erreur'); + + // Nettoyer : supprimer la règle de test + await repo.deleteRule(rule.ruleId); + printInfo('Règle temporaire supprimée'); + + return true; + } catch (error) { + printError(`Erreur processSignal : ${error.message}`); + return false; + } +} + +// ========================================================= +// EXÉCUTION DE TOUS LES TESTS +// ========================================================= +async function runAllTests() { + console.log('\n' + '═'.repeat(60)); + console.log('🧪 TESTS COMPLETS DU MODULE ALERTS'); + console.log('═'.repeat(60)); + + const results = { + connection: false, + prepareUser: false, + create: false, + read: false, + update: false, + delete: false, + console: false, + processSignal: false + }; + + try { + // Tests dans l'ordre + results.connection = await testConnection(); + + if (results.connection) { + results.prepareUser = await prepareTestUser(); + + if (results.prepareUser) { + results.create = await testCreateRule(); + results.read = await testReadRule(); + results.update = await testUpdateRule(); + results.delete = await testDeleteRule(); + results.processSignal = await testProcessSignal(); + } + + results.console = await testConsoleChannel(); + } + + } catch (error) { + console.error('\n❌ ERREUR FATALE:', error); + } + + // ========================================================= + // RÉSUMÉ + // ========================================================= + console.log('\n' + '═'.repeat(60)); + console.log('📊 RÉSUMÉ DES TESTS'); + console.log('═'.repeat(60)); + + const tests = [ + { name: 'Connexion DB', result: results.connection }, + { name: 'Préparation User', result: results.prepareUser }, + { name: 'CRUD - Create', result: results.create }, + { name: 'CRUD - Read', result: results.read }, + { name: 'CRUD - Update', result: results.update }, + { name: 'CRUD - Delete', result: results.delete }, + { name: 'Canal Console', result: results.console }, + { name: 'Service processSignal', result: results.processSignal } + ]; + + let passed = 0; + let failed = 0; + + tests.forEach(test => { + const icon = test.result ? '✅' : '❌'; + console.log(` ${icon} ${test.name}`); + if (test.result) passed++; + else failed++; + }); + + console.log('\n' + '─'.repeat(60)); + console.log(` Total: ${passed}/${tests.length} tests réussis`); + + if (failed === 0) { + console.log('\n 🎉 TOUS LES TESTS SONT PASSÉS !'); + } else { + console.log(`\n ⚠️ ${failed} test(s) échoué(s)`); + } + + console.log('═'.repeat(60) + '\n'); + + // Quitter + process.exit(failed === 0 ? 0 : 1); +} + +// ========================================================= +// LANCEMENT +// ========================================================= +runAllTests(); diff --git a/server/test-server.js b/server/test-server.js new file mode 100644 index 0000000..45ec6bc --- /dev/null +++ b/server/test-server.js @@ -0,0 +1,121 @@ +// ========================================================= +// MINI-SERVEUR DE TEST POUR LES ROUTES API +// ========================================================= +// Ce script démarre un serveur Express pour tester les +// routes du module Alerts via Postman, curl ou navigateur +// ========================================================= +// USAGE : node test-server.js +// ========================================================= + +import express from 'express'; +import dotenv from 'dotenv'; +import path from 'path'; +import { fileURLToPath } from 'url'; + +// Charger les variables d'environnement +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +dotenv.config({ path: path.resolve(__dirname, '.env') }); + +// ========================================================= +// IMPORTS DU MODULE ALERTS +// ========================================================= +import { initializeAlertsModule } from './modules/init-alerts.js'; + +// ========================================================= +// CRÉER L'APPLICATION EXPRESS +// ========================================================= +const app = express(); +app.use(express.json()); + +// ========================================================= +// INITIALISER LE MODULE ALERTS +// ========================================================= +console.log('\n🚀 Initialisation du module Alerts...\n'); + +// Import dynamique pour éviter l'erreur de connexion DB +import db from './config/db.js'; +import { createMySQLAdapter } from './modules/alerts/adapters/mysql.adapter.js'; +import { createAlertsRepo } from './modules/alerts/alerts.repo.js'; +import { createAlertsService } from './modules/alerts/alerts.service.js'; +import { createAlertsController } from './modules/alerts/alerts.controller.js'; +import { createAlertsRouter } from './modules/alerts/alerts.router.js'; + +const adapter = createMySQLAdapter(db); +const repo = createAlertsRepo(adapter); +const service = createAlertsService(repo); +const controller = createAlertsController(service, repo); +const router = createAlertsRouter(controller); + +// ========================================================= +// MONTER LES ROUTES +// ========================================================= +app.use('/api/alerts', router); + +// ========================================================= +// ROUTE DE TEST / HEALTH CHECK +// ========================================================= +app.get('/', (req, res) => { + res.json({ + status: 'ok', + message: 'Serveur de test Alerts', + timestamp: new Date().toISOString(), + routes: [ + 'GET /api/alerts/rules/:userId - Lister les règles', + 'GET /api/alerts/rules/detail/:id - Détail d\'une règle', + 'POST /api/alerts/rules - Créer une règle', + 'PUT /api/alerts/rules/:id - Modifier une règle', + 'DELETE /api/alerts/rules/:id - Supprimer une règle', + 'GET /api/alerts/history/:userId - Historique des alertes', + 'POST /api/alerts/process-signal - Traiter un signal' + ] + }); +}); + +// ========================================================= +// DÉMARRER LE SERVEUR +// ========================================================= +const PORT = process.env.PORT || 3000; + +app.listen(PORT, () => { + console.log(` +╔═══════════════════════════════════════════════════════════╗ +║ 🧪 SERVEUR DE TEST - MODULE ALERTS ║ +╠═══════════════════════════════════════════════════════════╣ +║ ║ +║ Serveur démarré sur : http://localhost:${PORT} ║ +║ ║ +║ Routes disponibles : ║ +║ ─────────────────────────────────────────────────────── ║ +║ GET / → Health check ║ +║ GET /api/alerts/rules/:userId → Lister règles ║ +║ GET /api/alerts/rules/detail/:id → Détail règle ║ +║ POST /api/alerts/rules → Créer règle ║ +║ PUT /api/alerts/rules/:id → Modifier règle ║ +║ DELETE /api/alerts/rules/:id → Supprimer règle ║ +║ GET /api/alerts/history/:userId → Historique ║ +║ POST /api/alerts/process-signal → Traiter signal ║ +║ ║ +╚═══════════════════════════════════════════════════════════╝ + +📝 Exemples de tests avec curl : + +1. Créer une règle : + curl -X POST http://localhost:${PORT}/api/alerts/rules \\ + -H "Content-Type: application/json" \\ + -d '{"userId":"test-user","pairId":1,"channel":"CONSOLE","minConfidence":0.8}' + +2. Lister les règles : + curl http://localhost:${PORT}/api/alerts/rules/test-user + +3. Modifier une règle : + curl -X PUT http://localhost:${PORT}/api/alerts/rules/RULE_ID \\ + -H "Content-Type: application/json" \\ + -d '{"minConfidence":0.9,"enabled":true}' + +4. Supprimer une règle : + curl -X DELETE http://localhost:${PORT}/api/alerts/rules/RULE_ID + +Appuie sur Ctrl+C pour arrêter le serveur. +`); +}); -- 2.50.1