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 }); 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 }; 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', invalid_field: 'test' // Invalid field } ]; 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'); }); }); }); });