test(e2e): готовые роли через E2E_MIGRATOR_URL/E2E_APP_URL
Почему: TASK-GROK-3 п.1 — на общем кластере (параллельные прогоны приёмки, чужой стенд) пересоздание ролей krona_migrator/krona_staff_app со случайными паролями ломает соседей. Если заданы оба URL, global-setup не вызывает provision и не требует E2E_PG_ADMIN_URL; по умолчанию поведение прежнее. provision-pg получил --roles keep: пересоздать только БД, не меняя паролей (нужно accept-branch для отдельной БД на ветку). ctx.adminDatabaseUrl (E2E_DB_ADMIN_URL или E2E_PG_ADMIN_URL + БД стенда) — только для PILOT-REHOME. Проверено: run-e2e с E2E_MIGRATOR_URL/E2E_APP_URL без E2E_PG_ADMIN_URL — 45 failed | 9 passed, как на основе; повторный прогон на уже мигрированной БД — то же.
This commit is contained in:
parent
ba59a48ffd
commit
aa7f5f8bbd
3 changed files with 82 additions and 19 deletions
|
|
@ -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 <env-path> [--db name]',
|
||||
'Usage: E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --out <env-path> [--db name]\n' +
|
||||
' E2E_PG_ADMIN_URL=... node scripts/test/provision-pg.mjs --roles keep --db name',
|
||||
);
|
||||
process.exit(2);
|
||||
}
|
||||
|
|
@ -31,6 +35,13 @@ export async function provision() {
|
|||
});
|
||||
await admin.connect();
|
||||
try {
|
||||
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
|
||||
|
|
@ -50,6 +61,7 @@ export async function provision() {
|
|||
[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());
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ export interface E2EContext {
|
|||
terminalActorId: string;
|
||||
appDatabaseUrl: string;
|
||||
migrationDatabaseUrl: string;
|
||||
/** Админ (владелец кластера) в БД стенда; null, если не передан. Только для PILOT-REHOME. */
|
||||
adminDatabaseUrl: string | null;
|
||||
}
|
||||
|
||||
declare module 'vitest' {
|
||||
|
|
|
|||
|
|
@ -103,12 +103,47 @@ async function startServer(name: string, env: Record<string, string>): Promise<S
|
|||
throw new Error(`server ${name} did not become ready; see ${logPath}`);
|
||||
}
|
||||
|
||||
export default async function setup(project: TestProject) {
|
||||
mkdirSync(evidenceDir, { recursive: true });
|
||||
/**
|
||||
* Откуда брать БД и роли.
|
||||
* 1. E2E_MIGRATOR_URL и E2E_APP_URL заданы — роли и пароли уже есть (общий кластер,
|
||||
* параллельная работа других): стенд их не создаёт и не меняет, БД берётся из URL.
|
||||
* 2. Иначе (по умолчанию) — bootstrap-админ E2E_PG_ADMIN_URL создаёт роли со
|
||||
* случайными паролями и пересоздаёт БД E2E_DATABASE_NAME (scripts/test/provision-pg.mjs).
|
||||
* adminDatabaseUrl — админ в той же БД (нужен только сценарию PILOT-REHOME):
|
||||
* E2E_DB_ADMIN_URL, либо E2E_PG_ADMIN_URL с именем БД стенда.
|
||||
*/
|
||||
function resolveDatabase(): {
|
||||
migrationUrl: string;
|
||||
appUrl: string;
|
||||
adminDatabaseUrl: string | null;
|
||||
} {
|
||||
const adminUrl = process.env.E2E_PG_ADMIN_URL;
|
||||
const withDatabase = (url: string, database: string) => {
|
||||
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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue