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 adminUrl = process.env.E2E_PG_ADMIN_URL;
|
||||||
const envFile = args.get('--out');
|
const envFile = args.get('--out');
|
||||||
const database = args.get('--db') ?? 'krona_staff_e2e';
|
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(
|
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);
|
process.exit(2);
|
||||||
}
|
}
|
||||||
|
|
@ -31,7 +35,14 @@ export async function provision() {
|
||||||
});
|
});
|
||||||
await admin.connect();
|
await admin.connect();
|
||||||
try {
|
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
|
DO $$ BEGIN
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'krona_migrator') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'krona_migrator') THEN
|
||||||
CREATE ROLE krona_migrator LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS;
|
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;
|
CREATE ROLE krona_staff_app LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS;
|
||||||
END IF;
|
END IF;
|
||||||
END $$`);
|
END $$`);
|
||||||
const alterMigrator = await admin.query(
|
const alterMigrator = await admin.query(
|
||||||
"SELECT format('ALTER ROLE krona_migrator WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS', $1::text) AS sql",
|
"SELECT format('ALTER ROLE krona_migrator WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOBYPASSRLS', $1::text) AS sql",
|
||||||
[migratorPassword],
|
[migratorPassword],
|
||||||
);
|
);
|
||||||
await admin.query(alterMigrator.rows[0].sql);
|
await admin.query(alterMigrator.rows[0].sql);
|
||||||
const alterApp = await admin.query(
|
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",
|
"SELECT format('ALTER ROLE krona_staff_app WITH LOGIN PASSWORD %L NOSUPERUSER NOCREATEDB NOCREATEROLE NOINHERIT NOBYPASSRLS', $1::text) AS sql",
|
||||||
[appPassword],
|
[appPassword],
|
||||||
);
|
);
|
||||||
await admin.query(alterApp.rows[0].sql);
|
await admin.query(alterApp.rows[0].sql);
|
||||||
|
}
|
||||||
await admin.query(
|
await admin.query(
|
||||||
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`,
|
`SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()`,
|
||||||
[database],
|
[database],
|
||||||
|
|
@ -73,6 +85,10 @@ export async function provision() {
|
||||||
} finally {
|
} finally {
|
||||||
await dbAdmin.end();
|
await dbAdmin.end();
|
||||||
}
|
}
|
||||||
|
if (keepRoles) {
|
||||||
|
console.log(`recreated database=${database} (roles kept, passwords unchanged)`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const base = new URL(adminUrl);
|
const base = new URL(adminUrl);
|
||||||
const url = (user, password) => {
|
const url = (user, password) => {
|
||||||
const u = new URL(base.toString());
|
const u = new URL(base.toString());
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ export interface E2EContext {
|
||||||
terminalActorId: string;
|
terminalActorId: string;
|
||||||
appDatabaseUrl: string;
|
appDatabaseUrl: string;
|
||||||
migrationDatabaseUrl: string;
|
migrationDatabaseUrl: string;
|
||||||
|
/** Админ (владелец кластера) в БД стенда; null, если не передан. Только для PILOT-REHOME. */
|
||||||
|
adminDatabaseUrl: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module 'vitest' {
|
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}`);
|
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 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) {
|
if (!adminUrl) {
|
||||||
throw new Error(
|
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';
|
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);
|
const dbEnv = parseEnvFile(envFile);
|
||||||
rmSync(secretsDir, { recursive: true, force: true });
|
rmSync(secretsDir, { recursive: true, force: true });
|
||||||
const migrationUrl = dbEnv.DATABASE_MIGRATION_URL;
|
if (!dbEnv.DATABASE_MIGRATION_URL || !dbEnv.DATABASE_URL) {
|
||||||
const appUrl = dbEnv.DATABASE_URL;
|
throw new Error('provision did not produce connection URLs');
|
||||||
if (!migrationUrl || !appUrl) 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';
|
const migrationMode = process.env.E2E_MIGRATIONS ?? 'up';
|
||||||
runNode(
|
runNode(
|
||||||
['scripts/test/migrate-roundtrip.mjs', migrationMode === 'roundtrip' ? 'roundtrip' : 'up'],
|
['scripts/test/migrate-roundtrip.mjs', migrationMode === 'roundtrip' ? 'roundtrip' : 'up'],
|
||||||
|
|
@ -176,6 +220,7 @@ export default async function setup(project: TestProject) {
|
||||||
terminalActorId,
|
terminalActorId,
|
||||||
appDatabaseUrl: appUrl,
|
appDatabaseUrl: appUrl,
|
||||||
migrationDatabaseUrl: migrationUrl,
|
migrationDatabaseUrl: migrationUrl,
|
||||||
|
adminDatabaseUrl,
|
||||||
};
|
};
|
||||||
project.provide('e2e', context);
|
project.provide('e2e', context);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue