go-network-configurator/options_test.go
2026-07-01 15:17:11 -05:00

225 lines
7.4 KiB
Go

package netconfig
import (
"context"
"errors"
"net"
"os"
"path/filepath"
"testing"
"time"
)
// captureLogger records the last formatted message so SetLogger can be
// verified.
type captureLogger struct {
printfCalls int
printlnCalls int
}
func (c *captureLogger) Printf(format string, args ...interface{}) { c.printfCalls++ }
func (c *captureLogger) Println(args ...interface{}) { c.printlnCalls++ }
func TestConfigOptionsDefaults(t *testing.T) {
o := newConfigOptions()
if o.testAddress != defaultInternetTestAddress {
t.Errorf("default testAddress = %q, want %q", o.testAddress, defaultInternetTestAddress)
}
if o.skipConnectivityCheck {
t.Error("skipConnectivityCheck = true, want false by default")
}
if o.connectivityTimeout != defaultConnectivityTimeout {
t.Errorf("default connectivityTimeout = %v, want %v", o.connectivityTimeout, defaultConnectivityTimeout)
}
if o.pingCount != defaultPingCount {
t.Errorf("default pingCount = %d, want %d", o.pingCount, defaultPingCount)
}
if o.pingTimeout != defaultPingTimeout {
t.Errorf("default pingTimeout = %v, want %v", o.pingTimeout, defaultPingTimeout)
}
if o.backupRetention != defaultBackupRetention {
t.Errorf("default backupRetention = %d, want %d", o.backupRetention, defaultBackupRetention)
}
if o.allowPrimaryRemoval {
t.Error("allowPrimaryRemoval = true, want false by default")
}
if o.skipPanels {
t.Error("skipPanels = true, want false by default")
}
}
func TestWithTestAddress(t *testing.T) {
o := newConfigOptions(WithTestAddress("http://canary.example/health"))
if o.testAddress != "http://canary.example/health" {
t.Errorf("testAddress = %q, want the overridden value", o.testAddress)
}
// An empty address must not clobber the default.
o = newConfigOptions(WithTestAddress(""))
if o.testAddress != defaultInternetTestAddress {
t.Errorf("empty WithTestAddress changed default to %q", o.testAddress)
}
}
func TestWithConnectivityCheck(t *testing.T) {
if o := newConfigOptions(WithConnectivityCheck(false)); !o.skipConnectivityCheck {
t.Error("WithConnectivityCheck(false) did not set skipConnectivityCheck")
}
if o := newConfigOptions(WithConnectivityCheck(true)); o.skipConnectivityCheck {
t.Error("WithConnectivityCheck(true) set skipConnectivityCheck")
}
}
func TestWithSkipConnectivityCheck(t *testing.T) {
if o := newConfigOptions(WithSkipConnectivityCheck()); !o.skipConnectivityCheck {
t.Error("WithSkipConnectivityCheck did not set skipConnectivityCheck")
}
}
func TestSetLogger(t *testing.T) {
prev := logger
defer func() { logger = prev }()
cap := &captureLogger{}
SetLogger(cap)
if logger != cap {
t.Fatal("SetLogger did not replace the package logger")
}
logger.Printf("hi %d", 1)
logger.Println("hi")
if cap.printfCalls != 1 || cap.printlnCalls != 1 {
t.Errorf("logger not used: printf=%d println=%d", cap.printfCalls, cap.printlnCalls)
}
// A nil logger must be ignored, leaving the current logger in place.
SetLogger(nil)
if logger != cap {
t.Error("SetLogger(nil) replaced the logger")
}
}
func TestWithConnectivityTimeout(t *testing.T) {
if o := newConfigOptions(WithConnectivityTimeout(5 * time.Second)); o.connectivityTimeout != 5*time.Second {
t.Errorf("connectivityTimeout = %v, want 5s", o.connectivityTimeout)
}
// Non-positive values must not clobber the default.
if o := newConfigOptions(WithConnectivityTimeout(0)); o.connectivityTimeout != defaultConnectivityTimeout {
t.Errorf("zero WithConnectivityTimeout changed default to %v", o.connectivityTimeout)
}
}
func TestWithPingCount(t *testing.T) {
if o := newConfigOptions(WithPingCount(3)); o.pingCount != 3 {
t.Errorf("pingCount = %d, want 3", o.pingCount)
}
// Non-positive values must not clobber the default.
if o := newConfigOptions(WithPingCount(0)); o.pingCount != defaultPingCount {
t.Errorf("zero WithPingCount changed default to %d", o.pingCount)
}
}
func TestWithPingTimeout(t *testing.T) {
if o := newConfigOptions(WithPingTimeout(30 * time.Second)); o.pingTimeout != 30*time.Second {
t.Errorf("pingTimeout = %v, want 30s", o.pingTimeout)
}
if o := newConfigOptions(WithPingTimeout(0)); o.pingTimeout != defaultPingTimeout {
t.Errorf("zero WithPingTimeout changed default to %v", o.pingTimeout)
}
}
func TestWithBackupRetention(t *testing.T) {
if o := newConfigOptions(WithBackupRetention(10)); o.backupRetention != 10 {
t.Errorf("backupRetention = %d, want 10", o.backupRetention)
}
// Zero/negative disables pruning (keeps all backups).
if o := newConfigOptions(WithBackupRetention(0)); o.backupRetention != 0 {
t.Errorf("backupRetention = %d, want 0", o.backupRetention)
}
}
func TestWithAllowPrimaryRemoval(t *testing.T) {
if o := newConfigOptions(WithAllowPrimaryRemoval(true)); !o.allowPrimaryRemoval {
t.Error("WithAllowPrimaryRemoval(true) did not set allowPrimaryRemoval")
}
}
func TestWithSkipPanels(t *testing.T) {
if o := newConfigOptions(WithSkipPanels(true)); !o.skipPanels {
t.Error("WithSkipPanels(true) did not set skipPanels")
}
if o := newConfigOptions(WithSkipPanels(false)); o.skipPanels {
t.Error("WithSkipPanels(false) set skipPanels")
}
}
// applyIfaceDNS must invoke every backend even when an earlier one fails.
func TestApplyIfaceDNS(t *testing.T) {
failing := &fakeIfaceBackend{err: errors.New("boom")}
ok := &fakeIfaceBackend{}
backends := []namedIfaceBackend{
{name: "failing", backend: failing},
{name: "ok", backend: ok},
}
applyIfaceDNS(context.Background(), backends, "eth0", []net.IP{net.ParseIP("8.8.8.8")}, []string{"example.com"})
if failing.dnsCalls != 1 || ok.dnsCalls != 1 {
t.Errorf("dns calls: failing=%d ok=%d, want 1 each", failing.dnsCalls, ok.dnsCalls)
}
}
func TestIPStrings(t *testing.T) {
got := ipStrings([]net.IP{net.ParseIP("8.8.8.8"), nil, net.ParseIP("2001:4860:4860::8888")})
want := []string{"8.8.8.8", "2001:4860:4860::8888"}
if len(got) != len(want) {
t.Fatalf("ipStrings = %v, want %v", got, want)
}
for i := range want {
if got[i] != want[i] {
t.Errorf("ipStrings[%d] = %q, want %q", i, got[i], want[i])
}
}
}
// TestNetplanDNS round-trips DNS through the netplan backend: SetIfaceDNS writes
// the nameservers block and GetInterfaces parses it back.
func TestNetplanDNS(t *testing.T) {
tmpDir := t.TempDir()
config := "network:\n version: 2\n ethernets:\n eth0:\n addresses: [192.0.2.10/24]\n"
if err := os.WriteFile(filepath.Join(tmpDir, "01-test.yaml"), []byte(config), 0644); err != nil {
t.Fatal(err)
}
np, err := readNetplanConfigDirectory(tmpDir)
if err != nil {
t.Fatal(err)
}
servers := []net.IP{net.ParseIP("8.8.8.8"), net.ParseIP("2001:4860:4860::8888")}
if err := np.SetIfaceDNS(context.Background(), "eth0", servers, []string{"example.com", "corp.example"}); err != nil {
t.Fatal(err)
}
// Re-read from disk so we exercise the written config, not in-memory state.
np, err = readNetplanConfigDirectory(tmpDir)
if err != nil {
t.Fatal(err)
}
ifaces, err := np.GetInterfaces()
if err != nil {
t.Fatal(err)
}
var eth0 *Interface
for _, i := range ifaces {
if i.Name == "eth0" {
eth0 = i
}
}
if eth0 == nil {
t.Fatal("eth0 not found after SetIfaceDNS")
}
if len(eth0.DNS) != 2 || !eth0.DNS[0].Equal(servers[0]) || !eth0.DNS[1].Equal(servers[1]) {
t.Errorf("DNS = %v, want %v", eth0.DNS, servers)
}
if len(eth0.SearchDomains) != 2 || eth0.SearchDomains[0] != "example.com" || eth0.SearchDomains[1] != "corp.example" {
t.Errorf("SearchDomains = %v, want [example.com corp.example]", eth0.SearchDomains)
}
}