Fix jwt-token
This commit is contained in:
480
server/tests/services/alertService.test.js
Normal file
480
server/tests/services/alertService.test.js
Normal file
@@ -0,0 +1,480 @@
|
||||
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 device = await createTestDevice({ tenant_id: tenant.id });
|
||||
|
||||
// Create alert rule
|
||||
await models.AlertRule.create({
|
||||
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 device = await createTestDevice({ tenant_id: tenant.id });
|
||||
|
||||
// Create alert rule with strict criteria
|
||||
await models.AlertRule.create({
|
||||
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 device = await createTestDevice({ tenant_id: tenant.id });
|
||||
|
||||
// Create inactive alert rule
|
||||
await models.AlertRule.create({
|
||||
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 device = await createTestDevice({ tenant_id: tenant.id });
|
||||
|
||||
// Create multiple alert rules
|
||||
await models.AlertRule.create({
|
||||
tenant_id: tenant.id,
|
||||
name: 'Rule 1',
|
||||
drone_type: 2,
|
||||
min_rssi: -70,
|
||||
is_active: true
|
||||
});
|
||||
|
||||
await models.AlertRule.create({
|
||||
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 device = await createTestDevice({ tenant_id: tenant.id });
|
||||
const detection = await createTestDetection({ device_id: device.id });
|
||||
|
||||
const alertData = {
|
||||
rule_name: 'Test Alert',
|
||||
threat_level: 'high',
|
||||
message: 'Test alert message'
|
||||
};
|
||||
|
||||
const logEntry = await alertService.logAlert(detection, alertData);
|
||||
|
||||
expect(logEntry).to.exist;
|
||||
expect(logEntry.device_id).to.equal(device.id);
|
||||
expect(logEntry.rule_name).to.equal('Test Alert');
|
||||
expect(logEntry.threat_level).to.equal('high');
|
||||
});
|
||||
|
||||
it('should include detection and threat data in log', async () => {
|
||||
const device = await createTestDevice();
|
||||
const detection = await createTestDetection({
|
||||
device_id: device.id,
|
||||
drone_type: 2,
|
||||
rssi: -50
|
||||
});
|
||||
|
||||
const alertData = {
|
||||
rule_name: 'Critical Alert',
|
||||
threat_level: 'critical',
|
||||
message: 'Critical threat detected'
|
||||
};
|
||||
|
||||
const logEntry = await alertService.logAlert(detection, alertData);
|
||||
|
||||
expect(logEntry.drone_type).to.equal(2);
|
||||
expect(logEntry.rssi).to.equal(-50);
|
||||
expect(logEntry.drone_id).to.equal(detection.drone_id);
|
||||
});
|
||||
});
|
||||
|
||||
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 alertData = {
|
||||
threat_level: 'critical',
|
||||
message: 'Critical threat detected',
|
||||
device_name: 'Test Device'
|
||||
};
|
||||
|
||||
const phoneNumbers = ['+1987654321'];
|
||||
|
||||
const result = await alertService.sendSMSAlert(alertData, phoneNumbers);
|
||||
|
||||
expect(result.success).to.be.true;
|
||||
expect(alertService.twilioClient.messages.create.calledOnce).to.be.true;
|
||||
});
|
||||
|
||||
it('should not send SMS for low priority alerts', async () => {
|
||||
const alertData = {
|
||||
threat_level: 'low',
|
||||
message: 'Low threat detected'
|
||||
};
|
||||
|
||||
const phoneNumbers = ['+1987654321'];
|
||||
|
||||
const result = await alertService.sendSMSAlert(alertData, phoneNumbers);
|
||||
|
||||
// Should not send for low priority
|
||||
expect(alertService.twilioClient.messages.create.called).to.be.false;
|
||||
});
|
||||
|
||||
it('should handle Twilio errors gracefully', async () => {
|
||||
alertService.twilioClient.messages.create = sinon.stub().rejects(new Error('Twilio error'));
|
||||
|
||||
const alertData = {
|
||||
threat_level: 'critical',
|
||||
message: 'Critical threat detected'
|
||||
};
|
||||
|
||||
const phoneNumbers = ['+1987654321'];
|
||||
|
||||
const result = await alertService.sendSMSAlert(alertData, phoneNumbers);
|
||||
|
||||
expect(result.success).to.be.false;
|
||||
expect(result.error).to.include('Twilio error');
|
||||
});
|
||||
|
||||
it('should handle disabled Twilio', async () => {
|
||||
alertService.twilioEnabled = false;
|
||||
|
||||
const alertData = {
|
||||
threat_level: 'critical',
|
||||
message: 'Critical threat detected'
|
||||
};
|
||||
|
||||
const phoneNumbers = ['+1987654321'];
|
||||
|
||||
const result = await alertService.sendSMSAlert(alertData, phoneNumbers);
|
||||
|
||||
expect(result.success).to.be.false;
|
||||
expect(result.error).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()
|
||||
};
|
||||
|
||||
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()
|
||||
};
|
||||
|
||||
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() };
|
||||
|
||||
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() };
|
||||
|
||||
// Simulate clearing old alerts
|
||||
await alertService.clearExpiredAlerts(mockIo);
|
||||
|
||||
// Should emit clear notification for expired alerts
|
||||
expect(mockIo.emitToDashboard.called).to.be.true;
|
||||
});
|
||||
});
|
||||
});
|
||||
433
server/tests/services/droneTrackingService.test.js
Normal file
433
server/tests/services/droneTrackingService.test.js
Normal file
@@ -0,0 +1,433 @@
|
||||
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
|
||||
const { expect } = require('chai');
|
||||
const sinon = require('sinon');
|
||||
const DroneTrackingService = require('../../services/droneTrackingService');
|
||||
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, createTestDevice, createTestDetection } = require('../setup');
|
||||
|
||||
describe('DroneTrackingService', () => {
|
||||
let models, sequelize, trackingService;
|
||||
|
||||
before(async () => {
|
||||
({ models, sequelize } = await setupTestEnvironment());
|
||||
trackingService = new DroneTrackingService();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await teardownTestEnvironment();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
trackingService.activeDrones.clear();
|
||||
trackingService.removeAllListeners();
|
||||
});
|
||||
|
||||
describe('trackDetection', () => {
|
||||
it('should track new drone detection', async () => {
|
||||
const device = await createTestDevice();
|
||||
const detection = await createTestDetection({
|
||||
device_id: device.id,
|
||||
drone_id: 12345,
|
||||
rssi: -60,
|
||||
geo_lat: 59.3293,
|
||||
geo_lon: 18.0686
|
||||
});
|
||||
|
||||
trackingService.trackDetection(detection);
|
||||
|
||||
expect(trackingService.activeDrones.has(12345)).to.be.true;
|
||||
const droneData = trackingService.activeDrones.get(12345);
|
||||
expect(droneData.currentPosition.lat).to.equal(59.3293);
|
||||
expect(droneData.currentPosition.lon).to.equal(18.0686);
|
||||
});
|
||||
|
||||
it('should update existing drone tracking', async () => {
|
||||
const device = await createTestDevice();
|
||||
const detection1 = await createTestDetection({
|
||||
device_id: device.id,
|
||||
drone_id: 12345,
|
||||
rssi: -60,
|
||||
geo_lat: 59.3293,
|
||||
geo_lon: 18.0686
|
||||
});
|
||||
|
||||
const detection2 = await createTestDetection({
|
||||
device_id: device.id,
|
||||
drone_id: 12345,
|
||||
rssi: -55,
|
||||
geo_lat: 59.3300, // Moved slightly
|
||||
geo_lon: 18.0690
|
||||
});
|
||||
|
||||
trackingService.trackDetection(detection1);
|
||||
trackingService.trackDetection(detection2);
|
||||
|
||||
const droneData = trackingService.activeDrones.get(12345);
|
||||
expect(droneData.detectionHistory).to.have.length(2);
|
||||
expect(droneData.currentPosition.lat).to.equal(59.3300);
|
||||
expect(droneData.averageRSSI).to.be.closeTo(-57.5, 0.1);
|
||||
});
|
||||
|
||||
it('should calculate movement between detections', async () => {
|
||||
const device = await createTestDevice();
|
||||
const detection1 = await createTestDetection({
|
||||
device_id: device.id,
|
||||
drone_id: 12345,
|
||||
geo_lat: 59.3293,
|
||||
geo_lon: 18.0686
|
||||
});
|
||||
|
||||
const detection2 = await createTestDetection({
|
||||
device_id: device.id,
|
||||
drone_id: 12345,
|
||||
geo_lat: 59.3393, // ~1km north
|
||||
geo_lon: 18.0686
|
||||
});
|
||||
|
||||
trackingService.trackDetection(detection1);
|
||||
trackingService.trackDetection(detection2);
|
||||
|
||||
const droneData = trackingService.activeDrones.get(12345);
|
||||
expect(droneData.movementPattern.totalDistance).to.be.greaterThan(0);
|
||||
expect(droneData.movementPattern.direction).to.exist;
|
||||
});
|
||||
|
||||
it('should emit movement alert for significant movement', (done) => {
|
||||
const device = createTestDevice();
|
||||
|
||||
trackingService.on('movement_alert', (alertData) => {
|
||||
expect(alertData).to.exist;
|
||||
expect(alertData.droneId).to.equal(12345);
|
||||
expect(alertData.analysis).to.exist;
|
||||
done();
|
||||
});
|
||||
|
||||
// Create detections showing rapid movement
|
||||
const detection1 = {
|
||||
drone_id: 12345,
|
||||
device_id: 1,
|
||||
geo_lat: 59.3293,
|
||||
geo_lon: 18.0686,
|
||||
rssi: -60,
|
||||
server_timestamp: new Date()
|
||||
};
|
||||
|
||||
const detection2 = {
|
||||
drone_id: 12345,
|
||||
device_id: 1,
|
||||
geo_lat: 59.3393, // 1km movement
|
||||
geo_lon: 18.0686,
|
||||
rssi: -55,
|
||||
server_timestamp: new Date(Date.now() + 30000) // 30 seconds later
|
||||
};
|
||||
|
||||
trackingService.trackDetection(detection1);
|
||||
setTimeout(() => {
|
||||
trackingService.trackDetection(detection2);
|
||||
}, 100);
|
||||
});
|
||||
|
||||
it('should detect approach patterns', async () => {
|
||||
const device = await createTestDevice();
|
||||
const droneId = 12345;
|
||||
|
||||
// Simulate drone approaching (RSSI getting stronger)
|
||||
const detections = [
|
||||
{ rssi: -80, geo_lat: 59.3200, geo_lon: 18.0600 },
|
||||
{ rssi: -70, geo_lat: 59.3220, geo_lon: 18.0620 },
|
||||
{ rssi: -60, geo_lat: 59.3240, geo_lon: 18.0640 },
|
||||
{ rssi: -50, geo_lat: 59.3260, geo_lon: 18.0660 }
|
||||
];
|
||||
|
||||
for (const detection of detections) {
|
||||
await trackingService.trackDetection({
|
||||
drone_id: droneId,
|
||||
device_id: device.id,
|
||||
...detection,
|
||||
server_timestamp: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
const droneData = trackingService.activeDrones.get(droneId);
|
||||
expect(droneData.movementPattern.isApproaching).to.be.true;
|
||||
});
|
||||
|
||||
it('should detect retreat patterns', async () => {
|
||||
const device = await createTestDevice();
|
||||
const droneId = 12345;
|
||||
|
||||
// Simulate drone retreating (RSSI getting weaker)
|
||||
const detections = [
|
||||
{ rssi: -50, geo_lat: 59.3260, geo_lon: 18.0660 },
|
||||
{ rssi: -60, geo_lat: 59.3240, geo_lon: 18.0640 },
|
||||
{ rssi: -70, geo_lat: 59.3220, geo_lon: 18.0620 },
|
||||
{ rssi: -80, geo_lat: 59.3200, geo_lon: 18.0600 }
|
||||
];
|
||||
|
||||
for (const detection of detections) {
|
||||
await trackingService.trackDetection({
|
||||
drone_id: droneId,
|
||||
device_id: device.id,
|
||||
...detection,
|
||||
server_timestamp: new Date()
|
||||
});
|
||||
}
|
||||
|
||||
const droneData = trackingService.activeDrones.get(droneId);
|
||||
expect(droneData.movementPattern.isRetreating).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('analyzeMovement', () => {
|
||||
it('should analyze movement patterns correctly', () => {
|
||||
const positions = [
|
||||
{ lat: 59.3293, lon: 18.0686, timestamp: new Date('2023-01-01T10:00:00Z') },
|
||||
{ lat: 59.3300, lon: 18.0690, timestamp: new Date('2023-01-01T10:01:00Z') },
|
||||
{ lat: 59.3310, lon: 18.0695, timestamp: new Date('2023-01-01T10:02:00Z') }
|
||||
];
|
||||
|
||||
const analysis = trackingService.analyzeMovement(positions);
|
||||
|
||||
expect(analysis.totalDistance).to.be.greaterThan(0);
|
||||
expect(analysis.averageSpeed).to.be.greaterThan(0);
|
||||
expect(analysis.direction).to.exist;
|
||||
expect(analysis.isLinear).to.be.a('boolean');
|
||||
});
|
||||
|
||||
it('should detect circular patterns', () => {
|
||||
// Create positions in a rough circle
|
||||
const positions = [];
|
||||
const centerLat = 59.3293;
|
||||
const centerLon = 18.0686;
|
||||
const radius = 0.001; // Small radius
|
||||
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const angle = (i / 8) * 2 * Math.PI;
|
||||
positions.push({
|
||||
lat: centerLat + radius * Math.cos(angle),
|
||||
lon: centerLon + radius * Math.sin(angle),
|
||||
timestamp: new Date(Date.now() + i * 60000)
|
||||
});
|
||||
}
|
||||
|
||||
const analysis = trackingService.analyzeMovement(positions);
|
||||
|
||||
expect(analysis.isCircular).to.be.true;
|
||||
});
|
||||
|
||||
it('should calculate correct speeds', () => {
|
||||
const positions = [
|
||||
{ lat: 59.3293, lon: 18.0686, timestamp: new Date('2023-01-01T10:00:00Z') },
|
||||
{ lat: 59.3303, lon: 18.0686, timestamp: new Date('2023-01-01T10:01:00Z') } // ~1km in 1 minute
|
||||
];
|
||||
|
||||
const analysis = trackingService.analyzeMovement(positions);
|
||||
|
||||
// Should detect high speed (60 km/h)
|
||||
expect(analysis.averageSpeed).to.be.greaterThan(50);
|
||||
expect(analysis.maxSpeed).to.be.greaterThan(50);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cleanupOldTracks', () => {
|
||||
it('should remove old inactive drone tracks', async () => {
|
||||
const droneId = 12345;
|
||||
|
||||
// Add drone track
|
||||
trackingService.activeDrones.set(droneId, {
|
||||
droneId,
|
||||
firstSeen: new Date(Date.now() - 3600000), // 1 hour ago
|
||||
lastSeen: new Date(Date.now() - 1800000), // 30 minutes ago
|
||||
detectionHistory: []
|
||||
});
|
||||
|
||||
trackingService.cleanupOldTracks();
|
||||
|
||||
expect(trackingService.activeDrones.has(droneId)).to.be.false;
|
||||
});
|
||||
|
||||
it('should keep recent drone tracks', async () => {
|
||||
const droneId = 12345;
|
||||
|
||||
// Add recent drone track
|
||||
trackingService.activeDrones.set(droneId, {
|
||||
droneId,
|
||||
firstSeen: new Date(Date.now() - 300000), // 5 minutes ago
|
||||
lastSeen: new Date(Date.now() - 60000), // 1 minute ago
|
||||
detectionHistory: []
|
||||
});
|
||||
|
||||
trackingService.cleanupOldTracks();
|
||||
|
||||
expect(trackingService.activeDrones.has(droneId)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveTracking', () => {
|
||||
it('should return all active drone tracks', async () => {
|
||||
const device = await createTestDevice();
|
||||
|
||||
// Add multiple drones
|
||||
const detection1 = await createTestDetection({
|
||||
device_id: device.id,
|
||||
drone_id: 12345
|
||||
});
|
||||
const detection2 = await createTestDetection({
|
||||
device_id: device.id,
|
||||
drone_id: 67890
|
||||
});
|
||||
|
||||
trackingService.trackDetection(detection1);
|
||||
trackingService.trackDetection(detection2);
|
||||
|
||||
const activeTracks = trackingService.getActiveTracking();
|
||||
|
||||
expect(activeTracks).to.be.an('array');
|
||||
expect(activeTracks).to.have.length(2);
|
||||
expect(activeTracks.map(t => t.droneId)).to.include.members([12345, 67890]);
|
||||
});
|
||||
|
||||
it('should include movement analysis in active tracks', async () => {
|
||||
const device = await createTestDevice();
|
||||
const detection = await createTestDetection({
|
||||
device_id: device.id,
|
||||
drone_id: 12345
|
||||
});
|
||||
|
||||
trackingService.trackDetection(detection);
|
||||
|
||||
const activeTracks = trackingService.getActiveTracking();
|
||||
|
||||
expect(activeTracks[0].movementPattern).to.exist;
|
||||
expect(activeTracks[0].currentPosition).to.exist;
|
||||
expect(activeTracks[0].detectionCount).to.be.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDroneHistory', () => {
|
||||
it('should return detection history for specific drone', async () => {
|
||||
const device = await createTestDevice();
|
||||
const droneId = 12345;
|
||||
|
||||
// Add multiple detections
|
||||
const detection1 = await createTestDetection({
|
||||
device_id: device.id,
|
||||
drone_id: droneId
|
||||
});
|
||||
const detection2 = await createTestDetection({
|
||||
device_id: device.id,
|
||||
drone_id: droneId
|
||||
});
|
||||
|
||||
trackingService.trackDetection(detection1);
|
||||
trackingService.trackDetection(detection2);
|
||||
|
||||
const history = trackingService.getDroneHistory(droneId);
|
||||
|
||||
expect(history).to.exist;
|
||||
expect(history.detectionHistory).to.have.length(2);
|
||||
expect(history.droneId).to.equal(droneId);
|
||||
});
|
||||
|
||||
it('should return null for unknown drone', () => {
|
||||
const history = trackingService.getDroneHistory(99999);
|
||||
expect(history).to.be.null;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Distance and Speed Calculations', () => {
|
||||
it('should calculate distance between coordinates correctly', () => {
|
||||
const lat1 = 59.3293;
|
||||
const lon1 = 18.0686;
|
||||
const lat2 = 59.3393; // ~1.1km north
|
||||
const lon2 = 18.0686;
|
||||
|
||||
const distance = trackingService.calculateDistance(lat1, lon1, lat2, lon2);
|
||||
|
||||
expect(distance).to.be.closeTo(1110, 50); // ~1.1km in meters
|
||||
});
|
||||
|
||||
it('should calculate bearing correctly', () => {
|
||||
const lat1 = 59.3293;
|
||||
const lon1 = 18.0686;
|
||||
const lat2 = 59.3393; // North
|
||||
const lon2 = 18.0686;
|
||||
|
||||
const bearing = trackingService.calculateBearing(lat1, lon1, lat2, lon2);
|
||||
|
||||
expect(bearing).to.be.closeTo(0, 5); // Should be close to 0 degrees (north)
|
||||
});
|
||||
|
||||
it('should handle speed calculations with time differences', () => {
|
||||
const pos1 = {
|
||||
lat: 59.3293,
|
||||
lon: 18.0686,
|
||||
timestamp: new Date('2023-01-01T10:00:00Z')
|
||||
};
|
||||
|
||||
const pos2 = {
|
||||
lat: 59.3393,
|
||||
lon: 18.0686,
|
||||
timestamp: new Date('2023-01-01T10:01:00Z') // 1 minute later
|
||||
};
|
||||
|
||||
const speed = trackingService.calculateSpeed(pos1, pos2);
|
||||
|
||||
expect(speed).to.be.greaterThan(60); // Should be ~66 km/h (1.1km in 1 min)
|
||||
});
|
||||
});
|
||||
|
||||
describe('Threat Level Assessment', () => {
|
||||
it('should assess higher threat for approaching drones', async () => {
|
||||
const device = await createTestDevice();
|
||||
const droneId = 12345;
|
||||
|
||||
// Simulate approaching drone
|
||||
const detections = [
|
||||
{ rssi: -80, server_timestamp: new Date(Date.now() - 180000) },
|
||||
{ rssi: -70, server_timestamp: new Date(Date.now() - 120000) },
|
||||
{ rssi: -60, server_timestamp: new Date(Date.now() - 60000) },
|
||||
{ rssi: -50, server_timestamp: new Date() }
|
||||
];
|
||||
|
||||
for (const detection of detections) {
|
||||
trackingService.trackDetection({
|
||||
drone_id: droneId,
|
||||
device_id: device.id,
|
||||
geo_lat: 59.3293,
|
||||
geo_lon: 18.0686,
|
||||
...detection
|
||||
});
|
||||
}
|
||||
|
||||
const droneData = trackingService.activeDrones.get(droneId);
|
||||
expect(droneData.threatAssessment.level).to.be.oneOf(['high', 'critical']);
|
||||
});
|
||||
|
||||
it('should assess lower threat for retreating drones', async () => {
|
||||
const device = await createTestDevice();
|
||||
const droneId = 12345;
|
||||
|
||||
// Simulate retreating drone
|
||||
const detections = [
|
||||
{ rssi: -50, server_timestamp: new Date(Date.now() - 180000) },
|
||||
{ rssi: -60, server_timestamp: new Date(Date.now() - 120000) },
|
||||
{ rssi: -70, server_timestamp: new Date(Date.now() - 60000) },
|
||||
{ rssi: -80, server_timestamp: new Date() }
|
||||
];
|
||||
|
||||
for (const detection of detections) {
|
||||
trackingService.trackDetection({
|
||||
drone_id: droneId,
|
||||
device_id: device.id,
|
||||
geo_lat: 59.3293,
|
||||
geo_lon: 18.0686,
|
||||
...detection
|
||||
});
|
||||
}
|
||||
|
||||
const droneData = trackingService.activeDrones.get(droneId);
|
||||
expect(droneData.threatAssessment.level).to.be.oneOf(['low', 'medium']);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user