From 35cb55ab2055e1eed39548ba87d5219c774a9930 Mon Sep 17 00:00:00 2001 From: Alexander Borg Date: Sun, 14 Sep 2025 21:17:12 +0200 Subject: [PATCH] Fix jwt-token --- .../advanced/detectionProcessing.test.js | 797 ++++++++++++++++++ server/tests/index.test.js | 10 +- server/tests/package.json | 60 +- server/tests/routes/device.test.js | 584 +++++++++++++ server/tests/routes/healthcheck.test.js | 592 +++++++++++++ 5 files changed, 2012 insertions(+), 31 deletions(-) create mode 100644 server/tests/advanced/detectionProcessing.test.js create mode 100644 server/tests/routes/device.test.js create mode 100644 server/tests/routes/healthcheck.test.js diff --git a/server/tests/advanced/detectionProcessing.test.js b/server/tests/advanced/detectionProcessing.test.js new file mode 100644 index 0000000..1b2b325 --- /dev/null +++ b/server/tests/advanced/detectionProcessing.test.js @@ -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'); + } + }); + }); +}); diff --git a/server/tests/index.test.js b/server/tests/index.test.js index 29a2805..d99c50a 100644 --- a/server/tests/index.test.js +++ b/server/tests/index.test.js @@ -13,6 +13,8 @@ require('./middleware/validation.test'); require('./routes/auth.test'); require('./routes/detectors.test'); require('./routes/detections.test'); +require('./routes/device.test'); +require('./routes/healthcheck.test'); require('./services/alertService.test'); require('./services/droneTrackingService.test'); @@ -28,6 +30,7 @@ require('./models/heartbeat.test'); require('./utils/droneTypes.test'); require('./integration/workflows.test'); +require('./advanced/detectionProcessing.test'); require('./performance/load.test'); require('./security/vulnerabilities.test'); @@ -120,6 +123,8 @@ describe('Test Suite Summary', () => { 'routes/auth.test.js', 'routes/detectors.test.js', 'routes/detections.test.js', + 'routes/device.test.js', + 'routes/healthcheck.test.js', // Service tests 'services/alertService.test.js', @@ -140,6 +145,9 @@ describe('Test Suite Summary', () => { // Integration tests 'integration/workflows.test.js', + // Advanced processing tests + 'advanced/detectionProcessing.test.js', + // Performance tests 'performance/load.test.js', @@ -148,7 +156,7 @@ describe('Test Suite Summary', () => { ]; // 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`); }); diff --git a/server/tests/package.json b/server/tests/package.json index 10c09a4..e5e4776 100644 --- a/server/tests/package.json +++ b/server/tests/package.json @@ -3,36 +3,36 @@ "version": "1.0.0", "description": "Comprehensive test suite for UAM-ILS drone detection system", "scripts": { - "test": "mocha \"tests/**/*.test.js\" --recursive --timeout 10000 --exit", - "test:unit": "mocha \"tests/{middleware,routes,services,models,utils}/**/*.test.js\" --recursive --timeout 5000", - "test:integration": "mocha \"tests/integration/**/*.test.js\" --timeout 15000", - "test:performance": "mocha \"tests/performance/**/*.test.js\" --timeout 30000", - "test:security": "mocha \"tests/security/**/*.test.js\" --timeout 10000", - "test:watch": "mocha \"tests/**/*.test.js\" --recursive --watch", - "test:coverage": "nyc mocha \"tests/**/*.test.js\" --recursive --timeout 10000", - "test:middleware": "mocha \"tests/middleware/**/*.test.js\" --recursive", - "test:routes": "mocha \"tests/routes/**/*.test.js\" --recursive", - "test:services": "mocha \"tests/services/**/*.test.js\" --recursive", - "test:models": "mocha \"tests/models/**/*.test.js\" --recursive", - "test:utils": "mocha \"tests/utils/**/*.test.js\" --recursive", - "test:auth": "mocha \"tests/{middleware/auth*,routes/auth*}/**/*.test.js\" --recursive", - "test:tenant": "mocha \"tests/**/*tenant*.test.js\" --recursive", - "test:detection": "mocha \"tests/**/*{detection,detector}*.test.js\" --recursive", - "test:alerts": "mocha \"tests/**/*alert*.test.js\" --recursive", - "test:devices": "mocha \"tests/**/*device*.test.js\" --recursive", - "test:tracking": "mocha \"tests/**/*tracking*.test.js\" --recursive", - "test:validation": "mocha \"tests/**/*validation*.test.js\" --recursive", - "test:rbac": "mocha \"tests/**/*rbac*.test.js\" --recursive", - "test:security-full": "mocha \"tests/{security,middleware/auth*,middleware/rbac*,middleware/ip*}/**/*.test.js\" --recursive", - "test:db": "mocha \"tests/models/**/*.test.js\" --recursive", - "test:api": "mocha \"tests/routes/**/*.test.js\" --recursive --timeout 8000", - "test:business-logic": "mocha \"tests/services/**/*.test.js\" --recursive", - "test:workflows": "mocha \"tests/integration/workflows.test.js\" --timeout 15000", - "test:load": "mocha \"tests/performance/load.test.js\" --timeout 30000", - "test:vulnerabilities": "mocha \"tests/security/vulnerabilities.test.js\" --timeout 10000", - "test:summary": "mocha \"tests/index.test.js\"", - "test:quick": "mocha \"tests/{models,utils}/**/*.test.js\" --recursive --timeout 3000", - "test:critical": "mocha \"tests/{middleware/auth*,routes/auth*,services,security}/**/*.test.js\" --recursive --timeout 10000" + "test": "mocha \"**/*.test.js\" --recursive --timeout 10000 --exit", + "test:unit": "mocha \"{middleware,routes,services,models,utils}/**/*.test.js\" --recursive --timeout 5000", + "test:integration": "mocha \"integration/**/*.test.js\" --timeout 15000", + "test:performance": "mocha \"performance/**/*.test.js\" --timeout 30000", + "test:security": "mocha \"security/**/*.test.js\" --timeout 10000", + "test:watch": "mocha \"**/*.test.js\" --recursive --watch", + "test:coverage": "nyc mocha \"**/*.test.js\" --recursive --timeout 10000", + "test:middleware": "mocha \"middleware/**/*.test.js\" --recursive", + "test:routes": "mocha \"routes/**/*.test.js\" --recursive", + "test:services": "mocha \"services/**/*.test.js\" --recursive", + "test:models": "mocha \"models/**/*.test.js\" --recursive", + "test:utils": "mocha \"utils/**/*.test.js\" --recursive", + "test:auth": "mocha \"{middleware/auth*,routes/auth*}/**/*.test.js\" --recursive", + "test:tenant": "mocha \"**/*tenant*.test.js\" --recursive", + "test:detection": "mocha \"**/*{detection,detector}*.test.js\" --recursive", + "test:alerts": "mocha \"**/*alert*.test.js\" --recursive", + "test:devices": "mocha \"**/*device*.test.js\" --recursive", + "test:tracking": "mocha \"**/*tracking*.test.js\" --recursive", + "test:validation": "mocha \"**/*validation*.test.js\" --recursive", + "test:rbac": "mocha \"**/*rbac*.test.js\" --recursive", + "test:security-full": "mocha \"{security,middleware/auth*,middleware/rbac*,middleware/ip*}/**/*.test.js\" --recursive", + "test:db": "mocha \"models/**/*.test.js\" --recursive", + "test:api": "mocha \"routes/**/*.test.js\" --recursive --timeout 8000", + "test:business-logic": "mocha \"services/**/*.test.js\" --recursive", + "test:workflows": "mocha \"integration/workflows.test.js\" --timeout 15000", + "test:load": "mocha \"performance/load.test.js\" --timeout 30000", + "test:vulnerabilities": "mocha \"security/vulnerabilities.test.js\" --timeout 10000", + "test:summary": "mocha \"index.test.js\"", + "test:quick": "mocha \"{models,utils}/**/*.test.js\" --recursive --timeout 3000", + "test:critical": "mocha \"{middleware/auth*,routes/auth*,services,security}/**/*.test.js\" --recursive --timeout 10000" }, "devDependencies": { "mocha": "^10.2.0", diff --git a/server/tests/routes/device.test.js b/server/tests/routes/device.test.js new file mode 100644 index 0000000..a5a4d5a --- /dev/null +++ b/server/tests/routes/device.test.js @@ -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; + }); + }); +}); diff --git a/server/tests/routes/healthcheck.test.js b/server/tests/routes/healthcheck.test.js new file mode 100644 index 0000000..16d9536 --- /dev/null +++ b/server/tests/routes/healthcheck.test.js @@ -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'); + }); + }); + }); +});