Files
ctrld/cmd/cli/net_darwin_test.go
T
Anthony Wong 8ae09ac98e feat(cli): put the network state of the host in the log journal
The journal records the network state of the host and each change of
that state. The support team reads one file to see the interfaces, the
routes, the resolvers, and the recoveries around an incident. The same
file shows the health of the query path. A change that touches only
AirDrop or virtual interfaces runs no DNS work.
2026-09-28 16:49:05 +07:00

316 lines
9.5 KiB
Go

package cli
import (
"maps"
"strings"
"testing"
"time"
)
const listAllHardwarePortsFixture = `
Hardware Port: Wi-Fi
Device: en0
Ethernet Address: a0:78:17:68:56:3f
Hardware Port: iPhone USB
Device: en7
Ethernet Address: 6e:5a:6b:12:34:56
Hardware Port: Thunderbolt Bridge
Device: bridge0
Ethernet Address: 36:21:bb:3a:7a:40
Hardware Port: Ethernet Adapter (en6)
Device: en6
Ethernet Address: 3a:3e:fc:1e:ab:41
Hardware Port: com.wireguard.macos
Device:
VLAN Configurations
===================
`
const listNetworkServiceOrderFixture = `An asterisk (*) denotes that a network service is disabled.
(1) Wi-Fi
(Hardware Port: Wi-Fi, Device: en0)
(2) iPhone USB
(Hardware Port: iPhone USB, Device: en7)
(3) Thunderbolt Bridge
(Hardware Port: Thunderbolt Bridge, Device: bridge0)
(*) Ethernet Adapter (en6)
(Hardware Port: Ethernet Adapter (en6), Device: en6)
(4) ca-001-stg
(Hardware Port: com.wireguard.macos, Device: )
`
func Test_parseHardwarePorts(t *testing.T) {
ports := parseHardwarePorts(strings.NewReader(listAllHardwarePortsFixture))
addServiceNames(ports, strings.NewReader(listNetworkServiceOrderFixture))
tests := []struct {
device string
wantPort string
wantService string
}{
{"en0", "Wi-Fi", "Wi-Fi"},
{"en7", "iPhone USB", "iPhone USB"},
{"bridge0", "Thunderbolt Bridge", "Thunderbolt Bridge"},
{"en6", "Ethernet Adapter (en6)", ""},
}
if len(ports) != len(tests) {
t.Fatalf("parseHardwarePorts returned %d devices, want %d: %v", len(ports), len(tests), ports)
}
for _, tc := range tests {
t.Run(tc.device, func(t *testing.T) {
info := ports[tc.device]
if info.Port != tc.wantPort {
t.Errorf("port of %s = %q, want %q", tc.device, info.Port, tc.wantPort)
}
if info.Service != tc.wantService {
t.Errorf("service of %s = %q, want %q", tc.device, info.Service, tc.wantService)
}
})
}
}
func Test_networkServiceName(t *testing.T) {
tests := []struct {
device string
want string
}{
{"en0", "Wi-Fi"},
{"en7", "iPhone USB"},
{"bridge0", "Thunderbolt Bridge"},
{"en6", ""},
{"en9", ""},
}
for _, tc := range tests {
t.Run(tc.device, func(t *testing.T) {
got := networkServiceName(tc.device, strings.NewReader(listNetworkServiceOrderFixture))
if got != tc.want {
t.Errorf("networkServiceName(%q) = %q, want %q", tc.device, got, tc.want)
}
})
}
}
func Test_parseHardwarePorts_dropsATruncatedRead(t *testing.T) {
longLine := strings.Repeat("x", maxNetworksetupLine+1)
if got := parseHardwarePorts(strings.NewReader(listAllHardwarePortsFixture + longLine)); got != nil {
t.Errorf("parseHardwarePorts of a truncated read = %v, want nil", got)
}
if got := serviceNamesByDevice(strings.NewReader(listNetworkServiceOrderFixture + longLine)); got != nil {
t.Errorf("serviceNamesByDevice of a truncated read = %v, want nil", got)
}
}
func Test_readBoundedOutput(t *testing.T) {
out, err := readBoundedOutput(strings.NewReader(listAllHardwarePortsFixture))
if err != nil || string(out) != listAllHardwarePortsFixture {
t.Fatalf("readBoundedOutput of the fixture = %q, %v, want the fixture and no error", out, err)
}
if _, err := readBoundedOutput(strings.NewReader(strings.Repeat("x", maxNetworksetupOutput+1))); err == nil {
t.Fatal("readBoundedOutput of an output above the bound gave no error")
}
}
// stubHardwarePorts replaces the command and clock seams of the hardware port
// cache, and restores them with the cache itself.
func stubHardwarePorts(t *testing.T, read func() map[string]hardwarePortInfo, now func() time.Time) {
t.Helper()
readBefore, nowBefore := readHardwarePortsFn, hardwarePortsNowFn
hardwarePortsMu.Lock()
cacheBefore, readAtBefore := hardwarePortsCache, hardwarePortsReadAt
hardwarePortsCache, hardwarePortsReadAt = nil, time.Time{}
hardwarePortsMu.Unlock()
t.Cleanup(func() {
readHardwarePortsFn, hardwarePortsNowFn = readBefore, nowBefore
hardwarePortsMu.Lock()
hardwarePortsCache, hardwarePortsReadAt = cacheBefore, readAtBefore
hardwarePortsMu.Unlock()
})
readHardwarePortsFn, hardwarePortsNowFn = read, now
}
func Test_hardwarePortsByDevice_caches(t *testing.T) {
now := time.Unix(1700000000, 0)
reads := 0
stubHardwarePorts(t, func() map[string]hardwarePortInfo {
reads++
return map[string]hardwarePortInfo{"en0": {Port: "Wi-Fi", Service: "Wi-Fi"}}
}, func() time.Time { return now })
if got := hardwarePortsByDevice()["en0"].Port; got != "Wi-Fi" {
t.Fatalf("first read port = %q, want Wi-Fi", got)
}
now = now.Add(59 * time.Second)
hardwarePortsByDevice()
if reads != 1 {
t.Fatalf("reads within the cache window = %d, want 1", reads)
}
now = now.Add(2 * time.Second)
hardwarePortsByDevice()
if reads != 2 {
t.Fatalf("reads after the cache window = %d, want 2", reads)
}
}
func Test_hardwarePortsByDevice_keepsLastGoodRead(t *testing.T) {
now := time.Unix(1700000000, 0)
fail := false
stubHardwarePorts(t, func() map[string]hardwarePortInfo {
if fail {
return nil
}
return map[string]hardwarePortInfo{"en0": {Port: "Wi-Fi"}}
}, func() time.Time { return now })
hardwarePortsByDevice()
fail = true
now = now.Add(2 * time.Minute)
if got := hardwarePortsByDevice()["en0"].Port; got != "Wi-Fi" {
t.Fatalf("port after a failed read = %q, want Wi-Fi", got)
}
}
func Test_hardwarePortsByDevice_keepsTheWindowAfterAFailedRead(t *testing.T) {
now := time.Unix(1700000000, 0)
reads := 0
stubHardwarePorts(t, func() map[string]hardwarePortInfo {
reads++
return nil
}, func() time.Time { return now })
hardwarePortsByDevice()
now = now.Add(time.Second)
hardwarePortsByDevice()
if reads != 1 {
t.Fatalf("reads of a failing command within the window = %d, want 1", reads)
}
now = now.Add(2 * time.Minute)
hardwarePortsByDevice()
if reads != 2 {
t.Fatalf("reads after the window = %d, want 2", reads)
}
}
func Test_refreshInterfaceMeta_dropsTheReadWindow(t *testing.T) {
now := time.Unix(1700000000, 0)
reads := 0
stubHardwarePorts(t, func() map[string]hardwarePortInfo {
reads++
return map[string]hardwarePortInfo{"en0": {Port: "Wi-Fi"}}
}, func() time.Time { return now })
hardwarePortsByDevice()
hardwarePortsByDevice()
if reads != 1 {
t.Fatalf("reads within the window = %d, want 1", reads)
}
refreshInterfaceMeta()
if got := hardwarePortsByDevice()["en0"].Port; got != "Wi-Fi" {
t.Fatalf("port after a refresh = %q, want Wi-Fi", got)
}
if reads != 2 {
t.Fatalf("reads after a refresh = %d, want 2", reads)
}
}
func Test_hardwarePortsByDevice_readsOutsideTheLock(t *testing.T) {
now := time.Unix(1700000000, 0)
stubHardwarePorts(t, func() map[string]hardwarePortInfo {
// A command runs outside the mutex, so this read can take it.
if !hardwarePortsMu.TryLock() {
t.Error("the command ran under the hardware ports mutex")
} else {
hardwarePortsMu.Unlock()
}
return map[string]hardwarePortInfo{"en0": {Port: "Wi-Fi"}}
}, func() time.Time { return now })
if got := hardwarePortsByDevice()["en0"].Port; got != "Wi-Fi" {
t.Fatalf("port = %q, want Wi-Fi", got)
}
}
// fixtureHardwarePorts returns the port map of the two command fixtures.
func fixtureHardwarePorts() map[string]hardwarePortInfo {
ports := parseHardwarePorts(strings.NewReader(listAllHardwarePortsFixture))
addServiceNames(ports, strings.NewReader(listNetworkServiceOrderFixture))
return ports
}
func Test_validInterfacesMap_comesFromThePortCache(t *testing.T) {
now := time.Unix(1700000000, 0)
reads := 0
stubHardwarePorts(t, func() map[string]hardwarePortInfo {
reads++
return fixtureHardwarePorts()
}, func() time.Time { return now })
want := map[string]struct{}{"en0": {}, "en7": {}, "bridge0": {}, "en6": {}}
if got := validInterfacesMap(); !maps.Equal(got, want) {
t.Fatalf("validInterfacesMap = %v, want %v", got, want)
}
validInterfacesMap()
hardwarePortsByDevice()
if reads != 1 {
t.Fatalf("reads for two maps and one lookup = %d, want 1", reads)
}
}
func Test_validInterfacesMap_namesNoInterfaceWithoutARead(t *testing.T) {
now := time.Unix(1700000000, 0)
stubHardwarePorts(t, func() map[string]hardwarePortInfo { return nil }, func() time.Time { return now })
if got := validInterfacesMap(); len(got) != 0 {
t.Fatalf("validInterfacesMap after a failed read = %v, want an empty set", got)
}
}
// Test_transitionTestGlobalsReadNoPorts counts the port reads of the tests
// that take the transition globals. A test must read the ports of no host.
func Test_transitionTestGlobalsReadNoPorts(t *testing.T) {
now := time.Unix(1700000000, 0)
reads := 0
stubHardwarePorts(t, func() map[string]hardwarePortInfo {
reads++
return fixtureHardwarePorts()
}, func() time.Time { return now })
TestNetworkChangeDoesNotRestoreDownDefaultRouteSources(t)
TestNetworkChangeSourceMovedToUpInterface(t)
if reads != 0 {
t.Fatalf("port reads of two transition tests = %d, want 0", reads)
}
}
func Test_platformInterfaceMeta(t *testing.T) {
now := time.Unix(1700000000, 0)
stubHardwarePorts(t, func() map[string]hardwarePortInfo {
return map[string]hardwarePortInfo{"en7": {Port: "iPhone USB", Service: "iPhone USB"}}
}, func() time.Time { return now })
class, hardwarePort, service := platformInterfaceMeta("en7")
if class != "" {
t.Errorf("class = %q, want empty", class)
}
if hardwarePort != "iPhone USB" || service != "iPhone USB" {
t.Errorf("port, service = %q, %q, want iPhone USB twice", hardwarePort, service)
}
if class, hardwarePort, service = platformInterfaceMeta("utun3"); class != "" || hardwarePort != "" || service != "" {
t.Errorf("unknown device gave %q, %q, %q, want empty strings", class, hardwarePort, service)
}
if platformVirtualInterfaces() != nil {
t.Error("platformVirtualInterfaces is not nil on darwin")
}
}