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
119 lines
9.6 KiB
TypeScript
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")
|
|
|