119 lines
8.1 KiB
TypeScript
119 lines
8.1 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { test } from "node:test"
|
|
import { McpError } from "../modules/mcp/security.js"
|
|
import type { ProbeStorageSql } from "../services/server-probe-journal-maintenance.js"
|
|
import { captureDatabaseSourceObservation, assertDatabaseSourceObservationCurrent } from "./database-source-observation.js"
|
|
|
|
const options = () => ({ deadlineAt: Date.now() + 2000 })
|
|
const row = (): Record<string, unknown> => ({ system_identifier: "18446744073709551615", database_oid: "16384", database_name: "fixture",
|
|
session_user: "operator", current_user: "operator", in_recovery: false, server_version_num: "180006", backend_pid: 1234,
|
|
transaction_timestamp: "2026-10-25 12:34:56.123456+00", snapshot: "100:110:101,105,109",
|
|
isolation: "repeatable read", read_only: "on", explicit_transaction: true })
|
|
function fixture(rows: Record<string, unknown>[][] = [[row()]]) {
|
|
const calls: { text: string; timeout: number; signal?: AbortSignal }[] = []
|
|
const sql: ProbeStorageSql = { async query(text, values, queryOptions) {
|
|
assert.deepEqual(values, []); assert(queryOptions)
|
|
calls.push({ text, timeout: queryOptions.timeoutMs, signal: queryOptions.signal })
|
|
return { rows: rows[Math.min(calls.length - 1, rows.length - 1)] }
|
|
} }
|
|
return { sql, calls }
|
|
}
|
|
const safe = (error: unknown) => error instanceof McpError && error.message === "Текущий источник и снимок БД не подтверждены"
|
|
|
|
test("fixed genuine facts are immutable, exact and explicitly lack generation authority", async () => {
|
|
const f = fixture(), observation = await captureDatabaseSourceObservation(f.sql, options())
|
|
assert.equal(observation.source.systemIdentifier, "18446744073709551615")
|
|
assert.equal(observation.source.databaseOid, 16384)
|
|
assert.equal(observation.snapshot.transactionTimestamp, "2026-10-25 12:34:56.123456+00")
|
|
assert.equal(observation.ready, false); assert.equal(observation.generationAuthenticated, false); assert.equal(observation.requiresExternalSourceBinding, true)
|
|
assert(Object.isFrozen(observation)); assert(Object.isFrozen(observation.source)); assert(Object.isFrozen(observation.snapshot))
|
|
assert.equal(f.calls.length, 2)
|
|
assert(f.calls.every(call => call.timeout >= 1 && call.timeout <= 10000))
|
|
assert(f.calls[0].text.includes("pg_catalog.pg_control_system()")); assert(f.calls[0].text.includes("pg_catalog.pg_current_snapshot()"))
|
|
assert(!/\b(BEGIN|COMMIT|ROLLBACK|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|pg_export_snapshot)\b/i.test(f.calls[0].text))
|
|
await assertDatabaseSourceObservationCurrent(f.sql, observation, options()); assert.equal(f.calls.length, 3)
|
|
})
|
|
|
|
test("every source or transaction dimension drifting between reads is refused", async t => {
|
|
const changes: Record<string, unknown> = { system_identifier: "9223372036854775808", database_oid: "16385", database_name: "other",
|
|
session_user: "another", current_user: "another", in_recovery: true, server_version_num: "180007", backend_pid: 1235,
|
|
transaction_timestamp: "2026-10-25 12:34:56.123457+00", snapshot: "100:111:101,105,109" }
|
|
for (const [key, value] of Object.entries(changes)) await t.test(key, async () => {
|
|
const f = fixture([[row()], [{ ...row(), [key]: value }]])
|
|
await assert.rejects(captureDatabaseSourceObservation(f.sql, options()), safe)
|
|
})
|
|
})
|
|
|
|
test("same-lease verifier rejects changed transaction after fixed data collection", async () => {
|
|
const f = fixture([[row()], [row()], [{ ...row(), backend_pid: 44 }]])
|
|
const observation = await captureDatabaseSourceObservation(f.sql, options())
|
|
await assert.rejects(assertDatabaseSourceObservationCurrent(f.sql, observation, options()), safe)
|
|
})
|
|
|
|
test("JSON/copies and cross-client source records never establish source binding", async () => {
|
|
const first = fixture(), second = fixture(), observation = await captureDatabaseSourceObservation(first.sql, options())
|
|
await assert.rejects(assertDatabaseSourceObservationCurrent(first.sql, structuredClone(observation), options()), safe)
|
|
await assert.rejects(assertDatabaseSourceObservationCurrent(first.sql, { ...observation }, options()), safe)
|
|
await assert.rejects(assertDatabaseSourceObservationCurrent(second.sql, observation, options()), safe)
|
|
assert.equal(first.calls.length, 2); assert.equal(second.calls.length, 0)
|
|
})
|
|
|
|
test("expired or aborted requests perform no SQL", async () => {
|
|
const f = fixture(), controller = new AbortController(); controller.abort()
|
|
for (const value of [{ deadlineAt: Date.now() - 1 }, { deadlineAt: NaN }, { ...options(), signal: controller.signal }]) {
|
|
await assert.rejects(captureDatabaseSourceObservation(f.sql, value), safe)
|
|
}
|
|
assert.equal(f.calls.length, 0)
|
|
})
|
|
|
|
test("original deadline and signal remain authoritative during later pairing", async () => {
|
|
const f = fixture(), controller = new AbortController(), original = { deadlineAt: Date.now() + 40, signal: controller.signal }
|
|
const observation = await captureDatabaseSourceObservation(f.sql, original)
|
|
controller.abort()
|
|
await assert.rejects(assertDatabaseSourceObservationCurrent(f.sql, observation, options()), safe)
|
|
const other = fixture(), deadline = { deadlineAt: Date.now() + 40 }
|
|
const timed = await captureDatabaseSourceObservation(other.sql, deadline)
|
|
deadline.deadlineAt += 100000
|
|
await new Promise(resolve => setTimeout(resolve, 50))
|
|
await assert.rejects(assertDatabaseSourceObservationCurrent(other.sql, timed, options()), safe)
|
|
assert.equal(f.calls.length, 2); assert.equal(other.calls.length, 2)
|
|
})
|
|
|
|
test("strict singleton/schema and explicit RR read-only transaction required", async () => {
|
|
const missing = row(); delete missing.current_user
|
|
const variants = [[], [row(), row()], [missing], [{ ...row(), extra: "private" }], [{ ...row(), read_only: "off" }],
|
|
[{ ...row(), isolation: "read committed" }], [{ ...row(), explicit_transaction: false }], [{ ...row(), backend_pid: 1.5 }],
|
|
[{ ...row(), in_recovery: "false" }], [{ ...row(), transaction_timestamp: "not a timestamp" }],
|
|
[{ ...row(), transaction_timestamp: "2026-99-99 12:34:56.123456+00" }], [{ ...row(), server_version_num: "120000" }]]
|
|
for (const rows of variants) await assert.rejects(captureDatabaseSourceObservation(fixture([rows]).sql, options()), safe)
|
|
})
|
|
|
|
test("uint64, OID and snapshot range/order/size validation is fail closed", async () => {
|
|
const variants = [{ system_identifier: "18446744073709551616" }, { system_identifier: "0" }, { system_identifier: "01" },
|
|
{ database_oid: "4294967296" }, { database_oid: "0" }, { database_oid: "01" }, { database_oid: "1.1" },
|
|
{ snapshot: "110:100:" }, { snapshot: "100:110:105,105" }, { snapshot: "100:110:109,105" },
|
|
{ snapshot: "100:110:99" }, { snapshot: "100:110:110" }, { snapshot: "100:110:0105" },
|
|
{ snapshot: "100:18446744073709551616:" }, { snapshot: "100:110" }, { snapshot: "100:110::" },
|
|
{ snapshot: "0:5000:" + Array.from({ length: 4097 }, (_, index) => index).join(",") },
|
|
{ snapshot: "0:1:" + "1".repeat(65536) }]
|
|
for (const change of variants) await assert.rejects(captureDatabaseSourceObservation(fixture([[{ ...row(), ...change }]]).sql, options()), safe)
|
|
const empty = fixture([[{ ...row(), snapshot: "100:100:" }]])
|
|
assert.equal((await captureDatabaseSourceObservation(empty.sql, options())).snapshot.snapshot, "100:100:")
|
|
})
|
|
|
|
test("provider errors never leak", async () => {
|
|
const privateSql: ProbeStorageSql = { query: async () => { throw new Error("private-provider password db-url") } }
|
|
await assert.rejects(captureDatabaseSourceObservation(privateSql, options()), safe)
|
|
})
|
|
|
|
test("abort fences after caller-owned SQL settles and prevents a second read", async () => {
|
|
let resolve!: (value: { rows: Record<string, unknown>[] }) => void, queries = 0
|
|
const controller = new AbortController()
|
|
const sql: ProbeStorageSql = { query: () => { queries++; return new Promise(done => { resolve = done }) } }
|
|
const pending = captureDatabaseSourceObservation(sql, { ...options(), signal: controller.signal })
|
|
controller.abort()
|
|
// Foreign SQL remains caller-owned until it settles; this helper must not
|
|
// race it and allow a caller to reuse its backend while work is in flight.
|
|
resolve({ rows: [row()] }); await assert.rejects(pending, safe); assert.equal(queries, 1)
|
|
})
|