Files
harbor-net/test/server/device-inventory.test.js
T
dokril 116686a138
Build and Deploy Gateway / build-and-push (push) Successful in 26s
Build and Deploy Gateway / deploy (push) Successful in 14s
Implement Harbor gateway device ecosystem support
2026-08-31 02:06:22 +03:00

1584 lines
60 KiB
JavaScript

import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { parseNeighborSnapshot, readNeighborSnapshot } from '../../dist/server/adapters/neighbors.js';
import {
createDeviceInventoryService,
createVendorLookup,
DEVICE_INVENTORY_SCHEMA_VERSION,
deviceId,
migrateDeviceInventoryState,
} from '../../dist/server/services/deviceInventoryService.js';
import { fingerprintDirectDevices } from '../../dist/server/services/devicePolicyService.js';
import { createJsonStore } from '../../dist/server/services/stateStore.js';
test('container neighbors are hidden while a real 172 LAN device remains valid', () => {
const observedAt = '2026-08-07T12:00:00.000Z';
const parsed = parseNeighborSnapshot([
{ dst: '172.17.0.2', dev: 'docker0', lladdr: '00:11:22:33:44:51', state: ['REACHABLE'] },
{ dst: '172.18.0.2', dev: 'br-1234', lladdr: '00:11:22:33:44:52', state: ['REACHABLE'] },
{ dst: '172.19.0.2', dev: 'veth1234', lladdr: '00:11:22:33:44:53', state: ['REACHABLE'] },
{ dst: '172.20.0.7', dev: 'eth0', lladdr: '00:11:22:33:44:54', state: ['REACHABLE'] },
], observedAt);
assert.deepEqual(parsed.map(({ ip, interface: deviceInterface }) => [ip, deviceInterface]), [
['172.20.0.7', 'eth0'],
]);
const storedDevice = (ip, deviceInterface) => ({
id: `dev_${ip.replaceAll('.', '').padEnd(16, '0').slice(0, 16)}`,
alias: '',
pinned: false,
hostname: null,
manufacturer: null,
mac: ip === '172.17.0.2' ? '00:11:22:33:44:51' : '00:11:22:33:44:54',
ip,
interface: deviceInterface,
firstSeenAt: observedAt,
lastSeenAt: observedAt,
source: 'neighbor',
confidence: 'high',
});
const migrated = migrateDeviceInventoryState({
schemaVersion: 2,
devices: [storedDevice('172.17.0.2', 'docker0'), storedDevice('172.20.0.7', 'eth0')],
traffic: { baselinesByMac: {}, totalsByMac: {}, rebaselineMacs: [] },
});
assert.deepEqual(migrated.devices.map(({ ip }) => ip), ['172.20.0.7']);
});
test('malformed persisted devices and remote observations cannot enter canonical inventory', async (t) => {
const observedAt = '2026-08-08T12:00:00.000Z';
const mac = '00:11:22:33:44:55';
const persisted = {
id: 'legacy-device-id',
alias: 42,
pinned: 'yes',
hostname: 42,
manufacturer: [],
mac,
ip: '192.168.50.7',
interface: 'eth0',
firstSeenAt: observedAt,
lastSeenAt: observedAt,
source: null,
confidence: 'untrusted',
};
const migrated = migrateDeviceInventoryState({
schemaVersion: DEVICE_INVENTORY_SCHEMA_VERSION,
devices: [
persisted,
{ ...persisted, mac: 'invalid' },
{ ...persisted, mac: '00:11:22:33:44:66', ip: 'not-an-ip' },
{ ...persisted, mac: '00:11:22:33:44:77', lastSeenAt: 'not-a-date' },
],
});
assert.deepEqual(migrated.devices, [{
...persisted,
id: deviceId(mac),
alias: '',
pinned: false,
deprioritized: false,
hostname: null,
manufacturer: null,
source: 'neighbor',
confidence: 'high',
}]);
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-guard-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
});
store.remove();
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: [
{ ip: '192.168.50.8', mac: '00:11:22:33:44:88', interface: 'eth0', observedAt, active: true },
{ ip: 'bad', mac: '00:11:22:33:44:99', interface: 'eth0', observedAt, active: true },
{ ip: '192.168.50.9', mac: 'invalid', interface: 'eth0', observedAt, active: true },
{ ip: '192.168.50.10', mac: '00:11:22:33:44:aa', interface: 'docker0', observedAt, active: true },
{ ip: '192.168.50.11', mac: '00:11:22:33:44:bb', interface: 'eth0', observedAt, active: 'yes' },
{ ip: '192.168.50.12', mac: '00:11:22:33:44:cc', interface: 'eth0', observedAt: 'bad', active: true },
],
}),
now: () => new Date(observedAt),
});
const snapshot = await service.refresh();
assert.deepEqual(snapshot.devices.map(({ mac: deviceMac, ip }) => [deviceMac, ip]), [
['00:11:22:33:44:88', '192.168.50.8'],
]);
});
test('device inventory resolves hostnames without making reverse lookup a refresh dependency', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-hostname-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({ filePath: path.join(directory, 'devices.json'), defaultValue: {} });
const observedAt = '2026-08-11T12:00:00.000Z';
const mac = '00:11:22:33:44:55';
let ip = '192.168.50.10';
let lookupFails = false;
const lookups = [];
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: [{ ip, mac, interface: 'br0', observedAt, active: true }],
}),
resolveHostname: async (address) => {
lookups.push(address);
if (lookupFails) throw new Error('reverse DNS unavailable');
return 'living-room.local.';
},
now: () => new Date(observedAt),
});
let snapshot = await service.refresh();
assert.equal(snapshot.devices[0].hostname, 'living-room.local');
snapshot = await service.refresh();
assert.deepEqual(lookups, ['192.168.50.10']);
ip = '192.168.50.11';
lookupFails = true;
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].ip, ip);
assert.equal(snapshot.devices[0].hostname, 'living-room.local');
assert.deepEqual(lookups, ['192.168.50.10', '192.168.50.11']);
});
test('device inventory discovers, merges, persists metadata and expires devices after 30 days', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-devices-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({ filePath: path.join(directory, 'devices.json'), defaultValue: {} });
const ouiPath = path.join(directory, 'oui.txt');
fs.writeFileSync(ouiPath, '00-11-22 (hex)\t\tExample Devices\n');
const vendor = createVendorLookup(ouiPath);
assert.equal(vendor('00:11:22:33:44:55'), 'Example Devices');
assert.equal(vendor('02:11:22:33:44:55'), null);
let current = new Date('2026-07-01T10:00:00.000Z');
let observation = {
observedAt: current.toISOString(),
observations: parseNeighborSnapshot([
{ dst: '2001:db8::10', dev: 'br0', lladdr: '00:11:22:33:44:55', state: ['STALE'] },
{ dst: '192.168.50.10', dev: 'br0', lladdr: '00:11:22:33:44:55', state: ['REACHABLE'] },
{ dst: '192.168.50.10', dev: 'br0', lladdr: '00:11:22:33:44:55', state: ['STALE'] },
{ dst: '192.168.50.11', dev: 'br0', state: ['INCOMPLETE'] },
], current.toISOString()),
error: null,
};
let observeCalls = 0;
const service = createDeviceInventoryService({
store,
observe: async () => {
observeCalls += 1;
return observation;
},
vendor,
now: () => current,
});
const [firstRefresh, sharedRefresh] = await Promise.all([service.refresh(), service.refresh()]);
let snapshot = firstRefresh;
assert.strictEqual(sharedRefresh, firstRefresh);
assert.equal(observeCalls, 1);
assert.equal(snapshot.devices.length, 1);
assert.equal(snapshot.devices[0].manufacturer, 'Example Devices');
assert.equal(snapshot.devices[0].status, 'online');
assert.equal(snapshot.devices[0].confidence, 'high');
assert.equal(snapshot.devices[0].interface, 'br0');
observation = {
observedAt: current.toISOString(),
observations: parseNeighborSnapshot([
{ dst: '192.168.50.10', dev: 'br0', lladdr: '00:11:22:33:44:55', state: ['REACHABLE'] },
{ dst: '192.168.50.12', dev: 'br0', lladdr: '00:11:22:33:44:55', state: ['STALE'] },
], current.toISOString()),
error: null,
};
snapshot = await service.refresh();
assert.equal(snapshot.devices.length, 1);
assert.equal(snapshot.devices[0].confidence, 'ambiguous');
assert.equal(snapshot.devices[0].ip, '192.168.50.10');
current = new Date('2026-07-01T10:05:00.000Z');
observation = {
observedAt: current.toISOString(),
observations: parseNeighborSnapshot([
{ dst: '192.168.50.10', dev: 'br0', lladdr: '00:11:22:33:44:55', state: ['STALE'] },
], current.toISOString()),
error: null,
};
const firstSeen = snapshot.devices[0].lastSeenAt;
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].lastSeenAt, firstSeen);
assert.equal(snapshot.devices[0].status, 'recent');
snapshot = service.update(snapshot.devices[0].id, { alias: 'Телевизор', pinned: true }, snapshot.revision);
const namedId = snapshot.devices[0].id;
const restarted = createDeviceInventoryService({ store, observe: async () => observation, vendor, now: () => current });
assert.deepEqual(restarted.snapshot().devices[0], snapshot.devices[0]);
assert.throws(
() => restarted.update(snapshot.devices[0].id, { pinned: false }, snapshot.revision - 1),
(error) => error.code === 'STATE_CONFLICT',
);
current = new Date('2026-07-01T10:06:00.000Z');
observation = {
observedAt: current.toISOString(),
observations: parseNeighborSnapshot([
{ dst: '192.168.50.18', dev: 'br0', lladdr: '00:11:22:33:44:55', state: ['REACHABLE'] },
], current.toISOString()),
error: null,
};
snapshot = await restarted.refresh();
assert.equal(snapshot.devices.length, 1);
assert.equal(snapshot.devices[0].id, namedId);
assert.equal(snapshot.devices[0].ip, '192.168.50.18');
assert.equal(snapshot.devices[0].alias, 'Телевизор');
assert.equal(snapshot.devices[0].pinned, true);
observation = {
observedAt: current.toISOString(),
observations: parseNeighborSnapshot([
{ dst: '192.168.50.18', dev: 'br0', lladdr: '00:11:22:33:44:66', state: ['REACHABLE'] },
], current.toISOString()),
error: null,
};
snapshot = await restarted.refresh();
const replacement = snapshot.devices.find(({ mac: deviceMac }) => deviceMac === '00:11:22:33:44:66');
assert.notEqual(replacement.id, namedId);
assert.equal(replacement.alias, '');
assert.equal(replacement.pinned, false);
snapshot = restarted.update(namedId, { deprioritized: true }, snapshot.revision);
const deprioritized = snapshot.devices.find(({ id }) => id === namedId);
assert.equal(deprioritized.pinned, false);
assert.equal(deprioritized.deprioritized, true);
assert.throws(
() => restarted.update(namedId, { pinned: true, deprioritized: true }, snapshot.revision),
(error) => error.code === 'REQUEST_INVALID',
);
const persisted = createDeviceInventoryService({ store, observe: async () => observation, vendor, now: () => current });
snapshot = persisted.snapshot();
assert.equal(snapshot.devices.find(({ id }) => id === namedId).deprioritized, true);
snapshot = persisted.update(replacement.id, { pinned: true }, snapshot.revision);
assert.equal(snapshot.devices.find(({ id }) => id === replacement.id).pinned, true);
observation = { observedAt: current.toISOString(), observations: [], error: 'source unavailable' };
snapshot = await persisted.refresh();
assert.equal(snapshot.devices.length, 2);
assert.equal(snapshot.source.error, 'source unavailable');
current = new Date('2026-08-02T10:00:00.000Z');
observation = { observedAt: current.toISOString(), observations: [], error: 'source unavailable' };
snapshot = await persisted.refresh();
assert.equal(snapshot.devices.length, 2);
assert.equal(snapshot.source.error, 'source unavailable');
observation = { observedAt: current.toISOString(), observations: [], error: null };
snapshot = await persisted.refresh();
assert.equal(snapshot.devices.length, 0);
const failed = readNeighborSnapshot(() => ({ status: 1, stderr: 'not available' }), () => current);
assert.deepEqual(failed.observations, []);
assert.match(failed.error, /not available/);
});
test('device traffic totals persist exact deltas across polls and process epochs', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-traffic-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
});
const observedAt = '2026-08-07T12:00:00.000Z';
const mac = '00:11:22:33:44:55';
const neighbor = {
observedAt,
observations: [{
ip: '192.168.50.7',
mac,
interface: 'eth0',
observedAt,
active: true,
}],
error: null,
};
let traffic = {
epoch: 'epoch-a',
generation: 'rules-a',
observedAt,
source: { error: null },
devices: [{
ip: '192.168.50.7',
mac,
interface: 'eth0',
uploadBytes: '9007199254740993',
downloadBytes: '100',
proxyUploadBytes: '1000',
proxyDownloadBytes: '2000',
}],
};
let trafficError = null;
const createService = () => createDeviceInventoryService({
store,
observe: () => neighbor,
observeTraffic: () => {
if (trafficError) throw trafficError;
return traffic;
},
});
let service = createService();
let snapshot = await service.refresh();
assert.equal(snapshot.devices[0].uploadBytes, '9007199254740993');
assert.equal(snapshot.devices[0].downloadBytes, '100');
assert.equal(snapshot.devices[0].trafficObservedAt, observedAt);
assert.equal(snapshot.devices[0].proxyUploadBytes, '1000');
assert.equal(snapshot.devices[0].proxyDownloadBytes, '2000');
assert.deepEqual(snapshot.source.traffic, {
lastObservedAt: observedAt,
error: null,
proxy: { lastObservedAt: observedAt, error: null },
});
assert.deepEqual(snapshot.traffic, {
gatewayBytes: '9007199254741093',
proxyBytes: '3000',
uploadBytes: '9007199254741993',
downloadBytes: '2100',
totalBytes: '9007199254744093',
gatewayObservedAt: observedAt,
proxyObservedAt: observedAt,
observedAt,
history: [],
});
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].uploadBytes, '9007199254740993');
assert.equal(snapshot.devices[0].downloadBytes, '100');
traffic = {
...traffic,
devices: [{
...traffic.devices[0],
uploadBytes: '9007199254740995',
downloadBytes: '150',
proxyUploadBytes: '1010',
proxyDownloadBytes: '2050',
}],
};
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].uploadBytes, '9007199254740995');
assert.equal(snapshot.devices[0].downloadBytes, '150');
assert.equal(snapshot.devices[0].proxyUploadBytes, '1010');
assert.equal(snapshot.devices[0].proxyDownloadBytes, '2050');
assert.equal(snapshot.traffic.gatewayBytes, '9007199254741145');
assert.equal(snapshot.traffic.proxyBytes, '3060');
assert.equal(snapshot.traffic.uploadBytes, '9007199254742005');
assert.equal(snapshot.traffic.downloadBytes, '2200');
service = createService();
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].uploadBytes, '9007199254740995');
assert.equal(snapshot.devices[0].downloadBytes, '150');
traffic = {
...traffic,
epoch: 'epoch-b',
generation: 'rules-b',
devices: [{
...traffic.devices[0],
uploadBytes: '10',
downloadBytes: '20',
proxyUploadBytes: '3',
proxyDownloadBytes: '4',
}],
};
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].uploadBytes, '9007199254741005');
assert.equal(snapshot.devices[0].downloadBytes, '170');
assert.equal(snapshot.devices[0].proxyUploadBytes, '1013');
assert.equal(snapshot.devices[0].proxyDownloadBytes, '2054');
assert.equal(snapshot.traffic.totalBytes, '9007199254744242');
traffic = {
...traffic,
devices: [{ ...traffic.devices[0], uploadBytes: '9', downloadBytes: '20' }],
};
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].uploadBytes, '9007199254741005');
assert.match(snapshot.source.traffic.error, /уменьшился/);
traffic = {
...traffic,
observedAt: '2026-08-07T12:00:15.000Z',
devices: [{
...traffic.devices[0],
uploadBytes: '12',
downloadBytes: '25',
proxyUploadBytes: '2',
proxyDownloadBytes: '4',
}],
};
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].uploadBytes, '9007199254741007');
assert.equal(snapshot.devices[0].downloadBytes, '175');
assert.equal(snapshot.devices[0].proxyUploadBytes, '1013');
assert.equal(snapshot.devices[0].proxyDownloadBytes, '2054');
assert.equal(snapshot.source.traffic.error, null);
assert.match(snapshot.source.traffic.proxy.error, /уменьшился/);
assert.equal(snapshot.traffic.proxyBytes, '3067');
assert.equal(snapshot.traffic.gatewayObservedAt, '2026-08-07T12:00:15.000Z');
assert.equal(snapshot.traffic.proxyObservedAt, observedAt);
assert.equal(snapshot.traffic.observedAt, observedAt);
trafficError = new Error('traffic unavailable');
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].uploadBytes, '9007199254741007');
assert.equal(snapshot.source.traffic.lastObservedAt, '2026-08-07T12:00:15.000Z');
assert.equal(snapshot.source.traffic.error, 'traffic unavailable');
assert.equal(snapshot.traffic.totalBytes, '9007199254744249');
});
test('device traffic history stays in bounded service memory and survives client snapshots', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-history-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'devices.json');
const store = createJsonStore({ filePath, defaultValue: {}, migrate: migrateDeviceInventoryState });
const mac = '00:11:22:33:44:55';
let observedAt = '2026-08-07T12:00:00.000Z';
let uploadBytes = '100';
let proxyDownloadBytes = '20';
const createService = () => createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: [{ ip: '192.168.50.7', mac, interface: 'eth0', observedAt, active: true }],
}),
observeTraffic: () => ({
epoch: 'epoch-a',
generation: 'rules-a',
observedAt,
source: { error: null },
devices: [{
ip: '192.168.50.7', mac, interface: 'eth0', uploadBytes, downloadBytes: '0',
proxyUploadBytes: '0', proxyDownloadBytes,
}],
}),
});
const service = createService();
assert.deepEqual((await service.refresh()).devices[0].trafficHistory, []);
observedAt = '2026-08-07T12:00:15.000Z';
uploadBytes = '130';
proxyDownloadBytes = '25';
const snapshot = await service.refresh();
assert.equal(snapshot.trafficHistoryCapacity, 120);
assert.deepEqual(snapshot.devices[0].trafficHistory, [{
observedAt,
gatewayBytes: '30',
proxyBytes: '5',
uploadBytes: '30',
downloadBytes: '5',
}]);
assert.deepEqual(snapshot.traffic.history, [{
observedAt,
gatewayBytes: '30',
proxyBytes: '5',
uploadBytes: '30',
downloadBytes: '5',
}]);
assert.deepEqual(service.snapshot().devices[0].trafficHistory, snapshot.devices[0].trafficHistory);
assert.doesNotMatch(fs.readFileSync(filePath, 'utf8'), /trafficHistory/);
assert.deepEqual(createService().snapshot().devices[0].trafficHistory, []);
assert.deepEqual(createService().snapshot().traffic.history, []);
});
test('device outbound history keeps sing-box routes and kernel Direct on separate reset-safe series', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-outbound-history-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'devices.json');
const store = createJsonStore({ filePath, defaultValue: {}, migrate: migrateDeviceInventoryState });
const mac = '00:11:22:33:44:55';
const id = deviceId(mac);
let observedAt = '2026-08-12T12:00:00.000Z';
let epoch = 'epoch-a';
let vpn = ['100', '200'];
let trackedDirect = ['10', '20'];
let unknown = ['1', '2'];
let directIpv4 = ['40', '60'];
let gateway = ['100', '200'];
let proxyIngress = ['10', '20'];
const createService = () => createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: [{ ip: '192.168.50.7', mac, interface: 'eth0', observedAt, active: true }],
}),
observeTraffic: () => ({
epoch,
generation: 'rules-a',
observedAt,
source: { error: null },
direct: { uploadBytes: directIpv4[0], downloadBytes: directIpv4[1] },
devices: [{
ip: '192.168.50.7', mac, interface: 'eth0', uploadBytes: gateway[0], downloadBytes: gateway[1],
proxyUploadBytes: proxyIngress[0], proxyDownloadBytes: proxyIngress[1],
directUploadBytes: directIpv4[0], directDownloadBytes: directIpv4[1],
}],
}),
observeDomainTraffic: () => ({
epoch,
observedAt,
source: { error: null },
routes: [
{ deviceId: id, source: 'gateway', outbound: 'vpn', uploadBytes: vpn[0], downloadBytes: vpn[1] },
{ deviceId: id, source: 'proxy', outbound: 'direct', uploadBytes: trackedDirect[0], downloadBytes: trackedDirect[1] },
{ deviceId: id, source: 'gateway', outbound: 'unknown', uploadBytes: unknown[0], downloadBytes: unknown[1] },
],
series: [],
}),
});
let service = createService();
let snapshot = await service.refresh();
assert.deepEqual(snapshot.devices[0].outboundTraffic, {
observedAt,
singboxObservedAt: observedAt,
directIpv4ObservedAt: observedAt,
vpnBytes: '300',
directTrackedBytes: '30',
directIpv4Bytes: '100',
unknownBytes: '3',
});
assert.deepEqual(snapshot.devices[0].outboundTrafficHistory, []);
observedAt = '2026-08-12T12:00:15.000Z';
vpn = ['150', '250'];
trackedDirect = ['30', '50'];
unknown = ['4', '8'];
directIpv4 = ['70', '90'];
snapshot = await service.refresh();
assert.deepEqual(snapshot.devices[0].outboundTraffic, {
observedAt,
singboxObservedAt: observedAt,
directIpv4ObservedAt: observedAt,
vpnBytes: '400',
directTrackedBytes: '80',
directIpv4Bytes: '160',
unknownBytes: '12',
});
assert.deepEqual(snapshot.devices[0].outboundTrafficHistory, [{
observedAt,
vpnBytes: '100',
directTrackedBytes: '50',
directIpv4Bytes: '60',
unknownBytes: '9',
}]);
const globalBeforeReset = structuredClone(snapshot.traffic);
const resetRevision = snapshot.revision;
snapshot = await service.resetTraffic(resetRevision);
assert.equal(snapshot.devices[0].uploadBytes, '0');
assert.equal(snapshot.devices[0].downloadBytes, '0');
assert.equal(snapshot.devices[0].proxyUploadBytes, '0');
assert.equal(snapshot.devices[0].proxyDownloadBytes, '0');
assert.deepEqual(snapshot.devices[0].trafficHistory, []);
assert.deepEqual(snapshot.devices[0].outboundTraffic, {
observedAt,
singboxObservedAt: observedAt,
directIpv4ObservedAt: observedAt,
vpnBytes: '0',
directTrackedBytes: '0',
directIpv4Bytes: '0',
unknownBytes: '0',
});
assert.deepEqual(snapshot.devices[0].outboundTrafficHistory, []);
assert.deepEqual(snapshot.traffic, globalBeforeReset);
assert.deepEqual(store.read().traffic.outboundBaselinesByDeviceId[id], {
routeEpoch: 'epoch-a',
directEpoch: 'epoch-a',
vpnBytes: '400',
directTrackedBytes: '80',
directIpv4Bytes: '160',
unknownBytes: '12',
});
assert.equal(service.metricsSnapshot().domainTraffic.routes[0].uploadBytes, '150');
assert.equal(service.metricsSnapshot().directTraffic.series[0].uploadBytes, '70');
await assert.rejects(
service.resetTraffic(resetRevision),
(error) => error.code === 'STATE_CONFLICT',
);
observedAt = '2026-08-12T12:00:30.000Z';
gateway = ['115', '225'];
proxyIngress = ['13', '24'];
vpn = ['160', '260'];
trackedDirect = ['35', '55'];
unknown = ['4', '8'];
directIpv4 = ['75', '95'];
service = createService();
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].uploadBytes, '15');
assert.equal(snapshot.devices[0].downloadBytes, '25');
assert.equal(snapshot.devices[0].proxyUploadBytes, '3');
assert.equal(snapshot.devices[0].proxyDownloadBytes, '4');
assert.deepEqual(snapshot.devices[0].outboundTraffic, {
observedAt,
singboxObservedAt: observedAt,
directIpv4ObservedAt: observedAt,
vpnBytes: '20',
directTrackedBytes: '10',
directIpv4Bytes: '10',
unknownBytes: '0',
});
observedAt = '2026-08-12T12:00:45.000Z';
epoch = 'epoch-b';
vpn = ['3', '4'];
trackedDirect = ['1', '2'];
unknown = ['0', '1'];
directIpv4 = ['5', '6'];
snapshot = await service.refresh();
assert.deepEqual(snapshot.devices[0].outboundTraffic, {
observedAt,
singboxObservedAt: observedAt,
directIpv4ObservedAt: observedAt,
vpnBytes: '7',
directTrackedBytes: '3',
directIpv4Bytes: '11',
unknownBytes: '1',
});
assert.deepEqual(snapshot.devices[0].outboundTrafficHistory.at(-1), {
observedAt,
vpnBytes: '0',
directTrackedBytes: '0',
directIpv4Bytes: '0',
unknownBytes: '0',
});
assert.doesNotMatch(fs.readFileSync(filePath, 'utf8'), /outboundTrafficHistory/);
assert.doesNotMatch(fs.readFileSync(filePath, 'utf8'), /outboundTraffic/);
});
test('global traffic stays monotonic when a device expires and returns in the same epoch', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-global-traffic-retention-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
});
const mac = '00:11:22:33:44:55';
let observedAt = '2026-07-01T12:00:00.000Z';
let present = true;
let uploadBytes = '100';
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: present
? [{ ip: '192.168.50.7', mac, interface: 'eth0', observedAt, active: true }]
: [],
}),
observeTraffic: () => ({
epoch: 'epoch-a', generation: 'rules-a', observedAt, source: { error: null },
devices: [{ mac, uploadBytes, downloadBytes: '0' }],
}),
});
assert.equal((await service.refresh()).traffic.totalBytes, '100');
present = false;
observedAt = '2026-08-07T12:00:00.000Z';
let snapshot = await service.refresh();
assert.equal(snapshot.devices.length, 0);
assert.equal(snapshot.traffic.totalBytes, '100');
present = true;
snapshot = await service.refresh();
assert.equal(snapshot.traffic.totalBytes, '100');
uploadBytes = '110';
assert.equal((await service.refresh()).traffic.totalBytes, '110');
});
test('legacy dataplane samples preserve saved proxy totals while Gateway totals keep advancing', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-proxy-legacy-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
});
const observedAt = '2026-08-07T12:00:00.000Z';
const mac = '00:11:22:33:44:55';
const neighbor = {
observedAt,
error: null,
observations: [
{ ip: '192.168.50.7', mac, interface: 'eth0', observedAt, active: true },
{ ip: '172.17.0.2', mac: '00:11:22:33:44:66', interface: 'docker0', observedAt, active: true },
],
};
let row = {
mac,
uploadBytes: '10',
downloadBytes: '20',
proxyUploadBytes: '30',
proxyDownloadBytes: '40',
};
const service = createDeviceInventoryService({
store,
observe: () => neighbor,
observeTraffic: () => ({
epoch: 'epoch-a', generation: 'rules-a', observedAt, source: { error: null }, devices: [row],
}),
});
let snapshot = await service.refresh();
assert.equal(snapshot.devices.length, 1);
assert.equal(snapshot.devices[0].proxyUploadBytes, '30');
row = { mac, uploadBytes: '15', downloadBytes: '27' };
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].uploadBytes, '15');
assert.equal(snapshot.devices[0].downloadBytes, '27');
assert.equal(snapshot.devices[0].proxyUploadBytes, '30');
assert.equal(snapshot.devices[0].proxyDownloadBytes, '40');
assert.equal(snapshot.source.traffic.proxy.error, null);
});
test('a proxy regression rejects every device in that proxy sample atomically', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-proxy-atomic-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
});
const observedAt = '2026-08-07T12:00:00.000Z';
const macs = ['00:11:22:33:44:55', '00:11:22:33:44:66'];
const neighbor = {
observedAt,
error: null,
observations: macs.map((mac, index) => ({
ip: `192.168.50.${index + 7}`, mac, interface: 'eth0', observedAt, active: true,
})),
};
let rows = [
{ mac: macs[0], uploadBytes: '10', downloadBytes: '20', proxyUploadBytes: '30', proxyDownloadBytes: '40' },
{ mac: macs[1], uploadBytes: '50', downloadBytes: '60', proxyUploadBytes: '70', proxyDownloadBytes: '80' },
];
const service = createDeviceInventoryService({
store,
observe: () => neighbor,
observeTraffic: () => ({
epoch: 'epoch-a', generation: 'rules-a', observedAt, source: { error: null }, devices: rows,
}),
});
await service.refresh();
const before = structuredClone(store.read().traffic.proxy);
rows = [
{ mac: macs[0], uploadBytes: '11', downloadBytes: '22', proxyUploadBytes: '35', proxyDownloadBytes: '45' },
{ mac: macs[1], uploadBytes: '52', downloadBytes: '63', proxyUploadBytes: '69', proxyDownloadBytes: '80' },
];
const snapshot = await service.refresh();
const after = store.read().traffic.proxy;
assert.deepEqual(after.baselinesByMac, before.baselinesByMac);
assert.deepEqual(after.totalsByMac, before.totalsByMac);
assert.match(snapshot.source.traffic.proxy.error, /уменьшился/);
assert.equal(snapshot.devices.find(({ mac }) => mac === macs[0]).uploadBytes, '11');
});
test('device policy is independent from pinning, persists, and keeps the last applied mode on failure', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-policy-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
initializeMissing: true,
backupWhen: () => true,
});
const observedAt = '2026-08-07T12:00:00.000Z';
const mac = '00:11:22:33:44:55';
store.update((state) => ({
...state,
revision: state.revision + 1,
devices: [{
id: deviceId(mac),
alias: '',
pinned: false,
deprioritized: false,
hostname: null,
manufacturer: null,
mac,
ip: '192.168.50.7',
interface: 'eth0',
firstSeenAt: observedAt,
lastSeenAt: observedAt,
source: 'neighbor',
confidence: 'high',
}],
}));
let observations = [{
ip: '192.168.50.7',
mac,
interface: 'eth0',
observedAt,
active: true,
}];
let activeDevices = [];
let generation = 0;
let policyEpoch = 'policy-epoch-a';
let failApply = false;
let malformedAck = false;
const appliedSets = [];
const policySnapshot = () => ({
epoch: policyEpoch,
generation: `policy-rules-${generation}`,
fingerprint: fingerprintDirectDevices(activeDevices),
observedAt,
appliedIds: activeDevices.map(({ id }) => id),
});
const createService = () => createDeviceInventoryService({
store,
observe: () => ({ observedAt, observations, error: null }),
observePolicy: policySnapshot,
applyPolicies: async (devices) => {
appliedSets.push(structuredClone(devices));
if (failApply) throw new Error('iptables unavailable');
if (malformedAck) return { ...policySnapshot(), fingerprint: 'invalid' };
activeDevices = structuredClone(devices);
generation += 1;
return policySnapshot();
},
});
let service = createService();
let snapshot = await service.refresh();
const id = snapshot.devices[0].id;
assert.equal(snapshot.devices[0].desiredPolicy, 'vpn');
assert.equal(snapshot.devices[0].appliedPolicy, 'vpn');
snapshot = await service.setPolicy(id, 'direct', snapshot.revision);
assert.deepEqual(appliedSets.at(-1), [{ id, ip: '192.168.50.7', mac, interface: 'eth0' }]);
assert.equal(snapshot.devices[0].desiredPolicy, 'direct');
assert.equal(snapshot.devices[0].appliedPolicy, 'direct');
assert.equal(snapshot.devices[0].policyStatus, 'applied');
assert.equal(store.read().schemaVersion, DEVICE_INVENTORY_SCHEMA_VERSION);
assert.equal(store.read().policy.schemaVersion, 1);
service = createService();
snapshot = await service.reconcilePolicies();
assert.equal(snapshot.devices[0].appliedPolicy, 'direct');
snapshot = service.update(id, { pinned: true }, snapshot.revision);
snapshot = service.update(id, { pinned: false }, snapshot.revision);
assert.equal(snapshot.devices[0].appliedPolicy, 'direct');
policyEpoch = 'policy-epoch-b';
activeDevices = [];
failApply = true;
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].desiredPolicy, 'direct');
assert.equal(snapshot.devices[0].appliedPolicy, 'vpn');
assert.equal(snapshot.devices[0].policyStatus, 'failed');
failApply = false;
snapshot = await service.refresh();
assert.equal(snapshot.devices[0].appliedPolicy, 'direct');
assert.equal(snapshot.devices[0].policyStatus, 'applied');
observations = [
{ ...observations[0], interface: 'eth0' },
{ ...observations[0], interface: 'eth1' },
];
snapshot = await service.refresh();
assert.deepEqual(appliedSets.at(-1), []);
assert.equal(snapshot.devices[0].confidence, 'ambiguous');
assert.equal(snapshot.devices[0].desiredPolicy, 'direct');
assert.equal(snapshot.devices[0].appliedPolicy, 'vpn');
assert.equal(snapshot.devices[0].policyStatus, 'pending');
snapshot = await service.setPolicy(id, 'vpn', snapshot.revision);
assert.equal(snapshot.devices[0].policyStatus, 'applied');
observations = [{ ...observations[0], interface: 'eth0' }];
snapshot = await service.refresh();
snapshot = await service.setPolicy(id, 'direct', snapshot.revision);
const secondMac = '00:11:22:33:44:66';
observations.push({
ip: '192.168.50.8',
mac: secondMac,
interface: 'eth0',
observedAt,
active: true,
});
failApply = true;
snapshot = await service.refresh();
const secondId = snapshot.devices.find((device) => device.mac === secondMac).id;
const byId = new Map(snapshot.devices.map((device) => [device.id, device]));
assert.equal(byId.get(id).desiredPolicy, 'direct');
assert.equal(byId.get(id).appliedPolicy, 'direct');
assert.equal(byId.get(id).policyStatus, 'applied');
assert.equal(byId.get(secondId).desiredPolicy, 'direct');
assert.equal(byId.get(secondId).appliedPolicy, 'vpn');
assert.equal(byId.get(secondId).policyStatus, 'failed');
assert.equal(snapshot.source.policy.error, 'iptables unavailable');
failApply = false;
malformedAck = true;
await assert.rejects(
service.setPolicy(secondId, 'direct', snapshot.revision),
(error) => error.code === 'DEVICE_POLICY_APPLY_FAILED',
);
snapshot = service.snapshot();
assert.equal(snapshot.devices.find((device) => device.id === secondId).appliedPolicy, 'vpn');
assert.match(snapshot.source.policy.error, /acknowledgement/);
assert.throws(
() => service.setPolicy(secondId, 'block', snapshot.revision),
(error) => error.code === 'REQUEST_INVALID',
);
});
test('new devices default to Direct while known and ambiguous devices keep truthful applied routes', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-new-device-policy-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const observedAt = '2026-08-31T12:00:00.000Z';
const knownMac = '00:11:22:33:44:10';
const newMac = '00:11:22:33:44:20';
const ambiguousMac = '00:11:22:33:44:30';
const retainedMac = '00:11:22:33:44:40';
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
initializeMissing: true,
});
store.update((state) => ({
...state,
revision: state.revision + 1,
devices: [{
id: deviceId(knownMac),
alias: '',
pinned: false,
deprioritized: false,
hostname: null,
manufacturer: null,
mac: knownMac,
ip: '192.168.50.10',
interface: 'eth0',
firstSeenAt: '2026-08-30T12:00:00.000Z',
lastSeenAt: '2026-08-30T12:00:00.000Z',
source: 'neighbor',
confidence: 'high',
}],
policy: {
...state.policy,
byMac: {
[retainedMac]: {
desired: 'direct',
applied: 'direct',
status: 'failed',
appliedAt: '2026-08-29T12:00:00.000Z',
error: 'cleanup failed',
operationId: null,
},
},
},
}));
let observations = [
{ ip: '192.168.50.10', mac: knownMac, interface: 'eth0', observedAt, active: true },
{ ip: '192.168.50.20', mac: newMac, interface: 'eth0', observedAt, active: true },
{ ip: '192.168.50.30', mac: ambiguousMac, interface: 'eth0', observedAt, active: true },
{ ip: '192.168.50.31', mac: ambiguousMac, interface: 'eth1', observedAt, active: true },
{ ip: '192.168.50.40', mac: retainedMac, interface: 'eth0', observedAt, active: true },
];
let activeDevices = [];
let generation = 0;
let failApply = false;
const appliedSets = [];
const policySnapshot = () => ({
epoch: 'policy-epoch',
generation: `policy-rules-${generation}`,
fingerprint: fingerprintDirectDevices(activeDevices),
observedAt,
appliedIds: activeDevices.map(({ id }) => id),
});
const createService = () => createDeviceInventoryService({
store,
observe: () => ({ observedAt, observations, error: null }),
observePolicy: policySnapshot,
applyPolicies: async (devices) => {
appliedSets.push(structuredClone(devices));
if (failApply) throw new Error('iptables unavailable');
activeDevices = structuredClone(devices);
generation += 1;
return policySnapshot();
},
});
let service = createService();
let snapshot = await service.refresh();
const byMac = new Map(snapshot.devices.map((device) => [device.mac, device]));
assert.equal(byMac.get(knownMac).desiredPolicy, 'vpn');
assert.equal(byMac.get(knownMac).appliedPolicy, 'vpn');
assert.equal(byMac.get(newMac).desiredPolicy, 'direct');
assert.equal(byMac.get(newMac).appliedPolicy, 'direct');
assert.equal(byMac.get(ambiguousMac).desiredPolicy, 'direct');
assert.equal(byMac.get(ambiguousMac).appliedPolicy, 'vpn');
assert.equal(byMac.get(ambiguousMac).policyStatus, 'pending');
assert.equal(byMac.get(retainedMac).appliedPolicy, 'direct');
assert.deepEqual(appliedSets.at(-1).map(({ mac }) => mac).sort(), [newMac, retainedMac].sort());
observations = observations.filter(({ mac, interface: deviceInterface }) => (
mac !== ambiguousMac || deviceInterface === 'eth0'
));
snapshot = await service.refresh();
assert.equal(snapshot.devices.find(({ mac }) => mac === ambiguousMac).appliedPolicy, 'direct');
const newId = snapshot.devices.find(({ mac }) => mac === newMac).id;
snapshot = await service.setPolicy(newId, 'vpn', snapshot.revision);
assert.equal(snapshot.devices.find(({ mac }) => mac === newMac).appliedPolicy, 'vpn');
service = createService();
snapshot = await service.refresh();
assert.equal(snapshot.devices.find(({ mac }) => mac === newMac).appliedPolicy, 'vpn');
const reappearedMac = '00:11:22:33:44:50';
observations.push({ ip: '192.168.50.50', mac: reappearedMac, interface: 'eth0', observedAt, active: true });
failApply = true;
snapshot = await service.refresh();
const failed = snapshot.devices.find(({ mac }) => mac === reappearedMac);
assert.equal(failed.desiredPolicy, 'direct');
assert.equal(failed.appliedPolicy, 'vpn');
assert.equal(failed.policyStatus, 'failed');
failApply = false;
service = createService();
snapshot = await service.refresh();
assert.equal(snapshot.devices.find(({ mac }) => mac === reappearedMac).appliedPolicy, 'direct');
});
test('reappearing device preserves retained applied Direct when cleanup acknowledgement failed', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-retained-device-policy-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const observedAt = '2026-08-31T12:00:00.000Z';
const mac = '00:11:22:33:44:60';
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
initializeMissing: true,
});
store.update((state) => ({
...state,
policy: {
...state.policy,
byMac: {
[mac]: {
desired: 'direct',
applied: 'direct',
status: 'failed',
appliedAt: '2026-08-30T12:00:00.000Z',
error: 'cleanup failed',
operationId: null,
},
},
},
}));
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: [{ ip: '192.168.50.60', mac, interface: 'eth0', observedAt, active: true }],
}),
applyPolicies: async () => { throw new Error('iptables unavailable'); },
});
const snapshot = await service.refresh();
assert.equal(snapshot.devices[0].desiredPolicy, 'direct');
assert.equal(snapshot.devices[0].appliedPolicy, 'direct');
assert.equal(snapshot.devices[0].policyStatus, 'failed');
assert.equal(store.read().policy.byMac[mac].appliedAt, '2026-08-30T12:00:00.000Z');
});
test('device inventory v1 migration creates a versioned backup', (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-migration-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'devices.json');
fs.writeFileSync(filePath, JSON.stringify({ schemaVersion: 1, revision: 4, devices: [] }));
const store = createJsonStore({
filePath,
defaultValue: {},
migrate: migrateDeviceInventoryState,
backupWhen: (before, after) => before?.schemaVersion !== after.schemaVersion,
});
const migrated = store.read();
assert.equal(migrated.schemaVersion, DEVICE_INVENTORY_SCHEMA_VERSION);
assert.equal(migrated.policy.schemaVersion, 1);
assert.deepEqual(migrated.traffic.baselinesByMac, {});
assert.match(store.migration?.backupPath || '', /\.backup-v1-/);
assert.ok(fs.existsSync(store.migration.backupPath));
});
test('device inventory backs up and normalizes a malformed additive policy checkpoint', (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-policy-migration-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'devices.json');
fs.writeFileSync(filePath, JSON.stringify({
schemaVersion: 2,
revision: 3,
devices: [],
traffic: { baselinesByMac: {}, totalsByMac: {}, rebaselineMacs: [] },
policy: {
schemaVersion: 1,
defaultMode: 'vpn',
dataplaneEpoch: 42,
byMac: {
broken: { desired: 'direct', applied: 'vpn', status: 'failed' },
},
},
}));
const store = createJsonStore({
filePath,
defaultValue: {},
migrate: migrateDeviceInventoryState,
backupWhen: () => true,
});
const migrated = store.read();
assert.equal(migrated.schemaVersion, DEVICE_INVENTORY_SCHEMA_VERSION);
assert.equal(migrated.policy.schemaVersion, 1);
assert.equal(migrated.policy.dataplaneEpoch, null);
assert.deepEqual(migrated.policy.byMac, {});
assert.match(migrated.policy.lastError, /device policy checkpoint/);
assert.match(store.migration?.backupPath || '', /\.backup-v2-/);
assert.ok(fs.existsSync(store.migration.backupPath));
});
test('device inventory backs up malformed v2 traffic and re-baselines without double counting', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-corrupt-traffic-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'devices.json');
const observedAt = '2026-08-07T12:00:00.000Z';
const macs = [
'00:11:22:33:44:55',
'00:11:22:33:44:66',
'00:11:22:33:44:77',
'00:11:22:33:44:88',
'00:11:22:33:44:99',
];
const [firstMac, secondMac, missingBaselineMac, missingTotalMac, expiredMac] = macs;
const devices = macs.map((mac, index) => ({
id: `device-${index}`,
alias: '',
pinned: false,
hostname: null,
manufacturer: null,
mac,
ip: `192.168.50.${index + 7}`,
interface: 'eth0',
firstSeenAt: mac === expiredMac ? '2026-06-01T12:00:00.000Z' : observedAt,
lastSeenAt: mac === expiredMac ? '2026-06-01T12:00:00.000Z' : observedAt,
source: 'neighbor',
confidence: 'high',
}));
fs.writeFileSync(filePath, JSON.stringify({
schemaVersion: 2,
revision: 4,
lastObservedAt: observedAt,
lastError: null,
devices,
traffic: {
epoch: 'epoch-a',
generation: 'rules-a',
lastObservedAt: observedAt,
lastError: null,
baselinesByMac: {
[firstMac]: { epoch: 'epoch-a', uploadBytes: 'broken', downloadBytes: '100' },
[secondMac]: { epoch: 'epoch-a', uploadBytes: '50', downloadBytes: '60' },
[missingTotalMac]: { epoch: 'epoch-a', uploadBytes: '90', downloadBytes: '100' },
[expiredMac]: { epoch: 'epoch-a', uploadBytes: '110', downloadBytes: '120' },
},
totalsByMac: {
[firstMac]: { uploadBytes: '500', downloadBytes: '600', observedAt },
[secondMac]: { uploadBytes: 'broken', downloadBytes: '700', observedAt },
[missingBaselineMac]: { uploadBytes: '800', downloadBytes: '900', observedAt },
[expiredMac]: { uploadBytes: 'broken', downloadBytes: '1000', observedAt },
},
},
}));
const store = createJsonStore({
filePath,
defaultValue: {},
migrate: migrateDeviceInventoryState,
backupWhen: () => true,
});
const migrated = store.read();
assert.match(store.migration?.backupPath || '', /\.backup-v2-/);
assert.ok(fs.existsSync(store.migration.backupPath));
assert.deepEqual(migrated.traffic.totalsByMac[firstMac], {
uploadBytes: '500',
downloadBytes: '600',
observedAt,
});
assert.equal(migrated.traffic.totalsByMac[secondMac], undefined);
assert.deepEqual(
new Set(migrated.traffic.rebaselineMacs),
new Set([firstMac, secondMac, missingBaselineMac, missingTotalMac, expiredMac]),
);
assert.equal(migrated.traffic.global.gateway.uploadBytes, '1300');
assert.equal(migrated.traffic.global.gateway.downloadBytes, '1500');
assert.deepEqual(
new Set(migrated.traffic.global.gateway.rebaselineMacs),
new Set([firstMac, secondMac, missingBaselineMac, missingTotalMac, expiredMac]),
);
let counters = [
{ mac: firstMac, uploadBytes: '200', downloadBytes: '300' },
{ mac: secondMac, uploadBytes: '70', downloadBytes: '80' },
{ mac: missingBaselineMac, uploadBytes: '110', downloadBytes: '120' },
{ mac: missingTotalMac, uploadBytes: '130', downloadBytes: '140' },
];
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: devices.filter(({ mac }) => mac !== expiredMac)
.map(({ ip, mac, interface: deviceInterface }) => ({
ip,
mac,
interface: deviceInterface,
observedAt,
active: true,
})),
}),
observeTraffic: () => ({
epoch: 'epoch-a',
generation: 'rules-a',
observedAt,
source: { error: null },
devices: counters,
}),
});
let snapshot = await service.refresh();
let byMac = new Map(snapshot.devices.map((device) => [device.mac, device]));
assert.equal(byMac.get(firstMac).uploadBytes, '500');
assert.equal(byMac.get(secondMac).uploadBytes, '0');
assert.equal(byMac.get(missingBaselineMac).uploadBytes, '800');
assert.equal(byMac.get(missingTotalMac).uploadBytes, '0');
assert.equal(byMac.has(expiredMac), false);
assert.equal(snapshot.traffic.totalBytes, '2800');
assert.deepEqual(store.read().traffic.rebaselineMacs, []);
assert.equal(snapshot.source.traffic.error, null);
counters = [
{ mac: firstMac, uploadBytes: '250', downloadBytes: '330' },
{ mac: secondMac, uploadBytes: '75', downloadBytes: '90' },
{ mac: missingBaselineMac, uploadBytes: '115', downloadBytes: '125' },
{ mac: missingTotalMac, uploadBytes: '150', downloadBytes: '160' },
];
snapshot = await service.refresh();
byMac = new Map(snapshot.devices.map((device) => [device.mac, device]));
assert.equal(byMac.get(firstMac).uploadBytes, '550');
assert.equal(byMac.get(firstMac).downloadBytes, '630');
assert.equal(byMac.get(secondMac).uploadBytes, '5');
assert.equal(byMac.get(secondMac).downloadBytes, '10');
assert.equal(byMac.get(missingBaselineMac).uploadBytes, '805');
assert.equal(byMac.get(missingBaselineMac).downloadBytes, '905');
assert.equal(byMac.get(missingTotalMac).uploadBytes, '20');
assert.equal(byMac.get(missingTotalMac).downloadBytes, '20');
assert.equal(snapshot.traffic.totalBytes, '2945');
});
test('malformed proxy totals are backed up and an expired recovery marker is cleared', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-corrupt-proxy-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const filePath = path.join(directory, 'devices.json');
const mac = '00:11:22:33:44:55';
fs.writeFileSync(filePath, JSON.stringify({
schemaVersion: 2,
revision: 1,
devices: [{
id: 'dev_0123456789abcdef',
alias: '',
pinned: false,
mac,
ip: '192.168.50.7',
interface: 'eth0',
firstSeenAt: '2026-06-01T12:00:00.000Z',
lastSeenAt: '2026-06-01T12:00:00.000Z',
source: 'neighbor',
confidence: 'high',
}],
traffic: {
baselinesByMac: {}, totalsByMac: {}, rebaselineMacs: [],
proxy: {
schemaVersion: 1,
baselinesByMac: { [mac]: { epoch: 'epoch-a', uploadBytes: '1', downloadBytes: '2' } },
totalsByMac: { [mac]: { uploadBytes: 'broken', downloadBytes: '4' } },
rebaselineMacs: [],
},
},
}));
const store = createJsonStore({
filePath,
defaultValue: {},
migrate: migrateDeviceInventoryState,
backupWhen: () => true,
});
assert.match(store.read().traffic.proxy.lastError, /proxy traffic checkpoint/);
assert.ok(fs.existsSync(store.migration.backupPath));
const service = createDeviceInventoryService({
store,
now: () => new Date('2026-08-07T12:00:00.000Z'),
observe: () => ({ observedAt: '2026-08-07T12:00:00.000Z', observations: [], error: null }),
observeTraffic: () => ({
epoch: 'epoch-a', generation: 'rules-a', observedAt: '2026-08-07T12:00:00.000Z',
source: { error: null }, devices: [],
}),
});
const snapshot = await service.refresh();
assert.equal(snapshot.devices.length, 0);
assert.deepEqual(store.read().traffic.proxy.rebaselineMacs, []);
assert.equal(snapshot.source.traffic.proxy.error, null);
});
test('an old dataplane without domain traffic keeps inventory refresh and existing metrics available', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-domain-compat-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({ filePath: path.join(directory, 'devices.json'), defaultValue: {} });
const service = createDeviceInventoryService({
store,
observe: () => ({ observedAt: '2026-08-08T10:00:00.000Z', observations: [], error: null }),
observeTraffic: () => ({
epoch: 'epoch-a',
generation: 'rules-a',
observedAt: '2026-08-08T10:00:00.000Z',
source: { error: null },
direct: { uploadBytes: '0', downloadBytes: '0' },
devices: [],
}),
observeDomainTraffic: () => { throw new Error('Dataplane HTTP 404'); },
});
const snapshot = await service.refresh();
assert.equal(snapshot.devices.length, 0);
assert.equal(service.metricsSnapshot().domainTraffic.source.error, 'Dataplane HTTP 404');
assert.deepEqual(service.metricsSnapshot().directTraffic, {
epoch: 'epoch-a',
observedAt: '2026-08-08T10:00:00.000Z',
source: { error: null },
uploadBytes: '0',
downloadBytes: '0',
series: [],
});
assert.equal(service.metricsSnapshot().traffic.totalBytes, '0');
});
test('direct IPv4 counters stay metrics-only and keep the last good transport snapshot', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-direct-metrics-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({ filePath: path.join(directory, 'devices.json'), defaultValue: {} });
const observedAt = '2026-08-12T10:00:00.000Z';
const mac = '00:11:22:33:44:55';
let fail = false;
let includeDirect = true;
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: [{ ip: '192.168.50.7', mac, interface: 'eth0', observedAt, active: true }],
}),
observeTraffic: () => {
if (fail) throw new Error('Dataplane direct counters unavailable');
return {
epoch: 'epoch-a',
generation: 'rules-a',
observedAt,
source: { error: null },
direct: includeDirect ? { uploadBytes: '56', downloadBytes: '78' } : undefined,
devices: [{
mac,
uploadBytes: '100',
downloadBytes: '200',
...(includeDirect ? { directUploadBytes: '12', directDownloadBytes: '34' } : {}),
}],
};
},
});
await service.refresh();
assert.deepEqual(service.metricsSnapshot().directTraffic.series, [{
deviceId: deviceId(mac),
uploadBytes: '12',
downloadBytes: '34',
}]);
assert.equal(service.metricsSnapshot().directTraffic.uploadBytes, '56');
assert.equal(service.metricsSnapshot().directTraffic.downloadBytes, '78');
assert.equal(Object.hasOwn(service.snapshot().devices[0], 'directUploadBytes'), false);
assert.doesNotMatch(JSON.stringify(store.read()), /directUploadBytes|directDownloadBytes/);
fail = true;
await service.refresh();
assert.deepEqual(service.metricsSnapshot().directTraffic.series, [{
deviceId: deviceId(mac),
uploadBytes: '12',
downloadBytes: '34',
}]);
assert.equal(service.metricsSnapshot().directTraffic.source.error, 'Dataplane direct counters unavailable');
fail = false;
includeDirect = false;
await service.refresh();
assert.deepEqual(service.metricsSnapshot().directTraffic.series, []);
assert.equal(service.metricsSnapshot().directTraffic.source.error, null);
});
test('device tag catalog validates, orders and mutates assignments under one revision', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-tags-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
});
const observedAt = '2026-08-31T12:00:00.000Z';
const mac = '00:11:22:33:44:55';
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt,
error: null,
observations: [{ ip: '192.168.50.10', mac, interface: 'br0', observedAt, active: true }],
}),
now: () => new Date(observedAt),
});
let snapshot = await service.refresh();
snapshot = service.createTag(' Home ', snapshot.revision);
const home = snapshot.tags[0];
assert.match(home.id, /^tag_[a-f0-9]{16}$/);
assert.equal(home.name, 'Home');
assert.throws(
() => service.createTag('home', snapshot.revision),
(error) => error.code === 'DEVICE_TAG_NAME_CONFLICT',
);
assert.throws(
() => service.createTag('x'.repeat(25), snapshot.revision),
(error) => error.code === 'REQUEST_INVALID',
);
snapshot = service.createTag('Work', snapshot.revision);
const work = snapshot.tags[1];
snapshot = service.update(snapshot.devices[0].id, { tagIds: [work.id, home.id] }, snapshot.revision);
assert.deepEqual(snapshot.devices[0].tagIds, [home.id, work.id]);
assert.deepEqual(store.read().tags.byMac[mac], [home.id, work.id]);
assert.throws(
() => service.update(snapshot.devices[0].id, { tagIds: [home.id, work.id] }, snapshot.revision - 1),
(error) => error.code === 'STATE_CONFLICT',
);
assert.equal(
service.update(snapshot.devices[0].id, { tagIds: [home.id, work.id] }, snapshot.revision).revision,
snapshot.revision,
);
snapshot = service.renameTag(home.id, 'HOME', snapshot.revision);
const renamedRevision = snapshot.revision;
assert.equal(snapshot.tags[0].name, 'HOME');
assert.throws(
() => service.renameTag(home.id, 'HOME', renamedRevision - 1),
(error) => error.code === 'STATE_CONFLICT',
);
assert.equal(service.renameTag(home.id, 'HOME', renamedRevision).revision, renamedRevision);
assert.throws(
() => service.update(snapshot.devices[0].id, { tagIds: ['tag_ffffffffffffffff'] }, renamedRevision),
(error) => error.code === 'DEVICE_TAG_NOT_FOUND',
);
while (snapshot.tags.length < 9) snapshot = service.createTag(`Tag ${snapshot.tags.length}`, snapshot.revision);
snapshot = service.update(snapshot.devices[0].id, { tagIds: snapshot.tags.slice(0, 8).map(({ id }) => id) }, snapshot.revision);
assert.throws(
() => service.update(snapshot.devices[0].id, { tagIds: snapshot.tags.slice(0, 9).map(({ id }) => id) }, snapshot.revision),
(error) => error.code === 'REQUEST_INVALID',
);
snapshot = service.deleteTag(home.id, snapshot.revision);
assert.equal(snapshot.tags.some(({ id }) => id === home.id), false);
assert.equal(snapshot.devices[0].tagIds.includes(home.id), false);
while (snapshot.tags.length < 32) snapshot = service.createTag(`More ${snapshot.tags.length}`, snapshot.revision);
assert.throws(
() => service.createTag('Overflow', snapshot.revision),
(error) => error.code === 'REQUEST_INVALID',
);
});
test('tag migration recovers safe values and retention removes assignment but keeps catalog', async (t) => {
const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'harbor-device-tag-retention-'));
t.after(() => fs.rmSync(directory, { recursive: true, force: true }));
const store = createJsonStore({
filePath: path.join(directory, 'devices.json'),
defaultValue: {},
migrate: migrateDeviceInventoryState,
});
let current = new Date('2026-07-01T12:00:00.000Z');
const mac = '00:11:22:33:44:66';
let present = true;
const service = createDeviceInventoryService({
store,
observe: () => ({
observedAt: current.toISOString(),
error: null,
observations: present
? [{ ip: '192.168.50.12', mac, interface: 'br0', observedAt: current.toISOString(), active: true }]
: [],
}),
now: () => current,
});
let snapshot = await service.refresh();
snapshot = service.createTag('Дом', snapshot.revision);
const tag = snapshot.tags[0];
snapshot = service.update(snapshot.devices[0].id, { tagIds: [tag.id] }, snapshot.revision);
const recovered = migrateDeviceInventoryState({
...store.read(),
tags: {
schemaVersion: 1,
items: [tag, { ...tag, id: 'tag_ffffffffffffffff' }, { id: 'bad', name: 'Bad' }],
byMac: { [mac.toUpperCase()]: [tag.id, tag.id, 'tag_ffffffffffffffff'] },
},
});
assert.deepEqual(recovered.tags, {
schemaVersion: 1,
items: [tag],
byMac: { [mac]: [tag.id] },
});
assert.throws(
() => migrateDeviceInventoryState({ ...store.read(), tags: { schemaVersion: 2 } }),
/Unsupported device tags schemaVersion/,
);
present = false;
current = new Date('2026-08-02T12:00:00.000Z');
snapshot = await service.refresh();
assert.equal(snapshot.devices.length, 0);
assert.deepEqual(snapshot.tags, [tag]);
assert.deepEqual(store.read().tags.byMac, {});
present = true;
current = new Date('2026-08-03T12:00:00.000Z');
snapshot = await service.refresh();
assert.deepEqual(snapshot.devices[0].tagIds, []);
assert.deepEqual(snapshot.tags, [tag]);
});