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'); const { authenticateToken, setModels } = require('../../middleware/auth'); describe('Device Routes', () => { let app, models, sequelize, deviceRoutes; before(async () => { ({ models, sequelize } = await setupTestEnvironment()); // Inject models globally for routes and into auth middleware for testing global.__TEST_MODELS__ = models; setModels(models); // Require device routes AFTER setting up global models deviceRoutes = require('../../routes/device'); // Setup express app for testing app = express(); app.use(express.json()); app.use(authenticateToken); app.use('/devices', deviceRoutes); }); after(async () => { // Clean up global test models delete global.__TEST_MODELS__; 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.detections_24h).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; }); }); });