fix(api): сохранить неизвестные значения истории скорости

This commit is contained in:
Denozordec committed 2026-10-04 05:19:34 +07:00
1 parent e5ff69e243
commit a558412a27
13 files changed
+119 -70

No files matched your search

+6 -27
View File
@@ -1,3 +1,4 @@
import { mapSpeedHistoryRun } from "../services/speed-history-dto.js"
import type { FastifyPluginAsyncZod } from "@fastify/type-provider-zod"
import { asc, desc, eq } from "drizzle-orm"
import { db } from "../db/index.js"
@@ -206,33 +207,11 @@ const uptimeRoutes: FastifyPluginAsyncZod = async (app) => {
app.get("/uptime/speed-test/runs", async (_req, reply) => {
const rows = await db.select().from(uptimeSpeedTestRuns).orderBy(desc(uptimeSpeedTestRuns.createdAt)).limit(200)
return reply.send({
runs: rows.map((r) => ({
id: r.id,
probeId: r.probeId ?? null,
srcServerId: String(r.srcServerId),
dstServerId: String(r.dstServerId),
srcInterface: r.srcInterface || "",
dstInterface: r.dstInterface || "",
srcAddress: r.srcAddress ?? null,
dstAddress: r.dstAddress ?? null,
srcInterfaceAddress: r.srcInterfaceAddress ?? null,
dstInterfaceAddress: r.dstInterfaceAddress ?? null,
protocol: r.protocol === "udp" ? "udp" : "tcp",
direction: r.direction === "transmit" || r.direction === "receive" ? r.direction : "both",
durationSec: Math.max(3, r.durationSec || 10),
txAvgMbps: r.txAvgMbps ?? 0,
rxAvgMbps: r.rxAvgMbps ?? 0,
afterBtPing: {
rttMs: r.pingRttMs ?? null,
lossPct: r.pingLossPct ?? null,
error: r.pingError ?? null,
},
status: r.status === "error" ? "error" : "done",
error: r.error ?? null,
createdAt: r.createdAt,
})),
})
try {
return reply.send({ runs: rows.map(mapSpeedHistoryRun) })
} catch {
return reply.code(502).send({ error: "История скорости содержит некорректную запись. Результаты не подтверждены." })
}
})
app.put("/uptime/speed-probes", async (req, reply) => {
@@ -0,0 +1,10 @@
import { test } from "node:test"
import assert from "node:assert/strict"
import { mapSpeedHistoryRun, type StoredSpeedRun } from "./speed-history-dto.js"
import { speedHistoryResponseSchema } from "@mmapp/contracts"
const row:StoredSpeedRun={id:"run-1",probeId:null,srcServerId:1,dstServerId:2,srcInterface:null,dstInterface:null,srcAddress:null,dstAddress:null,srcInterfaceAddress:null,dstInterfaceAddress:null,protocol:"tcp",direction:"both",durationSec:10,txAvgMbps:null,rxAvgMbps:0,pingRttMs:null,pingLossPct:0,pingError:null,status:"done",error:null,createdAt:"2026-10-04T00:00:00Z"}
test("История: неизвестный TX остаётся null, подтверждённые нули RX/loss остаются нулями",()=>{const dto=mapSpeedHistoryRun(row);assert.equal(dto.txAvgMbps,null);assert.equal(dto.rxAvgMbps,0);assert.equal(dto.afterBtPing?.rttMs,null);assert.equal(dto.afterBtPing?.lossPct,0)})
test("История: нельзя подменить invalid status/protocol/direction/duration/date/rate/server",()=>{for(const invalid of [{status:"running"},{status:"unknown"},{protocol:"unknown"},{direction:"unknown"},{durationSec:0},{createdAt:"bad"},{txAvgMbps:-1},{rxAvgMbps:NaN},{srcServerId:0}])assert.throws(()=>mapSpeedHistoryRun({...row,...invalid}))})
test("История: полный DTO, пустой список и уникальные ID",()=>{const dto=mapSpeedHistoryRun(row);assert.deepEqual(speedHistoryResponseSchema.parse({runs:[]}),{runs:[]});assert.equal(speedHistoryResponseSchema.safeParse({runs:[dto,dto]}).success,false);assert.equal(speedHistoryResponseSchema.safeParse({runs:[{...dto,txAvgMbps:undefined}]}).success,false)})
test("История: timestamp PostgreSQL нормализуется без изменения времени",()=>{assert.equal(mapSpeedHistoryRun({...row,createdAt:"2026-10-04 07:00:00+07"}).createdAt,"2026-10-04T00:00:00.000Z")})
+17
View File
@@ -0,0 +1,17 @@
import { speedHistoryRunSchema } from "@mmapp/contracts"
export type StoredSpeedRun = {
id:string;probeId:string|null;srcServerId:number;dstServerId:number;srcInterface:string|null;dstInterface:string|null;
srcAddress:string|null;dstAddress:string|null;srcInterfaceAddress:string|null;dstInterfaceAddress:string|null;
protocol:string;direction:string;durationSec:number;txAvgMbps:number|null;rxAvgMbps:number|null;
pingRttMs:number|null;pingLossPct:number|null;pingError:string|null;status:string;error:string|null;createdAt:string;
}
export function mapSpeedHistoryRun(row:StoredSpeedRun) {
return speedHistoryRunSchema.parse({
id:row.id,probeId:row.probeId,srcServerId:String(row.srcServerId),dstServerId:String(row.dstServerId),
srcInterface:row.srcInterface ?? "",dstInterface:row.dstInterface ?? "",
srcAddress:row.srcAddress,dstAddress:row.dstAddress,srcInterfaceAddress:row.srcInterfaceAddress,dstInterfaceAddress:row.dstInterfaceAddress,
protocol:row.protocol,direction:row.direction,durationSec:row.durationSec,
txAvgMbps:row.txAvgMbps,rxAvgMbps:row.rxAvgMbps,status:row.status,error:row.error,createdAt:new Date(row.createdAt).toISOString(),
afterBtPing:{rttMs:row.pingRttMs,lossPct:row.pingLossPct,error:row.pingError},
})
}