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