Files
MikrotikManager/backend/src/services/wireguard-dependency-read.test.ts
T

100 lines
5.1 KiB
TypeScript

import assert from "node:assert/strict"
import { EventEmitter } from "node:events"
import http from "node:http"
import { test, type TestContext } from "node:test"
import { isRouterOsPreHeaderGetReset, MikrotikClient } from "./mikrotik.js"
import { createWireguardDependencyReader } from "./wireguard-interface-delete.js"
type Outcome = "reset" | "late-reset" | "informational-reset" | "body-reset" | "ok" | "refused" | "invalid-json"
const reset = () => Object.assign(new Error("fixture reset"), { code: "ECONNRESET" })
function fixture(t: TestContext, outcomes: Outcome[]) {
const paths: string[] = [], timeouts: number[] = []
t.mock.method(http, "request", (options: http.RequestOptions, receive: (res: unknown) => void) => {
assert.equal(options.method, "GET")
paths.push(options.path!)
const request = new EventEmitter()
const outcome = outcomes.shift()
assert(outcome)
return Object.assign(request, {
end() {
if (outcome === "reset") return request.emit("error", reset())
if (outcome === "informational-reset") {
request.emit("information", { statusCode: 103 })
return request.emit("error", reset())
}
const response = Object.assign(new EventEmitter(), { headers: {}, statusCode: outcome === "refused" ? 429 : 200 })
receive(response)
if (outcome === "late-reset") return request.emit("error", reset())
if (outcome === "body-reset") return response.emit("error", reset())
response.emit("data", Buffer.from(outcome === "invalid-json" ? "!" : "[]"))
response.emit("end")
},
})
})
const client = new MikrotikClient({ host: "fixture.invalid", port: 80, useSsl: false, verifySsl: false, username: "fixture", password: "fixture" })
return {
paths, timeouts,
client: { get<T>(path: string, timeout?: number, signal?: AbortSignal, maxBytes?: number) {
assert.equal(maxBytes, 1_048_576)
timeouts.push(timeout!)
return client.get<T>(path, timeout, signal, maxBytes)
} },
}
}
test("two distinct dependency resets can retry with fresh authority and the original read deadlines", async t => {
const { client, paths, timeouts } = fixture(t, ["reset", "ok", "reset", "ok", "reset"])
let authorized = 0, clock = 0
const read = createWireguardDependencyReader(client, async () => { if (++authorized === 2) clock += 120 }, () => clock)
assert.deepEqual(await read("/interface"), [])
assert.deepEqual(await read("/routing/ospf/neighbor"), [])
await assert.rejects(read("/ip/route"), { code: "STATE_UNAVAILABLE" })
assert.equal(authorized, 5)
assert.deepEqual(paths, ["/rest/interface", "/rest/interface", "/rest/routing/ospf/neighbor", "/rest/routing/ospf/neighbor", "/rest/ip/route"])
assert.deepEqual(timeouts, [10_000, 9_880, 10_000, 10_000, 10_000])
})
test("the same catalogue cannot use the second retry slot", async t => {
const { client, paths } = fixture(t, ["reset", "reset", "reset"])
const read = createWireguardDependencyReader(client, async () => {})
await assert.rejects(read("/interface"), { code: "STATE_UNAVAILABLE" })
await assert.rejects(read("/interface"), { code: "STATE_UNAVAILABLE" })
assert.equal(paths.length, 3)
})
test("post-header reset, refusal, malformed body and unproven reset cannot replay", async t => {
const { client, paths } = fixture(t, ["late-reset", "informational-reset", "body-reset", "refused", "invalid-json"])
const read = createWireguardDependencyReader(client, async () => {})
for (let index = 0; index < 5; index++) await assert.rejects(read("/interface"), { code: "STATE_UNAVAILABLE" })
assert.equal(paths.length, 5)
const synthetic = reset()
assert.equal(isRouterOsPreHeaderGetReset(synthetic), false)
let calls = 0
const unknown = createWireguardDependencyReader({ async get() { calls++; throw synthetic } }, async () => {})
await assert.rejects(unknown("/interface"), { code: "STATE_UNAVAILABLE" })
assert.equal(calls, 1)
})
test("IPv4 and IPv6 reads share the dependency retry budget", async t => {
const { client, paths } = fixture(t, ["reset", "ok", "reset", "ok", "reset"])
const read = createWireguardDependencyReader(client, async () => {})
assert.deepEqual(await read("/ip/address"), [])
assert.deepEqual(await read("/ipv6/address"), [])
await assert.rejects(read("/interface"), { code: "STATE_UNAVAILABLE" })
assert.deepEqual(paths, ["/rest/ip/address", "/rest/ip/address", "/rest/ipv6/address", "/rest/ipv6/address", "/rest/interface"])
})
test("retry authority denial and deadline expiry stop before transport", async t => {
const { client, paths } = fixture(t, ["reset", "reset"])
const denied = new Error("authority denied")
let authorized = 0
const read = createWireguardDependencyReader(client, async () => { if (++authorized === 2) throw denied })
await assert.rejects(read("/interface"), error => error === denied)
let clock = 0
const expired = createWireguardDependencyReader(client, async () => { if (++authorized === 4) clock = 10_000 }, () => clock)
await assert.rejects(expired("/interface"), { code: "STATE_UNAVAILABLE" })
assert.equal(authorized, 4)
assert.equal(paths.length, 2)
})