Files
MikrotikManager/backend/src/routes/recursive-routes.test.ts
T
Denozordec e6a2e4009d
Docker images / prepare-release (push) Successful in 10s
Docker images / backend-test (push) Successful in 5m2s
Docker images / frontend-image (push) Successful in 2m46s
Docker images / updater-image (push) Successful in 42s
Docker images / backend-image (push) Successful in 1m54s
Docker images / publish-release (push) Successful in 15s
Docker images / notify-webhook (push) Successful in 2s
fix(routes): читать рекурсивные маршруты с незаданными свойствами
2026-10-16 00:28:45 +07:00

119 lines
9.6 KiB
TypeScript

import assert from "node:assert/strict"
import Fastify from "fastify"
import { validatorCompiler, serializerCompiler } from "@fastify/type-provider-zod"
import { createRecursiveRoutes, type RecursiveRouteDependencies } from "./recursive-routes.js"
import { RecursiveApplyError } from "../services/recursive-apply.js"
const row = { id: "one", dstAddress: "10.0.0.0/24", gateway: "192.0.2.1", distance: 0, scope: 0, targetScope: 0, routingTable: "main", checkGateway: "ping", country: "", comment: "Маршрут", disabled: false }
const server = { id: 1, name: "Тест", enabled: true, type: "home-router" } as NonNullable<Awaited<ReturnType<RecursiveRouteDependencies["getServer"]>>>
let rawReads = 0, lateReadFailure = false, applyError: Error | undefined
let lookup = 0, writes = 0, revisions = 0, fail = false, stored = [row], payload: unknown = [row], revisionServer = 1, section = "recursive-routes"
const observed = { ".id": "*1", static: "true", "dst-address": row.dstAddress, gateway: row.gateway, distance: "0", scope: "0", "target-scope": "0", "routing-table": "main", "check-gateway": "ping", disabled: "false", comment: "MikrotikManager:recursive Тест" }
let rawPayload: unknown = [observed], rawFailure = false, savedSnapshots = 0
let propertyFailure = false, propertyReads = 0
const applied: Array<{ rows: typeof row[]; source: string }> = []
const app = Fastify()
app.setValidatorCompiler(validatorCompiler); app.setSerializerCompiler(serializerCompiler)
await app.register(createRecursiveRoutes({
getServer: async id => { lookup++; return id === 1 ? server : undefined },
getRoutes: async () => { if (lateReadFailure) throw Error("Сбой чтения после применения"); return stored },
readRaw: async () => { rawReads++; if (rawFailure) throw new Error("Тест опроса"); return rawPayload },
readProperty: async (_server, _id, property) => { propertyReads++; if (propertyFailure) throw Error("Свойство недоступно"); return property === "disabled" ? { ret: "false" } : [] },
saveObserved: async (_id, rows) => { savedSnapshots++; stored = rows as typeof row[] },
apply: async (device, rows, source) => {
assert.equal(device.id, 1); writes++; applied.push({ rows: rows as typeof row[], source })
if (fail) throw new Error("Тест частичного применения")
if (applyError) throw applyError
stored = rows as typeof row[]; return { pushed: rows.length, deleted: 0 }
},
getRevision: async () => {
revisions++
return { id: "rev", serverId: revisionServer, section, payload } as NonNullable<Awaited<ReturnType<RecursiveRouteDependencies["getRevision"]>>>
},
}), { prefix: "/api" })
const invalid = [
{}, { serverId: 1 }, { serverId: 1, routes: null }, { serverId: 1, routes: {} },
{ serverId: null, routes: [] }, { serverId: "srv1", routes: [] }, { serverId: "9007199254740993", routes: [] },
{ serverId: 1, routes: [{}] }, { serverId: 1, routes: [{ ...row, disabled: "false" }] },
{ serverId: 1, routes: [{ ...row, distance: -1 }] }, { serverId: 1, routes: [{ ...row, scope: 256 }] },
{ serverId: 1, routes: [{ ...row, targetScope: 1.5 }] }, { serverId: 1, routes: [{ ...row, gateway: "" }] },
{ serverId: 1, routes: [row, row] }, { serverId: 1, routes: [row], source: "rollback" },
...[{...row,dstAddress:"999.1.1.1/24"},{...row,dstAddress:"10.0.0.0/33"},{...row,gateway:"ether1"},{...row,gateway:"192.0.2.1%"},{...row,checkGateway:"anything"}].map(route=>({serverId:1,routes:[route]})),
]
for (const body of invalid) {
const result = await app.inject({ method: "PUT", url: "/api/recursive-routes", payload: body })
assert.equal(result.statusCode, 400, JSON.stringify(body))
assert.equal(lookup, 0); assert.equal(writes, 0)
}
const empty = await app.inject({ method: "PUT", url: "/api/recursive-routes", payload: { serverId: "1", routes: [] } })
assert.equal(empty.statusCode, 200); assert.deepEqual(empty.json(), { ok: true, serverId: 1, routes: [], pushedRoutes: 0 })
const known = await app.inject({ method: "PUT", url: "/api/recursive-routes", payload: { serverId: 1, routes: [row], source: "copy" } })
assert.equal(known.statusCode, 200); assert.equal(known.json().routes[0].distance, 0); assert.equal(known.json().routes[0].scope, 0)
assert.equal(applied.at(-1)?.source, "copy")
fail = true
const failed = await app.inject({ method: "PUT", url: "/api/recursive-routes", payload: { serverId: 1, routes: [row] } })
assert.equal(failed.statusCode, 500); assert.equal(failed.json().ok, undefined); fail = false
for (const body of [{}, { serverId: 0 }, { serverId: "srv1" }]) {
const before = writes, reads = revisions
const result = await app.inject({ method: "POST", url: "/api/recursive-routes/revisions/rev/restore", payload: body })
assert.equal(result.statusCode, 400); assert.equal(writes, before); assert.equal(revisions, reads)
}
revisionServer = 2
const before = writes
assert.equal((await app.inject({ method: "POST", url: "/api/recursive-routes/revisions/rev/restore", payload: { serverId: 1 } })).statusCode, 400)
assert.equal(writes, before); revisionServer = 1
section = "filters"
assert.equal((await app.inject({ method: "POST", url: "/api/recursive-routes/revisions/rev/restore", payload: { serverId: 1 } })).statusCode, 400)
assert.equal(writes, before); section = "recursive-routes"
for (payload of [null, {}, [{}], [{ ...row, disabled: "false" }], [{ ...row, distance: null }]]) {
const result = await app.inject({ method: "POST", url: "/api/recursive-routes/revisions/rev/restore", payload: { serverId: 1 } })
assert.equal(result.statusCode, 409); assert.equal(writes, before)
}
payload = [{ ...row, id: undefined }]
const restored = await app.inject({ method: "POST", url: "/api/recursive-routes/revisions/rev/restore", payload: { serverId: 1 } })
assert.equal(restored.statusCode, 200); assert.equal(restored.json().serverId, 1); assert.equal(applied.at(-1)?.source, "rollback")
assert.equal(restored.json().routes[0].distance, 0)
payload = []
const restoredEmpty = await app.inject({ method: "POST", url: "/api/recursive-routes/revisions/rev/restore", payload: { serverId: 1 } })
assert.deepEqual(restoredEmpty.json(), { ok: true, serverId: 1, routes: [], pushedRoutes: 0 })
const catalog=await app.inject({ method: "GET", url: "/api/recursive-routes/catalog?serverId=1" })
assert.equal(catalog.statusCode,200); assert.equal(catalog.json().routes[0].distance,0); assert.equal(catalog.json().serverId,1)
const savedBefore=savedSnapshots
for(const bad of [[{...observed,distance:undefined}], {}, [{...observed,blackhole:"true"}]]) {
rawPayload=bad
const result=await app.inject({method:"GET",url:"/api/recursive-routes/catalog?serverId=1"})
assert.equal(result.statusCode,503);assert.equal(result.json().routes,undefined);assert.equal(savedSnapshots,savedBefore)
}
rawFailure=true
assert.equal((await app.inject({method:"GET",url:"/api/recursive-routes/catalog?serverId=1"})).statusCode,503)
rawFailure=false;rawPayload=[]
assert.deepEqual((await app.inject({method:"GET",url:"/api/recursive-routes/catalog?serverId=1"})).json(),{serverId:1,routes:[],live:true,stale:false})
rawPayload=[{...observed,disabled:undefined,"check-gateway":undefined}]
const recovered=await app.inject({method:"GET",url:"/api/recursive-routes/catalog?serverId=1"})
assert.equal(recovered.statusCode,200);assert.equal(recovered.json().routes[0].disabled,false);assert.equal(recovered.json().routes[0].checkGateway,"none");assert.equal(propertyReads,2)
propertyFailure=true
const beforePropertyFailure=savedSnapshots
const failedProperty=await app.inject({method:"GET",url:"/api/recursive-routes/catalog?serverId=1"})
assert.equal(failedProperty.statusCode,503);assert.equal(failedProperty.json().routes,undefined);assert.equal(savedSnapshots,beforePropertyFailure)
propertyFailure=false
rawPayload=[{".id":"one",gateway:"192.0.2.1"}]
assert.equal((await app.inject({method:"GET",url:"/api/recursive-routes/gateways/catalog?serverId=1"})).json().gateways[0].status,"unknown")
for (const type of ["home-router", "exit-node"] as const) {
server.enabled=false;server.type=type
const counts={writes,rawReads,savedSnapshots}
for(const url of ["/recursive-routes/catalog?serverId=1","/recursive-routes/gateways/catalog?serverId=1","/recursive-routes?serverId=1","/recursive-routes/gateways?serverId=1"])assert.equal((await app.inject({method:"GET",url:"/api"+url})).statusCode,409)
assert.equal((await app.inject({method:"PUT",url:"/api/recursive-routes",payload:{serverId:1,routes:[]}})).statusCode,409)
assert.equal((await app.inject({method:"POST",url:"/api/recursive-routes/revisions/rev/restore",payload:{serverId:1}})).statusCode,409)
assert.deepEqual({writes,rawReads,savedSnapshots},counts)
}
server.enabled=true
for(const state of ["partial","unknown","applied"] as const){
applyError=new RecursiveApplyError("Сбой операции",{state,confirmedWrites:1,createdIds:["*1"],deletedIds:[],needsReview:true,metadataRecorded:false})
for(const restore of [false,true]){const r=await app.inject({method:restore?"POST":"PUT",url:restore?"/api/recursive-routes/revisions/rev/restore":"/api/recursive-routes",payload:restore?{serverId:1}:{serverId:1,routes:[]}});assert.equal(r.statusCode,502);assert.equal(r.json().state,state);assert.equal(r.json().confirmedWrites,1);assert.equal(r.json().ok,undefined)}
}
applyError=undefined;lateReadFailure=true
const late=await app.inject({method:"PUT",url:"/api/recursive-routes",payload:{serverId:1,routes:[row]}});assert.equal(late.statusCode,502);assert.equal(late.json().state,"applied");assert.equal(late.json().metadataRecorded,true)
lateReadFailure=false
await app.close()
console.log("recursive routes HTTP: invalid/explicit-empty/zero/copy/error/restore ownership and payload passed without DB/RouterOS")