Fix jwt-token

This commit is contained in:
2025-09-14 21:17:12 +02:00
parent 019eb8c2b2
commit 35cb55ab20
5 changed files with 2012 additions and 31 deletions

View 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');
}
});
});
});

View File

@@ -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`);
}); });

View File

@@ -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",

View 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;
});
});
});

View 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');
});
});
});
});