Add Gateway device inventory panel
This commit is contained in:
@@ -20,11 +20,15 @@ test('control uses the dataplane socket protocol', async () => {
|
||||
assert.equal(status.gatewayBackendVersion, '0.1.0');
|
||||
assert.equal(status.singBoxVersion, '1.12.13');
|
||||
await client.apply();
|
||||
const devices = await client.observeDevices();
|
||||
assert.equal(devices.running, true);
|
||||
assert.equal(client.running, true);
|
||||
await client.restart();
|
||||
assert.equal((await client.stop()).running, false);
|
||||
assert.deepEqual(requests, [
|
||||
'GET /status /run/dataplane.sock',
|
||||
'POST /apply /run/dataplane.sock',
|
||||
'GET /devices /run/dataplane.sock',
|
||||
'POST /restart /run/dataplane.sock',
|
||||
'POST /stop /run/dataplane.sock',
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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 '../../src/server/adapters/neighbors.js';
|
||||
import {
|
||||
createDeviceInventoryService,
|
||||
createVendorLookup,
|
||||
} from '../../src/server/services/deviceInventoryService.js';
|
||||
import { createJsonStore } from '../../src/server/services/stateStore.js';
|
||||
|
||||
test('device inventory discovers, merges, persists metadata and expires anonymous devices', 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: '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,
|
||||
};
|
||||
const service = createDeviceInventoryService({
|
||||
store,
|
||||
observe: async () => observation,
|
||||
vendor,
|
||||
now: () => current,
|
||||
});
|
||||
|
||||
let snapshot = await service.refresh();
|
||||
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');
|
||||
|
||||
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 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',
|
||||
);
|
||||
|
||||
observation = { observedAt: current.toISOString(), observations: [], error: 'source unavailable' };
|
||||
snapshot = await restarted.refresh();
|
||||
assert.equal(snapshot.devices.length, 1);
|
||||
assert.equal(snapshot.source.error, 'source unavailable');
|
||||
|
||||
snapshot = restarted.update(snapshot.devices[0].id, { alias: '', pinned: false }, snapshot.revision);
|
||||
current = new Date('2026-08-02T10:00:00.000Z');
|
||||
observation = { observedAt: current.toISOString(), observations: [], error: null };
|
||||
snapshot = await restarted.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/);
|
||||
});
|
||||
Reference in New Issue
Block a user