fix(api): проверить каталог speed-проб и изолировать источник

This commit is contained in:
Denozordec committed 2026-10-04 10:31:02 +07:00
1 parent f30319ac55
commit d4f2961f76
13 files changed
+123 -51

No files matched your search

+6 -22
View File
@@ -1,3 +1,4 @@
import { mapSpeedProbeRead } from "../services/speed-probe-dto.js"
import { mapSpeedHistoryRun } from "../services/speed-history-dto.js"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { asc, desc, eq } from "drizzle-orm"
@@ -181,28 +182,11 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/uptime/speed-probes", async (_req, reply) => {
const rows = await db.select().from(uptimeSpeedProbes).orderBy(asc(uptimeSpeedProbes.sortOrder))
return reply.send({
probes: rows.map((r) => ({
id: r.id,
srcServerId: String(r.srcServerId),
dstServerId: String(r.dstServerId),
srcInterface: r.srcInterface || "",
dstInterface: r.dstInterface || "",
protocol: r.protocol === "udp" ? "udp" : "tcp",
direction: r.direction === "transmit" || r.direction === "receive" ? r.direction : "both",
durationSec: String(Math.max(3, r.durationSec || 10)),
enabled: r.enabled !== false,
lastRunAt: r.lastRunAt ?? null,
lastTxAvgMbps: r.lastTxAvgMbps ?? null,
lastRxAvgMbps: r.lastRxAvgMbps ?? null,
lastStatus: r.lastStatus ?? null,
lastError: r.lastError ?? null,
lastPingRttMs: r.lastPingRttMs ?? null,
lastPingLossPct: r.lastPingLossPct ?? null,
lastPingAt: r.lastPingAt ?? null,
lastPingError: r.lastPingError ?? null,
})),
})
try {
return reply.send({ probes: rows.map(mapSpeedProbeRead) })
} catch {
return reply.code(502).send({ error: "Каталог speed-проб содержит некорректную запись. Данные не подтверждены." })
}
})
app.get("/uptime/speed-test/runs", async (_req, reply) => {
@@ -0,0 +1,8 @@
import { test } from "node:test"
import assert from "node:assert/strict"
import { mapSpeedProbeRead } from "./speed-probe-dto.js"
import type { UptimeSpeedProbeRow } from "../db/schema.js"
const row:UptimeSpeedProbeRow={id:"probe-1",srcServerId:1,dstServerId:2,srcInterface:"",dstInterface:"",protocol:"tcp",direction:"both",durationSec:10,enabled:false,lastRunAt:null,lastTxAvgMbps:null,lastRxAvgMbps:0,lastStatus:null,lastError:null,lastPingRttMs:null,lastPingLossPct:0,lastPingAt:null,lastPingError:null,sortOrder:0,createdAt:"2026-10-04T00:00:00Z",updatedAt:"2026-10-04T00:00:00Z"}
test("Speed probe: null/zero/disabled и неизвестный status не подменяются",()=>{const dto=mapSpeedProbeRead(row);assert.equal(dto.enabled,false);assert.equal(dto.lastTxAvgMbps,null);assert.equal(dto.lastRxAvgMbps,0);assert.equal(dto.lastStatus,null);assert.equal(dto.lastPingRttMs,null);assert.equal(dto.lastPingLossPct,0)})
test("Speed probe: timestamp PostgreSQL сохраняет instant",()=>{assert.equal(mapSpeedProbeRead({...row,lastRunAt:"2026-10-04 07:00:00+07"}).lastRunAt,"2026-10-04T00:00:00.000Z")})
test("Speed probe: некорректная DB запись отклоняется без fallback",()=>{for(const raw of [{...row,protocol:"bad"},{...row,direction:"bad"},{...row,durationSec:0},{...row,enabled:null},{...row,lastStatus:"bad"},{...row,lastTxAvgMbps:-1},{...row,lastRunAt:"bad"},{...row,srcServerId:0}])assert.throws(()=>mapSpeedProbeRead(raw as UptimeSpeedProbeRow))})
+13
View File
@@ -0,0 +1,13 @@
import { speedProbeReadSchema } from "@mmapp/contracts"
import type { UptimeSpeedProbeRow } from "../db/schema.js"
const timestamp = (value: string | null) => value === null ? null : new Date(value).toISOString()
export function mapSpeedProbeRead(row: UptimeSpeedProbeRow) {
return speedProbeReadSchema.parse({
id: row.id, srcServerId: String(row.srcServerId), dstServerId: String(row.dstServerId),
srcInterface: row.srcInterface, dstInterface: row.dstInterface,
protocol: row.protocol, direction: row.direction, durationSec: String(row.durationSec), enabled: row.enabled,
lastRunAt: timestamp(row.lastRunAt), lastTxAvgMbps: row.lastTxAvgMbps, lastRxAvgMbps: row.lastRxAvgMbps,
lastStatus: row.lastStatus, lastError: row.lastError,
lastPingRttMs: row.lastPingRttMs, lastPingLossPct: row.lastPingLossPct, lastPingAt: timestamp(row.lastPingAt), lastPingError: row.lastPingError,
})
}