619 lines
20 KiB
JavaScript
619 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 global middleware
|
|
app.use(checkIPRestriction);
|
|
|
|
// Add routes
|
|
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',
|
|
domain: 'test-tenant.example.com',
|
|
allow_registration: true,
|
|
auth_provider: 'local'
|
|
});
|
|
|
|
// 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`);
|
|
});
|
|
});
|
|
});
|