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