Files
qinglong/test/back/postgresqlSchemaReadiness.test.cjs

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,
);
});