165 lines
8.8 KiB
JavaScript
165 lines
8.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
|
|
import { parseDeviceSnapshot } from '../../.test-dist/src/web/features/devices/deviceSnapshot.js';
|
|
|
|
const root = path.resolve(import.meta.dirname, '../..');
|
|
const page = fs.readFileSync(path.join(root, 'src/web/components/ClientOverviewPage.tsx'), 'utf8');
|
|
const feature = fs.readFileSync(path.join(root, 'src/web/features/devices/DevicesFeature.tsx'), 'utf8');
|
|
const panel = fs.readFileSync(path.join(root, 'src/web/features/devices/DevicesPanel.tsx'), 'utf8');
|
|
const model = fs.readFileSync(path.join(root, 'src/web/features/devices/deviceSnapshot.ts'), 'utf8');
|
|
const boundary = fs.readFileSync(path.join(root, 'src/web/features/devices/index.ts'), 'utf8');
|
|
|
|
test('devices feature is the sole public owner and legacy component paths are gone', () => {
|
|
assert.match(boundary, /DevicesPanel[\s\S]*DevicesToggle,[\s\S]*GatewayTrafficSummary,[\s\S]*useDevicesFeature/);
|
|
assert.equal(fs.existsSync(path.join(root, 'src/web/components/DevicesPanel.jsx')), false);
|
|
assert.equal(fs.existsSync(path.join(root, 'src/web/components/TrafficChart.jsx')), false);
|
|
assert.equal((page.match(/useDevicesFeature\(/g) || []).length, 1);
|
|
assert.equal((page.match(/<DevicesToggle/g) || []).length, 1);
|
|
assert.equal((page.match(/<GatewayTrafficSummary/g) || []).length, 1);
|
|
assert.equal((page.match(/<DevicesPanel/g) || []).length, 1);
|
|
assert.match(page, /useDevicesFeature\(\{[\s\S]*listDevices: actions\.listDevices[\s\S]*refreshDevices: actions\.refreshDevices[\s\S]*resetDeviceTraffic: actions\.resetDeviceTraffic[\s\S]*updateDevice: actions\.updateDevice[\s\S]*setDevicePolicy: actions\.setDevicePolicy[\s\S]*\}\)/);
|
|
assert.match(page, /<DevicesPanel feature=\{devicesFeature\} \/>/);
|
|
assert.doesNotMatch(page, /DEVICE_AUTO_REFRESH_MS|deviceSnapshot|deviceStatus|deviceError|devicesRefreshing|deviceRefreshCycle|devicesPanelRef|devicesToggleRef|devicesCloseRef|function loadDevices|client-devices-toggle|className="client-gateway-summary"/);
|
|
assert.doesNotMatch([feature, panel].join('\n'), /from ['"][^'"]*\/api\/|ConnectionPanel|SubscriptionFeature|RoutingFeature|DiagnosticsPanel/);
|
|
assert.doesNotMatch(panel, /listDevices|requestDeviceUpdate|setDevicePolicy|STATE_CONFLICT|DEVICE_POLICY_APPLY_FAILED|parseDeviceSnapshot/);
|
|
});
|
|
|
|
test('device controller preserves Gateway-only polling, monotonic publication and drawer lifecycle', () => {
|
|
assert.match(feature, /if \(!isGateway\) return undefined;[\s\S]*load\(\)/);
|
|
assert.match(feature, /discover \? refreshDevices\(\) : listDevices\(\)/);
|
|
assert.match(feature, /setTimeout\(\(\) => load\(true\), DEVICE_AUTO_REFRESH_MS\)/);
|
|
assert.match(feature, /setSnapshot\(\(current\) => !current \|\| next\.revision > current\.revision \? next : current\)/);
|
|
assert.match(feature, /finally \{[\s\S]*setRefreshing\(false\)[\s\S]*setRefreshCycle/);
|
|
assert.match(feature, /closeRef\.current\?\.focus\(\)[\s\S]*panelRef\.current\?\.contains[\s\S]*toggleRef\.current\?\.focus\(\)/);
|
|
assert.match(feature, /resetOpen[\s\S]*if \(resetOpen\) return/);
|
|
assert.match(feature, /resetDeviceTraffic\(snapshot\.revision\)[\s\S]*STATE_CONFLICT[\s\S]*resetDeviceTraffic\(latest\.revision\)/);
|
|
assert.match(panel, /<ConfirmationDialog[\s\S]*title="Сбросить статистику\?"[\s\S]*confirmLabel="Сбросить"[\s\S]*busy=\{resetting\}/);
|
|
assert.match(page, /<DevicesToggle[\s\S]*onToggle=\{\(\) => switchDrawer\('devices'\)\}/);
|
|
assert.match(page, /<GatewayTrafficSummary feature=\{devicesFeature\} now=\{now\}/);
|
|
});
|
|
|
|
test('all unknown inventory results pass one capability-aware runtime parser', () => {
|
|
const observedAt = '2026-08-08T12:34:56.000Z';
|
|
const valid = {
|
|
revision: 3,
|
|
devices: [{
|
|
id: 'dev_0123456789abcdef',
|
|
alias: null,
|
|
hostname: null,
|
|
mac: '00:11:22:33:44:55',
|
|
ip: null,
|
|
firstSeenAt: observedAt,
|
|
lastSeenAt: null,
|
|
status: 'online',
|
|
pinned: true,
|
|
deprioritized: false,
|
|
downloadBytes: '12',
|
|
uploadBytes: '30',
|
|
proxyDownloadBytes: '0',
|
|
proxyUploadBytes: '0',
|
|
policyStatus: 'applied',
|
|
policyError: null,
|
|
desiredPolicy: 'vpn',
|
|
appliedPolicy: 'vpn',
|
|
confidence: 'high',
|
|
trafficHistory: [{ observedAt, gatewayBytes: '42', proxyBytes: '0' }],
|
|
outboundTraffic: {
|
|
observedAt,
|
|
singboxObservedAt: observedAt,
|
|
directIpv4ObservedAt: observedAt,
|
|
vpnBytes: '30',
|
|
directTrackedBytes: '8',
|
|
directIpv4Bytes: '4',
|
|
unknownBytes: '0',
|
|
},
|
|
outboundTrafficHistory: [{
|
|
observedAt,
|
|
vpnBytes: '30',
|
|
directTrackedBytes: '8',
|
|
directIpv4Bytes: '4',
|
|
unknownBytes: '0',
|
|
}],
|
|
}],
|
|
trafficHistoryCapacity: 120,
|
|
traffic: {
|
|
gatewayBytes: '42',
|
|
proxyBytes: '0',
|
|
totalBytes: '42',
|
|
gatewayObservedAt: observedAt,
|
|
proxyObservedAt: null,
|
|
observedAt: observedAt,
|
|
history: [{ observedAt, gatewayBytes: '42', proxyBytes: '0' }],
|
|
},
|
|
source: {
|
|
kind: 'neighbor',
|
|
lastObservedAt: observedAt,
|
|
error: null,
|
|
traffic: {
|
|
lastObservedAt: observedAt,
|
|
error: null,
|
|
proxy: { lastObservedAt: null, error: null },
|
|
},
|
|
policy: { lastAppliedAt: null, error: null },
|
|
},
|
|
extra: { retained: true },
|
|
};
|
|
const oldBackend = parseDeviceSnapshot(valid);
|
|
assert.equal(oldBackend.taggingSupported, false);
|
|
assert.deepEqual(oldBackend.tags, []);
|
|
assert.deepEqual(oldBackend.devices[0].tagIds, []);
|
|
assert.deepEqual(oldBackend.extra, valid.extra);
|
|
|
|
const supported = {
|
|
...valid,
|
|
tags: [{ id: 'tag_0123456789abcdef', name: 'Умный дом' }],
|
|
devices: [{ ...valid.devices[0], tagIds: ['tag_0123456789abcdef'] }],
|
|
};
|
|
const parsedSupported = parseDeviceSnapshot(supported);
|
|
assert.equal(parsedSupported.taggingSupported, true);
|
|
assert.deepEqual(parsedSupported.tags, supported.tags);
|
|
assert.deepEqual(parsedSupported.devices[0].tagIds, ['tag_0123456789abcdef']);
|
|
for (const invalid of [
|
|
null,
|
|
{},
|
|
{ ...valid, revision: -1 },
|
|
{ ...valid, revision: 1.5 },
|
|
{ ...valid, devices: undefined },
|
|
{ ...valid, trafficHistoryCapacity: 0 },
|
|
{ ...valid, traffic: undefined },
|
|
{ ...valid, source: undefined },
|
|
{ ...valid, devices: [{ ...valid.devices[0], id: 'not-a-device' }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], mac: 'not-a-mac' }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], downloadBytes: 'not-bytes' }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], uploadBytes: '-1' }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], proxyUploadBytes: 1 }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], status: 'connected' }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], deprioritized: 'yes' }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], pinned: true, deprioritized: true }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], desiredPolicy: 'automatic' }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], policyStatus: 'queued' }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], confidence: 'unknown' }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], firstSeenAt: undefined }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], firstSeenAt: 'not-a-date' }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], lastSeenAt: 'not-a-date' }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], trafficHistory: [{ observedAt, gatewayBytes: '1' }] }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], outboundTraffic: { observedAt, vpnBytes: '1' } }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], outboundTraffic: { ...valid.devices[0].outboundTraffic, directIpv4ObservedAt: 'not-a-date' } }] },
|
|
{ ...valid, devices: [{ ...valid.devices[0], outboundTrafficHistory: [{ observedAt, vpnBytes: '1' }] }] },
|
|
{ ...supported, tags: [{ id: 'bad', name: 'Дом' }] },
|
|
{ ...supported, tags: [{ ...supported.tags[0], name: ' '.repeat(2) }] },
|
|
{ ...supported, devices: [{ ...valid.devices[0] }] },
|
|
{ ...supported, devices: [{ ...supported.devices[0], tagIds: ['tag_ffffffffffffffff'] }] },
|
|
{ ...valid, traffic: { ...valid.traffic, totalBytes: -4 } },
|
|
{ ...valid, traffic: { ...valid.traffic, observedAt: 'not-a-date' } },
|
|
{ ...valid, source: { ...valid.source, kind: 'arp' } },
|
|
{ ...valid, source: { ...valid.source, traffic: { proxy: [] } } },
|
|
]) assert.throws(() => parseDeviceSnapshot(invalid), TypeError);
|
|
assert.match(feature, /publish\(await \(discover \? refreshDevices\(\) : listDevices\(\)\)\)/);
|
|
assert.ok((feature.match(/parseDeviceSnapshot\(await/g) || []).length >= 6);
|
|
assert.match(feature, /DEVICE_POLICY_APPLY_FAILED[\s\S]*publish\(parseDeviceSnapshot\(await listDevices\(\)\)\)/);
|
|
assert.doesNotMatch(model, /\sas\s(?:DeviceSnapshot|Record<string, unknown>)/);
|
|
});
|