428 lines
12 KiB
JavaScript
428 lines
12 KiB
JavaScript
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('Detectors Routes', () => {
|
|
let app, models, sequelize, detectorsRoutes;
|
|
|
|
before(async () => {
|
|
console.log('🔧 DEBUG: detectors.test.js before() - global.__TEST_MODELS__ before setupTestEnvironment:', !!global.__TEST_MODELS__);
|
|
({ models, sequelize } = await setupTestEnvironment());
|
|
console.log('🔧 DEBUG: detectors.test.js before() - global.__TEST_MODELS__ after setupTestEnvironment:', !!global.__TEST_MODELS__);
|
|
|
|
// IMPORTANT: Require routes AFTER setupTestEnvironment so they use global test models
|
|
console.log('🔧 DEBUG: About to require detectors route - global.__TEST_MODELS__:', !!global.__TEST_MODELS__);
|
|
detectorsRoutes = require('../../routes/detectors');
|
|
console.log('🔧 DEBUG: Required detectors route - global.__TEST_MODELS__:', !!global.__TEST_MODELS__);
|
|
|
|
// Setup express app for testing (matching production structure)
|
|
app = express();
|
|
app.use(express.json());
|
|
|
|
// Add mock Socket.io middleware for tests
|
|
app.use((req, res, next) => {
|
|
req.io = {
|
|
emit: () => {}, // Mock emit function that does nothing
|
|
emitToDashboard: () => {},
|
|
emitToDevice: () => {}
|
|
};
|
|
next();
|
|
});
|
|
|
|
// Mount routes under /api like in production
|
|
app.use('/api/detectors', detectorsRoutes);
|
|
});
|
|
|
|
after(async () => {
|
|
await teardownTestEnvironment();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await cleanDatabase();
|
|
});
|
|
|
|
describe('POST /detectors - Detection Data', () => {
|
|
it('should accept valid detection from approved device', async () => {
|
|
const tenant = await createTestTenant();
|
|
const device = await createTestDevice({
|
|
id: 1941875381,
|
|
tenant_id: tenant.id,
|
|
is_approved: true,
|
|
is_active: true
|
|
});
|
|
|
|
const detectionData = {
|
|
device_id: device.id,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: Date.now(),
|
|
drone_type: 2,
|
|
rssi: -65,
|
|
freq: 2400,
|
|
drone_id: 1001
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(detectionData);
|
|
|
|
expect(response.status).to.equal(201);
|
|
expect(response.body.success).to.be.true;
|
|
expect(response.body.message).to.equal('Drone detection recorded successfully');
|
|
});
|
|
|
|
it('should reject detection from unknown device', async () => {
|
|
const detectionData = {
|
|
device_id: 999999999, // Non-existent device
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: Date.now(),
|
|
drone_type: 2,
|
|
rssi: -65,
|
|
freq: 2400,
|
|
drone_id: 1001
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(detectionData);
|
|
|
|
expect(response.status).to.equal(404);
|
|
expect(response.body.success).to.be.false;
|
|
expect(response.body.error).to.equal('Device not registered');
|
|
expect(response.body.registration_required).to.be.true;
|
|
});
|
|
|
|
it('should reject detection from unapproved device', async () => {
|
|
const device = await createTestDevice({
|
|
id: 1941875381,
|
|
is_approved: false,
|
|
is_active: true
|
|
});
|
|
|
|
const detectionData = {
|
|
device_id: device.id,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: Date.now(),
|
|
drone_type: 2,
|
|
rssi: -65,
|
|
freq: 2400,
|
|
drone_id: 1001
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(detectionData);
|
|
|
|
expect(response.status).to.equal(403);
|
|
expect(response.body.success).to.be.false;
|
|
expect(response.body.error).to.equal('Device not approved');
|
|
expect(response.body.approval_required).to.be.true;
|
|
});
|
|
|
|
it('should handle drone type 0 (None) detections when debug enabled', async () => {
|
|
process.env.STORE_DRONE_TYPE0 = 'true';
|
|
|
|
const device = await createTestDevice({
|
|
is_approved: true,
|
|
is_active: true
|
|
});
|
|
|
|
const detectionData = {
|
|
device_id: device.id,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: Date.now(),
|
|
drone_type: 0, // None type
|
|
rssi: -65,
|
|
freq: 2400,
|
|
drone_id: 1001
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(detectionData);
|
|
|
|
expect(response.status).to.equal(201);
|
|
expect(response.body.success).to.be.true;
|
|
|
|
delete process.env.STORE_DRONE_TYPE0;
|
|
});
|
|
|
|
it('should skip drone type 0 detections when debug disabled', async () => {
|
|
process.env.STORE_DRONE_TYPE0 = 'false';
|
|
|
|
const device = await createTestDevice({
|
|
is_approved: true,
|
|
is_active: true
|
|
});
|
|
|
|
const detectionData = {
|
|
device_id: device.id,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: Date.now(),
|
|
drone_type: 0,
|
|
rssi: -65,
|
|
freq: 2400,
|
|
drone_id: 1001
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(detectionData);
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.success).to.be.true;
|
|
expect(response.body.message).to.include('debug mode');
|
|
|
|
delete process.env.STORE_DRONE_TYPE0;
|
|
});
|
|
|
|
it('should validate required detection fields', async () => {
|
|
const device = await createTestDevice({ is_approved: true });
|
|
|
|
const invalidData = {
|
|
device_id: device.id,
|
|
// missing required fields
|
|
geo_lat: 59.3293
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(invalidData);
|
|
|
|
expect(response.status).to.equal(400);
|
|
expect(response.body.success).to.be.false;
|
|
});
|
|
|
|
it('should validate coordinate ranges', async () => {
|
|
const device = await createTestDevice({ is_approved: true });
|
|
|
|
const invalidData = {
|
|
device_id: device.id,
|
|
geo_lat: 91, // Invalid latitude
|
|
geo_lon: 181, // Invalid longitude
|
|
device_timestamp: Date.now(),
|
|
drone_type: 2,
|
|
rssi: -65,
|
|
freq: 2400,
|
|
drone_id: 1001
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(invalidData);
|
|
|
|
expect(response.status).to.equal(400);
|
|
expect(response.body.success).to.be.false;
|
|
});
|
|
|
|
it('should trigger alerts for critical detections', async () => {
|
|
const device = await createTestDevice({ is_approved: true });
|
|
|
|
const criticalDetection = {
|
|
device_id: device.id,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: Date.now(),
|
|
drone_type: 2, // Orlan - military drone
|
|
rssi: -35, // Strong signal = close proximity
|
|
freq: 2400,
|
|
drone_id: 1001
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(criticalDetection);
|
|
|
|
expect(response.status).to.equal(201);
|
|
expect(response.body.success).to.be.true;
|
|
});
|
|
|
|
it('should handle detection with optional fields', async () => {
|
|
const device = await createTestDevice({ is_approved: true });
|
|
|
|
const detectionData = {
|
|
device_id: device.id,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: Date.now(),
|
|
drone_type: 2,
|
|
rssi: -65,
|
|
freq: 2400,
|
|
drone_id: 1001,
|
|
confidence_level: 0.85,
|
|
signal_duration: 2500
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(detectionData);
|
|
|
|
expect(response.status).to.equal(201);
|
|
expect(response.body.success).to.be.true;
|
|
});
|
|
});
|
|
|
|
describe('POST /detectors - Heartbeat Data', () => {
|
|
it('should accept valid heartbeat', async () => {
|
|
const heartbeatData = {
|
|
type: 'heartbeat',
|
|
key: 'device_123_key',
|
|
device_id: 123,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(heartbeatData);
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.success).to.be.true;
|
|
expect(response.body.message).to.equal('Heartbeat received');
|
|
});
|
|
|
|
it('should require key field for heartbeat', async () => {
|
|
const heartbeatData = {
|
|
type: 'heartbeat',
|
|
// missing key
|
|
device_id: 123
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(heartbeatData);
|
|
|
|
expect(response.status).to.equal(400);
|
|
expect(response.body.success).to.be.false;
|
|
});
|
|
|
|
it('should handle heartbeat with minimal data', async () => {
|
|
const heartbeatData = {
|
|
type: 'heartbeat',
|
|
key: 'device_123_key'
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(heartbeatData);
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.success).to.be.true;
|
|
});
|
|
|
|
it('should validate required fields', async () => {
|
|
const heartbeatData = {
|
|
type: 'heartbeat'
|
|
// Missing required key field
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(heartbeatData);
|
|
|
|
expect(response.status).to.equal(400);
|
|
expect(response.body.success).to.be.false;
|
|
});
|
|
|
|
it('should store heartbeat when debug enabled', async () => {
|
|
process.env.STORE_HEARTBEATS = 'true';
|
|
|
|
const heartbeatData = {
|
|
type: 'heartbeat',
|
|
key: 'device_123_key',
|
|
device_id: 123
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(heartbeatData);
|
|
|
|
expect(response.status).to.equal(200);
|
|
expect(response.body.success).to.be.true;
|
|
|
|
delete process.env.STORE_HEARTBEATS;
|
|
});
|
|
});
|
|
|
|
describe('Error Handling', () => {
|
|
it('should handle invalid JSON payload', async () => {
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.set('Content-Type', 'application/json')
|
|
.send('invalid json{');
|
|
|
|
expect(response.status).to.equal(400);
|
|
});
|
|
|
|
it('should handle database connection errors', async () => {
|
|
// Mock database error
|
|
const originalFindOne = models.Device.findOne;
|
|
models.Device.findOne = sinon.stub().rejects(new Error('Database connection failed'));
|
|
|
|
const detectionData = {
|
|
device_id: 123,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: Date.now(),
|
|
drone_type: 2,
|
|
rssi: -65,
|
|
freq: 2400,
|
|
drone_id: 1001
|
|
};
|
|
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send(detectionData);
|
|
|
|
expect(response.status).to.equal(500);
|
|
expect(response.body.success).to.be.false;
|
|
|
|
// Restore original method
|
|
models.Device.findOne = originalFindOne;
|
|
});
|
|
|
|
it('should handle missing required fields gracefully', async () => {
|
|
const response = await request(app)
|
|
.post('/api/detectors')
|
|
.send({});
|
|
|
|
expect(response.status).to.equal(400);
|
|
expect(response.body.success).to.be.false;
|
|
expect(response.body.error).to.include('Invalid payload');
|
|
});
|
|
|
|
it('should log detection data for debugging', async () => {
|
|
process.env.LOG_ALL_DETECTIONS = 'true';
|
|
const consoleSpy = sinon.spy(console, 'log');
|
|
|
|
const device = await createTestDevice({ is_approved: true });
|
|
const detectionData = {
|
|
device_id: device.id,
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
device_timestamp: Date.now(),
|
|
drone_type: 2,
|
|
rssi: -65,
|
|
freq: 2400,
|
|
drone_id: 1001
|
|
};
|
|
|
|
await request(app)
|
|
.post('/api/detectors')
|
|
.send(detectionData);
|
|
|
|
expect(consoleSpy.called).to.be.true;
|
|
|
|
consoleSpy.restore();
|
|
delete process.env.LOG_ALL_DETECTIONS;
|
|
});
|
|
});
|
|
});
|