807 lines
28 KiB
JavaScript
807 lines
28 KiB
JavaScript
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
|
|
const { expect } = require('chai');
|
|
const sinon = require('sinon');
|
|
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, createTestUser, createTestTenant, createTestDevice, generateTestToken } = require('../setup');
|
|
|
|
describe('Drone Detection Advanced Processing', () => {
|
|
let models, sequelize;
|
|
|
|
before(async () => {
|
|
({ models, sequelize } = await setupTestEnvironment());
|
|
});
|
|
|
|
after(async () => {
|
|
await teardownTestEnvironment();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await cleanDatabase();
|
|
});
|
|
|
|
describe('Detection Pattern Analysis', () => {
|
|
it('should analyze drone movement patterns', async () => {
|
|
const tenant = await createTestTenant();
|
|
const device = await createTestDevice({
|
|
tenant_id: tenant.id,
|
|
is_approved: true,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686
|
|
});
|
|
|
|
const droneId = 12345;
|
|
const detections = [];
|
|
|
|
// Create a pattern of detections showing drone movement
|
|
const baseTime = Date.now();
|
|
const movementPattern = [
|
|
{ lat: 59.3293, lon: 18.0686, rssi: -80, distance: 1000 },
|
|
{ lat: 59.3295, lon: 18.0688, rssi: -75, distance: 800 },
|
|
{ lat: 59.3297, lon: 18.0690, rssi: -70, distance: 600 },
|
|
{ lat: 59.3299, lon: 18.0692, rssi: -65, distance: 400 },
|
|
{ lat: 59.3301, lon: 18.0694, rssi: -60, distance: 200 }
|
|
];
|
|
|
|
for (let i = 0; i < movementPattern.length; i++) {
|
|
const point = movementPattern[i];
|
|
const detection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: point.lat,
|
|
geo_lon: point.lon,
|
|
device_timestamp: new Date(baseTime + i * 30000), // 30 seconds apart
|
|
drone_type: 2,
|
|
rssi: point.rssi,
|
|
freq: 2400,
|
|
drone_id: droneId,
|
|
threat_level: 'medium'
|
|
});
|
|
detections.push(detection);
|
|
}
|
|
|
|
// Analyze movement pattern
|
|
const analyzeMovementPattern = (detections) => {
|
|
if (detections.length < 2) return null;
|
|
|
|
const sortedDetections = detections.sort((a, b) =>
|
|
new Date(a.device_timestamp) - new Date(b.device_timestamp)
|
|
);
|
|
|
|
let totalDistance = 0;
|
|
let isApproaching = true;
|
|
let averageSpeed = 0;
|
|
|
|
for (let i = 1; i < sortedDetections.length; i++) {
|
|
const prev = sortedDetections[i - 1];
|
|
const curr = sortedDetections[i];
|
|
|
|
// Calculate distance between points
|
|
const distance = calculateDistance(
|
|
prev.geo_lat, prev.geo_lon,
|
|
curr.geo_lat, curr.geo_lon
|
|
);
|
|
totalDistance += distance;
|
|
|
|
// Check if approaching (RSSI getting stronger)
|
|
if (curr.rssi <= prev.rssi) {
|
|
isApproaching = false;
|
|
}
|
|
|
|
// Calculate speed
|
|
const timeDiff = new Date(curr.device_timestamp) - new Date(prev.device_timestamp);
|
|
const speed = distance / (timeDiff / 1000); // meters per second
|
|
averageSpeed += speed;
|
|
}
|
|
|
|
averageSpeed = averageSpeed / (sortedDetections.length - 1);
|
|
|
|
return {
|
|
totalDistance,
|
|
isApproaching,
|
|
averageSpeed,
|
|
duration: new Date(sortedDetections[sortedDetections.length - 1].device_timestamp) -
|
|
new Date(sortedDetections[0].device_timestamp),
|
|
detectionCount: sortedDetections.length
|
|
};
|
|
};
|
|
|
|
const calculateDistance = (lat1, lon1, lat2, lon2) => {
|
|
const R = 6371000; // Earth's radius in meters
|
|
const φ1 = lat1 * Math.PI / 180;
|
|
const φ2 = lat2 * Math.PI / 180;
|
|
const Δφ = (lat2 - lat1) * Math.PI / 180;
|
|
const Δλ = (lon2 - lon1) * Math.PI / 180;
|
|
|
|
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
|
|
Math.cos(φ1) * Math.cos(φ2) *
|
|
Math.sin(Δλ/2) * Math.sin(Δλ/2);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
|
|
return R * c;
|
|
};
|
|
|
|
const pattern = analyzeMovementPattern(detections);
|
|
|
|
expect(pattern).to.exist;
|
|
expect(pattern.totalDistance).to.be.greaterThan(0);
|
|
expect(pattern.isApproaching).to.be.true; // RSSI strengthening
|
|
expect(pattern.averageSpeed).to.be.greaterThan(0);
|
|
expect(pattern.detectionCount).to.equal(5);
|
|
});
|
|
|
|
it('should detect stationary drone patterns', async () => {
|
|
const tenant = await createTestTenant();
|
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
|
const droneId = 67890;
|
|
|
|
// Create detections from same location (stationary drone)
|
|
const stationaryDetections = [];
|
|
for (let i = 0; i < 10; i++) {
|
|
const detection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.3293, // Same coordinates
|
|
geo_lon: 18.0686,
|
|
device_timestamp: new Date(Date.now() + i * 60000), // 1 minute apart
|
|
drone_type: 3,
|
|
rssi: -65 + Math.random() * 10 - 5, // Small RSSI variation
|
|
freq: 2400,
|
|
drone_id: droneId
|
|
});
|
|
stationaryDetections.push(detection);
|
|
}
|
|
|
|
const analyzeStationaryPattern = (detections) => {
|
|
const coordinates = detections.map(d => ({ lat: d.geo_lat, lon: d.geo_lon }));
|
|
|
|
// Calculate variance in coordinates
|
|
const latVariance = calculateVariance(coordinates.map(c => c.lat));
|
|
const lonVariance = calculateVariance(coordinates.map(c => c.lon));
|
|
|
|
const isStationary = latVariance < 0.0001 && lonVariance < 0.0001; // Very small variance
|
|
|
|
return {
|
|
isStationary,
|
|
latVariance,
|
|
lonVariance,
|
|
detectionCount: detections.length,
|
|
duration: new Date(detections[detections.length - 1].device_timestamp) -
|
|
new Date(detections[0].device_timestamp)
|
|
};
|
|
};
|
|
|
|
const calculateVariance = (values) => {
|
|
const mean = values.reduce((sum, val) => sum + val, 0) / values.length;
|
|
const squaredDiffs = values.map(val => Math.pow(val - mean, 2));
|
|
return squaredDiffs.reduce((sum, diff) => sum + diff, 0) / values.length;
|
|
};
|
|
|
|
const pattern = analyzeStationaryPattern(stationaryDetections);
|
|
|
|
expect(pattern.isStationary).to.be.true;
|
|
expect(pattern.latVariance).to.be.lessThan(0.0001);
|
|
expect(pattern.lonVariance).to.be.lessThan(0.0001);
|
|
expect(pattern.detectionCount).to.equal(10);
|
|
});
|
|
|
|
it('should identify swarm drone patterns', async () => {
|
|
const tenant = await createTestTenant();
|
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
|
|
|
// Create multiple drones detected simultaneously (swarm pattern)
|
|
const swarmDetections = [];
|
|
const baseTime = Date.now();
|
|
const swarmCenter = { lat: 59.3293, lon: 18.0686 };
|
|
|
|
for (let droneIndex = 0; droneIndex < 5; droneIndex++) {
|
|
for (let timeIndex = 0; timeIndex < 3; timeIndex++) {
|
|
const detection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: swarmCenter.lat + (Math.random() - 0.5) * 0.01, // Within 1km radius
|
|
geo_lon: swarmCenter.lon + (Math.random() - 0.5) * 0.01,
|
|
device_timestamp: new Date(baseTime + timeIndex * 10000), // 10 seconds apart
|
|
drone_type: 2,
|
|
rssi: -60 - Math.random() * 20,
|
|
freq: 2400,
|
|
drone_id: 1000 + droneIndex
|
|
});
|
|
swarmDetections.push(detection);
|
|
}
|
|
}
|
|
|
|
const detectSwarmPattern = (detections, timeWindow = 60000) => {
|
|
// Group detections by time windows
|
|
const timeGroups = {};
|
|
|
|
detections.forEach(detection => {
|
|
const timeKey = Math.floor(new Date(detection.device_timestamp).getTime() / timeWindow);
|
|
if (!timeGroups[timeKey]) {
|
|
timeGroups[timeKey] = [];
|
|
}
|
|
timeGroups[timeKey].push(detection);
|
|
});
|
|
|
|
// Find groups with multiple unique drones
|
|
const swarmGroups = Object.values(timeGroups).filter(group => {
|
|
const uniqueDrones = new Set(group.map(d => d.drone_id));
|
|
return uniqueDrones.size >= 3; // 3 or more drones detected simultaneously
|
|
});
|
|
|
|
const maxConcurrentDrones = Math.max(...Object.values(timeGroups).map(group =>
|
|
new Set(group.map(d => d.drone_id)).size
|
|
));
|
|
|
|
return {
|
|
isSwarm: swarmGroups.length > 0,
|
|
maxConcurrentDrones,
|
|
swarmGroupCount: swarmGroups.length,
|
|
totalUniqueDrones: new Set(detections.map(d => d.drone_id)).size
|
|
};
|
|
};
|
|
|
|
const swarmAnalysis = detectSwarmPattern(swarmDetections);
|
|
|
|
expect(swarmAnalysis.isSwarm).to.be.true;
|
|
expect(swarmAnalysis.maxConcurrentDrones).to.be.greaterThan(2);
|
|
expect(swarmAnalysis.totalUniqueDrones).to.equal(5);
|
|
});
|
|
});
|
|
|
|
describe('Advanced Threat Assessment', () => {
|
|
it('should assess threat level based on proximity to sensitive areas', async () => {
|
|
const tenant = await createTestTenant();
|
|
const device = await createTestDevice({
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.3293, // Stockholm coordinates
|
|
geo_lon: 18.0686,
|
|
location_description: 'Airport Security Zone'
|
|
});
|
|
|
|
// Define sensitive areas
|
|
const sensitiveAreas = [
|
|
{ name: 'Airport Runway', lat: 59.3293, lon: 18.0686, radius: 1000, threat_multiplier: 3.0 },
|
|
{ name: 'Government Building', lat: 59.3300, lon: 18.0700, radius: 500, threat_multiplier: 2.5 },
|
|
{ name: 'Power Plant', lat: 59.3280, lon: 18.0650, radius: 2000, threat_multiplier: 2.0 }
|
|
];
|
|
|
|
const assessThreatLevel = (detection, sensitiveAreas) => {
|
|
let baseThreatLevel = 1.0;
|
|
let maxThreatMultiplier = 1.0;
|
|
let proximityFactors = [];
|
|
|
|
sensitiveAreas.forEach(area => {
|
|
const distance = calculateDistance(
|
|
detection.geo_lat, detection.geo_lon,
|
|
area.lat, area.lon
|
|
);
|
|
|
|
if (distance <= area.radius) {
|
|
const proximityFactor = 1 - (distance / area.radius);
|
|
const threatIncrease = area.threat_multiplier * proximityFactor;
|
|
proximityFactors.push({
|
|
area: area.name,
|
|
distance,
|
|
proximityFactor,
|
|
threatIncrease
|
|
});
|
|
maxThreatMultiplier = Math.max(maxThreatMultiplier, threatIncrease);
|
|
}
|
|
});
|
|
|
|
const finalThreatLevel = baseThreatLevel * maxThreatMultiplier;
|
|
|
|
let threatCategory;
|
|
if (finalThreatLevel >= 2.5) threatCategory = 'critical';
|
|
else if (finalThreatLevel >= 2.0) threatCategory = 'high';
|
|
else if (finalThreatLevel >= 1.5) threatCategory = 'medium';
|
|
else threatCategory = 'low';
|
|
|
|
return {
|
|
threatLevel: finalThreatLevel,
|
|
threatCategory,
|
|
proximityFactors
|
|
};
|
|
};
|
|
|
|
const calculateDistance = (lat1, lon1, lat2, lon2) => {
|
|
const R = 6371000;
|
|
const φ1 = lat1 * Math.PI / 180;
|
|
const φ2 = lat2 * Math.PI / 180;
|
|
const Δφ = (lat2 - lat1) * Math.PI / 180;
|
|
const Δλ = (lon2 - lon1) * Math.PI / 180;
|
|
|
|
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
|
|
Math.cos(φ1) * Math.cos(φ2) *
|
|
Math.sin(Δλ/2) * Math.sin(Δλ/2);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
|
|
return R * c;
|
|
};
|
|
|
|
// Test detection near airport (high threat)
|
|
const airportDetection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.3295, // Very close to airport
|
|
geo_lon: 18.0688,
|
|
device_timestamp: new Date(),
|
|
drone_type: 2,
|
|
rssi: -45,
|
|
freq: 2400,
|
|
drone_id: 1001
|
|
});
|
|
|
|
const airportThreat = assessThreatLevel(airportDetection, sensitiveAreas);
|
|
expect(airportThreat.threatCategory).to.be.oneOf(['high', 'critical']);
|
|
expect(airportThreat.proximityFactors).to.have.length.greaterThan(0);
|
|
|
|
// Test detection far from sensitive areas (low threat)
|
|
const remoteDetection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.4000, // Far from sensitive areas
|
|
geo_lon: 18.1000,
|
|
device_timestamp: new Date(),
|
|
drone_type: 2,
|
|
rssi: -80,
|
|
freq: 2400,
|
|
drone_id: 1002
|
|
});
|
|
|
|
const remoteThreat = assessThreatLevel(remoteDetection, sensitiveAreas);
|
|
expect(remoteThreat.threatCategory).to.equal('low');
|
|
expect(remoteThreat.proximityFactors).to.have.length(0);
|
|
});
|
|
|
|
it('should factor drone type into threat assessment', async () => {
|
|
const tenant = await createTestTenant();
|
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
|
|
|
const droneTypes = require('../../utils/droneTypes');
|
|
|
|
const assessDroneThreat = (droneType, additionalFactors = {}) => {
|
|
const droneInfo = droneTypes.getDroneTypeInfo(droneType);
|
|
|
|
let threatScore = 0;
|
|
|
|
// Base threat from drone type
|
|
switch (droneInfo.threatLevel) {
|
|
case 'critical': threatScore += 4; break;
|
|
case 'high': threatScore += 3; break;
|
|
case 'medium': threatScore += 2; break;
|
|
case 'low': threatScore += 1; break;
|
|
}
|
|
|
|
// Additional factors
|
|
if (additionalFactors.strongSignal) threatScore += 1; // Close proximity
|
|
if (additionalFactors.militaryFreq) threatScore += 2; // Military frequency
|
|
if (additionalFactors.jamming) threatScore += 3; // Electronic warfare
|
|
if (additionalFactors.nightTime) threatScore += 1; // Suspicious timing
|
|
|
|
return {
|
|
baseScore: threatScore,
|
|
droneInfo,
|
|
factors: additionalFactors,
|
|
finalThreatLevel: threatScore >= 6 ? 'critical' :
|
|
threatScore >= 4 ? 'high' :
|
|
threatScore >= 2 ? 'medium' : 'low'
|
|
};
|
|
};
|
|
|
|
// Test military drone (high threat)
|
|
const militaryDetection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: new Date(),
|
|
drone_type: 2, // Orlan (military)
|
|
rssi: -45, // Strong signal
|
|
freq: 2400,
|
|
drone_id: 1003
|
|
});
|
|
|
|
const militaryThreat = assessDroneThreat(2, {
|
|
strongSignal: true,
|
|
militaryFreq: true
|
|
});
|
|
expect(militaryThreat.finalThreatLevel).to.be.oneOf(['high', 'critical']);
|
|
|
|
// Test commercial drone (lower threat)
|
|
const commercialDetection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: new Date(),
|
|
drone_type: 8, // DJI (commercial)
|
|
rssi: -75, // Weak signal
|
|
freq: 2400,
|
|
drone_id: 1004
|
|
});
|
|
|
|
const commercialThreat = assessDroneThreat(8, { strongSignal: false });
|
|
expect(commercialThreat.finalThreatLevel).to.be.oneOf(['low', 'medium']);
|
|
});
|
|
|
|
it('should assess threat escalation over time', async () => {
|
|
const tenant = await createTestTenant();
|
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
|
const droneId = 12345;
|
|
|
|
// Create escalating threat scenario
|
|
const escalationDetections = [];
|
|
const baseTime = Date.now();
|
|
|
|
const scenarios = [
|
|
{ time: 0, rssi: -80, lat: 59.3200, threat: 'low' }, // Distant
|
|
{ time: 300000, rssi: -70, lat: 59.3220, threat: 'low' }, // Approaching
|
|
{ time: 600000, rssi: -60, lat: 59.3240, threat: 'medium' }, // Closer
|
|
{ time: 900000, rssi: -50, lat: 59.3260, threat: 'medium' }, // Getting close
|
|
{ time: 1200000, rssi: -40, lat: 59.3280, threat: 'high' } // Very close
|
|
];
|
|
|
|
for (const scenario of scenarios) {
|
|
const detection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: scenario.lat,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: new Date(baseTime + scenario.time),
|
|
drone_type: 2,
|
|
rssi: scenario.rssi,
|
|
freq: 2400,
|
|
drone_id: droneId
|
|
});
|
|
escalationDetections.push(detection);
|
|
}
|
|
|
|
const analyzeEscalation = (detections) => {
|
|
const sortedDetections = detections.sort((a, b) =>
|
|
new Date(a.device_timestamp) - new Date(b.device_timestamp)
|
|
);
|
|
|
|
const escalationEvents = [];
|
|
let previousThreatLevel = 0;
|
|
|
|
sortedDetections.forEach((detection, index) => {
|
|
// Simple threat scoring based on RSSI
|
|
let currentThreatLevel;
|
|
if (detection.rssi > -50) currentThreatLevel = 3; // High
|
|
else if (detection.rssi > -65) currentThreatLevel = 2; // Medium
|
|
else currentThreatLevel = 1; // Low
|
|
|
|
if (currentThreatLevel > previousThreatLevel) {
|
|
escalationEvents.push({
|
|
timestamp: detection.device_timestamp,
|
|
fromLevel: previousThreatLevel,
|
|
toLevel: currentThreatLevel,
|
|
rssi: detection.rssi,
|
|
position: { lat: detection.geo_lat, lon: detection.geo_lon }
|
|
});
|
|
}
|
|
|
|
previousThreatLevel = currentThreatLevel;
|
|
});
|
|
|
|
return {
|
|
escalationCount: escalationEvents.length,
|
|
maxThreatLevel: Math.max(...sortedDetections.map(d => d.rssi > -50 ? 3 : d.rssi > -65 ? 2 : 1)),
|
|
escalationEvents,
|
|
totalDuration: new Date(sortedDetections[sortedDetections.length - 1].device_timestamp) -
|
|
new Date(sortedDetections[0].device_timestamp)
|
|
};
|
|
};
|
|
|
|
const escalationAnalysis = analyzeEscalation(escalationDetections);
|
|
|
|
expect(escalationAnalysis.escalationCount).to.be.greaterThan(0);
|
|
expect(escalationAnalysis.maxThreatLevel).to.equal(3);
|
|
expect(escalationAnalysis.escalationEvents).to.have.length.greaterThan(2);
|
|
});
|
|
});
|
|
|
|
describe('Detection Correlation and Clustering', () => {
|
|
it('should correlate detections from multiple devices', async () => {
|
|
const tenant = await createTestTenant();
|
|
|
|
const device1 = await createTestDevice({
|
|
id: 101,
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686
|
|
});
|
|
|
|
const device2 = await createTestDevice({
|
|
id: 102,
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.3300,
|
|
geo_lon: 18.0700
|
|
});
|
|
|
|
const droneId = 99999;
|
|
const detectionTime = new Date();
|
|
|
|
// Same drone detected by multiple devices
|
|
const detection1 = await models.DroneDetection.create({
|
|
device_id: device1.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.3295,
|
|
geo_lon: 18.0690,
|
|
device_timestamp: detectionTime,
|
|
drone_type: 2,
|
|
rssi: -60,
|
|
drone_id: droneId
|
|
});
|
|
|
|
const detection2 = await models.DroneDetection.create({
|
|
device_id: device2.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.3297,
|
|
geo_lon: 18.0692,
|
|
device_timestamp: new Date(detectionTime.getTime() + 5000), // 5 seconds later
|
|
drone_type: 2,
|
|
rssi: -65,
|
|
drone_id: droneId
|
|
});
|
|
|
|
const correlateDetections = (detections, timeWindow = 30000, maxDistance = 5000) => {
|
|
const correlationGroups = [];
|
|
|
|
detections.forEach(detection => {
|
|
let foundGroup = false;
|
|
|
|
correlationGroups.forEach(group => {
|
|
const timeMatch = group.detections.some(d =>
|
|
Math.abs(new Date(d.device_timestamp) - new Date(detection.device_timestamp)) <= timeWindow
|
|
);
|
|
|
|
const droneMatch = group.droneId === detection.drone_id;
|
|
|
|
if (timeMatch && droneMatch) {
|
|
group.detections.push(detection);
|
|
group.devices.add(detection.device_id);
|
|
foundGroup = true;
|
|
}
|
|
});
|
|
|
|
if (!foundGroup) {
|
|
correlationGroups.push({
|
|
droneId: detection.drone_id,
|
|
detections: [detection],
|
|
devices: new Set([detection.device_id])
|
|
});
|
|
}
|
|
});
|
|
|
|
return correlationGroups.map(group => ({
|
|
droneId: group.droneId,
|
|
detectionCount: group.detections.length,
|
|
deviceCount: group.devices.size,
|
|
isCorrelated: group.devices.size > 1,
|
|
timeSpan: group.detections.length > 1 ?
|
|
Math.max(...group.detections.map(d => new Date(d.device_timestamp))) -
|
|
Math.min(...group.detections.map(d => new Date(d.device_timestamp))) : 0,
|
|
detections: group.detections
|
|
}));
|
|
};
|
|
|
|
const allDetections = [detection1, detection2];
|
|
const correlations = correlateDetections(allDetections);
|
|
|
|
expect(correlations).to.have.length(1);
|
|
expect(correlations[0].isCorrelated).to.be.true;
|
|
expect(correlations[0].deviceCount).to.equal(2);
|
|
expect(correlations[0].detectionCount).to.equal(2);
|
|
});
|
|
|
|
it('should identify detection clusters in geographic areas', async () => {
|
|
const tenant = await createTestTenant();
|
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
|
|
|
// Create clustered detections (multiple drones in same area)
|
|
const clusterCenter = { lat: 59.3293, lon: 18.0686 };
|
|
const clusterDetections = [];
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
const detection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: clusterCenter.lat + (Math.random() - 0.5) * 0.005, // Within ~500m
|
|
geo_lon: clusterCenter.lon + (Math.random() - 0.5) * 0.005,
|
|
device_timestamp: new Date(Date.now() + i * 60000),
|
|
drone_type: Math.floor(Math.random() * 5) + 1,
|
|
rssi: -60 - Math.random() * 20,
|
|
drone_id: 2000 + i
|
|
});
|
|
clusterDetections.push(detection);
|
|
}
|
|
|
|
// Create isolated detection (not in cluster)
|
|
const isolatedDetection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.4000, // Far from cluster
|
|
geo_lon: 18.1000,
|
|
device_timestamp: new Date(),
|
|
drone_type: 2,
|
|
rssi: -70,
|
|
drone_id: 3000
|
|
});
|
|
|
|
const clusterDetectionsArray = [...clusterDetections, isolatedDetection];
|
|
|
|
const identifyClusters = (detections, maxClusterDistance = 1000, minClusterSize = 3) => {
|
|
const clusters = [];
|
|
const processed = new Set();
|
|
|
|
detections.forEach((detection, index) => {
|
|
if (processed.has(index)) return;
|
|
|
|
const cluster = [detection];
|
|
processed.add(index);
|
|
|
|
detections.forEach((otherDetection, otherIndex) => {
|
|
if (otherIndex === index || processed.has(otherIndex)) return;
|
|
|
|
const distance = calculateDistance(
|
|
detection.geo_lat, detection.geo_lon,
|
|
otherDetection.geo_lat, otherDetection.geo_lon
|
|
);
|
|
|
|
if (distance <= maxClusterDistance) {
|
|
cluster.push(otherDetection);
|
|
processed.add(otherIndex);
|
|
}
|
|
});
|
|
|
|
if (cluster.length >= minClusterSize) {
|
|
clusters.push({
|
|
center: {
|
|
lat: cluster.reduce((sum, d) => sum + d.geo_lat, 0) / cluster.length,
|
|
lon: cluster.reduce((sum, d) => sum + d.geo_lon, 0) / cluster.length
|
|
},
|
|
detections: cluster,
|
|
size: cluster.length,
|
|
uniqueDrones: new Set(cluster.map(d => d.drone_id)).size,
|
|
timeSpan: Math.max(...cluster.map(d => new Date(d.device_timestamp))) -
|
|
Math.min(...cluster.map(d => new Date(d.device_timestamp)))
|
|
});
|
|
}
|
|
});
|
|
|
|
return clusters;
|
|
};
|
|
|
|
const calculateDistance = (lat1, lon1, lat2, lon2) => {
|
|
const R = 6371000;
|
|
const φ1 = lat1 * Math.PI / 180;
|
|
const φ2 = lat2 * Math.PI / 180;
|
|
const Δφ = (lat2 - lat1) * Math.PI / 180;
|
|
const Δλ = (lon2 - lon1) * Math.PI / 180;
|
|
|
|
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
|
|
Math.cos(φ1) * Math.cos(φ2) *
|
|
Math.sin(Δλ/2) * Math.sin(Δλ/2);
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
|
|
|
return R * c;
|
|
};
|
|
|
|
const clusters = identifyClusters(clusterDetectionsArray);
|
|
|
|
expect(clusters).to.have.length(1); // One cluster found
|
|
expect(clusters[0].size).to.be.greaterThan(3);
|
|
expect(clusters[0].uniqueDrones).to.be.greaterThan(3);
|
|
expect(Math.abs(clusters[0].center.lat - clusterCenter.lat)).to.be.lessThan(0.01);
|
|
});
|
|
});
|
|
|
|
describe('Real-time Detection Processing', () => {
|
|
it('should handle rapid detection streams efficiently', async () => {
|
|
const tenant = await createTestTenant();
|
|
const device = await createTestDevice({
|
|
tenant_id: tenant.id,
|
|
is_approved: true
|
|
});
|
|
|
|
const processDetectionStream = async (detections) => {
|
|
const results = [];
|
|
const startTime = Date.now();
|
|
|
|
for (const detectionData of detections) {
|
|
const detection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
...detectionData
|
|
});
|
|
|
|
// Simulate real-time processing
|
|
const processingResult = {
|
|
id: detection.id,
|
|
processed_at: new Date(),
|
|
threat_assessment: detectionData.rssi > -50 ? 'high' : 'medium',
|
|
processing_time: Date.now() - startTime
|
|
};
|
|
|
|
results.push(processingResult);
|
|
}
|
|
|
|
return {
|
|
processed_count: results.length,
|
|
total_time: Date.now() - startTime,
|
|
average_processing_time: results.reduce((sum, r) => sum + r.processing_time, 0) / results.length,
|
|
results
|
|
};
|
|
};
|
|
|
|
// Generate rapid detection stream
|
|
const detectionStream = [];
|
|
for (let i = 0; i < 20; i++) {
|
|
detectionStream.push({
|
|
geo_lat: 59.3293 + (Math.random() - 0.5) * 0.01,
|
|
geo_lon: 18.0686 + (Math.random() - 0.5) * 0.01,
|
|
device_timestamp: new Date(Date.now() + i * 1000),
|
|
drone_type: 2,
|
|
rssi: -60 - Math.random() * 30,
|
|
drone_id: 4000 + Math.floor(i / 4) // Multiple detections per drone
|
|
});
|
|
}
|
|
|
|
const streamResults = await processDetectionStream(detectionStream);
|
|
|
|
expect(streamResults.processed_count).to.equal(20);
|
|
expect(streamResults.total_time).to.be.lessThan(5000); // Should process quickly
|
|
expect(streamResults.average_processing_time).to.be.a('number');
|
|
});
|
|
|
|
it('should maintain detection accuracy under load', async () => {
|
|
const tenant = await createTestTenant();
|
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
|
|
|
// Test detection accuracy with varying signal strengths
|
|
const testCases = [
|
|
{ rssi: -40, expected_accuracy: 'high', expected_range: '<100m' },
|
|
{ rssi: -60, expected_accuracy: 'medium', expected_range: '100-500m' },
|
|
{ rssi: -80, expected_accuracy: 'low', expected_range: '>500m' }
|
|
];
|
|
|
|
const assessDetectionAccuracy = (rssi) => {
|
|
let accuracy, estimatedRange;
|
|
|
|
if (rssi > -50) {
|
|
accuracy = 'high';
|
|
estimatedRange = '<100m';
|
|
} else if (rssi > -70) {
|
|
accuracy = 'medium';
|
|
estimatedRange = '100-500m';
|
|
} else {
|
|
accuracy = 'low';
|
|
estimatedRange = '>500m';
|
|
}
|
|
|
|
return { accuracy, estimatedRange, confidence: Math.max(0, 100 + rssi) };
|
|
};
|
|
|
|
for (const testCase of testCases) {
|
|
const detection = await models.DroneDetection.create({
|
|
device_id: device.id,
|
|
tenant_id: tenant.id,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: new Date(),
|
|
drone_type: 2,
|
|
rssi: testCase.rssi,
|
|
drone_id: Math.floor(Math.random() * 10000)
|
|
});
|
|
|
|
const accuracy = assessDetectionAccuracy(detection.rssi);
|
|
|
|
expect(accuracy.accuracy).to.equal(testCase.expected_accuracy);
|
|
expect(accuracy.estimatedRange).to.equal(testCase.expected_range);
|
|
expect(accuracy.confidence).to.be.a('number');
|
|
}
|
|
});
|
|
});
|
|
});
|