forked from wangziqi/gongxue-base
149 lines
4.3 KiB
JavaScript
149 lines
4.3 KiB
JavaScript
export const DESTRUCTIVE_TEST_CONFIRMATION = 'SMOKE_SEED_LOCAL_OR_CI_ONLY';
|
|
|
|
const ALLOWED_ENVIRONMENTS = new Set(['local', 'test', 'ci']);
|
|
const KNOWN_PRODUCTION_DATABASE_USERS = new Set(['tiku_api', 'tiku_worker']);
|
|
|
|
function argumentValue(argv, name) {
|
|
const directIndex = argv.indexOf(name);
|
|
if (directIndex >= 0) return String(argv[directIndex + 1] || '').trim();
|
|
const prefix = `${name}=`;
|
|
return String(argv.find(value => value.startsWith(prefix)) || '').slice(prefix.length).trim();
|
|
}
|
|
|
|
export function resolveDestructiveTestConfirmation(
|
|
env = process.env,
|
|
argv = process.argv.slice(2),
|
|
) {
|
|
return argumentValue(argv, '--confirm') || String(env.SMOKE_SEED_CONFIRM || '').trim();
|
|
}
|
|
|
|
export function describeDatabaseTarget(databaseUrl) {
|
|
if (!databaseUrl || typeof databaseUrl !== 'string') {
|
|
throw new Error('DATABASE_URL is required');
|
|
}
|
|
|
|
let parsed;
|
|
try {
|
|
parsed = new URL(databaseUrl);
|
|
} catch {
|
|
throw new Error('DATABASE_URL must be a valid PostgreSQL URL');
|
|
}
|
|
if (!['postgres:', 'postgresql:'].includes(parsed.protocol)) {
|
|
throw new Error('DATABASE_URL must use the postgres or postgresql protocol');
|
|
}
|
|
if (!parsed.hostname || !parsed.pathname || parsed.pathname === '/') {
|
|
throw new Error('DATABASE_URL must include a host and database name');
|
|
}
|
|
|
|
return {
|
|
host: parsed.hostname,
|
|
port: parsed.port || '5432',
|
|
database: decodeURIComponent(parsed.pathname.slice(1)),
|
|
user: decodeURIComponent(parsed.username || ''),
|
|
};
|
|
}
|
|
|
|
function targetText(target, environment = 'unavailable') {
|
|
const safe = value => String(value || '[missing]')
|
|
.replace(/[\u0000-\u001f\u007f\s]+/g, '_')
|
|
.slice(0, 160);
|
|
return [
|
|
`host=${safe(target.host)}`,
|
|
`port=${safe(target.port)}`,
|
|
`database=${safe(target.database)}`,
|
|
`user=${safe(target.user)}`,
|
|
`databaseEnvironment=${safe(environment)}`,
|
|
].join(' ');
|
|
}
|
|
|
|
function refusal(operation, reason, target, environment) {
|
|
return new Error(
|
|
`Refusing ${operation}: ${reason}. Target: ${targetText(target, environment)}`,
|
|
);
|
|
}
|
|
|
|
function knownProductionReason(target) {
|
|
const user = target.user.toLowerCase();
|
|
const host = target.host.toLowerCase();
|
|
if (KNOWN_PRODUCTION_DATABASE_USERS.has(user)) {
|
|
return 'database user is reserved for production runtime';
|
|
}
|
|
if (host === 'tjszsb.com' || host.endsWith('.tjszsb.com')) {
|
|
return 'database host belongs to the production domain';
|
|
}
|
|
return '';
|
|
}
|
|
|
|
export async function assertDestructiveTestDatabase({
|
|
client,
|
|
databaseUrl,
|
|
confirmation = resolveDestructiveTestConfirmation(),
|
|
operation = 'destructive database test',
|
|
} = {}) {
|
|
let target;
|
|
try {
|
|
target = describeDatabaseTarget(databaseUrl);
|
|
} catch (error) {
|
|
throw new Error(`Refusing ${operation}: ${error.message}`);
|
|
}
|
|
|
|
if (confirmation !== DESTRUCTIVE_TEST_CONFIRMATION) {
|
|
throw refusal(
|
|
operation,
|
|
`explicit confirmation ${DESTRUCTIVE_TEST_CONFIRMATION} is required`,
|
|
target,
|
|
);
|
|
}
|
|
|
|
const productionReason = knownProductionReason(target);
|
|
if (productionReason) {
|
|
throw refusal(operation, productionReason, target);
|
|
}
|
|
if (!client || typeof client.query !== 'function') {
|
|
throw refusal(operation, 'a connected PostgreSQL client is required', target);
|
|
}
|
|
|
|
let result;
|
|
try {
|
|
result = await client.query(
|
|
`
|
|
select environment,
|
|
allow_destructive_tests as "allowDestructiveTests"
|
|
from app_private.environment_safety
|
|
where id = true
|
|
limit 1
|
|
`,
|
|
);
|
|
} catch {
|
|
throw refusal(
|
|
operation,
|
|
'database safety marker is unavailable or unreadable',
|
|
target,
|
|
);
|
|
}
|
|
|
|
const marker = result?.rows?.[0];
|
|
const environment = String(marker?.environment || 'missing').toLowerCase();
|
|
if (!marker) {
|
|
throw refusal(operation, 'database safety marker row is missing', target, environment);
|
|
}
|
|
if (!ALLOWED_ENVIRONMENTS.has(environment)) {
|
|
throw refusal(
|
|
operation,
|
|
`database environment ${environment} is not approved for destructive tests`,
|
|
target,
|
|
environment,
|
|
);
|
|
}
|
|
if (marker.allowDestructiveTests !== true) {
|
|
throw refusal(
|
|
operation,
|
|
'database marker does not allow destructive tests',
|
|
target,
|
|
environment,
|
|
);
|
|
}
|
|
|
|
return { target, environment, allowDestructiveTests: true };
|
|
}
|