Files
drone-detector/server/tests/integration/workflows.test.js
2025-09-17 21:49:48 +02:00

614 lines
20 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');
// Import all the routes and middleware for integration testing
const authRoutes = require('../../routes/auth');
const detectorsRoutes = require('../../routes/detectors');
const detectionsRoutes = require('../../routes/detections');
const deviceRoutes = require('../../routes/device');
const { authenticateToken } = require('../../middleware/auth');
const { checkIPRestriction } = require('../../middleware/ip-restriction');
const AlertService = require('../../services/alertService');
const DroneTrackingService = require('../../services/droneTrackingService');
describe('Integration Tests', () => {
let app, models, sequelize, alertService, trackingService;
before(async () => {
({ models, sequelize } = await setupTestEnvironment());
// Initialize services
alertService = new AlertService();
trackingService = new DroneTrackingService();
// Setup complete express app for integration testing
app = express();
app.use(express.json());
// Add middleware
app.use('/auth', authRoutes);
app.use('/detectors', detectorsRoutes);
app.use(authenticateToken);
app.use('/detections', detectionsRoutes);
app.use('/devices', deviceRoutes);
});
after(async () => {
await teardownTestEnvironment();
});
beforeEach(async () => {
await cleanDatabase();
alertService.activeAlerts.clear();
trackingService.clear();
});
describe('Complete User Registration and Login Flow', () => {
it('should complete full user registration workflow', async () => {
// 1. Create tenant with registration enabled
const tenant = await createTestTenant({
slug: 'test-tenant',
allow_registration: true
});
// 2. Register new user
const registrationResponse = await request(app)
.post('/auth/register')
.set('Host', 'test-tenant.example.com')
.send({
username: 'newuser',
email: 'newuser@example.com',
password: 'SecurePassword123!',
firstName: 'New',
lastName: 'User'
});
expect(registrationResponse.status).to.equal(201);
expect(registrationResponse.body.success).to.be.true;
expect(registrationResponse.body.data.user.username).to.equal('newuser');
// 3. Login with new user
const loginResponse = await request(app)
.post('/auth/login')
.send({
username: 'newuser',
password: 'SecurePassword123!'
});
expect(loginResponse.status).to.equal(200);
expect(loginResponse.body.success).to.be.true;
expect(loginResponse.body.data.token).to.exist;
const token = loginResponse.body.data.token;
// 4. Access protected endpoint
const protectedResponse = await request(app)
.get('/auth/me')
.set('Authorization', `Bearer ${token}`);
expect(protectedResponse.status).to.equal(200);
expect(protectedResponse.body.data.username).to.equal('newuser');
// 5. Verify user exists in database with correct tenant
const user = await models.User.findOne({
where: { username: 'newuser' },
include: [models.Tenant]
});
expect(user).to.exist;
expect(user.Tenant.slug).to.equal('test-tenant');
expect(user.is_active).to.be.true;
});
it('should prevent registration when disabled', async () => {
const tenant = await createTestTenant({
slug: 'no-reg-tenant',
allow_registration: false
});
const response = await request(app)
.post('/auth/register')
.set('Host', 'no-reg-tenant.example.com')
.send({
username: 'blocked',
email: 'blocked@example.com',
password: 'SecurePassword123!'
});
expect(response.status).to.equal(403);
expect(response.body.success).to.be.false;
expect(response.body.message).to.include('Registration not allowed');
});
});
describe('Complete Device Registration and Detection Flow', () => {
it('should complete full device lifecycle workflow', async () => {
const tenant = await createTestTenant();
const user = await createTestUser({
tenant_id: tenant.id,
role: 'admin'
});
const token = generateTestToken(user, tenant);
// 1. Register new device
const deviceData = {
id: 1941875381,
name: 'Integration Test Device',
geo_lat: 59.3293,
geo_lon: 18.0686,
location_description: 'Test Security Facility'
};
const registrationResponse = await request(app)
.post('/devices')
.set('Authorization', `Bearer ${token}`)
.send(deviceData);
expect(registrationResponse.status).to.equal(201);
expect(registrationResponse.body.success).to.be.true;
// 2. Device is initially unapproved - detection should be rejected
const unapprovedDetection = {
device_id: deviceData.id,
geo_lat: 59.3293,
geo_lon: 18.0686,
device_timestamp: Date.now(),
drone_type: 2,
rssi: -65,
freq: 2400,
drone_id: 1001
};
const rejectedResponse = await request(app)
.post('/detectors')
.send(unapprovedDetection);
expect(rejectedResponse.status).to.equal(403);
expect(rejectedResponse.body.approval_required).to.be.true;
// 3. Approve device
const approvalResponse = await request(app)
.put(`/devices/${deviceData.id}`)
.set('Authorization', `Bearer ${token}`)
.send({ is_approved: true });
expect(approvalResponse.status).to.equal(200);
// 4. Send detection from approved device
const detectionResponse = await request(app)
.post('/detectors')
.send(unapprovedDetection);
expect(detectionResponse.status).to.equal(201);
expect(detectionResponse.body.success).to.be.true;
// 5. Verify detection is stored and visible via API
const detectionsResponse = await request(app)
.get('/detections')
.set('Authorization', `Bearer ${token}`);
expect(detectionsResponse.status).to.equal(200);
expect(detectionsResponse.body.data.detections).to.have.length(1);
expect(detectionsResponse.body.data.detections[0].device_id).to.equal(deviceData.id);
// 6. Verify device status is updated
const deviceStatusResponse = await request(app)
.get('/devices')
.set('Authorization', `Bearer ${token}`);
expect(deviceStatusResponse.status).to.equal(200);
const devices = deviceStatusResponse.body.data;
const testDevice = devices.find(d => d.id === deviceData.id);
expect(testDevice).to.exist;
expect(testDevice.recent_detections_count).to.be.greaterThan(0);
});
it('should enforce tenant isolation in device operations', async () => {
// Setup two tenants
const tenant1 = await createTestTenant({ slug: 'tenant1' });
const tenant2 = await createTestTenant({ slug: 'tenant2' });
const user1 = await createTestUser({ tenant_id: tenant1.id, role: 'admin' });
const user2 = await createTestUser({ tenant_id: tenant2.id, role: 'admin' });
const token1 = generateTestToken(user1, tenant1);
const token2 = generateTestToken(user2, tenant2);
// Create device for tenant1
const device1 = await createTestDevice({
id: 111,
tenant_id: tenant1.id,
is_approved: true
});
// Create device for tenant2
const device2 = await createTestDevice({
id: 222,
tenant_id: tenant2.id,
is_approved: true
});
// User1 should only see tenant1 devices
const tenant1Response = await request(app)
.get('/devices')
.set('Authorization', `Bearer ${token1}`);
expect(tenant1Response.status).to.equal(200);
const tenant1Devices = tenant1Response.body.data;
expect(tenant1Devices).to.have.length(1);
expect(tenant1Devices[0].id).to.equal(111);
// User2 should only see tenant2 devices
const tenant2Response = await request(app)
.get('/devices')
.set('Authorization', `Bearer ${token2}`);
expect(tenant2Response.status).to.equal(200);
const tenant2Devices = tenant2Response.body.data;
expect(tenant2Devices).to.have.length(1);
expect(tenant2Devices[0].id).to.equal(222);
});
});
describe('Complete Alert and Tracking Workflow', () => {
it('should trigger alerts and track drone movement', 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 alert rule
const alertRule = await models.AlertRule.create({
tenant_id: tenant.id,
name: 'Critical Proximity Alert',
drone_type: 2,
min_rssi: -60,
is_active: true
});
// Mock socket.io for real-time notifications
const mockIo = {
emit: sinon.stub(),
emitToDashboard: sinon.stub(),
emitToDevice: sinon.stub(),
to: (room) => ({
emit: sinon.stub()
})
};
// Simulate drone approach with multiple detections
const droneId = 12345;
const detections = [
{ rssi: -80, geo_lat: 59.3200, distance: 5000 },
{ rssi: -70, geo_lat: 59.3220, distance: 2000 },
{ rssi: -55, geo_lat: 59.3240, distance: 800 },
{ rssi: -45, geo_lat: 59.3260, distance: 200 }
];
for (let i = 0; i < detections.length; i++) {
const detection = detections[i];
// Send detection
const response = await request(app)
.post('/detectors')
.send({
device_id: device.id,
geo_lat: detection.geo_lat,
geo_lon: 18.0686,
device_timestamp: Date.now() + (i * 30000), // 30 seconds apart
drone_type: 2,
rssi: detection.rssi,
freq: 2400,
drone_id: droneId
});
expect(response.status).to.equal(201);
// Process alert for this detection
const detectionRecord = await models.DroneDetection.findOne({
where: { drone_id: droneId },
order: [['id', 'DESC']]
});
await alertService.processDetectionAlert(detectionRecord, mockIo);
trackingService.trackDetection(detectionRecord);
}
// Verify alerts were triggered
const alertLogs = await models.AlertLog.findAll({
where: { device_id: device.id }
});
expect(alertLogs.length).to.be.greaterThan(0);
// Verify critical alerts for close proximity
const criticalAlerts = alertLogs.filter(alert => alert.threat_level === 'critical');
expect(criticalAlerts.length).to.be.greaterThan(0);
// Verify drone tracking
const activeTracks = trackingService.getActiveTracking();
expect(activeTracks).to.have.length(1);
expect(activeTracks[0].droneId).to.equal(droneId);
expect(activeTracks[0].movementPattern.isApproaching).to.be.true;
// Verify detections are visible via API
const detectionsResponse = await request(app)
.get('/detections')
.set('Authorization', `Bearer ${token}`);
expect(detectionsResponse.status).to.equal(200);
expect(detectionsResponse.body.data.detections).to.have.length(4);
// Verify real-time notifications were sent
expect(mockIo.emitToDashboard.called).to.be.true;
expect(mockIo.emitToDevice.called).to.be.true;
});
it('should handle high-frequency detection stream', async () => {
const device = await createTestDevice({ is_approved: true });
const droneId = 99999;
// Simulate rapid detection stream (realistic for active tracking)
const detectionPromises = [];
for (let i = 0; i < 10; i++) {
const detectionPromise = request(app)
.post('/detectors')
.send({
device_id: device.id,
geo_lat: 59.3293 + (i * 0.001), // Slight movement
geo_lon: 18.0686,
device_timestamp: Date.now() + (i * 1000), // 1 second apart
drone_type: 2,
rssi: -60 + i, // Varying signal strength
freq: 2400,
drone_id: droneId
});
detectionPromises.push(detectionPromise);
}
const responses = await Promise.all(detectionPromises);
// All detections should be accepted
responses.forEach(response => {
expect(response.status).to.equal(201);
});
// Verify all detections are stored
const storedDetections = await models.DroneDetection.findAll({
where: { drone_id: droneId }
});
expect(storedDetections).to.have.length(10);
// Verify tracking service handles rapid updates
storedDetections.forEach(detection => {
trackingService.trackDetection(detection);
});
const droneTrack = trackingService.getDroneHistory(droneId);
expect(droneTrack.detectionHistory).to.have.length(10);
expect(droneTrack.movementPattern.totalDistance).to.be.greaterThan(0);
});
});
describe('Multi-Tenant Data Isolation', () => {
it('should completely isolate tenant data across all endpoints', async () => {
// Create two complete tenant environments
const tenant1 = await createTestTenant({ slug: 'isolation-test-1' });
const tenant2 = await createTestTenant({ slug: 'isolation-test-2' });
const user1 = await createTestUser({ tenant_id: tenant1.id, role: 'admin' });
const user2 = await createTestUser({ tenant_id: tenant2.id, role: 'admin' });
const device1 = await createTestDevice({ tenant_id: tenant1.id, is_approved: true });
const device2 = await createTestDevice({ tenant_id: tenant2.id, is_approved: true });
const token1 = generateTestToken(user1, tenant1);
const token2 = generateTestToken(user2, tenant2);
// Create alert rules for each tenant
await models.AlertRule.create({
tenant_id: tenant1.id,
name: 'Tenant 1 Rule',
is_active: true
});
await models.AlertRule.create({
tenant_id: tenant2.id,
name: 'Tenant 2 Rule',
is_active: true
});
// Send detections from each device
await request(app).post('/detectors').send({
device_id: device1.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('/detectors').send({
device_id: device2.id,
geo_lat: 60.1699,
geo_lon: 24.9384,
device_timestamp: Date.now(),
drone_type: 3,
rssi: -70,
freq: 2400,
drone_id: 2001
});
// Test device isolation
const devices1 = await request(app)
.get('/devices')
.set('Authorization', `Bearer ${token1}`);
const devices2 = await request(app)
.get('/devices')
.set('Authorization', `Bearer ${token2}`);
expect(devices1.body.data).to.have.length(1);
expect(devices2.body.data).to.have.length(1);
expect(devices1.body.data[0].id).to.equal(device1.id);
expect(devices2.body.data[0].id).to.equal(device2.id);
// Test detection isolation
const detections1 = await request(app)
.get('/detections')
.set('Authorization', `Bearer ${token1}`);
const detections2 = await request(app)
.get('/detections')
.set('Authorization', `Bearer ${token2}`);
expect(detections1.body.data.detections).to.have.length(1);
expect(detections2.body.data.detections).to.have.length(1);
expect(detections1.body.data.detections[0].drone_id).to.equal(1001);
expect(detections2.body.data.detections[0].drone_id).to.equal(2001);
// Test cross-tenant access denial
const detection1Id = detections1.body.data.detections[0].id;
const crossTenantAccess = await request(app)
.get(`/detections/${detection1Id}`)
.set('Authorization', `Bearer ${token2}`); // Wrong tenant token
expect(crossTenantAccess.status).to.equal(404);
});
});
describe('Error Recovery and Edge Cases', () => {
it('should handle database connection failures gracefully', async () => {
// Mock database connection failure
const originalFindOne = models.Device.findOne;
models.Device.findOne = sinon.stub().rejects(new Error('Database connection lost'));
const response = await request(app)
.post('/detectors')
.send({
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
});
expect(response.status).to.equal(500);
expect(response.body.success).to.be.false;
// Restore original method
models.Device.findOne = originalFindOne;
});
it('should handle malformed detection data', async () => {
const malformedPayloads = [
{}, // Empty object
{ device_id: 'invalid' }, // Invalid device_id type
{ device_id: 123, geo_lat: 'invalid' }, // Invalid coordinate
{ device_id: 123, geo_lat: 91 }, // Out of range coordinate
null, // Null payload
'invalid json string' // Invalid JSON
];
for (const payload of malformedPayloads) {
const response = await request(app)
.post('/detectors')
.send(payload);
expect(response.status).to.be.oneOf([400, 500]);
expect(response.body.success).to.be.false;
}
});
it('should handle concurrent user operations', async () => {
const tenant = await createTestTenant();
const user = await createTestUser({ tenant_id: tenant.id, role: 'admin' });
const token = generateTestToken(user, tenant);
// Simulate concurrent device registrations
const devicePromises = [];
for (let i = 0; i < 5; i++) {
const devicePromise = request(app)
.post('/devices')
.set('Authorization', `Bearer ${token}`)
.send({
id: 1000 + i,
name: `Concurrent Device ${i}`,
geo_lat: 59.3293,
geo_lon: 18.0686
});
devicePromises.push(devicePromise);
}
const responses = await Promise.all(devicePromises);
// All should succeed
responses.forEach(response => {
expect(response.status).to.equal(201);
});
// Verify all devices were created
const devices = await models.Device.findAll({
where: { tenant_id: tenant.id }
});
expect(devices).to.have.length(5);
});
});
describe('Performance Under Load', () => {
it('should handle burst of detections efficiently', async () => {
const device = await createTestDevice({ is_approved: true });
const startTime = Date.now();
// Send 50 detections rapidly
const detectionPromises = [];
for (let i = 0; i < 50; i++) {
const promise = request(app)
.post('/detectors')
.send({
device_id: device.id,
geo_lat: 59.3293,
geo_lon: 18.0686,
device_timestamp: Date.now() + i,
drone_type: 2,
rssi: -65,
freq: 2400,
drone_id: 1000 + (i % 10) // 10 different drones
});
detectionPromises.push(promise);
}
const responses = await Promise.all(detectionPromises);
const endTime = Date.now();
// All should complete successfully
const successCount = responses.filter(r => r.status === 201).length;
expect(successCount).to.equal(50);
// Should complete within reasonable time (adjust threshold as needed)
const duration = endTime - startTime;
expect(duration).to.be.lessThan(10000); // 10 seconds max
console.log(`✅ Processed ${successCount} detections in ${duration}ms`);
});
});
});