Files
MikrotikManager/backend/src/services/mikrotik-response.test.ts
T

160 lines
9.1 KiB
TypeScript

import { test } from "node:test"
import assert from "node:assert/strict"
import { createServer } from "node:http"
import { MikrotikClient, MikrotikError, routerOsAccessHint } from "./mikrotik.js"
test("RouterOS access diagnostics distinguish login saturation without assuming a wrong password or retrying", async () => {
let requests = 0
const body = '{"detail":"too many concurrent logins (6)","error":500,"message":"Internal Server Error"}'
const server = createServer((req, res) => { requests++; req.resume(); res.writeHead(500); res.end(body) })
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve))
const address = server.address(); assert(address && typeof address !== "string")
const client = new MikrotikClient({ host: "127.0.0.1", port: address.port, useSsl: false, verifySsl: false, username: "fixture", password: "fixture" })
try {
for (const operation of [() => client.get("/interface"), () => client.put("/own", {})]) {
await assert.rejects(operation, (error: unknown) => {
assert(error instanceof MikrotikError)
assert.equal(error.statusCode, 500); assert.equal(error.body, body)
assert.match(error.message, /превышен лимит одновременных входов/)
return true
})
}
assert.equal(requests, 2, "Neither reads nor writes may silently retry a refused login")
assert.match(routerOsAccessHint(401, '{"message":"Unauthorized"}')!, /ограничения входа/)
assert.doesNotMatch(routerOsAccessHint(401, "")!, /Неверный логин или пароль/)
for (const unknown of ["not JSON", "null", '{"detail":6}', '{"detail":"disk failure"}']) {
assert.equal(routerOsAccessHint(500, unknown), undefined)
}
assert.equal(routerOsAccessHint(400, body), undefined)
} finally {
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
}
})
test("RouterOS print preserves native query arrays and property selection", async () => {
let received: unknown, target = "", method = ""
const server = createServer(async (req, res) => {
target = req.url ?? ""; method = req.method ?? ""
let body = ""; for await (const chunk of req) body += chunk.toString()
received = JSON.parse(body)
res.setHeader("content-type", "application/json"); res.end(JSON.stringify([{ ".id": "*1" }]))
})
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve))
const address = server.address(); assert(address && typeof address !== "string")
const client = new MikrotikClient({ host: "127.0.0.1", port: address.port, useSsl: false, verifySsl: false, username: "fixture", password: "fixture" })
try {
const result = await client.post("/ip/route/print", { ".query": ["comment"], ".proplist": [".id", "gateway"] })
assert.deepEqual(received, { ".query": ["comment"], ".proplist": [".id", "gateway"] })
assert.equal(target, "/rest/ip/route/print"); assert.equal(method, "POST")
assert.deepEqual(result, [{ ".id": "*1" }])
} finally {
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
}
})
test("RouterOS shared REST pool bounds logins across client instances and closes idle sockets", async () => {
let active=0,peak=0,requests=0
const connections=new Set<import("node:net").Socket>()
const server=createServer((req,res)=>{requests++;active++;peak=Math.max(peak,active);req.resume();setTimeout(()=>{res.end('{"ok":true}');active--},20)})
server.on("connection",socket=>{connections.add(socket);socket.on("close",()=>connections.delete(socket))})
await new Promise<void>(resolve=>server.listen(0,"127.0.0.1",resolve))
const address=server.address();assert(address&&typeof address!=="string")
const clients=Array.from({length:3},()=>new MikrotikClient({host:"127.0.0.1",port:address.port,useSsl:false,verifySsl:false,username:"fixture",password:"fixture"}))
try {
await Promise.all(Array.from({length:12},(_,i)=>i%2?clients[i%3]!.post("/own",{},2000):clients[i%3]!.get("/own",2000)))
assert.equal(requests,12);assert.equal(peak,2)
await new Promise<void>(resolve=>setTimeout(resolve,30));assert.equal(connections.size,0)
}finally{await new Promise<void>((resolve,reject)=>server.close(error=>error?reject(error):resolve()))}
})
test("RouterOS queued mutation that times out never reaches the device later", async () => {
let writes=0
const server=createServer((req)=>{req.resume();if(req.method!=="GET")writes++})
await new Promise<void>(resolve=>server.listen(0,"127.0.0.1",resolve))
const address=server.address();assert(address&&typeof address!=="string")
const client=new MikrotikClient({host:"127.0.0.1",port:address.port,useSsl:false,verifySsl:false,username:"fixture",password:"fixture"})
try {
const first=client.get("/hold1",300),second=client.get("/hold2",300),write=client.put("/own",{},40)
const results=await Promise.allSettled([first,second,write]);assert(results.every(result=>result.status==="rejected"));assert.equal(writes,0)
}finally{server.closeAllConnections();await new Promise<void>((resolve,reject)=>server.close(error=>error?reject(error):resolve()))}
})
const methods = ["get", "post", "put", "patch", "delete", "file"] as const
for (const method of methods) {
test(`RouterOS ${method}: interrupted response rejects promptly without a successful receipt`, async () => {
let requests = 0
const server = createServer((req, res) => {
req.resume()
req.on("end", () => {
requests++
res.writeHead(200, { "content-type": "application/json", "content-length": "1000" })
res.write('[{"status":')
setTimeout(() => res.destroy(), 20)
})
})
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve))
const address = server.address()
assert.ok(address && typeof address !== "string")
const client = new MikrotikClient({ host: "127.0.0.1", port: address.port, useSsl: false, verifySsl: false, username: "fixture", password: "fixture" })
let deadline: ReturnType<typeof setTimeout> | undefined
try {
const operation = method === "file" ? client.downloadFile({ id: "*A", name: "", size: 8 }, 500)
: method === "get" || method === "delete" ? client[method]("/fixture", 500)
: client[method]("/fixture", {}, 500)
await assert.rejects(Promise.race([operation, new Promise<never>((_, reject) => {
deadline = setTimeout(() => reject(Error("Request did not settle after interrupted response")), 1200)
})]), /ECONNRESET/)
assert.equal(requests, method === "file" ? 2 : 1, "Writes must not retry; file reads keep two existing compatibility strategies")
} finally {
clearTimeout(deadline)
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
}
})
}
test("RouterOS complete response: success, explicit HTTP rejection and body survive", async () => {
const server = createServer((req, res) => {
req.resume()
req.on("end", () => {
res.writeHead(req.url?.endsWith("/denied") ? 401 : 200, { "content-type": "application/json" })
res.end(req.url?.endsWith("/denied") ? '{"message":"Unauthorized"}' : '{"ack":"confirmed"}')
})
})
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve))
const address = server.address()
assert.ok(address && typeof address !== "string")
const client = new MikrotikClient({ host: "127.0.0.1", port: address.port, useSsl: false, verifySsl: false, username: "fixture", password: "fixture" })
try {
for (const method of ["get", "post", "put", "patch"] as const) {
assert.deepEqual(await (method === "get" ? client.get("/fixture", 500) : client[method]("/fixture", {}, 500)), { ack: "confirmed" })
await assert.rejects(method === "get" ? client.get("/denied", 500) : client[method]("/denied", {}, 500), /401.*Unauthorized/)
}
await client.delete("/fixture", 500)
await assert.rejects(client.delete("/denied", 500), /401.*Unauthorized/)
} finally {
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
}
})
test("RouterOS nonempty malformed JSON cannot confirm PATCH; explicit empty success stays valid", async () => {
const server = createServer((req, res) => {
req.resume()
req.on("end", () => {
res.writeHead(200, { "content-type": "application/json" })
res.end(req.url?.endsWith("/empty") ? "" : "{broken acknowledgement")
})
})
await new Promise<void>(resolve => server.listen(0, "127.0.0.1", resolve))
const address = server.address()
assert.ok(address && typeof address !== "string")
const client = new MikrotikClient({ host: "127.0.0.1", port: address.port, useSsl: false, verifySsl: false, username: "fixture", password: "fixture" })
try {
for (const method of ["get", "post", "put", "patch"] as const) {
await assert.rejects(method === "get" ? client.get("/malformed", 500) : client[method]("/malformed", {}, 500), /JSON|подтверждение/)
}
assert.deepEqual(await client.patch("/empty", {}, 500), {})
} finally {
await new Promise<void>((resolve, reject) => server.close(error => error ? reject(error) : resolve()))
}
})