Files
MikrotikManager/backend/src/services/server-poll-settings-epoch.sql-helper.ts
T

147 lines
11 KiB
TypeScript

/** Own-cluster-only acceptance of the epoch guard. Proposal mode applies DDL
* transactionally; numbered mode preserves its existing registered schema.
* All synthetic changes roll back; no service, router or scheduler starts. */
import assert from "node:assert/strict"
import { randomUUID } from "node:crypto"
import { readFile } from "node:fs/promises"
import { basename, dirname, resolve } from "node:path"
import type { Pool } from "pg"
import { assertServerPollSettingsEpochCatalog, SERVER_POLL_SETTINGS_EPOCH_INVENTORY_SQL } from "./server-poll-settings-epoch.js"
export async function acceptServerPollSettingsEpochSql(pool: Pool, numbered = false) {
assert.equal(process.env.MMAPP_OWN_SERVER_POLL_SETTINGS_EPOCH, "true")
assert.equal(process.env.MMAPP_OWN_SERVER_POLL_STORAGE_FRESH, "true")
assert.equal(process.env.NODE_ENV, "test")
const endpoint = new URL(process.env.DATABASE_URL ?? "")
assert.equal(endpoint.hostname, "127.0.0.1"); assert.equal(endpoint.pathname, "/postgres")
assert.equal(endpoint.username, "mcp_test")
assert(process.env.MCP_STATE_DIR && basename(dirname(resolve(process.env.MCP_STATE_DIR))).startsWith("mm-mcp-test-"))
const tables = ["traffic_settings", "uptime_settings", "internet_path_settings"] as const
const snapshots = async () => Promise.all(tables.map(async table =>
(await pool.query(`SELECT to_jsonb(t) AS row FROM public.${table} t ORDER BY id`)).rows))
const before = await snapshots(), owner = await pool.connect(), groups: string[] = []
try {
await owner.query("BEGIN")
for (const table of tables) await owner.query(`INSERT INTO public.${table}(id) VALUES(1) ON CONFLICT(id) DO NOTHING`)
const existing: Record<string, unknown>[] = []
for (const table of tables) existing.push((await owner.query(`SELECT to_jsonb(t) AS row FROM public.${table} t WHERE id=1`)).rows[0].row)
if (!numbered) await owner.query(await readFile(new URL("../db/proposals/server-poll-settings-epoch.sql", import.meta.url), "utf8"))
else assertServerPollSettingsEpochCatalog((await owner.query(SERVER_POLL_SETTINGS_EPOCH_INVENTORY_SQL, [tables])).rows)
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/
const row = async (table: typeof tables[number]) =>
(await owner.query(`SELECT to_jsonb(t) AS row FROM public.${table} t WHERE id=1`)).rows[0].row as Record<string, unknown>
for (const [index, table] of tables.entries()) {
let current = await row(table), epoch = String(current.poll_configuration_epoch)
assert.match(epoch, uuid)
const { poll_configuration_epoch: _epoch, ...preserved } = current
const expected = { ...existing[index] }; if (numbered) delete expected.poll_configuration_epoch
assert.deepEqual(preserved, expected)
assert.equal(_epoch, epoch)
groups.push(table + ":backfill-preserves-complete-row")
await owner.query(`UPDATE public.${table} SET last_collected_at=clock_timestamp(),last_duration_ms=17,
last_error='synthetic runtime',updated_at=clock_timestamp(),poll_configuration_epoch=NULL WHERE id=1`)
current = await row(table); assert.equal(current.poll_configuration_epoch, epoch)
assert.equal(current.last_error, "synthetic runtime"); assert.equal(current.last_duration_ms, 17)
await owner.query(`UPDATE public.${table} SET last_error=NULL,last_duration_ms=NULL,poll_configuration_epoch=$1 WHERE id=1`, [randomUUID()])
assert.equal((await row(table)).poll_configuration_epoch, epoch)
groups.push(table + ":runtime-and-caller-epoch-cannot-change-identity")
const originalInterval = current.interval_sec
await owner.query(`UPDATE public.${table} SET interval_sec=interval_sec+1,poll_configuration_epoch=$1 WHERE id=1`, [epoch])
current = await row(table); assert.notEqual(current.poll_configuration_epoch, epoch)
const middle = current.poll_configuration_epoch
await owner.query(`UPDATE public.${table} SET interval_sec=$1,poll_configuration_epoch=$2 WHERE id=1`, [originalInterval, epoch])
current = await row(table)
assert.equal(current.interval_sec, originalInterval)
assert.notEqual(current.poll_configuration_epoch, epoch); assert.notEqual(current.poll_configuration_epoch, middle)
epoch = String(current.poll_configuration_epoch)
groups.push(table + ":configuration-ABA-even-with-forged-old-epoch")
await owner.query(`ALTER TABLE public.${table} ADD COLUMN future_configuration jsonb`)
await owner.query(`UPDATE public.${table} SET future_configuration='{"nested":[1,true]}'::jsonb WHERE id=1`)
assert.notEqual((await row(table)).poll_configuration_epoch, epoch)
epoch = String((await row(table)).poll_configuration_epoch)
await owner.query(`UPDATE public.${table} SET created_at=created_at+interval '1 second' WHERE id=1`)
assert.notEqual((await row(table)).poll_configuration_epoch, epoch)
epoch = String((await row(table)).poll_configuration_epoch)
groups.push(table + ":unknown-fields-and-created-at-remain-configuration")
await owner.query("SAVEPOINT epoch_rollback")
await owner.query(`UPDATE public.${table} SET enabled=NOT enabled WHERE id=1`)
assert.notEqual((await row(table)).poll_configuration_epoch, epoch)
await owner.query("ROLLBACK TO SAVEPOINT epoch_rollback")
assert.equal((await row(table)).poll_configuration_epoch, epoch)
await owner.query("RELEASE SAVEPOINT epoch_rollback")
groups.push(table + ":rolled-back-configuration-preserves-identity")
await owner.query("SET LOCAL session_replication_role=replica")
await owner.query(`UPDATE public.${table} SET enabled=NOT enabled,poll_configuration_epoch=$1 WHERE id=1`, [epoch])
assert.notEqual((await row(table)).poll_configuration_epoch, epoch)
await owner.query("SET LOCAL session_replication_role=origin")
epoch = String((await row(table)).poll_configuration_epoch)
groups.push(table + ":always-trigger-enforced-in-replica-mode")
await owner.query(`DELETE FROM public.${table} WHERE id=1`)
await owner.query(`INSERT INTO public.${table}(id,poll_configuration_epoch) VALUES(1,$1)`, [epoch])
assert.notEqual((await row(table)).poll_configuration_epoch, epoch)
epoch = String((await row(table)).poll_configuration_epoch)
await owner.query(`UPDATE public.${table} SET enabled=enabled,poll_configuration_epoch=$1 WHERE id=1`, [randomUUID()])
assert.equal((await row(table)).poll_configuration_epoch, epoch)
groups.push(table + ":delete-reinsert-and-noop-forgery")
}
const inventory = async () => (await owner.query(SERVER_POLL_SETTINGS_EPOCH_INVENTORY_SQL, [tables])).rows
assertServerPollSettingsEpochCatalog(await inventory())
const refused = async (name: string, sql: string) => {
await owner.query("SAVEPOINT epoch_tamper")
try {
await owner.query(sql)
assert.throws(() => assertServerPollSettingsEpochCatalog([]))
const observed = await inventory()
assert.throws(() => assertServerPollSettingsEpochCatalog(observed),
(error: unknown) => error instanceof Error && "code" in error && error.code === "STATE_UNAVAILABLE")
} finally { await owner.query("ROLLBACK TO SAVEPOINT epoch_tamper"); await owner.query("RELEASE SAVEPOINT epoch_tamper") }
assertServerPollSettingsEpochCatalog(await inventory())
groups.push(name)
}
await refused("missing-guard-refused", "DROP TRIGGER server_poll_settings_epoch ON public.traffic_settings")
await refused("disabled-guard-refused", "ALTER TABLE public.uptime_settings DISABLE TRIGGER server_poll_settings_epoch")
await refused("ordinary-replica-bypass-guard-refused", "ALTER TABLE public.internet_path_settings ENABLE TRIGGER server_poll_settings_epoch")
await refused("extra-row-trigger-refused", `CREATE TRIGGER zz_extra_epoch BEFORE UPDATE ON public.traffic_settings
FOR EACH ROW EXECUTE FUNCTION public.server_poll_settings_epoch_guard()`)
await refused("forged-function-source-refused", `CREATE OR REPLACE FUNCTION public.server_poll_settings_epoch_guard()
RETURNS trigger LANGUAGE plpgsql SET search_path=pg_catalog AS 'BEGIN RETURN NEW; END;'`)
await refused("changed-function-search-path-refused", "ALTER FUNCTION public.server_poll_settings_epoch_guard() SET search_path=public")
await refused("missing-epoch-column-refused", "ALTER TABLE public.traffic_settings RENAME COLUMN poll_configuration_epoch TO unavailable_epoch")
await refused("nullable-epoch-refused", "ALTER TABLE public.uptime_settings ALTER COLUMN poll_configuration_epoch DROP NOT NULL")
await refused("changed-epoch-default-refused", "ALTER TABLE public.internet_path_settings ALTER COLUMN poll_configuration_epoch DROP DEFAULT")
const triggers = (await owner.query(`SELECT c.relname,t.tgenabled,t.tgtype,t.tgnargs,t.tgqual,
pn.nspname AS function_schema,p.proname,p.prosecdef,p.proconfig
FROM pg_catalog.pg_trigger t JOIN pg_catalog.pg_class c ON c.oid=t.tgrelid
JOIN pg_catalog.pg_namespace n ON n.oid=c.relnamespace JOIN pg_catalog.pg_proc p ON p.oid=t.tgfoid
JOIN pg_catalog.pg_namespace pn ON pn.oid=p.pronamespace
WHERE n.nspname='public' AND c.relname=ANY($1::text[]) AND t.tgname='server_poll_settings_epoch'
ORDER BY c.relname`, [tables])).rows
assert.equal(triggers.length, 3)
for (const trigger of triggers) {
assert.equal(trigger.tgenabled, "A"); assert.equal(trigger.tgtype, 23)
assert.equal(trigger.tgnargs, 0); assert.equal(trigger.tgqual, null)
assert.equal(trigger.function_schema, "public"); assert.equal(trigger.proname, "server_poll_settings_epoch_guard")
assert.equal(trigger.prosecdef, false); assert.deepEqual(trigger.proconfig, ["search_path=pg_catalog"])
}
groups.push("exact-always-row-before-insert-update-metadata")
await owner.query("ROLLBACK")
} finally {
try { await owner.query("ROLLBACK") } finally { owner.release(true) }
}
assert.deepEqual(await snapshots(), before)
if (!numbered) {
assert.equal((await pool.query("SELECT to_regprocedure('public.server_poll_settings_epoch_guard()') IS NULL AS absent")).rows[0].absent, true)
assert.equal((await pool.query(`SELECT count(*)::int AS n FROM information_schema.columns
WHERE table_schema='public' AND table_name=ANY($1::text[]) AND column_name='poll_configuration_epoch'`, [tables])).rows[0].n, 0)
} else assertServerPollSettingsEpochCatalog((await pool.query(SERVER_POLL_SETTINGS_EPOCH_INVENTORY_SQL, [tables])).rows)
groups.push(numbered ? "all-synthetic-data-and-tampering-rolled-back-numbered-guards-restored" : "all-proposal-ddl-and-synthetic-data-rolled-back")
console.log(numbered ? "Owned poll settings epoch numbered:" : "Owned poll settings epoch proposal:", JSON.stringify({ groups, groupCount: groups.length,
numberedMigration: numbered, productionActivated: false, approvalRuntimeWired: false, fullExecutionProofUnchanged: true }))
}