Files
ctrld/cmd/cli/net_darwin_test.go
T

339 lines
10 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")
}
}
// Test_serviceNamesByDevice_namesNoDisabledService drives output that holds no
// blank line between the blocks. The device of a disabled service must keep no
// name, and the name of the service before it is the wrong answer.
func Test_serviceNamesByDevice_namesNoDisabledService(t *testing.T) {
const fixture = `An asterisk (*) denotes that a network service is disabled.
(1) Wi-Fi
(Hardware Port: Wi-Fi, Device: en0)
(2) Thunderbolt Bridge
(Hardware Port: Thunderbolt Bridge, Device: bridge0)
(*) Ethernet Adapter (en6)
(Hardware Port: Ethernet Adapter (en6), Device: en6)
`
names := serviceNamesByDevice(strings.NewReader(fixture))
if got := names["en6"]; got != "" {
t.Errorf("service of the disabled en6 = %q, want no name", got)
}
if got := names["bridge0"]; got != "Thunderbolt Bridge" {
t.Errorf("service of bridge0 = %q, want %q", got, "Thunderbolt Bridge")
}
}