56 lines
5.1 KiB
TypeScript
56 lines
5.1 KiB
TypeScript
/** Real localhost HTTP, fixed mocked configuration boundary, no database. */
|
|
import assert from "node:assert/strict"
|
|
import { createServer } from "node:http"
|
|
import { once } from "node:events"
|
|
import { mock, test } from "node:test"
|
|
|
|
test("mutable RouterOS transport requires explicit valid managed source before locks/hooks/network", async t => {
|
|
const locks: number[][] = [], writes: { serverId: number; method: string; path: string }[] = [], requests: { method: string; path: string }[] = []
|
|
let refusePolicy = false
|
|
mock.module("./config-mutation-lock.js", { namedExports: {
|
|
withConfigMutationLock: async (ids: number[], work: () => Promise<unknown>) => { locks.push([...ids]); return work() },
|
|
withApplicationMutationLock: async (work: () => Promise<unknown>) => work(),
|
|
} })
|
|
mock.module("./config-mutation-policy.js", { namedExports: {
|
|
executeGuardedConfigurationWrite: async (serverId: number, method: string, path: string, work: () => Promise<unknown>) => { writes.push({ serverId, method, path }); if (refusePolicy) throw Error("Original policy refused"); return work() },
|
|
} })
|
|
const { MikrotikClient } = await import("./mikrotik.js")
|
|
const router = createServer((req, res) => { requests.push({ method: req.method!, path: req.url! }); req.resume(); res.setHeader("content-type", "application/json"); res.end('{"ack":"fixture"}') })
|
|
router.listen(0, "127.0.0.1"); await once(router, "listening"); const address = router.address(); assert(address && typeof address !== "string")
|
|
const endpoint = { host: "127.0.0.1", port: address.port, useSsl: false, verifySsl: true, username: "fixture", password: "fixture" }
|
|
try {
|
|
await t.test("missing/invalid IDs and params.id cannot authorize any mutable verb or beforeDispatch callback", async () => {
|
|
let hooks = 0
|
|
for (const id of [undefined, null, 0, -1, 1.5, Number.NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, "71"]) {
|
|
const client = new MikrotikClient({ ...endpoint, id: 71 } as typeof endpoint, id as number | undefined)
|
|
for (const write of [() => client.post("/interface/bridge/add", {}), () => client.put("/ip/address", {}, 1000, async () => { hooks++ }),
|
|
() => client.patch("/ip/address/*1", {}, 1000, async () => { hooks++ }), () => client.delete("/ip/address/*1", 1000, async () => { hooks++ })]) {
|
|
await assert.rejects(write(), error => !!error && typeof error === "object" && "code" in error && error.code === "MIKROTIK_MANAGED_SOURCE_REQUIRED")
|
|
}
|
|
}
|
|
assert.equal(hooks, 0); assert.equal(locks.length, 0); assert.equal(writes.length, 0); assert.equal(requests.length, 0)
|
|
})
|
|
await t.test("strict diagnostics still require a genuine private permit; forged tokens cannot borrow the id-less exception", async () => {
|
|
const strict = new MikrotikClient(endpoint, undefined, { requireBandwidthPermit: true }), before = requests.length
|
|
await assert.rejects(strict.post("/tool/bandwidth-test", { address: "192.0.2.2", duration: "3s" }))
|
|
for (const path of ["/tool/bandwidth-test", "/interface/bridge/add"]) await assert.rejects(strict.post(path, { address: "192.0.2.2", duration: "3s" }, 1000, undefined, undefined, {} as never))
|
|
assert.equal(requests.length, before); assert.equal(writes.length, 0); assert.equal(locks.length, 0)
|
|
await assert.rejects(new MikrotikClient(endpoint).post("/tool/bandwidth-test", {}), error => !!error && typeof error === "object" && "code" in error && error.code === "MIKROTIK_MANAGED_SOURCE_REQUIRED")
|
|
})
|
|
await t.test("unscoped GET and existing read/diagnostic POST allowlist retain transport behavior", async () => {
|
|
const client = new MikrotikClient(endpoint)
|
|
assert.deepEqual(await client.get("/interface"), { ack: "fixture" })
|
|
for (const path of ["/ip/address/get", "/ip/route/print", "/interface/monitor", "/ping", "/tool/ping", "/tool/traceroute", "/interface/monitor-traffic"]) assert.deepEqual(await client.post(path, {}), { ack: "fixture" })
|
|
assert.equal(requests.length, 8); assert.equal(writes.length, 0); assert.equal(locks.length, 0)
|
|
})
|
|
await t.test("actual constructor binding reaches both original lock and policy before each successful mutation", async () => {
|
|
const client = new MikrotikClient({ ...endpoint, id: 999 } as typeof endpoint, 71), before = requests.length
|
|
await client.post("/interface/bridge/add", {}); await client.put("/ip/address", {}); await client.patch("/ip/address/*1", {}); await client.delete("/ip/address/*1")
|
|
assert.deepEqual(locks, [[71], [71], [71], [71]]); assert(writes.every(write => write.serverId === 71))
|
|
assert.deepEqual(writes.map(write => write.method), ["POST", "PUT", "PATCH", "DELETE"]); assert.equal(requests.length - before, 4)
|
|
refusePolicy = true; const count = requests.length
|
|
await assert.rejects(client.patch("/ip/address/*1", {}), /Original policy refused/); assert.equal(requests.length, count)
|
|
})
|
|
} finally { router.closeAllConnections(); await new Promise<void>(resolve => router.close(() => resolve())); mock.restoreAll(); const { closePool } = await import("../db/index.js"); await closePool() }
|
|
})
|