const { describe, it, beforeEach, afterEach, before, after } = require('mocha'); const { expect } = require('chai'); const sinon = require('sinon'); const AlertService = require('../../services/alertService'); const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, createTestUser, createTestTenant, createTestDevice, createTestDetection } = require('../setup'); describe('AlertService', () => { let models, sequelize, alertService; before(async () => { ({ models, sequelize } = await setupTestEnvironment()); alertService = new AlertService(); }); after(async () => { await teardownTestEnvironment(); }); beforeEach(async () => { await cleanDatabase(); // Clear active alerts between tests alertService.activeAlerts.clear(); }); describe('assessThreatLevel', () => { it('should assess critical threat for very close drones', () => { const result = alertService.assessThreatLevel(-35, 2); expect(result.level).to.equal('critical'); expect(result.requiresImmediateAction).to.be.true; expect(result.priority).to.equal(1); expect(result.description).to.include('IMMEDIATE THREAT'); }); it('should assess high threat for close drones', () => { const result = alertService.assessThreatLevel(-50, 2); expect(result.level).to.equal('high'); expect(result.requiresImmediateAction).to.be.true; expect(result.priority).to.equal(2); expect(result.description).to.include('HIGH THREAT'); }); it('should assess medium threat for medium distance', () => { const result = alertService.assessThreatLevel(-65, 2); expect(result.level).to.equal('medium'); expect(result.requiresImmediateAction).to.be.false; expect(result.priority).to.equal(3); expect(result.description).to.include('MEDIUM THREAT'); }); it('should assess low threat for distant drones', () => { const result = alertService.assessThreatLevel(-80, 2); expect(result.level).to.equal('low'); expect(result.requiresImmediateAction).to.be.false; expect(result.priority).to.equal(4); expect(result.description).to.include('LOW THREAT'); }); it('should assess monitoring for very distant drones', () => { const result = alertService.assessThreatLevel(-90, 2); expect(result.level).to.equal('monitoring'); expect(result.requiresImmediateAction).to.be.false; expect(result.description).to.include('MONITORING'); }); it('should escalate military drones to critical regardless of distance', () => { // Test with a distant military drone (Orlan type 2) const result = alertService.assessThreatLevel(-85, 2); // Very far but military expect(result.level).to.equal('critical'); expect(result.requiresImmediateAction).to.be.true; expect(result.description).to.include('CRITICAL THREAT'); expect(result.description).to.include('IMMEDIATE RESPONSE REQUIRED'); }); it('should calculate estimated distance from RSSI', () => { const result = alertService.assessThreatLevel(-65, 2); expect(result.estimatedDistance).to.be.a('number'); expect(result.estimatedDistance).to.be.greaterThan(0); expect(result.rssi).to.equal(-65); }); it('should include drone type information', () => { const result = alertService.assessThreatLevel(-65, 2); expect(result.droneType).to.be.a('string'); expect(result.droneCategory).to.be.a('string'); expect(result.threatLevel).to.be.a('string'); }); it('should escalate professional drones by one threat level', () => { // Assuming drone type 13 is DJI (professional) const result = alertService.assessThreatLevel(-80, 13); // Would normally be low expect(result.level).to.equal('medium'); // Escalated from low expect(result.requiresImmediateAction).to.be.true; }); it('should handle racing drones appropriately', () => { // Test racing drone close proximity const result = alertService.assessThreatLevel(-50, 7); // FPV racing drone, close expect(result.level).to.equal('high'); expect(result.description).to.include('HIGH-SPEED'); }); }); describe('checkAlertRules', () => { it('should trigger alert when detection meets rule criteria', async () => { const tenant = await createTestTenant(); const user = await createTestUser({ tenant_id: tenant.id }); const device = await createTestDevice({ tenant_id: tenant.id }); // Create alert rule await models.AlertRule.create({ user_id: user.id, tenant_id: tenant.id, name: 'Test Rule', drone_type: 2, min_rssi: -70, max_distance: 1000, is_active: true }); const detection = await createTestDetection({ device_id: device.id, drone_type: 2, rssi: -60 // Stronger than min_rssi }); const alerts = await alertService.checkAlertRules(detection); expect(alerts).to.be.an('array'); expect(alerts).to.have.length(1); expect(alerts[0].rule_name).to.equal('Test Rule'); }); it('should not trigger alert when detection does not meet criteria', async () => { const tenant = await createTestTenant(); const user = await createTestUser({ tenant_id: tenant.id }); const device = await createTestDevice({ tenant_id: tenant.id }); // Create alert rule with strict criteria await models.AlertRule.create({ user_id: user.id, tenant_id: tenant.id, name: 'Strict Rule', drone_type: 2, min_rssi: -40, // Very strong signal required is_active: true }); const detection = await createTestDetection({ device_id: device.id, drone_type: 2, rssi: -80 // Weaker than min_rssi }); const alerts = await alertService.checkAlertRules(detection); expect(alerts).to.be.an('array'); expect(alerts).to.have.length(0); }); it('should not trigger alert for inactive rules', async () => { const tenant = await createTestTenant(); const user = await createTestUser({ tenant_id: tenant.id }); const device = await createTestDevice({ tenant_id: tenant.id }); // Create inactive alert rule await models.AlertRule.create({ user_id: user.id, tenant_id: tenant.id, name: 'Inactive Rule', drone_type: 2, min_rssi: -70, is_active: false // Inactive }); const detection = await createTestDetection({ device_id: device.id, drone_type: 2, rssi: -60 }); const alerts = await alertService.checkAlertRules(detection); expect(alerts).to.have.length(0); }); it('should handle multiple matching rules', async () => { const tenant = await createTestTenant(); const user = await createTestUser({ tenant_id: tenant.id }); const device = await createTestDevice({ tenant_id: tenant.id }); // Create multiple alert rules await models.AlertRule.create({ user_id: user.id, tenant_id: tenant.id, name: 'Rule 1', drone_type: 2, min_rssi: -70, is_active: true }); await models.AlertRule.create({ user_id: user.id, tenant_id: tenant.id, name: 'Rule 2', min_rssi: -70, // No specific drone type is_active: true }); const detection = await createTestDetection({ device_id: device.id, drone_type: 2, rssi: -60 }); const alerts = await alertService.checkAlertRules(detection); expect(alerts).to.have.length(2); }); it('should filter rules by tenant', async () => { const tenant1 = await createTestTenant({ slug: 'tenant1' }); const tenant2 = await createTestTenant({ slug: 'tenant2' }); const device1 = await createTestDevice({ tenant_id: tenant1.id }); // Create rules for different tenants await models.AlertRule.create({ tenant_id: tenant1.id, name: 'Tenant 1 Rule', drone_type: 2, min_rssi: -70, is_active: true }); await models.AlertRule.create({ tenant_id: tenant2.id, name: 'Tenant 2 Rule', drone_type: 2, min_rssi: -70, is_active: true }); const detection = await createTestDetection({ device_id: device1.id, drone_type: 2, rssi: -60 }); const alerts = await alertService.checkAlertRules(detection); expect(alerts).to.have.length(1); expect(alerts[0].rule_name).to.equal('Tenant 1 Rule'); }); }); describe('logAlert', () => { it('should create alert log entry', async () => { const tenant = await createTestTenant(); const user = await createTestUser({ tenant_id: tenant.id }); const device = await createTestDevice({ tenant_id: tenant.id }); const detection = await createTestDetection({ device_id: device.id }); // Create a test alert rule const rule = await models.AlertRule.create({ user_id: user.id, tenant_id: tenant.id, rule_name: 'Test Alert', min_rssi: -80, drone_type: 2, alert_channels: ['sms'], sms_phone_number: '+1234567890', is_active: true }); const logEntry = await alertService.logAlert(rule, detection, 'sms', '+1234567890'); expect(logEntry).to.exist; expect(logEntry.alert_rule_id).to.equal(rule.id); expect(logEntry.detection_id).to.equal(detection.id); expect(logEntry.alert_type).to.equal('sms'); expect(logEntry.status).to.equal('sent'); }); it('should include detection and threat data in log', async () => { const tenant = await createTestTenant(); const user = await createTestUser({ tenant_id: tenant.id }); const device = await createTestDevice({ tenant_id: tenant.id }); const detection = await createTestDetection({ device_id: device.id, drone_type: 2, rssi: -50 }); // Create a test alert rule const rule = await models.AlertRule.create({ user_id: user.id, tenant_id: tenant.id, rule_name: 'Critical Alert', min_rssi: -80, drone_type: 2, alert_channels: ['sms'], sms_phone_number: '+1234567890', is_active: true }); const logEntry = await alertService.logAlert(rule, detection, 'sms', '+1234567890'); expect(logEntry.alert_rule_id).to.equal(rule.id); expect(logEntry.detection_id).to.equal(detection.id); expect(logEntry.alert_type).to.equal('sms'); expect(logEntry.status).to.equal('sent'); }); }); describe('sendSMSAlert', () => { beforeEach(() => { // Mock Twilio for SMS tests alertService.twilioEnabled = true; alertService.twilioClient = { messages: { create: sinon.stub().resolves({ sid: 'test-message-id' }) } }; alertService.twilioPhone = '+1234567890'; }); it('should send SMS alert for critical threats', async () => { const tenant = await createTestTenant(); const device = await createTestDevice({ tenant_id: tenant.id }); const detection = await createTestDetection({ device_id: device.id }); const rule = { id: 1, name: 'Test Rule', priority: 'high' }; const phoneNumber = '+1987654321'; const message = 'Critical threat detected'; const result = await alertService.sendSMSAlert(phoneNumber, message, rule, detection); expect(result.status).to.equal('sent'); expect(alertService.twilioClient.messages.create.calledOnce).to.be.true; }); it('should not send SMS for low priority alerts', async () => { const tenant = await createTestTenant(); const device = await createTestDevice({ tenant_id: tenant.id }); const detection = await createTestDetection({ device_id: device.id }); // For this test, we'll test the priority logic in the calling function // sendSMSAlert itself always tries to send if called const rule = { id: 1, name: 'Test Rule', priority: 'low' }; const phoneNumber = '+1987654321'; const message = 'Low threat detected'; const result = await alertService.sendSMSAlert(phoneNumber, message, rule, detection); // SMS should still be sent since this method is called explicitly expect(result.status).to.equal('sent'); expect(alertService.twilioClient.messages.create.calledOnce).to.be.true; }); it('should handle Twilio errors gracefully', async () => { alertService.twilioClient.messages.create = sinon.stub().rejects(new Error('Twilio error')); const tenant = await createTestTenant(); const device = await createTestDevice({ tenant_id: tenant.id }); const detection = await createTestDetection({ device_id: device.id }); const rule = { id: 1, name: 'Test Rule', priority: 'high' }; const phoneNumber = '+1987654321'; const message = 'Critical threat detected'; const result = await alertService.sendSMSAlert(phoneNumber, message, rule, detection); expect(result.status).to.equal('failed'); expect(result.error_message).to.include('Twilio error'); }); it('should handle disabled Twilio', async () => { alertService.twilioEnabled = false; const tenant = await createTestTenant(); const device = await createTestDevice({ tenant_id: tenant.id }); const detection = await createTestDetection({ device_id: device.id }); const rule = { id: 1, name: 'Test Rule', priority: 'high' }; const phoneNumber = '+1987654321'; const message = 'Critical threat detected'; const result = await alertService.sendSMSAlert(phoneNumber, message, rule, detection); expect(result.status).to.equal('failed'); expect(result.error_message).to.include('not configured'); }); }); describe('processDetectionAlert', () => { it('should process complete alert workflow', async () => { const tenant = await createTestTenant(); const device = await createTestDevice({ tenant_id: tenant.id, name: 'Security Device' }); // Create alert rule await models.AlertRule.create({ tenant_id: tenant.id, name: 'Security Rule', drone_type: 2, min_rssi: -70, is_active: true }); const detection = await createTestDetection({ device_id: device.id, drone_type: 2, rssi: -50 // High threat }); // Mock socket.io const mockIo = { emit: sinon.stub(), emitToDashboard: sinon.stub(), emitToDevice: sinon.stub(), to: (room) => ({ emit: sinon.stub() }) }; const result = await alertService.processDetectionAlert(detection, mockIo); expect(result).to.exist; expect(result.alertsTriggered).to.be.greaterThan(0); expect(mockIo.emitToDashboard.called).to.be.true; }); it('should emit socket events for real-time updates', async () => { const device = await createTestDevice(); const detection = await createTestDetection({ device_id: device.id, drone_type: 2, rssi: -40 // Critical threat }); const mockIo = { emit: sinon.stub(), emitToDashboard: sinon.stub(), emitToDevice: sinon.stub(), to: (room) => ({ emit: sinon.stub() }) }; await alertService.processDetectionAlert(detection, mockIo); expect(mockIo.emitToDashboard.calledWith('new_alert')).to.be.true; expect(mockIo.emitToDevice.calledWith(device.id, 'device_alert')).to.be.true; }); }); describe('Alert Deduplication', () => { it('should prevent duplicate alerts for same drone', async () => { const device = await createTestDevice(); const droneId = 12345; // First detection const detection1 = await createTestDetection({ device_id: device.id, drone_id: droneId, drone_type: 2, rssi: -50 }); // Second detection from same drone shortly after const detection2 = await createTestDetection({ device_id: device.id, drone_id: droneId, drone_type: 2, rssi: -45 }); const mockIo = { emitToDashboard: sinon.stub(), emitToDevice: sinon.stub(), to: (room) => ({ emit: sinon.stub() }) }; await alertService.processDetectionAlert(detection1, mockIo); await alertService.processDetectionAlert(detection2, mockIo); // Should have deduplication logic to prevent spam expect(alertService.activeAlerts.has(droneId)).to.be.true; }); it('should send clear notification when drone disappears', async () => { const device = await createTestDevice(); const droneId = 12345; // Add active alert alertService.activeAlerts.set(droneId, { deviceId: device.id, lastSeen: new Date(), threatLevel: 'high' }); const mockIo = { emitToDashboard: sinon.stub(), emitToDevice: sinon.stub(), to: (room) => ({ emit: sinon.stub() }) }; // Simulate clearing old alerts await alertService.clearExpiredAlerts(mockIo); // Should emit clear notification for expired alerts expect(mockIo.emitToDashboard.called).to.be.true; }); }); });