Files
MikrotikManager/backend/src/services/alert-engine/collector-source-sql.test.ts
T

139 lines
12 KiB
TypeScript

import assert from "node:assert/strict"
import { pool, closePool } from "../../db/index.js"
import { initDatabase } from "../../db/bootstrap.js"
import { buildSignalSnapshotFromCollectors } from "./signals-from-collectors.js"
import { evaluateRule } from "./evaluate-rule.js"
import { getAlertsMeta, getAlertsOverview } from "../alerts-service.js"
import type { ApiAlertRule } from "../alerts-service.js"
const database = process.env.MMAPP_OWN_SQL_DB
if (!database) console.log("collector sources SQL: skipped; explicit owned database required")
else {
assert(/^mm_accept_166_\d+$/.test(database))
assert.equal(decodeURIComponent(new URL(process.env.DATABASE_URL!).pathname.slice(1)), database)
assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name, database)
const prefix = "own-sources170-" + Date.now() + "-", now = Date.now(), iso = (delta = 0) => new Date(now + delta).toISOString()
let sequence = 0, cases = 0
const ids: number[] = []
const snapshot = (job: string, rows: unknown[], delta = -1_000) => ({ v: 1, job, sampledAt: iso(delta), [job === "uptime_ping" ? "probes" : "servers"]: rows })
const insert = async (job: string, payload: unknown, delta = -1_000, status = "ok") => {
await pool.query("INSERT INTO scheduler_runs(id,job_key,started_at,finished_at,status,error,result_json) VALUES($1,$2,$3,$3,$4,$5,$6::jsonb)", [prefix + sequence++, job, iso(delta), status, status === "error" ? "owned failure" : null, payload === null ? null : JSON.stringify(payload)])
}
const reset = () => pool.query("DELETE FROM scheduler_runs WHERE id LIKE $1", [prefix + "%"])
const build = () => buildSignalSnapshotFromCollectors(() => now)
try {
await initDatabase()
for (const index of [1, 2]) ids.push(Number((await pool.query("INSERT INTO servers(name,host,enabled,updated_at) VALUES($1,$2,true,$3) RETURNING id", [prefix + index, `own-host-${index}`, iso(-900_000)])).rows[0].id))
await pool.query("INSERT INTO uptime_settings(id,enabled,resources_enabled,ping_enabled,interval_sec,probe_interval_sec) VALUES(1,true,true,true,15,15) ON CONFLICT(id) DO UPDATE SET enabled=true,resources_enabled=true,ping_enabled=true,interval_sec=15,probe_interval_sec=15")
await pool.query("INSERT INTO servers_api_ping_settings(id,enabled,interval_sec) VALUES(1,false,120) ON CONFLICT(id) DO UPDATE SET enabled=false,interval_sec=120")
await pool.query("INSERT INTO traffic_settings(id,enabled,interval_sec) VALUES(1,true,30) ON CONFLICT(id) DO UPDATE SET enabled=true,interval_sec=30")
for (const [index, interval] of [[0, 120], [1, 0]]) await pool.query("INSERT INTO uptime_probes(id,src_server_id,src_interface,name,target,enabled,interval_sec,updated_at) VALUES($1,$2,'ether1',$3,'own-target',true,$4,$5)", [prefix + "probe" + index, ids[index], prefix + "probe" + index, interval, iso(-900_000)])
const server = (index = 0) => ({ serverId: ids[index], name: prefix + (index + 1), host: `own-host-${index + 1}` })
const resource = (status = "offline", index = 0) => ({ ...server(index), status })
const traffic = { ...server(), ok: true, sumRxMbps: 0, sumTxMbps: 12 }
const probe = (index = 0) => ({ probeId: prefix + "probe" + index, name: prefix + "probe" + index, target: "own-target", srcServerId: ids[index], srcServerName: prefix + (index + 1), srcInterface: "ether1", ok: true, status: "up", rttMs: 5, lossPct: 0 })
await insert("uptime_resources", snapshot("uptime_resources", [resource("online")], -15_000), -15_000)
await insert("uptime_resources", snapshot("uptime_resources", [resource(), resource("online", 1)]), -1_000, "error")
await insert("traffic", snapshot("traffic", [traffic]))
await insert("uptime_ping", snapshot("uptime_ping", [probe()], -90_000), -90_000)
// More than the old 24-tick history; the slow probe must still survive due-only batches.
for (let index = 30; index > 0; index--) await insert("uptime_ping", snapshot("uptime_ping", [probe(1)], -index * 1_000), -index * 1_000)
let signals = await build()
assert.equal(signals.servers.length, 2); assert.equal(signals.servers[0].prevStatus, "online")
assert.equal(signals.trafficByServer[0].rxMbps, 0); assert.equal(signals.probes.length, 2); cases++
assert.deepEqual((await getAlertsMeta()).targetIssues, [])
const offlineRule = { id: "owned", name: "owned", type: "server", targets: [server().name], conditions: ["offline"], enabled: true } as ApiAlertRule
assert.ok(evaluateRule(offlineRule, signals))
await pool.query("UPDATE uptime_settings SET resources_enabled=false,ping_enabled=false WHERE id=1")
await pool.query("UPDATE traffic_settings SET enabled=false WHERE id=1")
signals = await build(); assert.deepEqual(signals.servers, []); assert.deepEqual(signals.probes, []); assert.deepEqual(signals.trafficByServer, []); cases++
assert((await getAlertsMeta()).targetIssues?.every(issue => issue.reason === "disabled-source"))
await pool.query("UPDATE uptime_settings SET resources_enabled=true,ping_enabled=true WHERE id=1")
await pool.query("UPDATE traffic_settings SET enabled=true WHERE id=1")
await pool.query("UPDATE servers SET enabled=false WHERE id=$1", [ids[0]])
signals = await build(); assert.equal(signals.servers.length, 1); assert.equal(signals.probes.length, 1); assert.deepEqual(signals.trafficByServer, []); cases++
await pool.query("UPDATE servers SET enabled=true WHERE id=$1", [ids[0]])
await pool.query("UPDATE servers SET name=$1 WHERE id=$2", [server().name, ids[1]])
signals = await build(); assert.deepEqual(signals.servers, []); assert.deepEqual(signals.trafficByServer, []); cases++
assert.deepEqual((await getAlertsMeta()).targetIssues?.filter(issue => issue.reason === "ambiguous").map(issue => issue.kind), ["server", "traffic"])
await pool.query("UPDATE servers SET name=$1 WHERE id=$2", [server(1).name, ids[1]])
for (const broken of [null, "{", { ...snapshot("uptime_resources", []), servers: {} }, snapshot("uptime_resources", [])]) {
await reset(); await insert("uptime_resources", snapshot("uptime_resources", [resource()], -15_000), -15_000)
await insert("uptime_resources", broken, -1_000, "error")
signals = await build(); assert.deepEqual(signals.servers, []); assert.equal(evaluateRule(offlineRule, signals), null); cases++
}
for (const [delta, field] of [[-46_000, "sample"], [16_000, "sample"], [-46_000, "finish"], [16_000, "finish"]] as const) {
await reset(); await insert("uptime_resources", snapshot("uptime_resources", [resource()], field === "sample" ? delta : -1_000), field === "finish" ? delta : -1_000)
assert.deepEqual((await build()).servers, []); cases++
}
await reset(); await insert("uptime_resources", snapshot("uptime_resources", [resource("unknown"), resource("online", 1)]), -1_000, "error")
signals = await build(); assert.equal(signals.servers.length, 1); assert.equal(signals.servers[0].name, server(1).name); cases++
await reset(); await insert("traffic", snapshot("traffic", [traffic], -30_000), -30_000)
await insert("traffic", snapshot("traffic", [{ ...server(), ok: false }]), -1_000, "error")
assert.deepEqual((await build()).trafficByServer, []); cases++
await reset(); await insert("uptime_ping", snapshot("uptime_ping", [probe()], -15_000), -15_000)
await insert("uptime_ping", null, -1_000, "error")
assert.deepEqual((await build()).probes, []); cases++
// The journal must use the same confirmed facts, rather than reviving native sample tables.
await pool.query("UPDATE uptime_probes SET updated_at=$1 WHERE id=$2", [iso(-900_000), probe().probeId])
await pool.query("INSERT INTO uptime_resource_samples(server_id,sampled_at,status) VALUES($1,$2,'offline')", [ids[0], iso(-1_000)])
await pool.query("INSERT INTO uptime_probe_samples(probe_id,sampled_at,rtt_ms,loss_pct,status) VALUES($1,$2,8,25,'warn')", [probe().probeId, iso(-1_000)])
await pool.query("INSERT INTO alert_history(id,rule_name,severity,message,sent_ok,fired_at) VALUES($1,'owned receipt','info','confirmed delivery receipt',true,$2)", [prefix + "receipt", iso(-1_000)])
const freshJournal = async () => {
await reset()
await insert("uptime_resources", snapshot("uptime_resources", [resource()]))
await insert("uptime_ping", snapshot("uptime_ping", [{ ...probe(), status: "warn", lossPct: 25, rttMs: 8 }]))
}
const journal = async () => {
const overview = await getAlertsOverview()
assert.equal(overview.history.filter(h => h.source === "db" && h.id === prefix + "receipt").length, 1)
assert.equal(overview.history.find(h => h.id === prefix + "receipt")?.sent, true)
return overview.history.filter(h => h.source === "live")
}
await freshJournal()
let live = await journal(); assert.equal(live.length, 2); assert(live.every(h => typeof h.firedAt === "string" && h.sent === false)); cases++
await pool.query("UPDATE alert_history SET fired_at=$1 WHERE id=$2", [iso(), prefix + "receipt"])
assert.equal((await getAlertsOverview()).history[0].id, prefix + "receipt"); cases++
await pool.query("UPDATE servers SET enabled=false WHERE id=$1", [ids[0]])
assert.deepEqual(await journal(), []); cases++
await pool.query("UPDATE servers SET enabled=true WHERE id=$1", [ids[0]])
await pool.query("UPDATE uptime_settings SET resources_enabled=false,ping_enabled=false WHERE id=1")
assert.deepEqual(await journal(), []); cases++
await pool.query("UPDATE uptime_settings SET resources_enabled=true,ping_enabled=true WHERE id=1")
await reset()
await insert("uptime_resources", snapshot("uptime_resources", [resource()], -3_600_000), -3_600_000)
await insert("uptime_ping", snapshot("uptime_ping", [probe()], -3_600_000), -3_600_000)
assert.deepEqual(await journal(), []); cases++
await freshJournal()
await insert("uptime_resources", null, 0, "error"); await insert("uptime_ping", null, 0, "error")
assert.deepEqual(await journal(), []); cases++
await reset(); await insert("uptime_resources", snapshot("uptime_resources", [])); await insert("uptime_ping", snapshot("uptime_ping", []))
assert.deepEqual(await journal(), []); cases++
await freshJournal()
await pool.query("UPDATE servers SET name=$1 WHERE id=$2", [server().name, ids[1]])
live = await journal(); assert.equal(live.length, 1); assert.equal(live[0].ruleName, "Ping-проба"); cases++
await pool.query("UPDATE servers SET name=$1 WHERE id=$2", [server(1).name, ids[1]])
await pool.query("UPDATE uptime_probes SET name=$1 WHERE id=$2", [prefix + "changed-probe", probe().probeId])
live = await journal(); assert.equal(live.length, 1); assert.equal(live[0].ruleName, "Доступность сервера"); cases++
await reset(); assert.deepEqual(await journal(), []); cases++
await pool.query("UPDATE uptime_probes SET name=$1 WHERE id=$2", [probe().name, probe().probeId])
await reset(); await insert("uptime_ping", snapshot("uptime_ping", [probe()], -15_000), -15_000)
await pool.query("UPDATE uptime_probes SET updated_at=$1 WHERE id=$2", [iso(), probe().probeId])
assert.deepEqual((await build()).probes, []); cases++
console.log(`collector sources owned SQL: ${cases} cases; real settings/identity/error/time/due-only/unknown/reconfiguration/evaluation passed`)
} finally {
await reset()
await pool.query("DELETE FROM alert_history WHERE id LIKE $1", [prefix + "%"])
await pool.query("DELETE FROM uptime_resource_samples WHERE server_id=ANY($1::bigint[])", [ids])
await pool.query("DELETE FROM uptime_probe_samples WHERE probe_id LIKE $1", [prefix + "%"])
await pool.query("DELETE FROM uptime_probes WHERE id LIKE $1", [prefix + "%"])
await pool.query("DELETE FROM servers WHERE id = ANY($1::bigint[])", [ids])
assert.equal((await pool.query("SELECT count(*)::int AS n FROM scheduler_runs WHERE id LIKE $1", [prefix + "%"])).rows[0].n, 0)
assert.equal((await pool.query("SELECT count(*)::int AS n FROM servers WHERE id=ANY($1::bigint[])", [ids])).rows[0].n, 0)
await closePool()
}
}