mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 00:38:14 +08:00
265 lines
7.7 KiB
JavaScript
265 lines
7.7 KiB
JavaScript
require('ts-node/register/transpile-only');
|
|
|
|
const assert = require('node:assert/strict');
|
|
const { test } = require('node:test');
|
|
const {
|
|
postgresqlMainMigrationStream,
|
|
} = require('../../back/migrations/postgresql');
|
|
const {
|
|
postgresqlControlSchemaContract,
|
|
} = require('../../back/migrations/postgresql/schemaContract');
|
|
const {
|
|
PostgresSchemaReadinessError,
|
|
assertPostgresSchemaReady,
|
|
} = require('../../back/migrations/postgresql/schemaReadiness');
|
|
|
|
function validHistory() {
|
|
return postgresqlMainMigrationStream.migrations.map((migration, index) => ({
|
|
streamId: 'postgresql-main',
|
|
dialect: 'postgresql',
|
|
migrationId: migration.id,
|
|
checksum: migration.checksum,
|
|
appliedAtMs: index + 1,
|
|
}));
|
|
}
|
|
|
|
function validPrivileges() {
|
|
const expected = {
|
|
schema_migrations: [true, false, false, false],
|
|
schema_capabilities: [true, false, false, false],
|
|
runs: [true, true, true, false],
|
|
run_attempts: [true, true, true, false],
|
|
run_events: [true, true, false, false],
|
|
run_retry_policies: [true, true, true, false],
|
|
};
|
|
return Object.entries(expected).map(
|
|
([
|
|
tableName,
|
|
[selectAllowed, insertAllowed, updateAllowed, deleteAllowed],
|
|
]) => ({
|
|
tableName,
|
|
selectAllowed,
|
|
insertAllowed,
|
|
updateAllowed,
|
|
deleteAllowed,
|
|
isOwner: false,
|
|
}),
|
|
);
|
|
}
|
|
|
|
function queryable(overrides = {}) {
|
|
const contract = postgresqlControlSchemaContract;
|
|
return {
|
|
async query(text) {
|
|
if (text.includes("current_setting('server_version_num')")) {
|
|
return {
|
|
rows: [
|
|
{
|
|
serverVersionNum: overrides.serverVersionNum ?? '160014',
|
|
currentUser: 'ql3_runtime',
|
|
inRecovery: overrides.inRecovery ?? false,
|
|
transactionReadOnly: overrides.transactionReadOnly ?? 'off',
|
|
},
|
|
],
|
|
};
|
|
}
|
|
if (text.includes('FROM "ql3"."schema_migrations"')) {
|
|
return { rows: overrides.history ?? validHistory() };
|
|
}
|
|
if (text.includes('FROM "ql3"."schema_capabilities"')) {
|
|
return {
|
|
rows: [
|
|
overrides.capability ?? {
|
|
contractName: 'control-core',
|
|
contractVersion: 2,
|
|
migrationId: 'pg-0003-run-retry-policy',
|
|
capabilities: { run_core: 1, run_retry_policy: 1 },
|
|
},
|
|
],
|
|
};
|
|
}
|
|
if (text.includes('FROM information_schema.columns')) {
|
|
const rows = contract.tables.flatMap((table) =>
|
|
table.columns.map((columnName) => ({
|
|
tableName: table.name,
|
|
columnName,
|
|
})),
|
|
);
|
|
if (overrides.extraTable) {
|
|
rows.push({ tableName: overrides.extraTable, columnName: 'id' });
|
|
}
|
|
return { rows };
|
|
}
|
|
if (text.includes('FROM pg_indexes')) {
|
|
return {
|
|
rows: [
|
|
...contract.indexes.map((indexName) => ({ indexName })),
|
|
...(overrides.extraIndex
|
|
? [{ indexName: overrides.extraIndex }]
|
|
: []),
|
|
],
|
|
};
|
|
}
|
|
if (text.includes('FROM pg_constraint')) {
|
|
return {
|
|
rows: [
|
|
...contract.checks.map((constraintName) => ({
|
|
constraintName,
|
|
constraintType: 'check',
|
|
})),
|
|
...contract.foreignKeys.map((constraintName) => ({
|
|
constraintName,
|
|
constraintType: 'foreign_key',
|
|
})),
|
|
...(overrides.extraConstraint
|
|
? [
|
|
{
|
|
constraintName: overrides.extraConstraint,
|
|
constraintType: 'check',
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
};
|
|
}
|
|
if (text.includes('FROM pg_catalog.pg_roles')) {
|
|
return {
|
|
rows: [
|
|
{
|
|
canLogin: true,
|
|
superuser: overrides.superuser ?? false,
|
|
createDatabase: false,
|
|
createRole: false,
|
|
replication: false,
|
|
bypassRowLevelSecurity: false,
|
|
databaseConnect: true,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
if (text.includes('has_schema_privilege')) {
|
|
return {
|
|
rows: [
|
|
{
|
|
schemaUsage: true,
|
|
schemaCreate: overrides.schemaCreate ?? false,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
if (text.includes('has_table_privilege')) {
|
|
return { rows: overrides.privileges ?? validPrivileges() };
|
|
}
|
|
throw new Error(`unexpected query: ${text}`);
|
|
},
|
|
};
|
|
}
|
|
|
|
test('accepts the exact PostgreSQL control schema and least-privilege runtime role', async () => {
|
|
const report = await assertPostgresSchemaReady(queryable());
|
|
assert.deepEqual(report, {
|
|
ready: true,
|
|
writablePrimary: true,
|
|
serverVersionNum: 160014,
|
|
serverMajor: 16,
|
|
currentUser: 'ql3_runtime',
|
|
contractName: 'control-core',
|
|
contractVersion: 2,
|
|
migrationIds: [
|
|
'pg-0001-schema-capability',
|
|
'pg-0002-run-core',
|
|
'pg-0003-run-retry-policy',
|
|
],
|
|
});
|
|
});
|
|
|
|
test('rejects a standby or read-only endpoint before schema inspection', async () => {
|
|
await assert.rejects(
|
|
assertPostgresSchemaReady(queryable({ inRecovery: true })),
|
|
(error) =>
|
|
error instanceof PostgresSchemaReadinessError &&
|
|
error.code === 'server_not_writable_primary' &&
|
|
error.facts.includes('in-recovery:true'),
|
|
);
|
|
await assert.rejects(
|
|
assertPostgresSchemaReady(queryable({ transactionReadOnly: 'on' })),
|
|
(error) =>
|
|
error instanceof PostgresSchemaReadinessError &&
|
|
error.code === 'server_not_writable_primary' &&
|
|
error.facts.includes('transaction-read-only:on'),
|
|
);
|
|
});
|
|
|
|
test('rejects unsupported server versions and capability drift', async () => {
|
|
await assert.rejects(
|
|
assertPostgresSchemaReady(queryable({ serverVersionNum: '150018' })),
|
|
(error) =>
|
|
error instanceof PostgresSchemaReadinessError &&
|
|
error.code === 'server_version_unsupported',
|
|
);
|
|
await assert.rejects(
|
|
assertPostgresSchemaReady(
|
|
queryable({
|
|
capability: {
|
|
contractName: 'control-core',
|
|
contractVersion: 3,
|
|
migrationId: 'pg-0003-run-retry-policy',
|
|
capabilities: { run_core: 1, run_retry_policy: 1 },
|
|
},
|
|
}),
|
|
),
|
|
(error) =>
|
|
error instanceof PostgresSchemaReadinessError &&
|
|
error.code === 'capability_invalid',
|
|
);
|
|
});
|
|
|
|
test('rejects unknown ql3 objects and an over-privileged runtime role', async () => {
|
|
await assert.rejects(
|
|
assertPostgresSchemaReady(
|
|
queryable({
|
|
extraTable: 'plugin_state',
|
|
extraIndex: 'plugin_state_pkey',
|
|
extraConstraint: 'plugin_state_payload_check',
|
|
}),
|
|
),
|
|
(error) =>
|
|
error instanceof PostgresSchemaReadinessError &&
|
|
error.code === 'schema_contract_invalid' &&
|
|
error.facts.includes('unknown-table:plugin_state') &&
|
|
error.facts.includes('unknown-check:plugin_state_payload_check'),
|
|
);
|
|
await assert.rejects(
|
|
assertPostgresSchemaReady(queryable({ schemaCreate: true })),
|
|
(error) =>
|
|
error instanceof PostgresSchemaReadinessError &&
|
|
error.code === 'runtime_role_invalid',
|
|
);
|
|
});
|
|
|
|
test('preserves database availability errors for the outer readiness layer', async () => {
|
|
const unavailable = new Error('database unavailable');
|
|
let calls = 0;
|
|
await assert.rejects(
|
|
assertPostgresSchemaReady({
|
|
async query() {
|
|
calls += 1;
|
|
if (calls === 1) {
|
|
return {
|
|
rows: [
|
|
{
|
|
serverVersionNum: '160014',
|
|
currentUser: 'ql3_runtime',
|
|
inRecovery: false,
|
|
transactionReadOnly: 'off',
|
|
},
|
|
],
|
|
};
|
|
}
|
|
throw unavailable;
|
|
},
|
|
}),
|
|
(error) => error === unavailable,
|
|
);
|
|
});
|