fix(netflow): проверять месяц и точные календарные границы
This commit is contained in:
1 parent
0c14da190b
commit
555bd31596
13 files changed
+291
-25
No files matched your search
@@ -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()
|
||||
}
|
||||
Reference in new issue
Block a user