krona-staff/scripts/test/migrate-roundtrip.mjs
grok 5ac248660a test(db): стенд PostgreSQL с мигратором без SUPERUSER
Зачем: аудит показал, что 156 зелёных тестов на fakeDatabase не заметили,
что создание сотрудника, подтверждение смены и расчёт зарплаты всегда
падают на реальной PostgreSQL. Чтобы тесты и CI проверяли реальный SQL,
нужен воспроизводимый стенд с ролями как в pilot, но с минимальными
привилегиями:

- scripts/test/provision-pg.mjs: bootstrap-админ (только как DBA/entrypoint)
  создаёт krona_migrator (NOSUPERUSER NOCREATEDB NOCREATEROLE, владелец БД и
  схемы public) и krona_staff_app (NOSUPERUSER NOBYPASSRLS NOINHERIT); пароли
  crypto.randomBytes, пишутся только в env-файл 0600, в вывод не попадают.
  scripts/create-app-role.mjs здесь не используется, т.к. без SUPERUSER он
  падает (находка N6, продуктовый код не трогаем).
- scripts/test/migrate-roundtrip.mjs: up -> откат всех миграций -> up ->
  повторный up должен быть no-op; ролью мигратора, как требует AGENTS.md.

Проверено на PostgreSQL 17.11: 11 миграций применены, откачены, применены
повторно, третий up пустой.
2026-09-26 15:18:31 +07:00

46 lines
1.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env node
// up -> откат ВСЕХ миграций по одной -> up -> повторный up (идемпотентность).
// Запускается ролью мигратора (DATABASE_MIGRATION_URL), без SUPERUSER.
import { spawnSync } from 'node:child_process';
function run(direction) {
const result = spawnSync(process.execPath, ['scripts/migrate.mjs', direction], {
env: { ...process.env, ALLOW_MIGRATION_DOWN: direction === 'down' ? 'true' : 'false' },
encoding: 'utf8',
});
process.stdout.write(result.stdout);
process.stderr.write(result.stderr);
if (result.status !== 0) {
console.error(`migrate ${direction} failed with exit ${result.status}`);
process.exit(result.status ?? 1);
}
return result.stdout;
}
const mode = process.argv[2] ?? 'roundtrip';
console.log('== migrate up (1)');
const first = run('up');
if (mode === 'up') process.exit(0);
const applied = (first.match(/^applied /gm) ?? []).length;
console.log(`== rollback all (applied now: ${applied})`);
let rolledBack = 0;
for (let i = 0; i < 1000; i += 1) {
const out = run('down');
if (/no migration to roll back/.test(out)) break;
rolledBack += 1;
}
console.log(`== rolled back ${rolledBack} migrations`);
console.log('== migrate up (2)');
const second = run('up');
const reapplied = (second.match(/^applied /gm) ?? []).length;
console.log('== migrate up (3, must be no-op)');
const third = run('up');
if ((third.match(/^applied /gm) ?? []).length !== 0) {
console.error('third up applied migrations again: not idempotent');
process.exit(1);
}
if (rolledBack === 0 || reapplied !== rolledBack) {
console.error(`rollback/reapply mismatch: rolledBack=${rolledBack} reapplied=${reapplied}`);
process.exit(1);
}
console.log(`== roundtrip OK: rolledBack=${rolledBack} reapplied=${reapplied}`);