fix(api): проверять параметры и подтверждать результат MTU
This commit is contained in:
1 parent
68ccc49135
commit
33185fddae
6 files changed
+72
-45
No files matched your search
@@ -9,7 +9,7 @@ const app=Fastify();app.setValidatorCompiler(validatorCompiler);app.setSerialize
|
||||
let lookups=0
|
||||
await app.register(createExecRoutes({getServer:async()=>{lookups++;return undefined},createClient:()=>{throw new Error("unused")}}),{prefix:"/api"})
|
||||
await app.register(createProbeRoutes({getServer:async()=>{lookups++;return undefined},createClient:()=>{throw new Error("unused")}}),{prefix:"/api"})
|
||||
const send=(token?:string,url="/api/servers/1/exec")=>app.inject({method:"POST",url,payload:{command:"interface print"},headers:token?{authorization:"Bearer "+token}:{}})
|
||||
const send=(token?:string,url="/api/servers/1/exec")=>app.inject({method:"POST",url,payload:url.endsWith("/probes/run")?{tool:"ping",target:"192.0.2.2"}:{command:"interface print"},headers:token?{authorization:"Bearer "+token}:{}})
|
||||
assert.equal((await send()).statusCode,401);assert.equal(lookups,0)
|
||||
for(const url of ["/api/servers/1/exec","/api/servers/1/probes/run"])for(const permission of ["mm:servers:read","mm:network:write","mm:servers:write","mm:servers:admin"]){
|
||||
const token=await new SignJWT({sub:"test",apps:["mm"],permissions:[permission]}).setProtectedHeader({alg:"HS256"}).setIssuer("https://auth.test.local").setExpirationTime("1h").sign(new TextEncoder().encode("terminal-test-secret"))
|
||||
|
||||
@@ -4,15 +4,16 @@ import {validatorCompiler,serializerCompiler} from "@fastify/type-provider-zod"
|
||||
import {createProbeRoutes,type ProbeDependencies} from "./probes.js"
|
||||
import type {servers} from "../db/schema.js"
|
||||
const server={id:1,name:"Тест",host:"192.0.2.1",enabled:true,username:"test",password:"test"} as typeof servers.$inferSelect
|
||||
let exists=true,dbFailure=false,upstreamFailure=false,dnsOutput=";; QUESTION: example.test A\nexample.test IN A 192.0.2.2",resolves=0
|
||||
let exists=true,dbFailure=false,upstreamFailure=false,dnsOutput=";; QUESTION: example.test A\nexample.test IN A 192.0.2.2",resolves=0,lookups=0,peerEnabled=true
|
||||
let postOutput:unknown=[{seq:"1",received:"1",time:"1ms",address:"192.0.2.2"}]
|
||||
const calls:unknown[][]=[]
|
||||
const client={
|
||||
post:async(...args:unknown[])=>{calls.push(args);if(upstreamFailure)throw new Error("RouterOS недоступен");return [{seq:"1",received:"1",time:"1ms",address:"192.0.2.2"}]},
|
||||
post:async(...args:unknown[])=>{calls.push(args);if(upstreamFailure)throw new Error("RouterOS недоступен");return postOutput},
|
||||
getIpRoutes:async()=>{if(upstreamFailure)throw new Error("Routes недоступны");return []},
|
||||
bandwidthTest:async()=>{if(upstreamFailure)throw new Error("BW недоступен");return [{"tx-current":"0","rx-current":"42"}]},
|
||||
bandwidthTest:async(...args:unknown[])=>{calls.push(["bw",...args]);if(upstreamFailure)throw new Error("BW недоступен");return [{"tx-current":"0","rx-current":"42"}]},
|
||||
} as ReturnType<NonNullable<ProbeDependencies["createClient"]>>
|
||||
const app=Fastify();app.setValidatorCompiler(validatorCompiler);app.setSerializerCompiler(serializerCompiler)
|
||||
await app.register(createProbeRoutes({getServer:async()=>{if(dbFailure)throw new Error("DB unavailable");return exists?server:undefined},createClient:()=>client,resolveSource:async()=>{resolves++;return "192.0.2.1"},lookupDns:async()=>dnsOutput,findPeer:async()=>server}),{prefix:"/api"})
|
||||
await app.register(createProbeRoutes({getServer:async()=>{lookups++;if(dbFailure)throw new Error("DB unavailable");return exists?server:undefined},createClient:()=>client,resolveSource:async()=>{resolves++;return "192.0.2.1"},lookupDns:async()=>dnsOutput,findPeer:async()=>({...server,enabled:peerEnabled})}),{prefix:"/api"})
|
||||
const send=(payload:unknown,id="1")=>app.inject({method:"POST",url:"/api/servers/"+id+"/probes/run",payload:payload as object})
|
||||
for(const id of ["1oops","0","-1","1.2","9007199254740993"]){assert.equal((await send({tool:"ping",target:"192.0.2.2"},id)).statusCode,400);assert.equal(calls.length,0)}
|
||||
exists=false;assert.equal((await send({tool:"ping",target:"192.0.2.2"})).statusCode,404);exists=true
|
||||
@@ -21,5 +22,16 @@ for(const tool of ["ping","traceroute","dns","route","bandwidth","mtu"]){const b
|
||||
const trace=calls.find(row=>row[0]==="/tool/traceroute")!;assert.equal((trace[1] as Record<string,string>)["max-hops"],"5");assert.equal((trace[1] as Record<string,string>).timeout,"00:00:03");assert.equal((trace[1] as Record<string,string>)["src-address"],"192.0.2.1")
|
||||
upstreamFailure=true;for(const tool of ["ping","traceroute","route","bandwidth"]){const r=await send({tool,target:"192.0.2.2",bwRemoteAddress:"192.0.2.2"});assert.equal(r.statusCode,502,tool);assert.equal(typeof r.json().error,"string");assert.equal(r.json().output,undefined)}upstreamFailure=false
|
||||
dnsOutput=";; QUESTION: example.test A\n;; ERROR: ENOTFOUND";const dns=await send({tool:"dns",target:"example.test"});assert.equal(dns.statusCode,502);assert.match(dns.json().error,/ENOTFOUND/)
|
||||
for(const bad of [{pingCount:"5"},{pingCount:0},{pingSize:-1},{pingTtl:1.2},{traceUseDns:"false"},{traceProto:"junk"},{traceMaxHops:0},{traceHopTimeout:"junk"},{traceHopTimeout:"00:99:01"},{traceHopTimeout:"0s"},{traceProbeCount:0},{dnsType:"ANY"},{target:{}},{target:" "},{dstServerId:-1},{bwProto:"junk"},{bwDuration:0}]){
|
||||
const before=lookups, operations=calls.length; const response=await send({tool:"ping",target:"192.0.2.2",...bad}); assert.equal(response.statusCode,400,JSON.stringify(bad));assert.equal(lookups,before);assert.equal(calls.length,operations)
|
||||
}
|
||||
server.enabled=false;{const before=calls.length;assert.equal((await send({tool:"ping",target:"192.0.2.2"})).statusCode,409);assert.equal(calls.length,before)}server.enabled=true
|
||||
for(const unknown of [[],[{}],[{seq:"0",time:"junk"}],[{received:"99999999999999999999999999"}],[{seq:"junk",time:"1ms"}]]){postOutput=unknown;const before=calls.length;const response=await send({tool:"mtu",target:"192.0.2.2"});assert.equal(response.statusCode,502);assert.match(response.json().error,/не подтвердил/);assert.equal(calls.length,before+1)}
|
||||
postOutput=[{seq:"0",time:"00:00:00.001"}];assert.match((await send({tool:"mtu",target:"192.0.2.2"})).json().output,/MTU \(DF\): ~1500/)
|
||||
postOutput=[{seq:"0",status:"timeout"}];{const before=calls.length,response=await send({tool:"mtu",target:"192.0.2.2"});assert.equal(response.statusCode,200);assert.match(response.json().output,/could not determine/);assert.doesNotMatch(response.json().output,/fragment needed|✓ ok|~1500/);assert.equal(calls.length,before+10)}
|
||||
postOutput=[{seq:"1",received:"1",time:"1ms"}];await send({tool:"ping",target:"192.0.2.2",pingCount:999,pingSize:99999,pingTtl:999});{const body=calls.at(-1)![1] as Record<string,string>;assert.equal(body.count,"100");assert.equal(body.size,"8192");assert.equal(body.ttl,"255")}
|
||||
await send({tool:"bandwidth",bwRemoteAddress:"192.0.2.2",bwDuration:999});assert.equal((calls.at(-1)![1] as {durationSec:number}).durationSec,120)
|
||||
await send({tool:"traceroute",target:"192.0.2.2",traceHopTimeout:"800ms",traceUseDns:false});{const body=calls.at(-1)![1] as Record<string,string>;assert.equal(body.timeout,"00:00:00.800");assert.equal(body["use-dns"],"false")}
|
||||
peerEnabled=false;{const before=calls.length;assert.equal((await send({tool:"bandwidth",bwRemoteAddress:"192.0.2.2"})).statusCode,409);assert.equal(calls.length,before)}peerEnabled=true
|
||||
dbFailure=true;assert.equal((await send({tool:"ping",target:"192.0.2.2"})).statusCode,500)
|
||||
await app.close();console.log("Probes HTTP: strict id, existing allowlist, success/upstream errors, DNS inline error, trace budget and source binding; no DB/RouterOS/DNS")
|
||||
@@ -1,3 +1,4 @@
|
||||
import { z } from "zod"
|
||||
import dns from "node:dns/promises"
|
||||
import { eq } from "drizzle-orm"
|
||||
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
|
||||
@@ -6,7 +7,6 @@ import { servers } from "../db/schema.js"
|
||||
import { MikrotikClient } from "../services/mikrotik.js"
|
||||
import type { RosIpRoute, RosPingResult } from "../types/server.js"
|
||||
import { resolveRosSrcIpv4 } from "../utils/ros-src-address.js"
|
||||
import { abortAfterMs, mergeAbortSignals } from "../utils/abort-signals.js"
|
||||
|
||||
/**
|
||||
* Документация REST API RouterOS: если команда «бесконечна», сессия всё равно закрывается
|
||||
@@ -15,6 +15,24 @@ import { abortAfterMs, mergeAbortSignals } from "../utils/abort-signals.js"
|
||||
*/
|
||||
const ROS_REST_SESSION_MAX_MS = 58_000
|
||||
|
||||
const positiveInteger = z.number().int().positive().max(Number.MAX_SAFE_INTEGER)
|
||||
const targetText = z.string().trim().min(1).max(1024)
|
||||
const traceTime = z.string().trim().min(1).max(32).refine(value => {
|
||||
const unit = /^(\d+(?:\.\d+)?)s$|^(\d+)ms$/.exec(value)
|
||||
if (unit) return Number.isFinite(Number(unit[1] ?? unit[2])) && Number(unit[1] ?? unit[2]) > 0
|
||||
const clock = /^(\d{1,2}):(\d{2}):(\d{2})(?:\.(\d{1,3}))?$/.exec(value)
|
||||
return !!clock && Number(clock[2]) < 60 && Number(clock[3]) < 60 && [clock[1],clock[2],clock[3],clock[4]].some(part => Number(part) > 0)
|
||||
}, "Некорректный timeout traceroute")
|
||||
const probeRequestSchema = z.object({
|
||||
tool: z.string().trim().pipe(z.enum(["ping","traceroute","bandwidth","dns","route","mtu"])),
|
||||
target: targetText.optional(), pingCount: positiveInteger.optional(), pingSize: positiveInteger.optional(), pingTtl: positiveInteger.optional(),
|
||||
traceProto: z.enum(["icmp","udp","tcp"]).optional(), traceMaxHops: positiveInteger.optional(), traceHopTimeout: traceTime.optional(), traceProbeCount: positiveInteger.optional(), traceUseDns: z.boolean().optional(),
|
||||
dnsType: z.string().trim().toUpperCase().pipe(z.enum(["A","AAAA","MX","NS","TXT","CNAME","PTR"])).optional(),
|
||||
bwRemoteAddress: targetText.optional(), dstServerId: positiveInteger.optional(), bwProto: z.enum(["tcp","udp"]).optional(), bwDuration: positiveInteger.optional(),
|
||||
}).passthrough().superRefine((body, ctx) => {
|
||||
if (body.tool === "bandwidth" ? !body.bwRemoteAddress : !body.target) ctx.addIssue({ code: "custom", path: [body.tool === "bandwidth" ? "bwRemoteAddress" : "target"], message: "Обязательная цель диагностики" })
|
||||
})
|
||||
|
||||
/** Парсинг ввода пользователя (800ms, 1s, 00:00:01) → миллисекунды (10–3000). */
|
||||
function parseTraceHopInputToMs(raw: unknown): number {
|
||||
const s = String(raw ?? "").trim().toLowerCase()
|
||||
@@ -228,7 +246,7 @@ async function dnsLookupText(name: string, type: string): Promise<string> {
|
||||
async function mtuDiscover(client: ProbeClient, address: string, srcIpv4: string | null): Promise<string> {
|
||||
const sizes = [1500, 1492, 1480, 1476, 1472, 1468, 1400, 1280, 1024, 576]
|
||||
const lines: string[] = [`MTU discovery (do-not-fragment ping) → ${address}`, ""]
|
||||
let mtuFound = 0
|
||||
let mtuFound: number | null = null
|
||||
for (const size of sizes) {
|
||||
try {
|
||||
const body: Record<string, string> = {
|
||||
@@ -242,18 +260,21 @@ async function mtuDiscover(client: ProbeClient, address: string, srcIpv4: string
|
||||
if (srcIpv4) body["src-address"] = srcIpv4
|
||||
const rows = await client.post<RosPingResult[]>("/tool/ping", body, 25_000)
|
||||
const timeout = rows.some((r) => r.status === "timeout")
|
||||
if (!timeout) {
|
||||
const received = rows.some(r => /^\d+$/.test(String(r.received ?? "")) && Number(r.received) === 1)
|
||||
const reply = rows.some(r => /^[01]$/.test(String(r.seq ?? "")) && !r.status && /^(?:\d+(?:\.\d+)?(?:us|ms|s)|\d{1,2}:\d{2}:\d{2}(?:\.\d+)?)$/.test(String(r.time ?? "")))
|
||||
if (received || reply) {
|
||||
lines.push(` ${String(size).padStart(4)} ✓ ok`)
|
||||
mtuFound = size
|
||||
break
|
||||
}
|
||||
lines.push(` ${String(size).padStart(4)} ✗ fragment needed / timeout`)
|
||||
if (!timeout) throw new Error("RouterOS не подтвердил ответ MTU-пробы.")
|
||||
lines.push(String(size).padStart(4) + " timeout; причина не определена")
|
||||
} catch (e) {
|
||||
lines.push(` ${String(size).padStart(4)} ✗ ${e instanceof Error ? e.message : String(e)}`)
|
||||
throw new Error(lines.join("\n") + "\nMTU-проба " + size + ": " + (e instanceof Error ? e.message : String(e)))
|
||||
}
|
||||
}
|
||||
lines.push("")
|
||||
lines.push(mtuFound > 0 ? `MTU (DF): ~${mtuFound} bytes` : "MTU: could not determine")
|
||||
lines.push(mtuFound !== null ? `MTU (DF): ~${mtuFound} bytes` : "MTU: could not determine")
|
||||
return lines.join("\n")
|
||||
}
|
||||
|
||||
@@ -288,39 +309,13 @@ export function createProbeRoutes(deps: ProbeDependencies = {}): FastifyPluginAs
|
||||
const sid = parseServerId(String((req.params as { id?: string }).id ?? ""))
|
||||
if (sid === null) return reply.status(400).send({ error: "Invalid server id" })
|
||||
|
||||
if (!req.body || typeof req.body !== "object" || Array.isArray(req.body)) return reply.status(400).send({ error: "Diagnostic request must be an object" })
|
||||
const parsed = probeRequestSchema.safeParse(req.body)
|
||||
if (!parsed.success) return reply.status(400).send({ error: "Некорректные параметры диагностики: " + [...new Set(parsed.error.issues.map(issue => issue.path.join(".") || "body"))].join(", ") })
|
||||
const body = parsed.data
|
||||
const server = await getServer(sid)
|
||||
if (!server) return reply.status(404).send({ error: "Server not found" })
|
||||
|
||||
const body = req.body as {
|
||||
tool?: string
|
||||
target?: string
|
||||
pingCount?: number
|
||||
pingSize?: number
|
||||
pingTtl?: number
|
||||
traceProto?: string
|
||||
traceMaxHops?: number
|
||||
/** Таймаут одной пробы: 800ms, 1s или 00:00:01 (в API уходит как HH:MM:SS) */
|
||||
traceHopTimeout?: string
|
||||
/** Число проб на хоп (1–3). Для REST рекомендуется 1 из‑за лимита сессии ~60 с */
|
||||
traceProbeCount?: number
|
||||
/** Резолвить адреса хопов в имена (RouterOS: use-dns yes|no → REST true|false) */
|
||||
traceUseDns?: boolean
|
||||
dnsType?: string
|
||||
bwRemoteAddress?: string
|
||||
dstServerId?: number
|
||||
bwProto?: string
|
||||
bwDuration?: number
|
||||
}
|
||||
|
||||
const tool = String(body.tool ?? "").trim() as
|
||||
| "ping"
|
||||
| "traceroute"
|
||||
| "bandwidth"
|
||||
| "dns"
|
||||
| "route"
|
||||
| "mtu"
|
||||
if (!["ping", "traceroute", "bandwidth", "dns", "route", "mtu"].includes(tool)) return reply.status(400).send({ error: "unknown tool: " + tool })
|
||||
if (!server.enabled) return reply.status(409).send({ error: "Устройство выключено. Диагностика недоступна." })
|
||||
const tool = body.tool
|
||||
try {
|
||||
const client = createClient(server)
|
||||
const srcIpv4 = ["ping", "traceroute", "mtu"].includes(tool) ? await resolveSource(server.host) : null
|
||||
@@ -371,8 +366,9 @@ export function createProbeRoutes(deps: ProbeDependencies = {}): FastifyPluginAs
|
||||
const disconnectAbort = new AbortController()
|
||||
const onClientClose = () => disconnectAbort.abort()
|
||||
req.raw.once("close", onClientClose)
|
||||
const traceTimeout = setTimeout(() => disconnectAbort.abort(), traceWallMs)
|
||||
try {
|
||||
const sig = mergeAbortSignals(disconnectAbort.signal, abortAfterMs(traceWallMs))
|
||||
const sig = disconnectAbort.signal
|
||||
const rows = await client.post<unknown>("/tool/traceroute", traceBody, traceWallMs, sig)
|
||||
const note =
|
||||
effectiveMaxHops < maxHopsRequested
|
||||
@@ -380,6 +376,7 @@ export function createProbeRoutes(deps: ProbeDependencies = {}): FastifyPluginAs
|
||||
: ""
|
||||
output = note + fmtTraceroute(rows)
|
||||
} finally {
|
||||
clearTimeout(traceTimeout)
|
||||
req.raw.off("close", onClientClose)
|
||||
}
|
||||
break
|
||||
@@ -393,6 +390,7 @@ export function createProbeRoutes(deps: ProbeDependencies = {}): FastifyPluginAs
|
||||
error: "Не найден сервер назначения для bandwidth-test: добавьте узел с host = GRE remote или укажите dstServerId",
|
||||
})
|
||||
}
|
||||
if (!dst.enabled) return reply.status(409).send({ error: "Устройство назначения выключено. BW-тест недоступен." })
|
||||
const protocol = body.bwProto === "udp" ? "udp" : "tcp"
|
||||
const durationSec = Math.max(3, Math.min(120, Number(body.bwDuration) || 10))
|
||||
const rows = await client.bandwidthTest({
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
| 7 | Серверы и мониторинг: детали, формы, URL-контекст, права | Завершён в границах локальной приёмки 2026-09-23; внешние ограничения ниже |
|
||||
| 8 | Трафик и статистика | Завершён 2026-09-28 в границах локальной приёмки; ограничения в блоке 8.13 |
|
||||
| 9 | VPN, Firewall и Filters | Завершён в границах локальной приёмки 2026-09-30; блоки 9.1–9.64, ограничения в 9.64 |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.65; оптимизатор, карта и терминал приняты локально; диагностика /probes в работе |
|
||||
| 10 | Маршрутизация, диагностика, карта и терминал | В работе: 10.1–10.66; оптимизатор, карта и терминал приняты локально; диагностика /probes в работе |
|
||||
| 11А | Пользователи, контейнеры, сертификаты и резервные копии | Не начат |
|
||||
| 11Б | Оповещения, сбор данных, интеграции, релизы и настройки | Не начат |
|
||||
| 12 | Итоговая доступность, адаптивность, производительность и регрессия | Не начат |
|
||||
@@ -36,6 +36,14 @@
|
||||
|
||||
|
||||
|
||||
## Блок 10.66 — аргументы и достоверность MTU (2026-10-01)
|
||||
|
||||
- Zod проверяет typed tool/target/числа/boolean/enum/timeout до lookup; число не берётся из строки, false не превращается в true. Неполная цель/невалидный timeout/небезопасные значения дают 400. Существующие defaults и caps ping 100/8192/255, trace 64 hops/3 probes/10–3000 ms/budget 58 s и BW 3–120 s сохранены для валидных аргументов.
|
||||
- Disabled сервер/получатель BW дают 409 до сетевого действия. MTU требует подтверждённый received=1 либо корректный ответ seq/time; пустой/неизвестный/ошибочный ответ даёт 502 и прекращает новые пробы. Timeout не называется fragment needed; полный набор timeout остаётся неизвестным MTU, не 0/1500.
|
||||
- Таймер traceroute теперь снимается в finally после завершения; общий срок и отмена сохранены, тестовый процесс больше не ждёт лишние 58 секунд.
|
||||
- Backend test:probes/test:exec, build и lint прошли; HTTP DI покрывает bad types, disabled, unknown MTU, успешный seq=0, все timeout, caps/trace boolean/time. 2 frontend E2E подтвердили 403/502 и сохранение цели/повтор. JWT тест отправляет валидный probes payload; реальные RouterOS/БД/сеть не проверены.
|
||||
- Далее 10.67: достоверные ping/BW/route показатели и строгий IPv4/CIDR; затем отмена backend запроса, клавиатура/адаптивность, прежние эффекты и локальная приёмка /probes. Этап 10 ещё в работе.
|
||||
|
||||
## Блок 10.65 — честный HTTP контракт backend диагностики (2026-10-01)
|
||||
|
||||
- createProbeRoutes допускает DI поиска устройства/клиента/источника/DNS/BW peer; production defaults сохранены. Строгий положительный целый serverId запрещает parseInt префиксы; body должен быть объектом. Allowlist ping/traceroute/bandwidth/dns/route/mtu не расширен.
|
||||
|
||||
@@ -406,3 +406,12 @@ Shared контракт и backend observed исходные значения/д
|
||||
- Backend test:probes включён в общий test; DI HTTP тесты, test:exec/test:auth, backend build и lint прошли. JWT тест probes теперь регистрирует настоящий createProbeRoutes с подменой getServer: read/network write запрещены до lookup, servers write/admin допускаются до 404. Это не реальный портал/RouterOS/БД/DNS.
|
||||
- Далее 10.66: строгие typed аргументы с сохранением существующих caps/defaults, отказ disabled устройства, MTU не должен считать пустой ответ/неизвестность успешной пробой; затем метрики, адаптивность и приёмка /probes. Этап 10 ещё не завершён.
|
||||
|
||||
|
||||
## Блок 10.66 — аргументы и достоверность MTU (2026-10-01)
|
||||
|
||||
- Zod проверяет typed tool/target/числа/boolean/enum/timeout до lookup; число не берётся из строки, false не превращается в true. Неполная цель/невалидный timeout/небезопасные значения дают 400. Существующие defaults и caps ping 100/8192/255, trace 64 hops/3 probes/10–3000 ms/budget 58 s и BW 3–120 s сохранены для валидных аргументов.
|
||||
- Disabled сервер/получатель BW дают 409 до сетевого действия. MTU требует подтверждённый received=1 либо корректный ответ seq/time; пустой/неизвестный/ошибочный ответ даёт 502 и прекращает новые пробы. Timeout не называется fragment needed; полный набор timeout остаётся неизвестным MTU, не 0/1500.
|
||||
- Таймер traceroute теперь снимается в finally после завершения; общий срок и отмена сохранены, тестовый процесс больше не ждёт лишние 58 секунд.
|
||||
- Backend test:probes/test:exec, build и lint прошли; HTTP DI покрывает bad types, disabled, unknown MTU, успешный seq=0, все timeout, caps/trace boolean/time. 2 frontend E2E подтвердили 403/502 и сохранение цели/повтор. JWT тест отправляет валидный probes payload; реальные RouterOS/БД/сеть не проверены.
|
||||
- Далее 10.67: достоверные ping/BW/route показатели и строгий IPv4/CIDR; затем отмена backend запроса, клавиатура/адаптивность, прежние эффекты и локальная приёмка /probes. Этап 10 ещё в работе.
|
||||
|
||||
@@ -6,7 +6,7 @@ async function routes(page:Page){
|
||||
}
|
||||
test.beforeEach(async({page})=>routes(page))
|
||||
const run=(page:Page)=>page.getByRole("button",{name:"Запустить",exact:true})
|
||||
for(const [name,data,status] of [["malformed",{},200],["legacy",{output:" error: RouterOS недоступен"},200],["API",{error:"Нет доступа к диагностике"},403]] as const)test("Диагностика: "+name+" сохраняет цель и допускает повтор",async({page})=>{
|
||||
for(const [name,data,status] of [["malformed",{},200],["legacy",{output:" error: RouterOS недоступен"},200],["API",{error:"Нет доступа к диагностике"},403],["upstream",{error:"RouterOS недоступен"},502]] as const)test("Диагностика: "+name+" сохраняет цель и допускает повтор",async({page})=>{
|
||||
let fail=true;await page.route("**/api/servers/1/probes/run",r=>r.fulfill(fail?{json:data,status}:{json:{output:"PING подтверждён"}}))
|
||||
await page.goto("/probes?server=1");await expect(run(page)).toBeEnabled();const target=page.getByRole("textbox",{name:"Цель диагностики"});await target.fill("198.51.100.9");await run(page).click()
|
||||
await expect(page.getByText("ошибка",{exact:true})).toBeVisible();await expect(target).toHaveValue("198.51.100.9");await expect(run(page)).toBeEnabled()
|
||||
|
||||
Reference in new issue
Block a user