Fix jwt-token
This commit is contained in:
592
server/tests/routes/healthcheck.test.js
Normal file
592
server/tests/routes/healthcheck.test.js
Normal file
@@ -0,0 +1,592 @@
|
||||
const { describe, it, beforeEach, afterEach, before, after } = require('mocha');
|
||||
const { expect } = require('chai');
|
||||
const sinon = require('sinon');
|
||||
const request = require('supertest');
|
||||
const express = require('express');
|
||||
const { setupTestEnvironment, teardownTestEnvironment, cleanDatabase, createTestUser, createTestTenant, createTestDevice, generateTestToken } = require('../setup');
|
||||
|
||||
describe('Healthcheck Routes', () => {
|
||||
let app, models, sequelize;
|
||||
|
||||
before(async () => {
|
||||
({ models, sequelize } = await setupTestEnvironment());
|
||||
|
||||
// Setup express app for testing
|
||||
app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Mock authentication middleware for protected routes
|
||||
app.use((req, res, next) => {
|
||||
if (req.headers.authorization) {
|
||||
const token = req.headers.authorization.replace('Bearer ', '');
|
||||
try {
|
||||
const jwt = require('jsonwebtoken');
|
||||
const decoded = jwt.verify(token, process.env.JWT_SECRET || 'test-secret');
|
||||
req.user = { id: decoded.userId, tenant_id: decoded.tenantId };
|
||||
req.tenant = { id: decoded.tenantId };
|
||||
} catch (error) {
|
||||
return res.status(401).json({ success: false, message: 'Invalid token' });
|
||||
}
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Setup healthcheck routes
|
||||
const healthRoutes = require('../../routes/health');
|
||||
app.use('/health', healthRoutes);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await teardownTestEnvironment();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await cleanDatabase();
|
||||
});
|
||||
|
||||
describe('GET /health', () => {
|
||||
it('should return basic health status', async () => {
|
||||
const response = await request(app)
|
||||
.get('/health');
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.status).to.equal('ok');
|
||||
expect(response.body.timestamp).to.exist;
|
||||
expect(response.body.uptime).to.be.a('number');
|
||||
});
|
||||
|
||||
it('should include service version information', async () => {
|
||||
const response = await request(app)
|
||||
.get('/health');
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.version).to.exist;
|
||||
expect(response.body.service).to.equal('UAM-ILS Drone Detection System');
|
||||
});
|
||||
|
||||
it('should return health status quickly', async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health');
|
||||
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(responseTime).to.be.lessThan(1000); // Should respond within 1 second
|
||||
});
|
||||
|
||||
it('should not require authentication', async () => {
|
||||
// Test without any authentication headers
|
||||
const response = await request(app)
|
||||
.get('/health');
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /health/detailed', () => {
|
||||
it('should return detailed health information', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||
const token = generateTestToken(admin, tenant);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/detailed')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.status).to.equal('ok');
|
||||
expect(response.body.checks).to.exist;
|
||||
expect(response.body.checks.database).to.exist;
|
||||
expect(response.body.checks.memory).to.exist;
|
||||
});
|
||||
|
||||
it('should require authentication for detailed health', async () => {
|
||||
const response = await request(app)
|
||||
.get('/health/detailed');
|
||||
|
||||
expect(response.status).to.equal(401);
|
||||
});
|
||||
|
||||
it('should check database connectivity', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||
const token = generateTestToken(admin, tenant);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/detailed')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.checks.database.status).to.equal('healthy');
|
||||
expect(response.body.checks.database.responseTime).to.be.a('number');
|
||||
});
|
||||
|
||||
it('should include memory usage information', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||
const token = generateTestToken(admin, tenant);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/detailed')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.checks.memory.status).to.exist;
|
||||
expect(response.body.checks.memory.usage).to.be.a('number');
|
||||
expect(response.body.checks.memory.total).to.be.a('number');
|
||||
});
|
||||
|
||||
it('should require admin role for detailed health', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const user = await createTestUser({ tenant_id: tenant.id, role: 'user' });
|
||||
const token = generateTestToken(user, tenant);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/detailed')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /health/devices', () => {
|
||||
it('should return device health summary', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||
const token = generateTestToken(admin, tenant);
|
||||
|
||||
// Create test devices with different statuses
|
||||
const onlineDevice = await createTestDevice({
|
||||
tenant_id: tenant.id,
|
||||
is_approved: true
|
||||
});
|
||||
|
||||
const offlineDevice = await createTestDevice({
|
||||
tenant_id: tenant.id,
|
||||
is_approved: true
|
||||
});
|
||||
|
||||
// Create recent heartbeat for online device
|
||||
await models.Heartbeat.create({
|
||||
device_id: onlineDevice.id,
|
||||
tenant_id: tenant.id,
|
||||
timestamp: new Date(),
|
||||
status: 'online',
|
||||
cpu_usage: 25.5,
|
||||
memory_usage: 60.2,
|
||||
disk_usage: 45.0
|
||||
});
|
||||
|
||||
// Create old heartbeat for offline device
|
||||
await models.Heartbeat.create({
|
||||
device_id: offlineDevice.id,
|
||||
tenant_id: tenant.id,
|
||||
timestamp: new Date(Date.now() - 3600000), // 1 hour ago
|
||||
status: 'offline'
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/devices')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.summary).to.exist;
|
||||
expect(response.body.summary.total_devices).to.be.a('number');
|
||||
expect(response.body.summary.online_devices).to.be.a('number');
|
||||
expect(response.body.summary.offline_devices).to.be.a('number');
|
||||
});
|
||||
|
||||
it('should only show devices for user tenant', async () => {
|
||||
const tenant1 = await createTestTenant();
|
||||
const tenant2 = await createTestTenant();
|
||||
|
||||
const admin1 = await createTestUser({ tenant_id: tenant1.id, role: 'admin' });
|
||||
const admin2 = await createTestUser({ tenant_id: tenant2.id, role: 'admin' });
|
||||
|
||||
await createTestDevice({ tenant_id: tenant1.id });
|
||||
await createTestDevice({ tenant_id: tenant1.id });
|
||||
await createTestDevice({ tenant_id: tenant2.id });
|
||||
|
||||
const token1 = generateTestToken(admin1, tenant1);
|
||||
const response = await request(app)
|
||||
.get('/health/devices')
|
||||
.set('Authorization', `Bearer ${token1}`);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.summary.total_devices).to.equal(2); // Only tenant1 devices
|
||||
});
|
||||
|
||||
it('should include device status details', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||
const device = await createTestDevice({
|
||||
tenant_id: tenant.id,
|
||||
is_approved: true
|
||||
});
|
||||
const token = generateTestToken(admin, tenant);
|
||||
|
||||
await models.Heartbeat.create({
|
||||
device_id: device.id,
|
||||
tenant_id: tenant.id,
|
||||
timestamp: new Date(),
|
||||
status: 'online',
|
||||
cpu_usage: 15.5,
|
||||
memory_usage: 40.2,
|
||||
disk_usage: 25.0,
|
||||
temperature: 42.5
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/devices')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.devices).to.be.an('array');
|
||||
|
||||
const deviceStatus = response.body.devices.find(d => d.id === device.id);
|
||||
expect(deviceStatus).to.exist;
|
||||
expect(deviceStatus.status).to.equal('online');
|
||||
expect(deviceStatus.metrics).to.exist;
|
||||
expect(deviceStatus.metrics.cpu_usage).to.equal(15.5);
|
||||
});
|
||||
|
||||
it('should calculate device uptime', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||
const device = await createTestDevice({ tenant_id: tenant.id });
|
||||
const token = generateTestToken(admin, tenant);
|
||||
|
||||
// Create multiple heartbeats over time
|
||||
const now = new Date();
|
||||
const oneHourAgo = new Date(now.getTime() - 3600000);
|
||||
const twoHoursAgo = new Date(now.getTime() - 7200000);
|
||||
|
||||
await models.Heartbeat.bulkCreate([
|
||||
{
|
||||
device_id: device.id,
|
||||
tenant_id: tenant.id,
|
||||
timestamp: twoHoursAgo,
|
||||
status: 'online'
|
||||
},
|
||||
{
|
||||
device_id: device.id,
|
||||
tenant_id: tenant.id,
|
||||
timestamp: oneHourAgo,
|
||||
status: 'online'
|
||||
},
|
||||
{
|
||||
device_id: device.id,
|
||||
tenant_id: tenant.id,
|
||||
timestamp: now,
|
||||
status: 'online'
|
||||
}
|
||||
]);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/devices')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
const deviceStatus = response.body.devices.find(d => d.id === device.id);
|
||||
expect(deviceStatus.uptime_hours).to.be.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /health/devices/:id/heartbeat', () => {
|
||||
it('should accept heartbeat from approved device', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const device = await createTestDevice({
|
||||
tenant_id: tenant.id,
|
||||
is_approved: true
|
||||
});
|
||||
|
||||
const heartbeatData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'online',
|
||||
cpu_usage: 25.5,
|
||||
memory_usage: 60.2,
|
||||
disk_usage: 45.0,
|
||||
temperature: 38.5,
|
||||
signal_strength: -65
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/health/devices/${device.id}/heartbeat`)
|
||||
.send(heartbeatData);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.success).to.be.true;
|
||||
|
||||
// Verify heartbeat was saved
|
||||
const savedHeartbeat = await models.Heartbeat.findOne({
|
||||
where: { device_id: device.id },
|
||||
order: [['id', 'DESC']]
|
||||
});
|
||||
|
||||
expect(savedHeartbeat).to.exist;
|
||||
expect(savedHeartbeat.status).to.equal('online');
|
||||
expect(savedHeartbeat.cpu_usage).to.equal(25.5);
|
||||
});
|
||||
|
||||
it('should reject heartbeat from unapproved device', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const device = await createTestDevice({
|
||||
tenant_id: tenant.id,
|
||||
is_approved: false
|
||||
});
|
||||
|
||||
const heartbeatData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'online'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post(`/health/devices/${device.id}/heartbeat`)
|
||||
.send(heartbeatData);
|
||||
|
||||
expect(response.status).to.equal(403);
|
||||
expect(response.body.approval_required).to.be.true;
|
||||
});
|
||||
|
||||
it('should reject heartbeat from non-existent device', async () => {
|
||||
const heartbeatData = {
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'online'
|
||||
};
|
||||
|
||||
const response = await request(app)
|
||||
.post('/health/devices/999999/heartbeat')
|
||||
.send(heartbeatData);
|
||||
|
||||
expect(response.status).to.equal(404);
|
||||
});
|
||||
|
||||
it('should validate heartbeat data format', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const device = await createTestDevice({
|
||||
tenant_id: tenant.id,
|
||||
is_approved: true
|
||||
});
|
||||
|
||||
const invalidPayloads = [
|
||||
{}, // Missing required fields
|
||||
{ status: 'invalid_status' }, // Invalid status
|
||||
{ timestamp: 'invalid_date', status: 'online' }, // Invalid timestamp
|
||||
{
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'online',
|
||||
cpu_usage: 150 // Invalid percentage (>100)
|
||||
},
|
||||
{
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'online',
|
||||
temperature: -50 // Unrealistic temperature
|
||||
}
|
||||
];
|
||||
|
||||
for (const payload of invalidPayloads) {
|
||||
const response = await request(app)
|
||||
.post(`/health/devices/${device.id}/heartbeat`)
|
||||
.send(payload);
|
||||
|
||||
expect(response.status).to.be.oneOf([400, 422]);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle device status changes', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const device = await createTestDevice({
|
||||
tenant_id: tenant.id,
|
||||
is_approved: true
|
||||
});
|
||||
|
||||
// Send online heartbeat
|
||||
await request(app)
|
||||
.post(`/health/devices/${device.id}/heartbeat`)
|
||||
.send({
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'online',
|
||||
cpu_usage: 25.5
|
||||
});
|
||||
|
||||
// Send offline heartbeat
|
||||
const offlineResponse = await request(app)
|
||||
.post(`/health/devices/${device.id}/heartbeat`)
|
||||
.send({
|
||||
timestamp: new Date().toISOString(),
|
||||
status: 'offline'
|
||||
});
|
||||
|
||||
expect(offlineResponse.status).to.equal(200);
|
||||
|
||||
// Verify status change was recorded
|
||||
const heartbeats = await models.Heartbeat.findAll({
|
||||
where: { device_id: device.id },
|
||||
order: [['timestamp', 'ASC']]
|
||||
});
|
||||
|
||||
expect(heartbeats).to.have.length(2);
|
||||
expect(heartbeats[0].status).to.equal('online');
|
||||
expect(heartbeats[1].status).to.equal('offline');
|
||||
});
|
||||
|
||||
it('should handle high-frequency heartbeats efficiently', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const device = await createTestDevice({
|
||||
tenant_id: tenant.id,
|
||||
is_approved: true
|
||||
});
|
||||
|
||||
const heartbeatPromises = [];
|
||||
const startTime = Date.now();
|
||||
|
||||
// Send 10 heartbeats rapidly
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const promise = request(app)
|
||||
.post(`/health/devices/${device.id}/heartbeat`)
|
||||
.send({
|
||||
timestamp: new Date(startTime + i * 1000).toISOString(),
|
||||
status: 'online',
|
||||
cpu_usage: 20 + i,
|
||||
sequence: i
|
||||
});
|
||||
|
||||
heartbeatPromises.push(promise);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(heartbeatPromises);
|
||||
|
||||
// All should succeed
|
||||
responses.forEach(response => {
|
||||
expect(response.status).to.equal(200);
|
||||
});
|
||||
|
||||
// Verify all heartbeats were saved
|
||||
const savedHeartbeats = await models.Heartbeat.findAll({
|
||||
where: { device_id: device.id }
|
||||
});
|
||||
|
||||
expect(savedHeartbeats).to.have.length(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /health/metrics', () => {
|
||||
it('should return system metrics for admin', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||
const token = generateTestToken(admin, tenant);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/metrics')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.system).to.exist;
|
||||
expect(response.body.system.memory).to.exist;
|
||||
expect(response.body.system.cpu).to.exist;
|
||||
expect(response.body.database).to.exist;
|
||||
});
|
||||
|
||||
it('should require admin role for metrics', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const user = await createTestUser({ tenant_id: tenant.id, role: 'user' });
|
||||
const token = generateTestToken(user, tenant);
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/metrics')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(403);
|
||||
});
|
||||
|
||||
it('should include database performance metrics', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||
const token = generateTestToken(admin, tenant);
|
||||
|
||||
// Generate some database activity
|
||||
await createTestDevice({ tenant_id: tenant.id });
|
||||
await models.DroneDetection.create({
|
||||
device_id: 123,
|
||||
tenant_id: tenant.id,
|
||||
geo_lat: 59.3293,
|
||||
geo_lon: 18.0686,
|
||||
device_timestamp: new Date(),
|
||||
drone_type: 2
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/metrics')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.database.connection_pool).to.exist;
|
||||
expect(response.body.statistics).to.exist;
|
||||
expect(response.body.statistics.total_devices).to.be.a('number');
|
||||
expect(response.body.statistics.total_detections).to.be.a('number');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Health Check Edge Cases', () => {
|
||||
it('should handle database connection failures gracefully', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||
const token = generateTestToken(admin, tenant);
|
||||
|
||||
// Mock database failure
|
||||
const originalAuthenticate = sequelize.authenticate;
|
||||
sequelize.authenticate = sinon.stub().rejects(new Error('Database connection failed'));
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/detailed')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(200); // Should still respond
|
||||
expect(response.body.checks.database.status).to.equal('unhealthy');
|
||||
|
||||
// Restore original method
|
||||
sequelize.authenticate = originalAuthenticate;
|
||||
});
|
||||
|
||||
it('should detect when devices are not reporting', async () => {
|
||||
const tenant = await createTestTenant();
|
||||
const admin = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
|
||||
const device = await createTestDevice({ tenant_id: tenant.id });
|
||||
const token = generateTestToken(admin, tenant);
|
||||
|
||||
// Create old heartbeat (device went silent)
|
||||
await models.Heartbeat.create({
|
||||
device_id: device.id,
|
||||
tenant_id: tenant.id,
|
||||
timestamp: new Date(Date.now() - 7200000), // 2 hours ago
|
||||
status: 'online'
|
||||
});
|
||||
|
||||
const response = await request(app)
|
||||
.get('/health/devices')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
expect(response.status).to.equal(200);
|
||||
const deviceStatus = response.body.devices.find(d => d.id === device.id);
|
||||
expect(deviceStatus.status).to.be.oneOf(['offline', 'stale', 'unknown']);
|
||||
});
|
||||
|
||||
it('should handle concurrent health checks efficiently', async () => {
|
||||
const healthPromises = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const promise = request(app).get('/health');
|
||||
healthPromises.push(promise);
|
||||
}
|
||||
|
||||
const responses = await Promise.all(healthPromises);
|
||||
|
||||
responses.forEach(response => {
|
||||
expect(response.status).to.equal(200);
|
||||
expect(response.body.status).to.equal('ok');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user