forked from wangziqi/gongxue-base
204 lines
7.1 KiB
JavaScript
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');
|