mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-10-12 01:58:46 +02:00
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.
316 lines
9.5 KiB
Go
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")
|
|
}
|
|
}
|