149 lines
8.5 KiB
TypeScript
149 lines
8.5 KiB
TypeScript
import assert from "node:assert/strict"
|
|
import {
|
|
bpsToMbps,
|
|
bucketAvg,
|
|
buildTrafficFromSamples,
|
|
isLoopbackName,
|
|
mergeBuiltTraffic,
|
|
parseMonitorTraffic,
|
|
rateBpsFromDelta,
|
|
shouldIncludeIface,
|
|
type TrafficSampleLike,
|
|
} from "./traffic-rate.js"
|
|
|
|
assert.equal(isLoopbackName("lo"), true)
|
|
assert.equal(isLoopbackName("loopback"), true)
|
|
assert.equal(isLoopbackName("ether1"), false)
|
|
assert.equal(shouldIncludeIface("lo", true, false), false)
|
|
assert.equal(shouldIncludeIface("ether1", true, false), true)
|
|
assert.equal(shouldIncludeIface("ether1", false, false), false)
|
|
assert.equal(shouldIncludeIface("ether1", true, true), false)
|
|
assert.equal(shouldIncludeIface("lo", true, false, "lo"), true)
|
|
|
|
assert.equal(rateBpsFromDelta(1000, 2000, 0, 1000), 8000)
|
|
assert.equal(rateBpsFromDelta(1000, 500, 0, 1000), null)
|
|
assert.equal(rateBpsFromDelta(1000, 2000, 1000, 1000), null)
|
|
assert.equal(bpsToMbps(1_500_000), 1.5)
|
|
assert.equal(bpsToMbps(400_000), 0.4)
|
|
|
|
const buckets = bucketAvg(
|
|
[
|
|
{ t: 0, v: 10 },
|
|
{ t: 1000, v: 20 },
|
|
],
|
|
0,
|
|
1000,
|
|
4,
|
|
)
|
|
assert.equal(buckets.length, 4)
|
|
assert.equal(buckets[0], 10)
|
|
assert.equal(buckets[3], 20)
|
|
assert.equal(buckets[1], 0)
|
|
assert.equal(buckets[2], 0)
|
|
|
|
const t0 = "2026-09-06T10:00:00.000Z"
|
|
const t1 = "2026-09-06T10:00:30.000Z"
|
|
const t2 = "2026-09-06T10:01:00.000Z"
|
|
const start = Date.parse(t0)
|
|
const end = Date.parse(t2)
|
|
|
|
const samples: TrafficSampleLike[] = [
|
|
{ interfaceName: "ether1", sampledAt: t0, rxBytes: 1_000_000, txBytes: 500_000, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
{ interfaceName: "ether1", sampledAt: t1, rxBytes: 1_000_000 + 3_750_000, txBytes: 500_000 + 1_875_000, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
{ interfaceName: "ether1", sampledAt: t2, rxBytes: 100, txBytes: 50, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
{ interfaceName: "lo", sampledAt: t0, rxBytes: 0, txBytes: 0, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
{ interfaceName: "lo", sampledAt: t1, rxBytes: 9_000_000, txBytes: 9_000_000, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
]
|
|
|
|
const built = buildTrafficFromSamples(samples, start, end)
|
|
assert.ok(built.rxPeak > 0, "peak RX from delta")
|
|
assert.equal(built.rxNow, 1, "last valid delta after reset skip")
|
|
assert.ok(built.rxSeries.some((v) => v > 0), "bucket series not flat")
|
|
assert.ok(built.rxPeak <= 1.1, "lo excluded from peak")
|
|
|
|
const live = parseMonitorTraffic([
|
|
{ name: "ether1", "rx-bits-per-second": "2000000", "tx-bits-per-second": "500000" },
|
|
{ name: "lo", "rx-bits-per-second": "8000000", "tx-bits-per-second": "8000000" },
|
|
])
|
|
assert.equal(live.rxMbps, 2)
|
|
assert.equal(live.txMbps, 0.5)
|
|
|
|
const onceOnly = parseMonitorTraffic(
|
|
{ name: "ether1", "rx-bits-per-second": "1000000", "tx-bits-per-second": "0" },
|
|
{ onlyInterface: "ether1" },
|
|
)
|
|
assert.equal(onceOnly.rxMbps, 1)
|
|
|
|
const boundOnly = buildTrafficFromSamples(samples, start, end, ["ether1", "lo"])
|
|
assert.ok(boundOnly.rxPeak > 0, "bound list includes ether1")
|
|
assert.ok(boundOnly.rxPeak > built.rxPeak, "lo included when listed")
|
|
|
|
const merged = mergeBuiltTraffic([
|
|
buildTrafficFromSamples(samples, start, end, "ether1"),
|
|
buildTrafficFromSamples(samples, start, end, "ether1"),
|
|
])
|
|
assert.equal(merged.rxNow, built.rxNow * 2)
|
|
|
|
const wgSamples: TrafficSampleLike[] = [
|
|
{ interfaceName: "wg-msk", sampledAt: t0, rxBytes: 2_000_000, txBytes: 1_000_000, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
{ interfaceName: "wg-msk", sampledAt: t1, rxBytes: 2_000_000 + 1_875_000, txBytes: 1_000_000 + 937_500, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
{ interfaceName: "wg-msk", sampledAt: t2, rxBytes: 50, txBytes: 25, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
]
|
|
const userAgg = mergeBuiltTraffic([
|
|
buildTrafficFromSamples(samples, start, end, "ether1"),
|
|
buildTrafficFromSamples(wgSamples, start, end, "wg-msk"),
|
|
])
|
|
const listed = buildTrafficFromSamples([...samples, ...wgSamples], start, end, ["ether1", "wg-msk"])
|
|
assert.equal(userAgg.rxNow, listed.rxNow, "сумма привязанных ifaces = фильтр по списку имён")
|
|
assert.ok(userAgg.rxNow > built.rxNow, "агрегация пользователя больше одного iface")
|
|
|
|
const peerA: TrafficSampleLike[] = [
|
|
{ interfaceName: "wg-server", peerPublicKey: "peer-a", sampledAt: t0, rxBytes: 1_000_000, txBytes: 100_000, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
{ interfaceName: "wg-server", peerPublicKey: "peer-a", sampledAt: t1, rxBytes: 1_000_000 + 3_750_000, txBytes: 100_000 + 375_000, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
]
|
|
const peerB: TrafficSampleLike[] = [
|
|
{ interfaceName: "wg-server", peerPublicKey: "peer-b", sampledAt: t0, rxBytes: 500_000, txBytes: 50_000, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
{ interfaceName: "wg-server", peerPublicKey: "peer-b", sampledAt: t1, rxBytes: 500_000 + 1_875_000, txBytes: 50_000 + 187_500, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
]
|
|
const ifaceWg: TrafficSampleLike[] = [
|
|
{ interfaceName: "wg-server", sampledAt: t0, rxBytes: 10_000_000, txBytes: 2_000_000, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
{ interfaceName: "wg-server", sampledAt: t1, rxBytes: 10_000_000 + 7_500_000, txBytes: 2_000_000 + 750_000, rxBps: 0, txBps: 0, running: true, disabled: false },
|
|
]
|
|
const mixed = [...peerA, ...peerB, ...ifaceWg]
|
|
const rateA = buildTrafficFromSamples(mixed, start, end, "wg-server", "peer-a")
|
|
const rateB = buildTrafficFromSamples(mixed, start, end, "wg-server", "peer-b")
|
|
const rateIface = buildTrafficFromSamples(mixed, start, end, "wg-server")
|
|
assert.ok(rateA.rxNow > 0 && rateB.rxNow > 0, "скорость по каждому пиру")
|
|
assert.notEqual(rateA.rxNow, rateB.rxNow, "два пира одного iface — разный rate")
|
|
assert.ok(rateIface.rxNow > rateA.rxNow, "iface-level не суммирует пиров")
|
|
assert.equal(
|
|
buildTrafficFromSamples(mixed, start, end).rxNow,
|
|
rateIface.rxNow,
|
|
"режим сервера игнорирует семплы пиров",
|
|
)
|
|
|
|
console.log("traffic-rate tests ok")
|
|
|
|
// Observation metadata is opt-in: existing numeric consumers retain their DTO.
|
|
const observationStart=Date.parse("2026-10-03T00:00:00Z"),observationEnd=observationStart+60_000
|
|
const observationSample=(offset:number,rxBytes:number,txBytes:number,interfaceName="ether1")=>({interfaceName,sampledAt:new Date(observationStart+offset).toISOString(),rxBytes,txBytes,rxBps:0,txBps:0,running:true,disabled:false})
|
|
const observed=(rows:ReturnType<typeof observationSample>[])=>buildTrafficFromSamples(rows,observationStart,observationEnd,undefined,undefined,true)
|
|
assert.ok(observed([]).observations?.rxSeries.every(v=>!v));assert.equal(observed([]).observations?.rxNow,false)
|
|
assert.equal(observed([observationSample(0,10,10)]).observations?.rxNow,false)
|
|
const observedZero=observed([observationSample(0,10,10),observationSample(30_000,10,10)])
|
|
assert.equal(observedZero.rxNow,0);assert.equal(observedZero.observations?.rxNow,true);assert.equal(observedZero.observations?.rxSeries.filter(Boolean).length,1)
|
|
const partialReset=observed([observationSample(0,10,10),observationSample(30_000,1,20)])
|
|
assert.equal(partialReset.observations?.rxNow,false);assert.equal(partialReset.observations?.txNow,true)
|
|
const fullReset=observed([observationSample(0,10,10),observationSample(20_000,20,20),observationSample(40_000,1,1)])
|
|
assert.equal(fullReset.observations?.rxNow,false);assert.equal(fullReset.observations?.sampledAt,new Date(observationStart+40_000).toISOString())
|
|
const incomplete=observed([observationSample(0,10,10),observationSample(30_000,20,20),observationSample(0,10,10,"ether2")])
|
|
assert.equal(incomplete.observations?.rxNow,false);assert.ok(incomplete.observations?.rxSeries.every(v=>!v))
|
|
const legacyObservedZero=buildTrafficFromSamples([observationSample(0,10,10),observationSample(30_000,10,10)],observationStart,observationEnd)
|
|
const {observations:extra,...numericZero}=observedZero;assert.deepEqual(numericZero,legacyObservedZero);assert.ok(extra)
|
|
assert.equal(rateBpsFromDelta(NaN,10,0,1000),null);assert.equal(rateBpsFromDelta(-1,10,0,1000),null)
|
|
console.log("traffic observation metadata tests ok")
|
|
|
|
const complete=observed([observationSample(0,10,10),observationSample(30_000,20,20),observationSample(0,10,10,"ether2"),observationSample(30_000,20,20,"ether2")]);assert.equal(complete.observations?.rxNow,true)
|
|
const badTime=observed([{...observationSample(0,10,10),sampledAt:"invalid"},observationSample(30_000,20,20)]);assert.equal(badTime.observations?.rxNow,false)
|
|
assert.equal(rateBpsFromDelta(0,Number.MAX_VALUE,0,1000),null)
|