Files
harbor-net/test/server/live-traffic-route.test.js
T

209 lines
7.6 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import test from 'node:test';
import {
createLiveTrafficRoute,
enrichLiveTrafficDeviceLabels,
} from '../../dist/server/http/routes/liveTrafficRoute.js';
function response() {
return {
writeHead(status, headers) {
this.status = status;
this.headers = headers;
},
end(payload) {
this.rawPayload = payload;
this.payload = JSON.parse(payload);
},
};
}
const snapshot = {
apiVersion: 1,
epoch: 'sing-box-1700000000000',
sequence: 7,
observedAt: '2026-08-31T10:00:00.000Z',
capabilities: {
lifecycle: true,
deviceAttribution: false,
applicationAttribution: false,
},
source: {
transport: 'native',
state: 'live',
completeness: 'lifecycle',
singBoxVersion: '1.14.0-rc.5',
singBoxApiVersion: 4,
error: null,
unattributedUploadBytes: '0',
unattributedDownloadBytes: '0',
},
summary: {
active: 0,
recent: 0,
visible: 0,
recognized: 0,
unresolved: 0,
unresolvedOrigin: 0,
truncated: false,
},
connections: [],
};
test('split Gateway control reads the cached socket while combined Gateway keeps no collector', () => {
const index = readFileSync(new URL('../../src/server/index.ts', import.meta.url), 'utf8');
assert.match(index, /const liveTraffic = clientLiveTraffic \|\| \(remoteDataplane \? \{[\s\S]*observeLiveTraffic\(\)[\s\S]*\} : null\)/);
assert.match(index, /createLiveTrafficRoute\(\{[\s\S]*traffic: liveTraffic,[\s\S]*deviceInventory: remoteDataplane \? deviceInventory : null/);
assert.match(index, /clientLiveTraffic\?\.start\(\)/);
assert.doesNotMatch(index, /liveTraffic\?\.start\(\)/);
});
test('GET returns the injected cached snapshot without another data-source operation', async () => {
let snapshots = 0;
const route = createLiveTrafficRoute({
traffic: {
snapshot() {
snapshots += 1;
return snapshot;
},
},
});
const res = response();
assert.equal(await route.handle({ method: 'GET', url: '/api/traffic/live?ignored=1' }, res), true);
assert.equal(snapshots, 1);
assert.equal(res.status, 200);
assert.deepEqual(res.headers, { 'content-type': 'application/json; charset=utf-8' });
assert.deepEqual(res.payload, snapshot);
});
test('async Gateway snapshots are validated and known dev labels are enriched without changing summary', async () => {
const gatewaySnapshot = {
...snapshot,
capabilities: { ...snapshot.capabilities, deviceAttribution: true },
summary: {
active: 300,
recent: 0,
visible: 256,
recognized: 300,
unresolved: 0,
unresolvedOrigin: 299,
truncated: true,
},
connections: Array.from({ length: 256 }, (_, index) => ({
id: `connection-${String(index).padStart(3, '0')}`,
startedAt: new Date(Date.parse(snapshot.observedAt) - index * 1000).toISOString(),
closedAt: null,
inbound: { tag: 'tproxy-in', type: 'tproxy' },
network: 'tcp',
protocol: 'tls',
source: { ip: index === 0 ? '192.168.50.7' : '192.168.50.8', port: 50_000 + index },
destination: { domain: 'example.com', ip: '203.0.113.1', port: 443, provenance: 'sing-box' },
origin: index === 0
? { kind: 'device', id: 'dev_0011223344556677', label: '192.168.50.7', provenance: 'source-ip' }
: { kind: 'unknown', id: null, label: 'Неизвестное устройство', provenance: 'unknown' },
route: {
kind: 'vpn',
scope: 'local-sing-box',
outbound: 'proxy',
outboundType: 'selector',
chain: ['proxy'],
rule: 'default',
},
traffic: {
uploadBytes: '10',
downloadBytes: '20',
uploadBytesPerSecond: '1',
downloadBytesPerSecond: '2',
},
})),
};
const original = structuredClone(gatewaySnapshot);
const route = createLiveTrafficRoute({
traffic: { snapshot: async () => gatewaySnapshot },
deviceInventory: {
snapshot: () => ({
devices: [{
id: 'dev_0011223344556677',
alias: 'Гостиная',
hostname: 'tv.local',
ip: '192.168.50.7',
}],
}),
},
});
const res = response();
assert.equal(await route.handle({ method: 'GET', url: '/api/traffic/live' }, res), true);
assert.equal(res.payload.connections[0].origin.label, 'Гостиная');
assert.equal(res.payload.connections[1].origin.kind, 'unknown');
assert.deepEqual(res.payload.summary, gatewaySnapshot.summary);
assert.deepEqual(gatewaySnapshot, original);
});
test('device label enrichment follows alias, hostname and IP without identifying unknown origins', () => {
const connection = {
id: 'connection-1',
startedAt: snapshot.observedAt,
closedAt: null,
inbound: { tag: 'tproxy-in', type: 'tproxy' },
network: 'tcp',
protocol: 'tls',
source: { ip: '192.168.50.7', port: 50_000 },
destination: { domain: 'example.com', ip: '203.0.113.1', port: 443, provenance: 'sing-box' },
origin: { kind: 'device', id: 'dev_0011223344556677', label: '192.168.50.7', provenance: 'source-ip' },
route: { kind: 'vpn', scope: 'local-sing-box', outbound: 'proxy', outboundType: 'selector', chain: ['proxy'], rule: 'default' },
traffic: { uploadBytes: '1', downloadBytes: '2', uploadBytesPerSecond: '0', downloadBytesPerSecond: '0' },
};
const source = {
...snapshot,
capabilities: { ...snapshot.capabilities, deviceAttribution: true },
summary: { ...snapshot.summary, active: 1, visible: 1, recognized: 1 },
connections: [connection],
};
const labels = (device) => enrichLiveTrafficDeviceLabels(source, { devices: [device] })
.connections[0].origin.label;
assert.equal(labels({ id: connection.origin.id, alias: ' ТВ ', hostname: 'tv.local', ip: '192.168.50.7' }), 'ТВ');
assert.equal(labels({ id: connection.origin.id, alias: '', hostname: 'tv.local', ip: '192.168.50.7' }), 'tv.local');
assert.equal(labels({ id: connection.origin.id, alias: '', hostname: null, ip: '192.168.50.7' }), '192.168.50.7');
assert.equal(labels({ id: 'dev_ffffffffffffffff', alias: 'Чужой', ip: connection.source.ip }), connection.origin.label);
const unknown = { ...source, connections: [{ ...connection, origin: { kind: 'unknown', id: null, label: 'Неизвестно', provenance: 'unknown' } }] };
assert.equal(enrichLiveTrafficDeviceLabels(unknown, { devices: [{ ...connection.origin, id: 'dev_0011223344556677', alias: 'Не угадывать' }] }).connections[0].origin.label, 'Неизвестно');
});
test('route rejects malformed cached snapshots before responding', async () => {
const route = createLiveTrafficRoute({
traffic: { snapshot: async () => ({ ...snapshot, apiVersion: 2 }) },
});
await assert.rejects(
route.handle({ method: 'GET', url: '/api/traffic/live' }, response()),
/apiVersion 1/,
);
});
test('route ignores other paths and rejects mutation methods without reading the cache', async () => {
let snapshots = 0;
const route = createLiveTrafficRoute({
traffic: { snapshot: () => { snapshots += 1; return snapshot; } },
});
assert.equal(await route.handle({ method: 'GET', url: '/api/traffic/history' }, response()), false);
await assert.rejects(
route.handle({ method: 'POST', url: '/api/traffic/live' }, response()),
(error) => error.code === 'ENDPOINT_NOT_FOUND',
);
assert.equal(snapshots, 0);
});
test('route is unavailable when no traffic collector exists', async () => {
const route = createLiveTrafficRoute({ traffic: null });
await assert.rejects(
route.handle({ method: 'GET', url: '/api/traffic/live' }, response()),
(error) => error.code === 'ENDPOINT_NOT_FOUND',
);
});