Files
gongxue-base/scripts/destructive-test-database-guard-test.js
2026-07-12 19:26:57 +08:00

204 lines
7.1 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import {
DESTRUCTIVE_TEST_CONFIRMATION,
assertDestructiveTestDatabase,
describeDatabaseTarget,
resolveDestructiveTestConfirmation,
} from './lib/destructive-test-database-guard.js';
const LOCAL_URL = 'postgresql://postgres:postgres@127.0.0.1:54322/postgres';
const CONFIRMATION = DESTRUCTIVE_TEST_CONFIRMATION;
function clientWithMarker(environment, allowDestructiveTests) {
const queries = [];
return {
queries,
client: {
async query(sql) {
queries.push(String(sql).trim().replace(/\s+/g, ' '));
return { rows: [{ environment, allowDestructiveTests }], rowCount: 1 };
},
},
};
}
async function rejects(options, pattern) {
await assert.rejects(() => assertDestructiveTestDatabase(options), pattern);
}
{
const mock = clientWithMarker('local', true);
const result = await assertDestructiveTestDatabase({
client: mock.client,
databaseUrl: LOCAL_URL,
confirmation: CONFIRMATION,
operation: 'smoke seed',
});
assert.equal(result.environment, 'local');
assert.equal(mock.queries.length, 1);
assert.match(mock.queries[0], /^select environment,/i);
}
{
const mock = clientWithMarker('ci', true);
const result = await assertDestructiveTestDatabase({
client: mock.client,
databaseUrl: 'postgresql://ci_runner:secret@postgres-ci.internal:6432/tiku_ci',
confirmation: CONFIRMATION,
});
assert.equal(result.environment, 'ci');
}
for (const runtimeUser of ['tiku_api', 'tiku_worker']) {
const mock = clientWithMarker('local', true);
await rejects(
{
client: mock.client,
databaseUrl: `postgresql://${runtimeUser}:prod-secret@127.0.0.1:5432/postgres`,
confirmation: CONFIRMATION,
},
/reserved for production runtime/,
);
assert.equal(mock.queries.length, 0, 'known production targets must be rejected before SQL');
}
{
const missingMarkerClient = { query: async () => ({ rows: [], rowCount: 0 }) };
await rejects(
{
client: missingMarkerClient,
databaseUrl: 'postgresql://postgres:secret@127.0.0.1:15432/postgres',
confirmation: CONFIRMATION,
},
/marker row is missing/,
);
}
for (const environment of ['production', 'staging']) {
const mock = clientWithMarker(environment, true);
await rejects(
{ client: mock.client, databaseUrl: LOCAL_URL, confirmation: CONFIRMATION },
new RegExp(`environment ${environment} is not approved`),
);
}
{
const mock = clientWithMarker('local', false);
await rejects(
{ client: mock.client, databaseUrl: LOCAL_URL, confirmation: CONFIRMATION },
/does not allow destructive tests/,
);
}
for (const confirmation of ['', 'wrong-confirmation']) {
const mock = clientWithMarker('local', true);
await rejects(
{ client: mock.client, databaseUrl: LOCAL_URL, confirmation },
/explicit confirmation/,
);
assert.equal(mock.queries.length, 0, 'confirmation must be checked before SQL');
}
{
const queryErrorClient = { query: async () => { throw new Error('password=do-not-print'); } };
let error;
try {
await assertDestructiveTestDatabase({
client: queryErrorClient,
databaseUrl: LOCAL_URL,
confirmation: CONFIRMATION,
});
} catch (caught) {
error = caught;
}
assert.match(error?.message || '', /marker is unavailable or unreadable/);
assert.equal(error.message.includes('do-not-print'), false);
}
for (const malformed of ['', 'not-a-url', 'https://example.com/database', 'postgresql://localhost']) {
await rejects(
{ client: clientWithMarker('local', true).client, databaseUrl: malformed, confirmation: CONFIRMATION },
/Refusing destructive database test/,
);
}
{
const target = describeDatabaseTarget('postgresql://user:super-secret@db.example.test:5439/tiku_test');
assert.deepEqual(target, {
host: 'db.example.test',
port: '5439',
database: 'tiku_test',
user: 'user',
});
assert.equal(JSON.stringify(target).includes('super-secret'), false);
assert.equal(resolveDestructiveTestConfirmation({}, ['--confirm', CONFIRMATION]), CONFIRMATION);
assert.equal(resolveDestructiveTestConfirmation({ SMOKE_SEED_CONFIRM: CONFIRMATION }, []), CONFIRMATION);
}
const repoRoot = process.cwd();
const smokeSeed = fs.readFileSync(path.join(repoRoot, 'scripts', 'smoke-seed.js'), 'utf8');
const apiIntegration = fs.readFileSync(path.join(repoRoot, 'scripts', 'api-integration-test.js'), 'utf8');
const rlsTest = fs.readFileSync(path.join(repoRoot, 'scripts', 'rls-tenant-isolation-test.js'), 'utf8');
const autoBadgeConcurrency = fs.readFileSync(
path.join(repoRoot, 'scripts', 'auto-badge-concurrency-test.js'),
'utf8',
);
const migration = fs.readFileSync(
path.join(repoRoot, 'supabase', 'migrations', '202607120001_destructive_test_environment_safety.sql'),
'utf8',
);
const seed = fs.readFileSync(path.join(repoRoot, 'supabase', 'seed.sql'), 'utf8');
const packageJson = JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8'));
for (const [name, source] of [
['smoke seed', smokeSeed],
['API integration test', apiIntegration],
['RLS isolation test', rlsTest],
['auto badge concurrency test', autoBadgeConcurrency],
]) {
assert.match(source, /assertDestructiveTestDatabase\s*\(/, `${name} must invoke the shared database guard`);
}
const guardCall = smokeSeed.indexOf('assertDestructiveTestDatabase(');
const firstBegin = smokeSeed.search(/client\.query\(['"]begin['"]\)/i);
const firstWrite = smokeSeed.search(/\b(?:update|delete from|insert into)\s+public\./i);
assert.ok(guardCall >= 0, 'smoke seed must call the shared guard');
assert.ok(firstBegin > guardCall, 'smoke seed guard must run before BEGIN');
assert.ok(firstWrite > guardCall, 'smoke seed guard must run before persistent SQL writes');
assert.match(migration, /create table if not exists app_private\.environment_safety/i);
assert.match(migration, /environment in \('local', 'test', 'ci', 'staging', 'production'\)/i);
assert.match(migration, /allow_destructive_tests boolean not null default false/i);
assert.doesNotMatch(
migration,
/insert into app_private\.environment_safety/i,
'migrations must not automatically authorize destructive tests',
);
assert.match(
seed,
/insert into app_private\.environment_safety[\s\S]*values \(true, 'local', true\)/i,
'local Supabase seed must provision the local-only marker',
);
assert.equal(packageJson.scripts?.['test:api:remote'], undefined, 'full API integration must not expose a remote mode');
for (const scriptName of ['db:smoke-seed:test', 'test:api', 'test:rls']) {
assert.match(
packageJson.scripts?.[scriptName] || '',
new RegExp(DESTRUCTIVE_TEST_CONFIRMATION),
`${scriptName} must carry the exact destructive-test confirmation`,
);
}
assert.match(
packageJson.scripts?.['test:readiness'] || '',
/auto-badge-concurrency-test\.js --confirm=SMOKE_SEED_LOCAL_OR_CI_ONLY/,
'readiness must explicitly confirm its destructive concurrency test',
);
assert.ok(
packageJson.scripts?.['test:readiness']?.includes('destructive-test-database-guard-test.js'),
'readiness contracts must include the destructive database guard test',
);
console.log('[PASS] destructive test database fail-closed guard');