Files
MikrotikManager/backend/src/services/speed-run-persistence.test.ts
T

100 lines
5.2 KiB
TypeScript

import { test } from "node:test"
import assert from "node:assert/strict"
import { assertSpeedProbeEndpoints, matchesSpeedProbeEndpoints, persistSpeedRun, SpeedProbeEndpointsChanged, assertSpeedProbeContext, matchesSpeedProbeContext, SpeedProbeContextChanged, type SpeedProbeContext } from "./speed-run-persistence.js"
const target = { id: "probe", srcServerId: 1, dstServerId: 2 }
type State = { probe: (typeof target & { average: number }) | null; runs: Array<{ probeId: string | null; average: number }> }
function harness(failure?: "update" | "insert" | "commit") {
let state: State = { probe: { ...target, average: 7 }, runs: [] }
let updates = 0
return {
state: () => state,
updates: () => updates,
deps: {
transaction: async (work: (tx: State) => Promise<void>) => {
const tx = structuredClone(state)
await work(tx)
if (failure === "commit") throw Error("commit failed")
state = tx
},
updateProbe: async (tx: State, expected: typeof target) => {
updates++
if (!matchesSpeedProbeEndpoints(tx.probe, expected)) return false
tx.probe!.average = 0
if (failure === "update") throw Error("update failed")
return true
},
insertRun: async (tx: State, probeId: string | null) => {
tx.runs.push({ probeId, average: 0 })
if (failure === "insert") throw Error("insert failed")
},
},
}
}
test("preflight rejects foreign, changed or deleted probe endpoints", () => {
assert.doesNotThrow(() => assertSpeedProbeEndpoints(target, target))
for (const row of [null, undefined, {}, { ...target, id: "other" }, { ...target, srcServerId: 3 }, { ...target, dstServerId: 3 }, { ...target, srcServerId: "1" }]) {
assert.throws(() => assertSpeedProbeEndpoints(row, target), SpeedProbeEndpointsChanged)
}
})
test("one transaction publishes an explicit zero snapshot and associated history", async () => {
const h = harness()
await persistSpeedRun(target, h.deps)
assert.equal(h.state().probe!.average, 0)
assert.deepEqual(h.state().runs, [{ probeId: "probe", average: 0 }])
assert.equal(h.updates(), 1)
})
for (const changed of ["source", "destination", "deleted"] as const) test(`probe ${changed} during native test retains its snapshot and unlinks old run`, async () => {
const h = harness()
if (changed === "deleted") h.state().probe = null
else h.state().probe![changed === "source" ? "srcServerId" : "dstServerId"] = 3
await persistSpeedRun(target, h.deps)
assert.equal(h.state().probe?.average ?? 7, 7)
assert.deepEqual(h.state().runs, [{ probeId: null, average: 0 }])
})
for (const failure of ["update", "insert", "commit"] as const) test(`${failure} error never publishes a partial snapshot/history`, async () => {
const h = harness(failure)
await assert.rejects(persistSpeedRun(target, h.deps), new RegExp(failure))
assert.equal(h.state().probe!.average, 7)
assert.deepEqual(h.state().runs, [])
})
test("ad-hoc run writes history without touching any probe", async () => {
const h = harness()
await persistSpeedRun(null, h.deps)
assert.equal(h.state().probe!.average, 7)
assert.equal(h.updates(), 0)
assert.deepEqual(h.state().runs, [{ probeId: null, average: 0 }])
})
const context: SpeedProbeContext = { ...target, srcInterface: "ether1", dstInterface: "ether2", protocol: "tcp", direction: "both", durationSec: 10 }
const edits: Partial<SpeedProbeContext>[] = [{ srcInterface: "ether3" }, { dstInterface: "ether3" }, { protocol: "udp" }, { direction: "receive" }, { durationSec: 20 }]
test("preflight validates full context and ignores enable/order/telemetry changes", () => {
assert.doesNotThrow(() => assertSpeedProbeContext({ ...context, enabled: false, sortOrder: 9, lastTxAvgMbps: 0 }, context))
for (const edit of edits) assert.throws(() => assertSpeedProbeContext({ ...context, ...edit }, context), SpeedProbeContextChanged)
assert.throws(() => assertSpeedProbeContext(null, context), SpeedProbeEndpointsChanged)
})
for (const edit of edits) test(`changed ${Object.keys(edit)[0]} cannot publish success or failure into new context`, async () => {
for (const status of ["done", "error"]) {
const probe = { ...context, ...edit, average: 7, status: "done" }
const runs: Array<{ probeId: string | null; status: string }> = []
await persistSpeedRun(context, {
transaction: async work => work(probe),
updateProbe: async (_tx, expected) => {
if (!matchesSpeedProbeContext(probe, expected)) return false
probe.average = 0; probe.status = status; return true
},
insertRun: async (_tx, probeId) => { runs.push({ probeId, status }) },
})
assert.equal(probe.average, 7); assert.equal(probe.status, "done")
assert.deepEqual(runs, [{ probeId: null, status }])
}
})
test("matching full context publishes explicit zero with associated history", async () => {
const probe = { ...context, average: 7 }; let associated: string | null = null
await persistSpeedRun(context, {
transaction: async work => work(probe),
updateProbe: async (_tx, expected) => { assert.equal(matchesSpeedProbeContext(probe, expected), true); probe.average = 0; return true },
insertRun: async (_tx, probeId) => { associated = probeId },
})
assert.equal(probe.average, 0); assert.equal(associated, context.id)
})