Add live hotspot network diagnostics

This commit is contained in:
2026-07-31 21:01:02 +02:00
parent d1239d66af
commit 37e2cb154e
19 changed files with 490 additions and 11 deletions
+6 -1
View File
@@ -21,7 +21,8 @@ function testConfig(): AppConfig {
enabled: false,
statusPath: `/tmp/pi-car-companion-test-${process.pid}-missing-network-status.json`,
commandPath: `/tmp/pi-car-companion-test-${process.pid}-network-command.json`,
configurationRequestPath: `/tmp/pi-car-companion-test-${process.pid}-network-configuration.json`
configurationRequestPath: `/tmp/pi-car-companion-test-${process.pid}-network-configuration.json`,
activityPath: `/tmp/pi-car-companion-test-${process.pid}-network-activity.json`
},
adb: { enabled: false, executable: "adb", serial: null, timeoutMs: 3_000 }
};
@@ -189,6 +190,7 @@ describe("authentication boundary", () => {
it("keeps network status and controls behind authentication and explicit configuration", async () => {
const app = await createApp();
expect((await app.inject({ method: "GET", url: "/api/network" })).statusCode).toBe(401);
expect((await app.inject({ method: "GET", url: "/api/network/activity" })).statusCode).toBe(401);
const token = await csrf(app);
await setup(app, token);
@@ -203,6 +205,9 @@ describe("authentication boundary", () => {
const status = await app.inject({ method: "GET", url: "/api/network", headers: { cookie } });
expect(status.statusCode).toBe(200);
expect(status.json()).toMatchObject({ supported: false, enabled: false, mode: "disabled" });
const activity = await app.inject({ method: "GET", url: "/api/network/activity", headers: { cookie } });
expect(activity.statusCode).toBe(200);
expect(activity.json()).toMatchObject({ supported: false, clients: [], dnsQueries: [], flows: [] });
const action = await app.inject({
method: "POST",
+2 -1
View File
@@ -27,7 +27,8 @@ describe("ADB configuration", () => {
enabled: false,
statusPath: "/var/lib/pi-car-companion/network-status.json",
commandPath: "/var/lib/pi-car-companion/network-command.json",
configurationRequestPath: "/var/lib/pi-car-companion/network-configuration-request.json"
configurationRequestPath: "/var/lib/pi-car-companion/network-configuration-request.json",
activityPath: "/var/lib/pi-car-companion/network-activity.json"
});
});
});
+37
View File
@@ -0,0 +1,37 @@
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { readNetworkActivity } from "../src/network-activity.js";
const directories: string[] = [];
afterEach(async () => {
await Promise.all(directories.splice(0).map((directory) => rm(directory, { recursive: true, force: true })));
});
describe("network activity snapshot", () => {
it("accepts only bounded sanitized observer output", async () => {
const directory = await mkdtemp(join(tmpdir(), "pi-car-activity-test-"));
directories.push(directory);
const path = join(directory, "activity.json");
await writeFile(path, JSON.stringify({
collectedAt: "2026-07-31T20:00:00.000Z",
clients: [{ ipAddress: "10.42.0.20", macAddress: "aa:bb:cc:dd:ee:ff", hostname: "head-unit", leaseExpiresAt: null, state: "REACHABLE", connected: true }],
dnsQueries: [{ timestamp: "2026-07-31T20:00:00.000Z", clientAddress: "10.42.0.20", type: "A", name: "example.com" }],
flows: [{ protocol: "tcp", state: "ESTABLISHED", sourceAddress: "10.42.0.20", sourcePort: 50000, destinationAddress: "1.1.1.1", destinationPort: 443, packets: 4, bytes: 1000 }],
dnsAvailable: true,
flowsAvailable: true,
messages: []
}));
await expect(readNetworkActivity(path)).resolves.toMatchObject({ supported: true, clients: [{ hostname: "head-unit" }] });
});
it("does not expose malformed observer files", async () => {
const directory = await mkdtemp(join(tmpdir(), "pi-car-activity-test-"));
directories.push(directory);
const path = join(directory, "activity.json");
await writeFile(path, '{"clients":"not-an-array"}');
await expect(readNetworkActivity(path)).resolves.toMatchObject({ supported: false, clients: [] });
});
});
+2 -1
View File
@@ -29,7 +29,8 @@ function config(directory: string, enabled = true): AppConfig {
enabled,
statusPath: join(directory, "status.json"),
commandPath: join(directory, "command.json"),
configurationRequestPath: join(directory, "configuration.json")
configurationRequestPath: join(directory, "configuration.json"),
activityPath: join(directory, "activity.json")
},
adb: { enabled: false, executable: "adb", serial: null, timeoutMs: 3_000 }
};
+2 -1
View File
@@ -22,7 +22,8 @@ function config(updateStatusPath: string, versionFile: string): AppConfig {
enabled: false,
statusPath: join(tmpdir(), "missing-network-status.json"),
commandPath: join(tmpdir(), "network-command.json"),
configurationRequestPath: join(tmpdir(), "network-configuration.json")
configurationRequestPath: join(tmpdir(), "network-configuration.json"),
activityPath: join(tmpdir(), "network-activity.json")
},
adb: { enabled: false, executable: "adb", serial: null, timeoutMs: 3_000 }
};