374 lines
10 KiB
JavaScript
374 lines
10 KiB
JavaScript
// IMPORTANT: Set environment variables FIRST, before any other imports
|
|
process.env.NODE_ENV = 'test';
|
|
process.env.JWT_SECRET = 'test-jwt-secret-key-for-testing-only';
|
|
process.env.DATABASE_URL = ':memory:';
|
|
process.env.DB_DIALECT = 'sqlite';
|
|
process.env.DB_STORAGE = ':memory:';
|
|
process.env.DB_LOGGING = 'false';
|
|
|
|
const { Sequelize } = require('sequelize');
|
|
const path = require('path');
|
|
const sinon = require('sinon');
|
|
|
|
// Global Sinon setup - restore all spies after each test
|
|
afterEach(() => {
|
|
sinon.restore();
|
|
});
|
|
|
|
// Set additional test environment variables
|
|
|
|
// Test database configuration
|
|
const testDatabase = {
|
|
dialect: 'sqlite',
|
|
storage: ':memory:', // Use in-memory database for fast tests
|
|
logging: false, // Disable SQL logging in tests
|
|
sync: { force: true } // Always recreate tables for tests
|
|
};
|
|
|
|
let sequelize;
|
|
let models;
|
|
|
|
/**
|
|
* Setup test environment before all tests
|
|
*/
|
|
async function setupTestEnvironment() {
|
|
// Clear any existing global models
|
|
delete global.__TEST_MODELS__;
|
|
|
|
// Create test database connection
|
|
sequelize = new Sequelize(testDatabase);
|
|
|
|
// Import and initialize models with test sequelize instance
|
|
const Device = require('../models/Device')(sequelize);
|
|
const DroneDetection = require('../models/DroneDetection')(sequelize);
|
|
const Heartbeat = require('../models/Heartbeat')(sequelize);
|
|
const User = require('../models/User')(sequelize);
|
|
const AlertRule = require('../models/AlertRule')(sequelize);
|
|
const AlertLog = require('../models/AlertLog')(sequelize);
|
|
const Tenant = require('../models/Tenant')(sequelize);
|
|
const ManagementUser = require('../models/ManagementUser')(sequelize);
|
|
|
|
// Define associations
|
|
Device.hasMany(DroneDetection, { foreignKey: 'device_id', as: 'detections' });
|
|
DroneDetection.belongsTo(Device, { foreignKey: 'device_id', as: 'device' });
|
|
|
|
Device.hasMany(Heartbeat, { foreignKey: 'device_id', as: 'heartbeats' });
|
|
Heartbeat.belongsTo(Device, { foreignKey: 'device_id', as: 'device' });
|
|
|
|
User.hasMany(AlertRule, { foreignKey: 'user_id', as: 'alertRules' });
|
|
AlertRule.belongsTo(User, { foreignKey: 'user_id', as: 'user' });
|
|
|
|
AlertRule.hasMany(AlertLog, { foreignKey: 'alert_rule_id', as: 'logs' });
|
|
AlertLog.belongsTo(AlertRule, { foreignKey: 'alert_rule_id', as: 'rule' });
|
|
|
|
DroneDetection.hasMany(AlertLog, { foreignKey: 'detection_id', as: 'alerts' });
|
|
AlertLog.belongsTo(DroneDetection, { foreignKey: 'detection_id', as: 'detection' });
|
|
|
|
Device.hasMany(AlertLog, { foreignKey: 'device_id', as: 'alerts' });
|
|
AlertLog.belongsTo(Device, { foreignKey: 'device_id', as: 'device' });
|
|
|
|
// Tenant associations
|
|
Tenant.hasMany(User, { foreignKey: 'tenant_id', as: 'users' });
|
|
User.belongsTo(Tenant, { foreignKey: 'tenant_id', as: 'tenant' });
|
|
|
|
Tenant.hasMany(Device, { foreignKey: 'tenant_id', as: 'devices' });
|
|
Device.belongsTo(Tenant, { foreignKey: 'tenant_id', as: 'tenant' });
|
|
|
|
Tenant.hasMany(AlertRule, { foreignKey: 'tenant_id', as: 'alertRules' });
|
|
AlertRule.belongsTo(Tenant, { foreignKey: 'tenant_id', as: 'tenant' });
|
|
|
|
// Create models object
|
|
models = {
|
|
sequelize,
|
|
Sequelize,
|
|
Device,
|
|
DroneDetection,
|
|
Heartbeat,
|
|
User,
|
|
AlertRule,
|
|
AlertLog,
|
|
Tenant,
|
|
ManagementUser
|
|
};
|
|
|
|
// Store models globally for routes to access
|
|
global.__TEST_MODELS__ = models;
|
|
|
|
// Debug info
|
|
console.log(`🔧 DEBUG: Test sequelize instance: ${sequelize.constructor.name}`);
|
|
console.log(`🔧 DEBUG: Test database storage: ${sequelize.options.storage}`);
|
|
console.log(`🔧 DEBUG: Global models set:`, Object.keys(global.__TEST_MODELS__));
|
|
|
|
// Sync database
|
|
await sequelize.sync({ force: true });
|
|
|
|
// Return test context
|
|
return { sequelize, models };
|
|
}
|
|
|
|
/**
|
|
* Cleanup test environment after all tests
|
|
*/
|
|
async function teardownTestEnvironment() {
|
|
if (sequelize) {
|
|
await sequelize.close();
|
|
}
|
|
// Clear global models
|
|
delete global.__TEST_MODELS__;
|
|
}
|
|
|
|
/**
|
|
* Clean database between tests
|
|
*/
|
|
async function cleanDatabase() {
|
|
if (sequelize) {
|
|
try {
|
|
// Drop all tables and recreate them
|
|
await sequelize.drop();
|
|
await sequelize.sync({ force: true });
|
|
|
|
// Verify tables are clean
|
|
const tableNames = Object.keys(sequelize.models);
|
|
for (const tableName of tableNames) {
|
|
const count = await sequelize.models[tableName].count();
|
|
if (count > 0) {
|
|
console.warn(`Warning: ${tableName} table not properly cleaned, has ${count} records`);
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('Error cleaning database:', error);
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create test user with specified role and tenant
|
|
*/
|
|
async function createTestUser(userData = {}) {
|
|
const { User, Tenant } = models;
|
|
|
|
// Create default tenant if not exists
|
|
let tenant = await Tenant.findOne({ where: { slug: 'test-tenant' } });
|
|
if (!tenant) {
|
|
tenant = await Tenant.create({
|
|
name: 'Test Tenant',
|
|
slug: 'test-tenant',
|
|
domain: 'test.example.com',
|
|
is_active: true
|
|
});
|
|
}
|
|
|
|
const defaultUserData = {
|
|
username: userData.username || `testuser${Date.now()}${Math.floor(Math.random() * 1000)}`,
|
|
email: userData.email || `test${Date.now()}@example.com`,
|
|
password_hash: '$2b$10$dummyHashForTestingOnly',
|
|
role: 'admin',
|
|
tenant_id: tenant.id,
|
|
is_active: true,
|
|
...userData
|
|
};
|
|
|
|
return await User.create(defaultUserData);
|
|
}
|
|
|
|
/**
|
|
* Create test device with specified tenant
|
|
*/
|
|
async function createTestDevice(deviceData = {}) {
|
|
const { Device, Tenant } = models;
|
|
|
|
// Create default tenant if not exists
|
|
let tenant = await Tenant.findOne({ where: { slug: 'test-tenant' } });
|
|
if (!tenant) {
|
|
tenant = await Tenant.create({
|
|
name: 'Test Tenant',
|
|
slug: 'test-tenant',
|
|
domain: 'test.example.com',
|
|
is_active: true
|
|
});
|
|
}
|
|
|
|
const defaultDeviceData = {
|
|
name: 'Test Device',
|
|
geo_lat: 59.3293,
|
|
geo_lon: 18.0686,
|
|
location_description: 'Test Location',
|
|
tenant_id: tenant.id,
|
|
is_active: true,
|
|
is_approved: true,
|
|
...deviceData
|
|
};
|
|
|
|
// If a specific ID is provided, use upsert to ensure it's respected
|
|
if (deviceData.id) {
|
|
console.log(`🔧 DEBUG: Creating device with specific ID: ${deviceData.id} (type: ${typeof deviceData.id})`);
|
|
console.log(`🔧 DEBUG: Device data:`, defaultDeviceData);
|
|
const [device, created] = await Device.upsert(defaultDeviceData, {
|
|
returning: true
|
|
});
|
|
console.log(`🔧 DEBUG: Device ${created ? 'created' : 'updated'}: ID=${device.id} (type: ${typeof device.id}), approved=${device.is_approved}`);
|
|
|
|
// Verify the device exists immediately after creation
|
|
const verification = await Device.findByPk(device.id);
|
|
console.log(`🔧 DEBUG: Verification lookup: ${verification ? `Found device ${verification.id}` : 'Device not found after creation!'}`);
|
|
|
|
return device;
|
|
} else {
|
|
// Auto-generate ID when none provided
|
|
defaultDeviceData.id = Math.floor(Math.random() * 1000000000);
|
|
console.log(`🔧 DEBUG: Creating device with auto-generated ID: ${defaultDeviceData.id}`);
|
|
const device = await Device.create(defaultDeviceData);
|
|
console.log(`🔧 DEBUG: Auto-generated device created: ID=${device.id}, approved=${device.is_approved}`);
|
|
return device;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create test detection data
|
|
*/
|
|
async function createTestDetection(detectionData = {}) {
|
|
const { DroneDetection, Device } = models;
|
|
|
|
let device;
|
|
|
|
// If device_id is provided, try to find the existing device
|
|
if (detectionData.device_id) {
|
|
device = await Device.findByPk(detectionData.device_id);
|
|
}
|
|
|
|
// If no device found or no device_id provided, create a new device
|
|
if (!device) {
|
|
const deviceData = {};
|
|
if (detectionData.tenant_id) {
|
|
deviceData.tenant_id = detectionData.tenant_id;
|
|
}
|
|
device = await createTestDevice(deviceData);
|
|
}
|
|
|
|
// Remove device_id from detectionData to avoid overriding
|
|
const { device_id, ...restDetectionData } = detectionData;
|
|
|
|
const defaultDetectionData = {
|
|
device_id: device.id, // Always use the actual device ID
|
|
geo_lat: device.geo_lat,
|
|
geo_lon: device.geo_lon,
|
|
device_timestamp: Date.now(),
|
|
server_timestamp: new Date(),
|
|
drone_type: 2,
|
|
rssi: -65,
|
|
freq: 2400,
|
|
drone_id: Math.floor(Math.random() * 10000),
|
|
...restDetectionData
|
|
};
|
|
|
|
return await DroneDetection.create(defaultDetectionData);
|
|
}
|
|
|
|
/**
|
|
* Create test tenant
|
|
*/
|
|
async function createTestTenant(tenantData = {}) {
|
|
const { Tenant } = models;
|
|
|
|
const timestamp = Date.now();
|
|
const defaultTenantData = {
|
|
name: 'Test Tenant',
|
|
slug: 'test-tenant-' + timestamp,
|
|
domain: 'test-' + timestamp + '.example.com',
|
|
is_active: true,
|
|
...tenantData
|
|
};
|
|
|
|
return await Tenant.create(defaultTenantData);
|
|
}
|
|
|
|
/**
|
|
* Generate JWT token for test user
|
|
*/
|
|
function generateTestToken(user, tenant = null) {
|
|
const jwt = require('jsonwebtoken');
|
|
const payload = {
|
|
userId: user.id,
|
|
username: user.username,
|
|
role: user.role,
|
|
email: user.email
|
|
};
|
|
|
|
if (tenant) {
|
|
payload.tenantId = tenant.slug;
|
|
}
|
|
|
|
return jwt.sign(payload, process.env.JWT_SECRET || 'test-secret', { expiresIn: '1h' });
|
|
}
|
|
|
|
/**
|
|
* Mock Express request object
|
|
*/
|
|
function mockRequest(overrides = {}) {
|
|
return {
|
|
body: {},
|
|
params: {},
|
|
query: {},
|
|
headers: {},
|
|
get: function(header) {
|
|
return this.headers[header.toLowerCase()];
|
|
},
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mock Express response object
|
|
*/
|
|
function mockResponse() {
|
|
const res = {
|
|
statusCode: 200,
|
|
data: null,
|
|
status: function(code) {
|
|
this.statusCode = code;
|
|
return this;
|
|
},
|
|
json: function(data) {
|
|
this.data = data;
|
|
return this;
|
|
},
|
|
send: function(data) {
|
|
this.data = data;
|
|
return this;
|
|
},
|
|
setHeader: function(name, value) {
|
|
this.headers = this.headers || {};
|
|
this.headers[name] = value;
|
|
return this;
|
|
}
|
|
};
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Mock Express next function
|
|
*/
|
|
function mockNext() {
|
|
const errors = [];
|
|
const next = function(error) {
|
|
if (error) errors.push(error);
|
|
};
|
|
next.errors = errors;
|
|
return next;
|
|
}
|
|
|
|
module.exports = {
|
|
setupTestEnvironment,
|
|
teardownTestEnvironment,
|
|
cleanDatabase,
|
|
createTestUser,
|
|
createTestDevice,
|
|
createTestDetection,
|
|
createTestTenant,
|
|
generateTestToken,
|
|
mockRequest,
|
|
mockResponse,
|
|
mockNext
|
|
};
|