Files
cve-dashboard/backend/__tests__/migrations-idempotency.integration.test.js

116 lines
3.6 KiB
JavaScript
Raw Normal View History

// 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);
});