import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const { siMock, readFileMock } = vi.hoisted(() => ({ siMock: { networkInterfaces: vi.fn(), networkStats: vi.fn(), networkGatewayDefault: vi.fn(), }, readFileMock: vi.fn(), })); vi.mock("systeminformation", () => ({ default: siMock })); vi.mock("node:fs/promises", () => ({ readFile: readFileMock })); vi.mock("@/lib/server/modules/system/update-service", () => ({ isContainerRuntime: () => false })); import { getNetworkOverview, parseNameservers } from "@/lib/server/modules/network/overview-service"; function iface(name: string, extra: Record = {}) { return { iface: name, internal: false, operstate: "up", type: "wired", ip4: "", mac: "", speed: -1, default: false, dhcp: false, ...extra }; } describe("network overview", () => { let minute = 0; beforeEach(() => { // A minute apart per test, so the service's 2 s cache never carries over. vi.useFakeTimers(); minute += 1; vi.setSystemTime(Date.UTC(2026, 8, 27, 12, minute)); siMock.networkInterfaces.mockReset(); siMock.networkStats.mockReset(); siMock.networkGatewayDefault.mockReset(); readFileMock.mockReset(); }); afterEach(() => { vi.useRealTimers(); }); it("lists real interfaces with their traffic, default first, and hides container plumbing", async () => { siMock.networkInterfaces.mockResolvedValue([ iface("lo", { internal: true }), iface("docker0"), iface("veth12ab"), iface("br-5f2c"), iface("wlan0", { type: "wireless", operstate: "down" }), iface("tailscale0", { type: "virtual", ip4: "100.113.171.79" }), iface("eth0", { default: true, ip4: "192.168.1.43", ip4subnet: "255.255.255.0", mac: "aa:bb", speed: 1000, duplex: "full", dhcp: true }), ]); siMock.networkStats.mockResolvedValue([ { iface: "eth0", rx_bytes: 5_000_000, tx_bytes: 1_000_000, rx_sec: 125_000, tx_sec: 12_500, rx_errors: 0, tx_errors: 2, rx_dropped: 0, tx_dropped: 0 }, { iface: "tailscale0", rx_bytes: 10, tx_bytes: 20, rx_sec: null, tx_sec: -1, rx_errors: 0, tx_errors: 0, rx_dropped: 0, tx_dropped: 0 }, ]); siMock.networkGatewayDefault.mockResolvedValue("192.168.1.1"); readFileMock.mockImplementation(async (file: string) => { if (file === "/run/systemd/resolve/resolv.conf") return "nameserver 1.1.1.1\nnameserver 9.9.9.9\n"; return "nameserver 127.0.0.53\n"; }); const overview = await getNetworkOverview(); expect(overview.interfaces.map((i) => [i.iface, i.kind, i.up])).toEqual([ ["eth0", "wired", true], ["tailscale0", "vpn", true], ["wlan0", "wireless", false], ]); const eth0 = overview.interfaces[0]; expect(eth0).toMatchObject({ isDefault: true, ip4: "192.168.1.43", speedMbps: 1000, dhcp: true, rxBytes: 5_000_000, downloadMbps: 1, uploadMbps: 0.1, txErrors: 2, }); expect(overview.interfaces[1]).toMatchObject({ downloadMbps: null, uploadMbps: null, ip4: "100.113.171.79" }); expect(overview.gateway).toBe("192.168.1.1"); expect(overview.inContainer).toBe(false); expect(overview.dnsServers).toEqual(["1.1.1.1", "9.9.9.9"]); }); it("falls back to /etc/resolv.conf and reports no gateway as null", async () => { siMock.networkInterfaces.mockResolvedValue([iface("en0", { default: true })]); siMock.networkStats.mockResolvedValue([]); siMock.networkGatewayDefault.mockResolvedValue(""); readFileMock.mockImplementation(async (file: string) => { if (file === "/run/systemd/resolve/resolv.conf") throw new Error("ENOENT"); return "# generated\nnameserver 192.168.1.1\n"; }); const overview = await getNetworkOverview(); expect(overview.gateway).toBeNull(); expect(overview.dnsServers).toEqual(["192.168.1.1"]); expect(overview.interfaces[0]).toMatchObject({ iface: "en0", rxBytes: null, downloadMbps: null }); }); it("reads nameserver lines once each", () => { expect(parseNameservers("search lan\nnameserver 1.1.1.1\n nameserver 1.1.1.1\nnameserver ::1\n")).toEqual([ "1.1.1.1", "::1", ]); }); });