117 lines
6.1 KiB
TypeScript
117 lines
6.1 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import { mkdtemp, readFile, rm } from "node:fs/promises"
|
|
import { tmpdir } from "node:os"
|
|
import { join } from "node:path"
|
|
import { test } from "node:test"
|
|
import { chrExportDrainMs, executeDisabledChrCapture, type DisabledChrCaptureOptions } from "./traffic-flow-chr-disabled-runner.js"
|
|
|
|
test("export drain requires a bounded inactive-flow timeout plus two seconds", () => {
|
|
assert.equal(chrExportDrainMs("15s"), 17000)
|
|
assert.equal(chrExportDrainMs("00:00:30"), 32000)
|
|
for (const value of ["", "0s", "1m1s", "300", undefined]) {
|
|
assert.throws(() => chrExportDrainMs(value), /Inactive flow timeout/)
|
|
}
|
|
})
|
|
|
|
const cases = {
|
|
"create ACK loss": /Target creation outcome unknown/,
|
|
"unsealed direct capture": /capture producer stopped/,
|
|
"staged capture ACK missing": /capture acknowledgement unavailable/,
|
|
"staged enable ACK loss": /Target enable outcome unknown/,
|
|
"unsealed staged capture": /capture producer stopped/,
|
|
} as const
|
|
|
|
for (const mode of Object.keys(cases) as (keyof typeof cases)[]) test(mode, async () => {
|
|
const staged = mode.includes("staged")
|
|
const dir = await mkdtemp(join(tmpdir(), "mm-chr-disabled-"))
|
|
const actions: string[] = []
|
|
const cleanup: boolean[] = []
|
|
let topologyChecks = 0, topologyChecksAtCapture = 0
|
|
function router(index: number) {
|
|
const name = "nfa012bcde" + (index === 0 ? "ea" : "eb")
|
|
const baseline = { deviceId: String(index), flow: { ".id": "*F", enabled: "false", interfaces: "all", "inactive-flow-timeout": "15s" },
|
|
ipfix: { ".id": "*A", ...Object.fromEntries(["bytes", "packets", "src-address", "dst-address", "protocol", "src-port",
|
|
"dst-port", "in-interface", "out-interface"].map(key => [key, "true"])) }, targets: [] as Record<string, unknown>[] }
|
|
const state = structuredClone(baseline)
|
|
return { baseline, transitInterface: name, interfaces: [{ id: "*1", name, ifIndex: "1" },
|
|
{ id: "*2", name: "nfa012bcde" + (index === 0 ? "ma" : "mb"), ifIndex: "2" }] as const,
|
|
read: async () => state, counters: async () => {
|
|
if (staged) actions.push("counters:" + index)
|
|
return { startedAt: 1, observedAt: 1, rows: [] }
|
|
},
|
|
native: { async post() { throw Error("unexpected POST") },
|
|
async patch<T>(_path: string, body: Record<string, string>, _timeout?: number, fence?: () => Promise<unknown>) {
|
|
await fence?.()
|
|
actions.push("flow:" + index)
|
|
Object.assign(state.flow, body, { enabled: "true" })
|
|
return undefined as T
|
|
} },
|
|
targetNative: { async put<T>(_path: string, body: Record<string, string>, _timeout?: number, fence?: () => Promise<unknown>): Promise<T> {
|
|
await fence?.()
|
|
actions.push("target:" + index)
|
|
if (mode === "create ACK loss") throw Error("lost ACK")
|
|
assert.equal(body.disabled, staged ? "yes" : undefined)
|
|
state.targets.push({ ...body, ".id": "*C", disabled: body.disabled ?? "false" })
|
|
return { ".id": "*C" } as T
|
|
}, async patch<T>(path: string, body: Record<string, string>, _timeout?: number, fence?: () => Promise<unknown>) {
|
|
await fence?.()
|
|
assert.equal(path, "/ip/traffic-flow/target/*C")
|
|
assert.deepEqual(body, { disabled: "no" })
|
|
assert.equal(state.targets[0]?.disabled, "yes")
|
|
state.targets[0].disabled = "false"
|
|
actions.push("enable:" + index)
|
|
if (mode === "staged enable ACK loss") throw Error("lost ACK")
|
|
return { ".id": "*C" } as T
|
|
}, async delete() { actions.push("unexpected inverse") } } }
|
|
}
|
|
const journalPath = join(dir, "owned.jsonl")
|
|
try {
|
|
const options: DisabledChrCaptureOptions = {
|
|
journalPath, deadlineAt: Date.now() + 180000, ...(staged ? { stagedTarget: true as const } : {}),
|
|
scope: { sources: ["192.0.2.1", "192.0.2.2"], destination: "192.0.2.3", port: 50000 },
|
|
expected: [], serverIds: [1, 2], routers: [router(0), router(1)],
|
|
assertTopology: async () => { topologyChecks++ }, authorize: async () => {},
|
|
capture: async exercise => {
|
|
if (staged) actions.push("capture")
|
|
topologyChecksAtCapture = topologyChecks
|
|
await exercise(new AbortController().signal, staged && mode !== "staged capture ACK missing"
|
|
? { kind: "staged-ipfix-timeout-20s", captureDeadlineAt: Date.now() + 120000 } : undefined)
|
|
throw Error("unexpected capture")
|
|
},
|
|
generate: async () => {
|
|
if (staged) {
|
|
assert.equal(topologyChecks - topologyChecksAtCapture, 5) // Two per enable, one before generation.
|
|
actions.push("generate")
|
|
}
|
|
throw Error("capture producer stopped before seal")
|
|
},
|
|
drainCollector: async () => { actions.push("unexpected drain") },
|
|
reconcile: async () => {}, reportRouterCleanup: clean => cleanup.push(clean),
|
|
}
|
|
await assert.rejects(executeDisabledChrCapture(options), cases[mode])
|
|
await assert.rejects(executeDisabledChrCapture(options), { code: "EEXIST" })
|
|
const prefix = ["flow:0", "target:0", "flow:1", "target:1"]
|
|
assert.deepEqual(actions, {
|
|
"create ACK loss": prefix.slice(0, 2),
|
|
"unsealed direct capture": prefix,
|
|
"staged capture ACK missing": [...prefix, "capture"],
|
|
"staged enable ACK loss": [...prefix, "capture", "enable:0"],
|
|
"unsealed staged capture": [...prefix, "capture", "enable:0", "enable:1", "counters:0", "counters:1", "generate"],
|
|
}[mode])
|
|
assert.deepEqual(cleanup, [false])
|
|
const journal = (await readFile(journalPath, "utf8")).trim().split("\n").map(line => JSON.parse(line))
|
|
if (staged && mode !== "staged capture ACK missing") {
|
|
assert(journal.find(entry => entry.phase === "capture-ready"))
|
|
}
|
|
assert.deepEqual(journal.at(-1), {
|
|
"create ACK loss": { device: 0, action: "create", phase: "intent" },
|
|
"unsealed direct capture": { device: 1, action: "create", phase: "verified", id: "*C" },
|
|
"staged capture ACK missing": { phase: "targets-staged-disabled" },
|
|
"staged enable ACK loss": { device: 0, action: "enable", phase: "intent", id: "*C" },
|
|
"unsealed staged capture": { device: 1, action: "enable", phase: "verified", id: "*C" },
|
|
}[mode])
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true })
|
|
}
|
|
})
|