Files
drone-detector/server/tests/setup.js
2025-09-15 14:29:40 +02:00

424 lines
12 KiB
JavaScript

const { Sequelize } = require('sequelize');
const path = require('path');
// Set test environment variables
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';
// Test database configuration
const testDatabase = {
dialect: 'sqlite',
storage: ':memory:', // 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() {
// 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' });
// 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' });
// Create models object
models = {
sequelize,
Sequelize,
Device,
DroneDetection,
Heartbeat,
User,
AlertRule,
AlertLog,
Tenant,
ManagementUser
};
// Override the main models module with our test models
// This ensures that when other modules import '../models', they get our test models
const mainModelsPath = path.resolve(__dirname, '../models/index.js');
require.cache[mainModelsPath] = {
exports: models,
loaded: true,
id: mainModelsPath
};
// Inject test models into middleware modules
const authMiddleware = require('../middleware/auth');
authMiddleware.setModels(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();
}
}
/**
* Clean database between tests
*/
async function cleanDatabase() {
if (sequelize) {
try {
// For SQLite in-memory, completely drop all tables and recreate
await sequelize.drop();
await sequelize.sync({ force: true });
console.log('✅ Database cleaned successfully');
} catch (error) {
console.error('❌ Database cleanup failed:', error.message);
// Fallback: try alternative cleanup
try {
const models = sequelize.models;
for (const modelName of Object.keys(models)) {
await models[modelName].destroy({ where: {}, truncate: true });
}
} catch (fallbackError) {
console.error('❌ Fallback cleanup also failed:', fallbackError.message);
}
}
}
}
// Global counter for unique test data
let testCounter = 0;
/**
* Get a unique suffix for test data
*/
function getUniqueTestSuffix() {
testCounter++;
return Date.now() + '-' + testCounter + '-' + Math.random().toString(36).substr(2, 9) + '-' + process.hrtime.bigint().toString(36);
}
/**
* Create test user with specified role and tenant
*/
async function createTestUser(userData = {}) {
const { User, Tenant } = models;
// Generate unique suffix for this test run
const uniqueSuffix = getUniqueTestSuffix();
// Create or find tenant
let tenant;
if (userData.tenant_id && typeof userData.tenant_id === 'string' && userData.tenant_id !== 'UUIDV4') {
tenant = await Tenant.findByPk(userData.tenant_id);
}
if (!tenant) {
try {
tenant = await Tenant.create({
name: 'Test Tenant',
slug: 'test-tenant-' + uniqueSuffix,
domain: 'test-' + uniqueSuffix + '.example.com',
is_active: true
});
} catch (error) {
console.error('❌ Default tenant creation failed:', error.message);
console.error('❌ Validation errors:', error.errors);
throw error;
}
}
const defaultUserData = {
username: userData.username || 'testuser-' + uniqueSuffix,
email: userData.email || 'test-' + uniqueSuffix + '@example.com',
password_hash: '$2b$10$example.hash.for.testing.purposes.only',
role: 'admin',
tenant_id: tenant.id,
is_active: true,
...userData
};
// Remove any id field to let Sequelize auto-generate UUID
delete defaultUserData.id;
// Additional safety check - remove any UUIDV4 function objects
Object.keys(defaultUserData).forEach(key => {
if (defaultUserData[key] && typeof defaultUserData[key] === 'object' &&
defaultUserData[key].constructor && defaultUserData[key].constructor.name === 'UUIDV4') {
delete defaultUserData[key];
}
});
// Try manual UUID generation as a workaround
const { v4: uuidv4 } = require('uuid');
const userWithId = { ...defaultUserData, id: uuidv4() };
return await User.create(userWithId);
}
/**
* Create test device with specified tenant
*/
async function createTestDevice(deviceData = {}) {
const { Device, Tenant } = models;
// Generate unique suffix for this test run
const uniqueSuffix = getUniqueTestSuffix();
// Create or find tenant
let tenant;
if (deviceData.tenant_id && typeof deviceData.tenant_id === 'string' && deviceData.tenant_id !== 'UUIDV4') {
tenant = await Tenant.findByPk(deviceData.tenant_id);
}
if (!tenant) {
// Use manual UUID generation for tenant creation
const { v4: uuidv4 } = require('uuid');
const tenantWithId = {
id: uuidv4(),
name: 'Test Tenant',
slug: 'test-tenant-' + uniqueSuffix,
domain: 'test-' + uniqueSuffix + '.example.com',
is_active: true
};
tenant = await Tenant.create(tenantWithId);
}
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
};
// Remove any id field to let Sequelize auto-generate (for Device it's auto-increment)
delete defaultDeviceData.id;
return await Device.create(defaultDeviceData);
}
/**
* Create test detection data
*/
async function createTestDetection(detectionData = {}) {
const { DroneDetection, Device } = models;
// Create device if not provided
let device;
if (detectionData.device_id) {
device = await Device.findByPk(detectionData.device_id);
}
if (!device) {
device = await createTestDevice();
}
const defaultDetectionData = {
device_id: 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),
...detectionData
};
// Remove any id field to let Sequelize auto-generate UUID
delete defaultDetectionData.id;
// Additional safety check - remove any UUIDV4 function objects
Object.keys(defaultDetectionData).forEach(key => {
if (defaultDetectionData[key] && typeof defaultDetectionData[key] === 'object' &&
defaultDetectionData[key].constructor && defaultDetectionData[key].constructor.name === 'UUIDV4') {
delete defaultDetectionData[key];
}
});
// Try manual UUID generation as a workaround
const { v4: uuidv4 } = require('uuid');
const detectionWithId = { ...defaultDetectionData, id: uuidv4() };
return await DroneDetection.create(detectionWithId);
}
/**
* Create test tenant
*/
async function createTestTenant(tenantData = {}) {
const { Tenant } = models;
const uniqueSuffix = getUniqueTestSuffix();
const defaultTenantData = {
name: 'Test Tenant',
slug: 'test-tenant-' + uniqueSuffix,
domain: 'test-' + uniqueSuffix + '.example.com',
is_active: true,
...tenantData
};
// Remove any id field to let Sequelize auto-generate UUID
delete defaultTenantData.id;
// Additional safety check - remove any UUIDV4 function objects
Object.keys(defaultTenantData).forEach(key => {
if (defaultTenantData[key] && typeof defaultTenantData[key] === 'object' &&
defaultTenantData[key].constructor && defaultTenantData[key].constructor.name === 'UUIDV4') {
delete defaultTenantData[key];
}
});
try {
console.log('🔍 Creating tenant with data:', JSON.stringify(defaultTenantData, null, 2));
console.log('🔍 Data types:', Object.keys(defaultTenantData).map(key => `${key}: ${typeof defaultTenantData[key]}`));
// Try manual UUID generation as a workaround
const { v4: uuidv4 } = require('uuid');
const tenantWithId = { ...defaultTenantData, id: uuidv4() };
return await Tenant.create(tenantWithId);
} catch (error) {
console.error('❌ Tenant creation failed:', error.message);
console.error('❌ Validation errors:', error.errors);
console.error('❌ Tenant data:', defaultTenantData);
throw error;
}
}
/**
* 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
};