Files
MikrotikManager/backend/src/db/database-source-observation.test.ts
T

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