diff --git a/scripts/test/provision-pg.mjs b/scripts/test/provision-pg.mjs index 2db7ae5..95e79b2 100644 --- a/scripts/test/provision-pg.mjs +++ b/scripts/test/provision-pg.mjs @@ -14,9 +14,13 @@ for (let i = 2; i < process.argv.length; i += 2) args.set(process.argv[i], proce const adminUrl = process.env.E2E_PG_ADMIN_URL; const envFile = args.get('--out'); const database = args.get('--db') ?? 'krona_staff_e2e'; -if (!adminUrl || !envFile) { +// --roles keep: роли уже созданы (например, предыдущим вызовом accept-branch) — пароли НЕ меняются, +// только пересоздаётся БД. Нужно, чтобы параллельные/последовательные стенды не ломали друг другу пароли. +const keepRoles = args.get('--roles') === 'keep'; +if (!adminUrl || (!envFile && !keepRoles)) { console.error( - 'Usage: E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --out [--db name]', + 'Usage: E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --out [--db name]\n' + + ' E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --roles keep --db name', ); process.exit(2); } @@ -31,7 +35,14 @@ export async function provision() { }); await admin.connect(); try { - await admin.query(` + if (keepRoles) { + const existing = await admin.query( + `SELECT count(*)::int AS n FROM pg_roles WHERE rolname IN ('krona_migrator','krona_staff_app')`, + ); + if (existing.rows[0].n !== 2) + throw new Error('--roles keep: роли krona_migrator/krona_staff_app не созданы'); + } else { + await admin.query(` DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'krona_migrator') THEN CREATE ROLE krona_migrator LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS; @@ -40,16 +51,17 @@ export async function provision() { CREATE ROLE krona_staff_app LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS; END IF; END $$`); - const alterMigrator = await admin.query( - "SELECT format('ALTER ROLE krona_migrator WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS', $1::text) AS sql", - [migratorPassword], - ); - await admin.query(alterMigrator.rows[0].sql); - const alterApp = await admin.query( - "SELECT format('ALTER ROLE krona_staff_app WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS', $1::text) AS sql", - [appPassword], - ); - await admin.query(alterApp.rows[0].sql); + const alterMigrator = await admin.query( + "SELECT format('ALTER ROLE krona_migrator WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS', $1::text) AS sql", + [migratorPassword], + ); + await admin.query(alterMigrator.rows[0].sql); + const alterApp = await admin.query( + "SELECT format('ALTER ROLE krona_staff_app WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS', $1::text) AS sql", + [appPassword], + ); + await admin.query(alterApp.rows[0].sql); + } await admin.query( `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`, [database], @@ -73,6 +85,10 @@ export async function provision() { } finally { await dbAdmin.end(); } + if (keepRoles) { + console.log(`recreated database=${database} (roles kept, passwords unchanged)`); + return; + } const base = new URL(adminUrl); const url = (user, password) => { const u = new URL(base.toString()); diff --git a/tests/e2e/harness/context.ts b/tests/e2e/harness/context.ts index 8e9dfcc..05aa696 100644 --- a/tests/e2e/harness/context.ts +++ b/tests/e2e/harness/context.ts @@ -11,6 +11,8 @@ export interface E2EContext { terminalActorId: string; appDatabaseUrl: string; migrationDatabaseUrl: string; + /** Админ (владелец кластера) в БД стенда; null, если не передан. Только для PILOT-REHOME. */ + adminDatabaseUrl: string | null; } declare module 'vitest' { diff --git a/tests/e2e/harness/global-setup.ts b/tests/e2e/harness/global-setup.ts index 55ca631..7cb6361 100644 --- a/tests/e2e/harness/global-setup.ts +++ b/tests/e2e/harness/global-setup.ts @@ -103,12 +103,47 @@ async function startServer(name: string, env: Record): Promise { + const result = new URL(url); + result.pathname = `/${database}`; + return result.toString(); + }; + const presetMigrator = process.env.E2E_MIGRATOR_URL; + const presetApp = process.env.E2E_APP_URL; + if (presetMigrator && presetApp) { + const database = new URL(presetApp).pathname.slice(1); + console.log( + `[e2e] роли заданы E2E_MIGRATOR_URL/E2E_APP_URL: provision пропущен, БД ${database}`, + ); + return { + migrationUrl: presetMigrator, + appUrl: presetApp, + adminDatabaseUrl: + process.env.E2E_DB_ADMIN_URL ?? (adminUrl ? withDatabase(adminUrl, database) : null), + }; + } + if (presetMigrator || presetApp) { + throw new Error('E2E_MIGRATOR_URL и E2E_APP_URL задаются только вместе'); + } if (!adminUrl) { throw new Error( - 'E2E_PG_ADMIN_URL не задан: нужен bootstrap-админ PostgreSQL 17 (только для создания ролей и БД)', + 'E2E_PG_ADMIN_URL не задан: нужен bootstrap-админ PostgreSQL 17 (только для создания ролей и БД) ' + + 'или готовые роли в E2E_MIGRATOR_URL + E2E_APP_URL', ); } const database = process.env.E2E_DATABASE_NAME ?? 'krona_staff_e2e'; @@ -121,10 +156,19 @@ export default async function setup(project: TestProject) { ); const dbEnv = parseEnvFile(envFile); rmSync(secretsDir, { recursive: true, force: true }); - const migrationUrl = dbEnv.DATABASE_MIGRATION_URL; - const appUrl = dbEnv.DATABASE_URL; - if (!migrationUrl || !appUrl) throw new Error('provision did not produce connection URLs'); + if (!dbEnv.DATABASE_MIGRATION_URL || !dbEnv.DATABASE_URL) { + throw new Error('provision did not produce connection URLs'); + } + return { + migrationUrl: dbEnv.DATABASE_MIGRATION_URL, + appUrl: dbEnv.DATABASE_URL, + adminDatabaseUrl: process.env.E2E_DB_ADMIN_URL ?? withDatabase(adminUrl, database), + }; +} +export default async function setup(project: TestProject) { + mkdirSync(evidenceDir, { recursive: true }); + const { migrationUrl, appUrl, adminDatabaseUrl } = resolveDatabase(); const migrationMode = process.env.E2E_MIGRATIONS ?? 'up'; runNode( ['scripts/test/migrate-roundtrip.mjs', migrationMode === 'roundtrip' ? 'roundtrip' : 'up'], @@ -176,6 +220,7 @@ export default async function setup(project: TestProject) { terminalActorId, appDatabaseUrl: appUrl, migrationDatabaseUrl: migrationUrl, + adminDatabaseUrl, }; project.provide('e2e', context);