Fix jwt-token
This commit is contained in:
797
server/tests/advanced/detectionProcessing.test.js
Normal file
797
server/tests/advanced/detectionProcessing.test.js
Normal file
@@ -0,0 +1,797 @@
|
|||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
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,
|
||||||
|
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');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,6 +13,8 @@ require('./middleware/validation.test');
|
|||||||
require('./routes/auth.test');
|
require('./routes/auth.test');
|
||||||
require('./routes/detectors.test');
|
require('./routes/detectors.test');
|
||||||
require('./routes/detections.test');
|
require('./routes/detections.test');
|
||||||
|
require('./routes/device.test');
|
||||||
|
require('./routes/healthcheck.test');
|
||||||
|
|
||||||
require('./services/alertService.test');
|
require('./services/alertService.test');
|
||||||
require('./services/droneTrackingService.test');
|
require('./services/droneTrackingService.test');
|
||||||
@@ -28,6 +30,7 @@ require('./models/heartbeat.test');
|
|||||||
require('./utils/droneTypes.test');
|
require('./utils/droneTypes.test');
|
||||||
|
|
||||||
require('./integration/workflows.test');
|
require('./integration/workflows.test');
|
||||||
|
require('./advanced/detectionProcessing.test');
|
||||||
require('./performance/load.test');
|
require('./performance/load.test');
|
||||||
require('./security/vulnerabilities.test');
|
require('./security/vulnerabilities.test');
|
||||||
|
|
||||||
@@ -120,6 +123,8 @@ describe('Test Suite Summary', () => {
|
|||||||
'routes/auth.test.js',
|
'routes/auth.test.js',
|
||||||
'routes/detectors.test.js',
|
'routes/detectors.test.js',
|
||||||
'routes/detections.test.js',
|
'routes/detections.test.js',
|
||||||
|
'routes/device.test.js',
|
||||||
|
'routes/healthcheck.test.js',
|
||||||
|
|
||||||
// Service tests
|
// Service tests
|
||||||
'services/alertService.test.js',
|
'services/alertService.test.js',
|
||||||
@@ -140,6 +145,9 @@ describe('Test Suite Summary', () => {
|
|||||||
// Integration tests
|
// Integration tests
|
||||||
'integration/workflows.test.js',
|
'integration/workflows.test.js',
|
||||||
|
|
||||||
|
// Advanced processing tests
|
||||||
|
'advanced/detectionProcessing.test.js',
|
||||||
|
|
||||||
// Performance tests
|
// Performance tests
|
||||||
'performance/load.test.js',
|
'performance/load.test.js',
|
||||||
|
|
||||||
@@ -148,7 +156,7 @@ describe('Test Suite Summary', () => {
|
|||||||
];
|
];
|
||||||
|
|
||||||
// In a real environment, you would check if these files exist
|
// In a real environment, you would check if these files exist
|
||||||
expect(expectedTestFiles.length).to.be.greaterThan(20);
|
expect(expectedTestFiles.length).to.be.greaterThan(23);
|
||||||
console.log(`✅ Found ${expectedTestFiles.length} test files covering all system components`);
|
console.log(`✅ Found ${expectedTestFiles.length} test files covering all system components`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -3,36 +3,36 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "Comprehensive test suite for UAM-ILS drone detection system",
|
"description": "Comprehensive test suite for UAM-ILS drone detection system",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "mocha \"tests/**/*.test.js\" --recursive --timeout 10000 --exit",
|
"test": "mocha \"**/*.test.js\" --recursive --timeout 10000 --exit",
|
||||||
"test:unit": "mocha \"tests/{middleware,routes,services,models,utils}/**/*.test.js\" --recursive --timeout 5000",
|
"test:unit": "mocha \"{middleware,routes,services,models,utils}/**/*.test.js\" --recursive --timeout 5000",
|
||||||
"test:integration": "mocha \"tests/integration/**/*.test.js\" --timeout 15000",
|
"test:integration": "mocha \"integration/**/*.test.js\" --timeout 15000",
|
||||||
"test:performance": "mocha \"tests/performance/**/*.test.js\" --timeout 30000",
|
"test:performance": "mocha \"performance/**/*.test.js\" --timeout 30000",
|
||||||
"test:security": "mocha \"tests/security/**/*.test.js\" --timeout 10000",
|
"test:security": "mocha \"security/**/*.test.js\" --timeout 10000",
|
||||||
"test:watch": "mocha \"tests/**/*.test.js\" --recursive --watch",
|
"test:watch": "mocha \"**/*.test.js\" --recursive --watch",
|
||||||
"test:coverage": "nyc mocha \"tests/**/*.test.js\" --recursive --timeout 10000",
|
"test:coverage": "nyc mocha \"**/*.test.js\" --recursive --timeout 10000",
|
||||||
"test:middleware": "mocha \"tests/middleware/**/*.test.js\" --recursive",
|
"test:middleware": "mocha \"middleware/**/*.test.js\" --recursive",
|
||||||
"test:routes": "mocha \"tests/routes/**/*.test.js\" --recursive",
|
"test:routes": "mocha \"routes/**/*.test.js\" --recursive",
|
||||||
"test:services": "mocha \"tests/services/**/*.test.js\" --recursive",
|
"test:services": "mocha \"services/**/*.test.js\" --recursive",
|
||||||
"test:models": "mocha \"tests/models/**/*.test.js\" --recursive",
|
"test:models": "mocha \"models/**/*.test.js\" --recursive",
|
||||||
"test:utils": "mocha \"tests/utils/**/*.test.js\" --recursive",
|
"test:utils": "mocha \"utils/**/*.test.js\" --recursive",
|
||||||
"test:auth": "mocha \"tests/{middleware/auth*,routes/auth*}/**/*.test.js\" --recursive",
|
"test:auth": "mocha \"{middleware/auth*,routes/auth*}/**/*.test.js\" --recursive",
|
||||||
"test:tenant": "mocha \"tests/**/*tenant*.test.js\" --recursive",
|
"test:tenant": "mocha \"**/*tenant*.test.js\" --recursive",
|
||||||
"test:detection": "mocha \"tests/**/*{detection,detector}*.test.js\" --recursive",
|
"test:detection": "mocha \"**/*{detection,detector}*.test.js\" --recursive",
|
||||||
"test:alerts": "mocha \"tests/**/*alert*.test.js\" --recursive",
|
"test:alerts": "mocha \"**/*alert*.test.js\" --recursive",
|
||||||
"test:devices": "mocha \"tests/**/*device*.test.js\" --recursive",
|
"test:devices": "mocha \"**/*device*.test.js\" --recursive",
|
||||||
"test:tracking": "mocha \"tests/**/*tracking*.test.js\" --recursive",
|
"test:tracking": "mocha \"**/*tracking*.test.js\" --recursive",
|
||||||
"test:validation": "mocha \"tests/**/*validation*.test.js\" --recursive",
|
"test:validation": "mocha \"**/*validation*.test.js\" --recursive",
|
||||||
"test:rbac": "mocha \"tests/**/*rbac*.test.js\" --recursive",
|
"test:rbac": "mocha \"**/*rbac*.test.js\" --recursive",
|
||||||
"test:security-full": "mocha \"tests/{security,middleware/auth*,middleware/rbac*,middleware/ip*}/**/*.test.js\" --recursive",
|
"test:security-full": "mocha \"{security,middleware/auth*,middleware/rbac*,middleware/ip*}/**/*.test.js\" --recursive",
|
||||||
"test:db": "mocha \"tests/models/**/*.test.js\" --recursive",
|
"test:db": "mocha \"models/**/*.test.js\" --recursive",
|
||||||
"test:api": "mocha \"tests/routes/**/*.test.js\" --recursive --timeout 8000",
|
"test:api": "mocha \"routes/**/*.test.js\" --recursive --timeout 8000",
|
||||||
"test:business-logic": "mocha \"tests/services/**/*.test.js\" --recursive",
|
"test:business-logic": "mocha \"services/**/*.test.js\" --recursive",
|
||||||
"test:workflows": "mocha \"tests/integration/workflows.test.js\" --timeout 15000",
|
"test:workflows": "mocha \"integration/workflows.test.js\" --timeout 15000",
|
||||||
"test:load": "mocha \"tests/performance/load.test.js\" --timeout 30000",
|
"test:load": "mocha \"performance/load.test.js\" --timeout 30000",
|
||||||
"test:vulnerabilities": "mocha \"tests/security/vulnerabilities.test.js\" --timeout 10000",
|
"test:vulnerabilities": "mocha \"security/vulnerabilities.test.js\" --timeout 10000",
|
||||||
"test:summary": "mocha \"tests/index.test.js\"",
|
"test:summary": "mocha \"index.test.js\"",
|
||||||
"test:quick": "mocha \"tests/{models,utils}/**/*.test.js\" --recursive --timeout 3000",
|
"test:quick": "mocha \"{models,utils}/**/*.test.js\" --recursive --timeout 3000",
|
||||||
"test:critical": "mocha \"tests/{middleware/auth*,routes/auth*,services,security}/**/*.test.js\" --recursive --timeout 10000"
|
"test:critical": "mocha \"{middleware/auth*,routes/auth*,services,security}/**/*.test.js\" --recursive --timeout 10000"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"mocha": "^10.2.0",
|
"mocha": "^10.2.0",
|
||||||
|
|||||||
584
server/tests/routes/device.test.js
Normal file
584
server/tests/routes/device.test.js
Normal file
@@ -0,0 +1,584 @@
|
|||||||
|
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
|
||||||
|
const { expect } = require('chai');
|
||||||
|
const sinon = require('sinon');
|
||||||
|
const request = require('supertest');
|
||||||
|
const express = require('express');
|
||||||
|
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, createTestUser, createTestTenant, createTestDevice, generateTestToken } = require('../setup');
|
||||||
|
|
||||||
|
describe('Device Routes', () => {
|
||||||
|
let app, models, sequelize;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
({ models, sequelize } = await setupTestEnvironment());
|
||||||
|
|
||||||
|
// Setup express app for testing
|
||||||
|
app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Mock authentication middleware
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.headers.authorization) {
|
||||||
|
const token = req.headers.authorization.replace('Bearer ', '');
|
||||||
|
try {
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'test-secret');
|
||||||
|
req.user = { id: decoded.userId, tenant_id: decoded.tenantId };
|
||||||
|
req.tenant = { id: decoded.tenantId };
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(401).json({ success: false, message: 'Invalid token' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup device routes
|
||||||
|
const deviceRoutes = require('../../routes/device');
|
||||||
|
app.use('/devices', deviceRoutes);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await teardownTestEnvironment();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await cleanDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /devices', () => {
|
||||||
|
it('should return all devices for authenticated user', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const user = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const token = generateTestToken(user, tenant);
|
||||||
|
|
||||||
|
const device1 = await createTestDevice({
|
||||||
|
id: 123,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
name: 'Test Device 1',
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const device2 = await createTestDevice({
|
||||||
|
id: 124,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
name: 'Test Device 2',
|
||||||
|
is_approved: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.success).to.be.true;
|
||||||
|
expect(response.body.data).to.have.length(2);
|
||||||
|
|
||||||
|
const deviceIds = response.body.data.map(d => d.id);
|
||||||
|
expect(deviceIds).to.include.members([123, 124]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only return devices for user tenant', async () => {
|
||||||
|
const tenant1 = await createTestTenant({ slug: 'tenant1' });
|
||||||
|
const tenant2 = await createTestTenant({ slug: 'tenant2' });
|
||||||
|
|
||||||
|
const user1 = await createTestUser({ tenant_id: tenant1.id });
|
||||||
|
const user2 = await createTestUser({ tenant_id: tenant2.id });
|
||||||
|
|
||||||
|
await createTestDevice({ id: 111, tenant_id: tenant1.id });
|
||||||
|
await createTestDevice({ id: 222, tenant_id: tenant2.id });
|
||||||
|
|
||||||
|
const token1 = generateTestToken(user1, tenant1);
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/devices')
|
||||||
|
.set('Authorization', `Bearer ${token1}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.data).to.have.length(1);
|
||||||
|
expect(response.body.data[0].id).to.equal(111);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/devices');
|
||||||
|
|
||||||
|
expect(response.status).to.equal(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include device statistics', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const user = await createTestUser({ tenant_id: tenant.id });
|
||||||
|
const device = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
const token = generateTestToken(user, tenant);
|
||||||
|
|
||||||
|
// Create some detections
|
||||||
|
await models.DroneDetection.bulkCreate([
|
||||||
|
{
|
||||||
|
device_id: device.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
geo_lat: 59.3293,
|
||||||
|
geo_lon: 18.0686,
|
||||||
|
device_timestamp: new Date(Date.now() - 3600000), // 1 hour ago
|
||||||
|
drone_type: 2,
|
||||||
|
threat_level: 'medium'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
device_id: device.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
geo_lat: 59.3294,
|
||||||
|
geo_lon: 18.0687,
|
||||||
|
device_timestamp: new Date(Date.now() - 1800000), // 30 min ago
|
||||||
|
drone_type: 3,
|
||||||
|
threat_level: 'high'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
const deviceData = response.body.data[0];
|
||||||
|
expect(deviceData.recent_detections_count).to.be.greaterThan(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /devices/:id', () => {
|
||||||
|
it('should return specific device details', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const user = await createTestUser({ tenant_id: tenant.id });
|
||||||
|
const device = await createTestDevice({
|
||||||
|
id: 12345,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
name: 'Specific Device',
|
||||||
|
location_description: 'Test Location'
|
||||||
|
});
|
||||||
|
const token = generateTestToken(user, tenant);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/devices/${device.id}`)
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.success).to.be.true;
|
||||||
|
expect(response.body.data.id).to.equal(12345);
|
||||||
|
expect(response.body.data.name).to.equal('Specific Device');
|
||||||
|
expect(response.body.data.location_description).to.equal('Test Location');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not return device from different tenant', async () => {
|
||||||
|
const tenant1 = await createTestTenant();
|
||||||
|
const tenant2 = await createTestTenant();
|
||||||
|
|
||||||
|
const user1 = await createTestUser({ tenant_id: tenant1.id });
|
||||||
|
const device2 = await createTestDevice({ tenant_id: tenant2.id });
|
||||||
|
|
||||||
|
const token1 = generateTestToken(user1, tenant1);
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/devices/${device2.id}`)
|
||||||
|
.set('Authorization', `Bearer ${token1}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 404 for non-existent device', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const user = await createTestUser({ tenant_id: tenant.id });
|
||||||
|
const token = generateTestToken(user, tenant);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/devices/999999')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(404);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /devices', () => {
|
||||||
|
it('should create new device with admin role', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
role: 'admin'
|
||||||
|
});
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
const deviceData = {
|
||||||
|
id: 987654,
|
||||||
|
name: 'New Test Device',
|
||||||
|
geo_lat: 59.3293,
|
||||||
|
geo_lon: 18.0686,
|
||||||
|
location_description: 'New Device Location'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.post('/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.send(deviceData);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(201);
|
||||||
|
expect(response.body.success).to.be.true;
|
||||||
|
expect(response.body.data.id).to.equal(987654);
|
||||||
|
expect(response.body.data.name).to.equal('New Test Device');
|
||||||
|
|
||||||
|
// Verify device was saved to database
|
||||||
|
const savedDevice = await models.Device.findByPk(987654);
|
||||||
|
expect(savedDevice).to.exist;
|
||||||
|
expect(savedDevice.tenant_id).to.equal(tenant.id);
|
||||||
|
expect(savedDevice.is_approved).to.be.false; // Default value
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require admin role for device creation', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const user = await createTestUser({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
role: 'user' // Not admin
|
||||||
|
});
|
||||||
|
const token = generateTestToken(user, tenant);
|
||||||
|
|
||||||
|
const deviceData = {
|
||||||
|
id: 111111,
|
||||||
|
name: 'Unauthorized Device',
|
||||||
|
geo_lat: 59.3293,
|
||||||
|
geo_lon: 18.0686
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.post('/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.send(deviceData);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate required fields', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
role: 'admin'
|
||||||
|
});
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
const invalidPayloads = [
|
||||||
|
{}, // Missing all fields
|
||||||
|
{ name: 'No ID Device' }, // Missing device ID
|
||||||
|
{ id: 123 }, // Missing name
|
||||||
|
{ id: 123, name: 'No Location' } // Missing coordinates
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const payload of invalidPayloads) {
|
||||||
|
const response = await request(app)
|
||||||
|
.post('/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.send(payload);
|
||||||
|
|
||||||
|
expect(response.status).to.be.oneOf([400, 422]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent duplicate device IDs', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
// Create first device
|
||||||
|
await createTestDevice({ id: 555555, tenant_id: tenant.id });
|
||||||
|
|
||||||
|
// Attempt to create duplicate
|
||||||
|
const response = await request(app)
|
||||||
|
.post('/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.send({
|
||||||
|
id: 555555,
|
||||||
|
name: 'Duplicate Device',
|
||||||
|
geo_lat: 59.3293,
|
||||||
|
geo_lon: 18.0686
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).to.be.oneOf([400, 409, 422]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate coordinate ranges', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
const invalidCoordinates = [
|
||||||
|
{ geo_lat: 91, geo_lon: 18.0686 }, // Latitude too high
|
||||||
|
{ geo_lat: -91, geo_lon: 18.0686 }, // Latitude too low
|
||||||
|
{ geo_lat: 59.3293, geo_lon: 181 }, // Longitude too high
|
||||||
|
{ geo_lat: 59.3293, geo_lon: -181 }, // Longitude too low
|
||||||
|
{ geo_lat: 'invalid', geo_lon: 18.0686 }, // Invalid type
|
||||||
|
{ geo_lat: 59.3293, geo_lon: 'invalid' } // Invalid type
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const coords of invalidCoordinates) {
|
||||||
|
const response = await request(app)
|
||||||
|
.post('/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.send({
|
||||||
|
id: Math.floor(Math.random() * 1000000),
|
||||||
|
name: 'Invalid Coord Device',
|
||||||
|
...coords
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.status).to.be.oneOf([400, 422]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /devices/:id', () => {
|
||||||
|
it('should update device with admin role', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const device = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
name: 'Original Name',
|
||||||
|
is_approved: false
|
||||||
|
});
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
const updateData = {
|
||||||
|
name: 'Updated Device Name',
|
||||||
|
is_approved: true,
|
||||||
|
location_description: 'Updated Location'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.put(`/devices/${device.id}`)
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.send(updateData);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.success).to.be.true;
|
||||||
|
|
||||||
|
// Verify update in database
|
||||||
|
const updatedDevice = await models.Device.findByPk(device.id);
|
||||||
|
expect(updatedDevice.name).to.equal('Updated Device Name');
|
||||||
|
expect(updatedDevice.is_approved).to.be.true;
|
||||||
|
expect(updatedDevice.location_description).to.equal('Updated Location');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require admin role for updates', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const user = await createTestUser({ tenant_id: tenant.id, role: 'user' });
|
||||||
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
||||||
|
const token = generateTestToken(user, tenant);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.put(`/devices/${device.id}`)
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.send({ name: 'Unauthorized Update' });
|
||||||
|
|
||||||
|
expect(response.status).to.equal(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not update device from different tenant', async () => {
|
||||||
|
const tenant1 = await createTestTenant();
|
||||||
|
const tenant2 = await createTestTenant();
|
||||||
|
|
||||||
|
const admin1 = await createTestUser({ tenant_id: tenant1.id, role: 'admin' });
|
||||||
|
const device2 = await createTestDevice({ tenant_id: tenant2.id });
|
||||||
|
|
||||||
|
const token1 = generateTestToken(admin1, tenant1);
|
||||||
|
const response = await request(app)
|
||||||
|
.put(`/devices/${device2.id}`)
|
||||||
|
.set('Authorization', `Bearer ${token1}`)
|
||||||
|
.send({ name: 'Cross-tenant hack' });
|
||||||
|
|
||||||
|
expect(response.status).to.equal(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate update data', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
// Test invalid coordinate update
|
||||||
|
const response = await request(app)
|
||||||
|
.put(`/devices/${device.id}`)
|
||||||
|
.set('Authorization', `Bearer ${token}`)
|
||||||
|
.send({ geo_lat: 100 }); // Invalid latitude
|
||||||
|
|
||||||
|
expect(response.status).to.be.oneOf([400, 422]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('DELETE /devices/:id', () => {
|
||||||
|
it('should delete device with admin role', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.delete(`/devices/${device.id}`)
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.success).to.be.true;
|
||||||
|
|
||||||
|
// Verify deletion
|
||||||
|
const deletedDevice = await models.Device.findByPk(device.id);
|
||||||
|
expect(deletedDevice).to.be.null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require admin role for deletion', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const user = await createTestUser({ tenant_id: tenant.id, role: 'user' });
|
||||||
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
||||||
|
const token = generateTestToken(user, tenant);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.delete(`/devices/${device.id}`)
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not delete device from different tenant', async () => {
|
||||||
|
const tenant1 = await createTestTenant();
|
||||||
|
const tenant2 = await createTestTenant();
|
||||||
|
|
||||||
|
const admin1 = await createTestUser({ tenant_id: tenant1.id, role: 'admin' });
|
||||||
|
const device2 = await createTestDevice({ tenant_id: tenant2.id });
|
||||||
|
|
||||||
|
const token1 = generateTestToken(admin1, tenant1);
|
||||||
|
const response = await request(app)
|
||||||
|
.delete(`/devices/${device2.id}`)
|
||||||
|
.set('Authorization', `Bearer ${token1}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle deletion of device with associated data', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
// Create associated detection data
|
||||||
|
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,
|
||||||
|
threat_level: 'medium'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create associated alert logs
|
||||||
|
await models.AlertLog.create({
|
||||||
|
device_id: device.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
alert_type: 'proximity',
|
||||||
|
message: 'Test alert',
|
||||||
|
threat_level: 'medium'
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.delete(`/devices/${device.id}`)
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
|
||||||
|
// Verify device and associated data are handled properly
|
||||||
|
const deletedDevice = await models.Device.findByPk(device.id);
|
||||||
|
expect(deletedDevice).to.be.null;
|
||||||
|
|
||||||
|
// Check if associated data was also deleted (depending on cascade settings)
|
||||||
|
const associatedDetections = await models.DroneDetection.findAll({
|
||||||
|
where: { device_id: device.id }
|
||||||
|
});
|
||||||
|
const associatedAlerts = await models.AlertLog.findAll({
|
||||||
|
where: { device_id: device.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
// Depending on your cascade settings, these might be empty or still exist
|
||||||
|
// Adjust expectations based on your database schema
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Device Status and Health', () => {
|
||||||
|
it('should track device last seen timestamp', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const user = await createTestUser({ tenant_id: tenant.id });
|
||||||
|
const device = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
const token = generateTestToken(user, tenant);
|
||||||
|
|
||||||
|
// Simulate recent detection to update last seen
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get(`/devices/${device.id}`)
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
const deviceData = response.body.data;
|
||||||
|
expect(deviceData.last_seen).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate device online/offline status', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const user = await createTestUser({ tenant_id: tenant.id });
|
||||||
|
const token = generateTestToken(user, tenant);
|
||||||
|
|
||||||
|
// Create device with recent activity (online)
|
||||||
|
const onlineDevice = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await models.DroneDetection.create({
|
||||||
|
device_id: onlineDevice.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
geo_lat: 59.3293,
|
||||||
|
geo_lon: 18.0686,
|
||||||
|
device_timestamp: new Date(), // Recent
|
||||||
|
drone_type: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create device with old activity (offline)
|
||||||
|
const offlineDevice = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
|
||||||
|
await models.DroneDetection.create({
|
||||||
|
device_id: offlineDevice.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
geo_lat: 59.3293,
|
||||||
|
geo_lon: 18.0686,
|
||||||
|
device_timestamp: new Date(Date.now() - 3600000 * 24), // 24 hours ago
|
||||||
|
drone_type: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
const devices = response.body.data;
|
||||||
|
|
||||||
|
const online = devices.find(d => d.id === onlineDevice.id);
|
||||||
|
const offline = devices.find(d => d.id === offlineDevice.id);
|
||||||
|
|
||||||
|
// These assertions depend on your business logic for determining online status
|
||||||
|
expect(online).to.exist;
|
||||||
|
expect(offline).to.exist;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
592
server/tests/routes/healthcheck.test.js
Normal file
592
server/tests/routes/healthcheck.test.js
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
|
||||||
|
const { expect } = require('chai');
|
||||||
|
const sinon = require('sinon');
|
||||||
|
const request = require('supertest');
|
||||||
|
const express = require('express');
|
||||||
|
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, createTestUser, createTestTenant, createTestDevice, generateTestToken } = require('../setup');
|
||||||
|
|
||||||
|
describe('Healthcheck Routes', () => {
|
||||||
|
let app, models, sequelize;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
({ models, sequelize } = await setupTestEnvironment());
|
||||||
|
|
||||||
|
// Setup express app for testing
|
||||||
|
app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
|
||||||
|
// Mock authentication middleware for protected routes
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.headers.authorization) {
|
||||||
|
const token = req.headers.authorization.replace('Bearer ', '');
|
||||||
|
try {
|
||||||
|
const jwt = require('jsonwebtoken');
|
||||||
|
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'test-secret');
|
||||||
|
req.user = { id: decoded.userId, tenant_id: decoded.tenantId };
|
||||||
|
req.tenant = { id: decoded.tenantId };
|
||||||
|
} catch (error) {
|
||||||
|
return res.status(401).json({ success: false, message: 'Invalid token' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup healthcheck routes
|
||||||
|
const healthRoutes = require('../../routes/health');
|
||||||
|
app.use('/health', healthRoutes);
|
||||||
|
});
|
||||||
|
|
||||||
|
after(async () => {
|
||||||
|
await teardownTestEnvironment();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await cleanDatabase();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /health', () => {
|
||||||
|
it('should return basic health status', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health');
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.status).to.equal('ok');
|
||||||
|
expect(response.body.timestamp).to.exist;
|
||||||
|
expect(response.body.uptime).to.be.a('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include service version information', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health');
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.version).to.exist;
|
||||||
|
expect(response.body.service).to.equal('UAM-ILS Drone Detection System');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return health status quickly', async () => {
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health');
|
||||||
|
|
||||||
|
const responseTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(responseTime).to.be.lessThan(1000); // Should respond within 1 second
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not require authentication', async () => {
|
||||||
|
// Test without any authentication headers
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health');
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /health/detailed', () => {
|
||||||
|
it('should return detailed health information', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/detailed')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.status).to.equal('ok');
|
||||||
|
expect(response.body.checks).to.exist;
|
||||||
|
expect(response.body.checks.database).to.exist;
|
||||||
|
expect(response.body.checks.memory).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require authentication for detailed health', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/detailed');
|
||||||
|
|
||||||
|
expect(response.status).to.equal(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should check database connectivity', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/detailed')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.checks.database.status).to.equal('healthy');
|
||||||
|
expect(response.body.checks.database.responseTime).to.be.a('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include memory usage information', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/detailed')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.checks.memory.status).to.exist;
|
||||||
|
expect(response.body.checks.memory.usage).to.be.a('number');
|
||||||
|
expect(response.body.checks.memory.total).to.be.a('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require admin role for detailed health', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const user = await createTestUser({ tenant_id: tenant.id, role: 'user' });
|
||||||
|
const token = generateTestToken(user, tenant);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/detailed')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(403);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /health/devices', () => {
|
||||||
|
it('should return device health summary', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
// Create test devices with different statuses
|
||||||
|
const onlineDevice = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const offlineDevice = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create recent heartbeat for online device
|
||||||
|
await models.Heartbeat.create({
|
||||||
|
device_id: onlineDevice.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
timestamp: new Date(),
|
||||||
|
status: 'online',
|
||||||
|
cpu_usage: 25.5,
|
||||||
|
memory_usage: 60.2,
|
||||||
|
disk_usage: 45.0
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create old heartbeat for offline device
|
||||||
|
await models.Heartbeat.create({
|
||||||
|
device_id: offlineDevice.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
timestamp: new Date(Date.now() - 3600000), // 1 hour ago
|
||||||
|
status: 'offline'
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.summary).to.exist;
|
||||||
|
expect(response.body.summary.total_devices).to.be.a('number');
|
||||||
|
expect(response.body.summary.online_devices).to.be.a('number');
|
||||||
|
expect(response.body.summary.offline_devices).to.be.a('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only show devices for user tenant', async () => {
|
||||||
|
const tenant1 = await createTestTenant();
|
||||||
|
const tenant2 = await createTestTenant();
|
||||||
|
|
||||||
|
const admin1 = await createTestUser({ tenant_id: tenant1.id, role: 'admin' });
|
||||||
|
const admin2 = await createTestUser({ tenant_id: tenant2.id, role: 'admin' });
|
||||||
|
|
||||||
|
await createTestDevice({ tenant_id: tenant1.id });
|
||||||
|
await createTestDevice({ tenant_id: tenant1.id });
|
||||||
|
await createTestDevice({ tenant_id: tenant2.id });
|
||||||
|
|
||||||
|
const token1 = generateTestToken(admin1, tenant1);
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/devices')
|
||||||
|
.set('Authorization', `Bearer ${token1}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.summary.total_devices).to.equal(2); // Only tenant1 devices
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include device status details', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const device = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
await models.Heartbeat.create({
|
||||||
|
device_id: device.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
timestamp: new Date(),
|
||||||
|
status: 'online',
|
||||||
|
cpu_usage: 15.5,
|
||||||
|
memory_usage: 40.2,
|
||||||
|
disk_usage: 25.0,
|
||||||
|
temperature: 42.5
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.devices).to.be.an('array');
|
||||||
|
|
||||||
|
const deviceStatus = response.body.devices.find(d => d.id === device.id);
|
||||||
|
expect(deviceStatus).to.exist;
|
||||||
|
expect(deviceStatus.status).to.equal('online');
|
||||||
|
expect(deviceStatus.metrics).to.exist;
|
||||||
|
expect(deviceStatus.metrics.cpu_usage).to.equal(15.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate device uptime', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
// Create multiple heartbeats over time
|
||||||
|
const now = new Date();
|
||||||
|
const oneHourAgo = new Date(now.getTime() - 3600000);
|
||||||
|
const twoHoursAgo = new Date(now.getTime() - 7200000);
|
||||||
|
|
||||||
|
await models.Heartbeat.bulkCreate([
|
||||||
|
{
|
||||||
|
device_id: device.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
timestamp: twoHoursAgo,
|
||||||
|
status: 'online'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
device_id: device.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
timestamp: oneHourAgo,
|
||||||
|
status: 'online'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
device_id: device.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
timestamp: now,
|
||||||
|
status: 'online'
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
const deviceStatus = response.body.devices.find(d => d.id === device.id);
|
||||||
|
expect(deviceStatus.uptime_hours).to.be.a('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('POST /health/devices/:id/heartbeat', () => {
|
||||||
|
it('should accept heartbeat from approved device', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const device = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const heartbeatData = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'online',
|
||||||
|
cpu_usage: 25.5,
|
||||||
|
memory_usage: 60.2,
|
||||||
|
disk_usage: 45.0,
|
||||||
|
temperature: 38.5,
|
||||||
|
signal_strength: -65
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.post(`/health/devices/${device.id}/heartbeat`)
|
||||||
|
.send(heartbeatData);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.success).to.be.true;
|
||||||
|
|
||||||
|
// Verify heartbeat was saved
|
||||||
|
const savedHeartbeat = await models.Heartbeat.findOne({
|
||||||
|
where: { device_id: device.id },
|
||||||
|
order: [['id', 'DESC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(savedHeartbeat).to.exist;
|
||||||
|
expect(savedHeartbeat.status).to.equal('online');
|
||||||
|
expect(savedHeartbeat.cpu_usage).to.equal(25.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject heartbeat from unapproved device', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const device = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const heartbeatData = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'online'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.post(`/health/devices/${device.id}/heartbeat`)
|
||||||
|
.send(heartbeatData);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(403);
|
||||||
|
expect(response.body.approval_required).to.be.true;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject heartbeat from non-existent device', async () => {
|
||||||
|
const heartbeatData = {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'online'
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.post('/health/devices/999999/heartbeat')
|
||||||
|
.send(heartbeatData);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(404);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate heartbeat data format', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const device = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const invalidPayloads = [
|
||||||
|
{}, // Missing required fields
|
||||||
|
{ status: 'invalid_status' }, // Invalid status
|
||||||
|
{ timestamp: 'invalid_date', status: 'online' }, // Invalid timestamp
|
||||||
|
{
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'online',
|
||||||
|
cpu_usage: 150 // Invalid percentage (>100)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'online',
|
||||||
|
temperature: -50 // Unrealistic temperature
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const payload of invalidPayloads) {
|
||||||
|
const response = await request(app)
|
||||||
|
.post(`/health/devices/${device.id}/heartbeat`)
|
||||||
|
.send(payload);
|
||||||
|
|
||||||
|
expect(response.status).to.be.oneOf([400, 422]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle device status changes', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const device = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send online heartbeat
|
||||||
|
await request(app)
|
||||||
|
.post(`/health/devices/${device.id}/heartbeat`)
|
||||||
|
.send({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'online',
|
||||||
|
cpu_usage: 25.5
|
||||||
|
});
|
||||||
|
|
||||||
|
// Send offline heartbeat
|
||||||
|
const offlineResponse = await request(app)
|
||||||
|
.post(`/health/devices/${device.id}/heartbeat`)
|
||||||
|
.send({
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
status: 'offline'
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(offlineResponse.status).to.equal(200);
|
||||||
|
|
||||||
|
// Verify status change was recorded
|
||||||
|
const heartbeats = await models.Heartbeat.findAll({
|
||||||
|
where: { device_id: device.id },
|
||||||
|
order: [['timestamp', 'ASC']]
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(heartbeats).to.have.length(2);
|
||||||
|
expect(heartbeats[0].status).to.equal('online');
|
||||||
|
expect(heartbeats[1].status).to.equal('offline');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle high-frequency heartbeats efficiently', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const device = await createTestDevice({
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
is_approved: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const heartbeatPromises = [];
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
|
// Send 10 heartbeats rapidly
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const promise = request(app)
|
||||||
|
.post(`/health/devices/${device.id}/heartbeat`)
|
||||||
|
.send({
|
||||||
|
timestamp: new Date(startTime + i * 1000).toISOString(),
|
||||||
|
status: 'online',
|
||||||
|
cpu_usage: 20 + i,
|
||||||
|
sequence: i
|
||||||
|
});
|
||||||
|
|
||||||
|
heartbeatPromises.push(promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses = await Promise.all(heartbeatPromises);
|
||||||
|
|
||||||
|
// All should succeed
|
||||||
|
responses.forEach(response => {
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify all heartbeats were saved
|
||||||
|
const savedHeartbeats = await models.Heartbeat.findAll({
|
||||||
|
where: { device_id: device.id }
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(savedHeartbeats).to.have.length(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GET /health/metrics', () => {
|
||||||
|
it('should return system metrics for admin', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/metrics')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.system).to.exist;
|
||||||
|
expect(response.body.system.memory).to.exist;
|
||||||
|
expect(response.body.system.cpu).to.exist;
|
||||||
|
expect(response.body.database).to.exist;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require admin role for metrics', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const user = await createTestUser({ tenant_id: tenant.id, role: 'user' });
|
||||||
|
const token = generateTestToken(user, tenant);
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/metrics')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include database performance metrics', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
// Generate some database activity
|
||||||
|
await createTestDevice({ tenant_id: tenant.id });
|
||||||
|
await models.DroneDetection.create({
|
||||||
|
device_id: 123,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
geo_lat: 59.3293,
|
||||||
|
geo_lon: 18.0686,
|
||||||
|
device_timestamp: new Date(),
|
||||||
|
drone_type: 2
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/metrics')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.database.connection_pool).to.exist;
|
||||||
|
expect(response.body.statistics).to.exist;
|
||||||
|
expect(response.body.statistics.total_devices).to.be.a('number');
|
||||||
|
expect(response.body.statistics.total_detections).to.be.a('number');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Health Check Edge Cases', () => {
|
||||||
|
it('should handle database connection failures gracefully', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
// Mock database failure
|
||||||
|
const originalAuthenticate = sequelize.authenticate;
|
||||||
|
sequelize.authenticate = sinon.stub().rejects(new Error('Database connection failed'));
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/detailed')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200); // Should still respond
|
||||||
|
expect(response.body.checks.database.status).to.equal('unhealthy');
|
||||||
|
|
||||||
|
// Restore original method
|
||||||
|
sequelize.authenticate = originalAuthenticate;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should detect when devices are not reporting', async () => {
|
||||||
|
const tenant = await createTestTenant();
|
||||||
|
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||||
|
const device = await createTestDevice({ tenant_id: tenant.id });
|
||||||
|
const token = generateTestToken(admin, tenant);
|
||||||
|
|
||||||
|
// Create old heartbeat (device went silent)
|
||||||
|
await models.Heartbeat.create({
|
||||||
|
device_id: device.id,
|
||||||
|
tenant_id: tenant.id,
|
||||||
|
timestamp: new Date(Date.now() - 7200000), // 2 hours ago
|
||||||
|
status: 'online'
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await request(app)
|
||||||
|
.get('/health/devices')
|
||||||
|
.set('Authorization', `Bearer ${token}`);
|
||||||
|
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
const deviceStatus = response.body.devices.find(d => d.id === device.id);
|
||||||
|
expect(deviceStatus.status).to.be.oneOf(['offline', 'stale', 'unknown']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle concurrent health checks efficiently', async () => {
|
||||||
|
const healthPromises = [];
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const promise = request(app).get('/health');
|
||||||
|
healthPromises.push(promise);
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses = await Promise.all(healthPromises);
|
||||||
|
|
||||||
|
responses.forEach(response => {
|
||||||
|
expect(response.status).to.equal(200);
|
||||||
|
expect(response.body.status).to.equal('ok');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user