Device info endpoint + richer node summaries (GET /api/device, MCP device_info) (#13)
CI / build + unit (push) Successful in 36s
CI / build + unit (push) Successful in 36s
This commit was merged in pull request #13.
This commit is contained in:
@@ -4,7 +4,10 @@ import { describe, expect, it } from 'vitest';
|
||||
import { BROADCAST_ADDR, extractFrames, frame } from './codec.js';
|
||||
import {
|
||||
KAMIL_NUM,
|
||||
configSectionFrame,
|
||||
kamilNodeInfo,
|
||||
metadataFrame,
|
||||
moduleConfigSectionFrame,
|
||||
nodeInfoFrame,
|
||||
startBridge,
|
||||
waitFor,
|
||||
@@ -614,3 +617,143 @@ describe('MeshService resolveDest', () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('MeshService device info', () => {
|
||||
it('captures DeviceMetadata and exposes firmwareVersion', async () => {
|
||||
const bridge = await startBridge();
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
await waitFor(
|
||||
() => bridge.sockets.length === 1 && svc.status().connected,
|
||||
);
|
||||
|
||||
// before the sync: only the node number (null until myInfo arrives)
|
||||
expect(svc.deviceInfo()).toEqual({
|
||||
myNodeNum: null,
|
||||
localConfig: {},
|
||||
moduleConfig: {},
|
||||
});
|
||||
|
||||
bridge.sockets[0].write(
|
||||
metadataFrame({ firmwareVersion: '2.7.0.1', hasWifi: false, role: 3 }),
|
||||
);
|
||||
|
||||
await waitFor(() => svc.deviceInfo().firmwareVersion === '2.7.0.1');
|
||||
const info = svc.deviceInfo();
|
||||
expect(info.firmwareVersion).toBe('2.7.0.1');
|
||||
expect(info.metadata?.firmwareVersion).toBe('2.7.0.1'); // metadata stored as-is
|
||||
} finally {
|
||||
svc.onModuleDestroy();
|
||||
bridge.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('captures config/module-config sections and redacts key material', async () => {
|
||||
const bridge = await startBridge();
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
await waitFor(
|
||||
() => bridge.sockets.length === 1 && svc.status().connected,
|
||||
);
|
||||
|
||||
const secret = 'super-secret-wifi-psk';
|
||||
bridge.sockets[0].write(
|
||||
configSectionFrame('security', {
|
||||
publicKey: new Uint8Array([1, 2, 3]),
|
||||
privateKey: new Uint8Array([4, 5, 6]),
|
||||
adminKey: [new Uint8Array([7])],
|
||||
isManaged: true,
|
||||
}),
|
||||
);
|
||||
bridge.sockets[0].write(
|
||||
configSectionFrame('network', {
|
||||
wifiEnabled: false,
|
||||
wifiSsid: 'home',
|
||||
wifiPsk: secret,
|
||||
}),
|
||||
);
|
||||
bridge.sockets[0].write(configSectionFrame('device', { role: 3 }));
|
||||
bridge.sockets[0].write(
|
||||
moduleConfigSectionFrame('telemetry', {
|
||||
environmentUpdateInterval: 900,
|
||||
powerMeasurementEnabled: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(
|
||||
() =>
|
||||
Object.keys(svc.deviceInfo().localConfig).length === 3 &&
|
||||
Object.keys(svc.deviceInfo().moduleConfig).length === 1,
|
||||
);
|
||||
const info = svc.deviceInfo();
|
||||
expect(Object.keys(info.localConfig).sort()).toEqual([
|
||||
'device',
|
||||
'network',
|
||||
'security',
|
||||
]);
|
||||
expect(info.moduleConfig.telemetry).toMatchObject({
|
||||
environmentUpdateInterval: 900,
|
||||
powerMeasurementEnabled: true,
|
||||
});
|
||||
|
||||
// key material is replaced with a marker — never exposed
|
||||
expect(info.localConfig.security).toMatchObject({
|
||||
publicKey: 'REDACTED',
|
||||
privateKey: 'REDACTED',
|
||||
adminKey: 'REDACTED',
|
||||
isManaged: true, // non-secret fields pass through
|
||||
});
|
||||
expect(info.localConfig.network?.wifiPsk).toBe('REDACTED');
|
||||
const json = JSON.stringify(info);
|
||||
expect(json).not.toContain(secret);
|
||||
} finally {
|
||||
svc.onModuleDestroy();
|
||||
bridge.close();
|
||||
}
|
||||
});
|
||||
|
||||
it('summarizes NodeInfo snr/hopsAway/viaMqtt when present', async () => {
|
||||
const bridge = await startBridge();
|
||||
process.env.MESH_HOST = '127.0.0.1';
|
||||
process.env.MESH_PORT = String(bridge.port);
|
||||
const svc = new MeshService();
|
||||
try {
|
||||
svc.onModuleInit();
|
||||
await waitFor(
|
||||
() => bridge.sockets.length === 1 && svc.status().connected,
|
||||
);
|
||||
|
||||
bridge.sockets[0].write(nodeInfoFrame(KAMIL_NUM, kamilNodeInfo()));
|
||||
bridge.sockets[0].write(
|
||||
nodeInfoFrame(42, {
|
||||
user: { id: '!ot', longName: 'Other Node' },
|
||||
snr: -2.5,
|
||||
hopsAway: 1,
|
||||
viaMqtt: true,
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(async () => (await svc.nodes()).length === 2);
|
||||
const nodes = await svc.nodes();
|
||||
expect(nodes.find((n) => n.num === 42)).toEqual({
|
||||
num: 42,
|
||||
longName: 'Other Node',
|
||||
lastHeard: null,
|
||||
snr: -2.5,
|
||||
hopsAway: 1,
|
||||
viaMqtt: true,
|
||||
});
|
||||
// absent in the proto → omitted from the summary
|
||||
expect(nodes.find((n) => n.num === KAMIL_NUM)).not.toHaveProperty('snr');
|
||||
} finally {
|
||||
svc.onModuleDestroy();
|
||||
bridge.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user