fix(netflow): проверять месяц и точные календарные границы

This commit is contained in:
Denozordec committed 2026-10-11 18:27:41 +07:00
1 parent 0c14da190b
commit 555bd31596
13 files changed
+291 -25

No files matched your search

+10 -8
View File
@@ -4,7 +4,6 @@ import { appUsers, userInterfaceBindings } from "../db/schema.js"
import type {
FlowAnalyticsDto,
FlowBreakdownRow,
FlowClientsDto,
FlowEntityCard,
FlowExportersDto,
FlowMapEdge,
@@ -645,15 +644,16 @@ export async function safeBuildLiveFlowSample(q: Omit<FlowAnalyticsQuery, "minut
})
}
function monthBounds(month: string): { start: string; end: string } | null {
if (!/^\d{4}-\d{2}$/.test(month)) return null
export function flowMonthBounds(month: string): { start: string; end: string } | null {
if (typeof month !== "string" || !/^(?!0000)\d{4}-(0[1-9]|1[0-2])$/.test(month)) return null
const [yearRaw, monthRaw] = month.split("-")
const year = Number(yearRaw)
const monthIdx = Number(monthRaw)
if (!Number.isFinite(year) || monthIdx < 1 || monthIdx > 12) return null
const start = `${month}-01`
const endDate = new Date(Date.UTC(year, monthIdx, 1))
const end = endDate.toISOString().slice(0, 10)
// Date.UTC treats years0..99 as1900..1999. Calendar arithmetic also preserves
// the full ISO year at December9999 without truncating an extended date string.
const end = `${String(year + (monthIdx === 12 ? 1 : 0)).padStart(4, "0")}-${String(monthIdx === 12 ? 1 : monthIdx + 1).padStart(2, "0")}-01`
return { start, end }
}
@@ -676,9 +676,9 @@ function toBreakdown(
}
export async function getFlowMonthly(month: string, serverId?: number): Promise<FlowMonthlyDto> {
const bounds = monthBounds(month)
const bounds = flowMonthBounds(month)
if (!bounds) {
return { month, bytes: 0, countries: [], services: [], asns: [] }
throw Object.assign(new Error("Некорректный месяц отчёта"), { statusCode: 400 })
}
const params: Array<string | number> = [bounds.start, bounds.end]
let where = "day >= ? AND day < ? AND dim IN ('country', 'service', 'asn')"
@@ -705,7 +705,9 @@ export async function getFlowMonthly(month: string, serverId?: number): Promise<
} else if (row.dim === "service") services.push(rec)
else if (row.dim === "asn") asns.push(rec)
}
const daysInMonth = Math.max(1, Math.round((Date.parse(`${bounds.end}T00:00:00Z`) - Date.parse(`${bounds.start}T00:00:00Z`)) / 86_400_000))
const [year, monthIndex] = month.split("-").map(Number)
const leap = year! % 4 === 0 && (year! % 100 !== 0 || year! % 400 === 0)
const daysInMonth = [31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][monthIndex! - 1]!
const windowSec = daysInMonth * 86_400
const countryTotal = countries.reduce((a, r) => a + r.bytes, 0) || bytes || 1
return {
@@ -0,0 +1,44 @@
import assert from "node:assert/strict"
import { flowMonthBounds, getFlowMonthly } from "./traffic-flow-analytics.js"
import { pool, closePool } from "../db/index.js"
import { initDatabase } from "../db/bootstrap.js"
for (const [month, end] of [
["2026-10", "2026-11-01"], ["2026-12", "2027-01-01"],
["0001-01", "0001-02-01"], ["0099-12", "0100-01-01"],
["2024-02", "2024-03-01"], ["9999-12", "10000-01-01"],
]) assert.deepEqual(flowMonthBounds(month!), { start: `${month}-01`, end })
try {
for (const month of ["bad", "2026-00", "2026-13", "0000-01", "2026-1", "2026-10junk", "2026-10-01", " 2026-10"]) {
assert.equal(flowMonthBounds(month), null)
await assert.rejects(getFlowMonthly(month), (error: unknown) => error instanceof Error && (error as Error & { statusCode: number }).statusCode === 400)
}
const ownDb = process.env.MMAPP_OWN_TEST_DB
if (ownDb) {
assert(/^mm_accept_149_\d+$/.test(ownDb))
assert.equal((await pool.query("SELECT current_database() AS name")).rows[0].name, ownDb)
await initDatabase()
const serverId = Number((await pool.query("INSERT INTO servers (name,host) VALUES ('month-boundary-fixture','127.0.0.1') RETURNING id")).rows[0].id)
const cases = [
{ month: "0001-01", start: "0001-01-01", end: "0001-03-01", inside: "0001-01-15", outside: "0001-02-01", days: 31 },
{ month: "0099-12", start: "0099-12-01", end: "0100-02-01", inside: "0099-12-15", outside: "0100-01-01", days: 31 },
{ month: "1900-02", start: "1900-02-01", end: "1900-04-01", inside: "1900-02-28", outside: "1900-03-01", days: 28 },
{ month: "2000-02", start: "2000-02-01", end: "2000-04-01", inside: "2000-02-29", outside: "2000-03-01", days: 29 },
{ month: "9999-12", start: "9999-12-01", end: "10000-02-01", inside: "9999-12-15", outside: "10000-01-01", days: 31 },
]
for (const [i, item] of cases.entries()) {
// Fixed fixture DDL only, guarded by an exact owned database before migrations.
await pool.query(`CREATE TABLE flow_daily_dims_mm149_${i} PARTITION OF flow_daily_dims FOR VALUES FROM ('${item.start}') TO ('${item.end}')`)
await pool.query("INSERT INTO flow_daily_dims (server_id,day,dim,key,bytes,packets) VALUES ($1,$2,'country','US',1200,12),($1,$3,'country','US',12000,120)", [serverId, item.inside, item.outside])
const monthly = await getFlowMonthly(item.month, serverId)
assert.equal(monthly.month, item.month)
assert.equal(monthly.bytes, 1200, item.month)
assert.equal(monthly.countries[0]?.bytes, 1200)
assert.equal(monthly.countries[0]?.bps, 1200 * 8 / (item.days * 86400), item.month)
}
console.log("ISOLATED_MONTH_SQL_ACCEPTED: 5 exact intervals, outside rows excluded, leap-year rates verified")
} else console.log("Monthly SQL: skip (requires an owned isolated database)")
console.log("traffic-flow-month.test.ts: exact month bounds and invalid-before-SQL checks passed")
} finally {
await closePool()
}