fix(collectors): проверять реальные ответы настроек без подстановки defaults

This commit is contained in:
Denozordec committed 2026-10-01 21:59:40 +07:00
1 parent 5d5ea680aa
commit a0263c22a5
5 files changed
+91 -2

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
import assert from "node:assert/strict"
import { test } from "node:test"
import { getTrafficCollectorSettings, getServersApiPingSettings, getUptimeCollectorSettings, getInternetPathSettings } from "./collector-settings"
const base = "https://api.example.test"
const common = { enabled: false, intervalSec: 60, lastCollectedAt: null, lastDurationMs: 0, lastError: null, collectorRunning: false }
const retained = { ...common, retentionDays: 14 }
const uptime = { ...retained, resourcesEnabled: false, pingEnabled: true, speedEnabled: false, probeIntervalSec: 15, speedIntervalSec: 60 }
const json = (body: unknown) => new Response(JSON.stringify(body))
for (const [name, read, payload, path] of [
["traffic", getTrafficCollectorSettings, retained, "/api/traffic/settings"],
["REST ping", getServersApiPingSettings, common, "/api/servers-api-ping/settings"],
["uptime", getUptimeCollectorSettings, uptime, "/api/uptime/settings"],
["internet path", getInternetPathSettings, retained, "/api/internet-path/settings"],
] as const) {
test(name + ": exact GET preserves false/zero/null and PostgreSQL timestamp instant", async () => {
const original = globalThis.fetch
try {
globalThis.fetch = async (url, init) => { assert.equal(new URL(String(url)).pathname, path); assert.equal(init?.method, "GET"); return json(payload) }
const result = await read(base); assert.equal(result.enabled, false); assert.equal(result.lastDurationMs, 0); assert.equal(result.lastCollectedAt, null)
globalThis.fetch = async () => json({ ...payload, lastCollectedAt: "2026-10-01 00:00:00+00" }); assert.equal((await read(base)).lastCollectedAt, "2026-10-01T00:00:00.000Z")
} finally { globalThis.fetch = original }
})
test(name + ": malformed response is unknown without field defaults", async () => {
const original = globalThis.fetch
try {
for (const raw of [{}, { ...payload, enabled: undefined }, { ...payload, intervalSec: "60" }, { ...payload, intervalSec: 0 }, { ...payload, lastDurationMs: -1 }, { ...payload, lastCollectedAt: "bad" }]) { globalThis.fetch = async () => json(raw); await assert.rejects(() => read(base), /Настройки сборщика не подтверждены/) }
} finally { globalThis.fetch = original }
})
}
test("REST ping does not invent retention, uptime preserves separate flags and ignores nested scheduler metadata", async () => {
const original = globalThis.fetch
try {
globalThis.fetch = async () => json(common); assert.equal("retentionDays" in await getServersApiPingSettings(base), false)
globalThis.fetch = async () => json({ ...uptime, scheduler: { jobs: "malformed independent metadata" } }); const result = await getUptimeCollectorSettings(base); assert.equal(result.resourcesEnabled, false); assert.equal(result.pingEnabled, true); assert.equal(result.speedEnabled, false); assert.equal("scheduler" in result, false)
for (const key of ["resourcesEnabled", "pingEnabled", "speedEnabled", "probeIntervalSec", "speedIntervalSec"]) { globalThis.fetch = async () => json({ ...uptime, [key]: undefined }); await assert.rejects(() => getUptimeCollectorSettings(base)) }
} finally { globalThis.fetch = original }
})
test("retained collectors require actual retention and caller cancellation is preserved", async () => {
const original = globalThis.fetch
try {
globalThis.fetch = async () => json(common); await assert.rejects(() => getTrafficCollectorSettings(base)); await assert.rejects(() => getInternetPathSettings(base))
const controller = new AbortController(); controller.abort(); globalThis.fetch = async (_url, init) => { assert.equal(init?.signal?.aborted, true); throw controller.signal.reason }; await assert.rejects(() => getTrafficCollectorSettings(base, { signal: controller.signal }))
} finally { globalThis.fetch = original }
})
+26
View File
@@ -0,0 +1,26 @@
import { z } from "zod"
import { requestJson } from "./http-client"
const count = z.number().int().nonnegative().safe()
const timestamp = z.string().min(1).refine(value => Number.isFinite(Date.parse(value))).transform(value => new Date(value).toISOString())
const common = z.object({ enabled: z.boolean(), intervalSec: count.positive(), lastCollectedAt: timestamp.nullable(), lastDurationMs: count.nullable(), lastError: z.string().nullable(), collectorRunning: z.boolean().optional() })
const retained = common.extend({ retentionDays: count.positive() })
const uptime = retained.extend({ resourcesEnabled: z.boolean(), pingEnabled: z.boolean(), speedEnabled: z.boolean(), probeIntervalSec: count.positive(), speedIntervalSec: count.positive() })
async function settings<T>(baseUrl: string, path: string, schema: z.ZodType<T>, init?: RequestInit): Promise<T> {
const raw = await requestJson<unknown>(baseUrl, path, { ...init, method: "GET", signal: AbortSignal.any([...(init?.signal ? [init.signal] : []), AbortSignal.timeout(30_000)]) })
const result = schema.safeParse(raw)
if (!result.success) throw new Error("Настройки сборщика не подтверждены: получен некорректный ответ сервера")
return result.data
}
export function getTrafficCollectorSettings(baseUrl: string, init?: RequestInit) {
return settings(baseUrl, "/api/traffic/settings", retained, init)
}
export function getServersApiPingSettings(baseUrl: string, init?: RequestInit) {
return settings(baseUrl, "/api/servers-api-ping/settings", common, init)
}
export function getUptimeCollectorSettings(baseUrl: string, init?: RequestInit) {
// Scheduler metadata is obtained independently through the scheduler API.
return settings(baseUrl, "/api/uptime/settings", uptime, init)
}
export function getInternetPathSettings(baseUrl: string, init?: RequestInit) {
return settings(baseUrl, "/api/internet-path/settings", retained, init)
}