// Migration Idempotency Integration Test // This test requires a running PostgreSQL instance with DATABASE_URL configured in backend/.env. // It runs ALL Postgres migrations twice (via run-all.js) to verify they are idempotent (safe to re-run), // then checks that key tables and columns exist. // // SKIPS AUTOMATICALLY when DATABASE_URL is not set (e.g., in CI environments without DB access). // // Run separately: npx jest backend/__tests__/migrations-idempotency.integration.test.js --forceExit const { execSync } = require('child_process'); const path = require('path'); const fs = require('fs'); const BACKEND_DIR = path.join(__dirname, '..'); // Load .env manually to check for DATABASE_URL without triggering db.js process.exit function loadEnvFile() { const envPath = path.join(BACKEND_DIR, '.env'); if (!fs.existsSync(envPath)) return {}; const content = fs.readFileSync(envPath, 'utf8'); const vars = {}; for (const line of content.split('\n')) { const trimmed = line.trim(); if (!trimmed || trimmed.startsWith('#')) continue; const eqIdx = trimmed.indexOf('='); if (eqIdx === -1) continue; vars[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1); } return vars; } const envVars = loadEnvFile(); const hasDatabase = !!(process.env.DATABASE_URL || envVars.DATABASE_URL); // Skip entire suite if no database is available const describeIfDb = hasDatabase ? describe : describe.skip; let pool; if (hasDatabase) { // Set DATABASE_URL in process.env so db.js picks it up if (!process.env.DATABASE_URL && envVars.DATABASE_URL) { process.env.DATABASE_URL = envVars.DATABASE_URL; } pool = require('../db'); } function runAllMigrations() { execSync('node migrations/run-all.js', { cwd: BACKEND_DIR, stdio: 'pipe', timeout: 30000, }); } afterAll(async () => { if (pool) await pool.end(); }); describeIfDb('Migration Idempotency', () => { it('runs all migrations twice without errors (idempotent)', () => { // First run runAllMigrations(); // Second run — should not throw if migrations are truly idempotent runAllMigrations(); }, 30000); it('key tables exist after migrations', async () => { const expectedTables = [ 'compliance_items', 'compliance_item_history', 'compliance_notes', 'jira_tickets', 'ivanti_fp_submissions', ]; const { rows } = await pool.query(` SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_name = ANY($1) `, [expectedTables]); const foundTables = rows.map(r => r.table_name); for (const table of expectedTables) { expect(foundTables).toContain(table); } }, 30000); it('compliance_item_history has expected columns', async () => { const expectedColumns = [ 'id', 'hostname', 'field_name', 'old_value', 'new_value', 'change_reason', 'changed_by', 'changed_at', 'metric_id', ]; const { rows } = await pool.query(` SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'compliance_item_history' `); const foundColumns = rows.map(r => r.column_name); for (const col of expectedColumns) { expect(foundColumns).toContain(col); } }, 30000); });