package pxe import ( "net" "os" "path/filepath" "strings" "testing" ) // existingInterface returns any real interface on the host so the // Validate tests can exercise the happy path without hardcoding // "lo" (which exists on Linux but might be gated elsewhere). func existingInterface(t *testing.T) string { t.Helper() ifaces, err := net.Interfaces() if err != nil || len(ifaces) == 0 { t.Skipf("no network interfaces: %v", err) } return ifaces[0].Name } // seedTFTP drops zero-byte ipxe.efi + undionly.kpxe into dir so the // stat check passes. Callers can omit a name to simulate "missing". func seedTFTP(t *testing.T, dir string, names ...string) { t.Helper() if err := os.MkdirAll(dir, 0o755); err != nil { t.Fatalf("mkdir tftp: %v", err) } for _, name := range names { if err := os.WriteFile(filepath.Join(dir, name), nil, 0o644); err != nil { t.Fatalf("seed %s: %v", name, err) } } } func goodCfg(t *testing.T, tftpRoot string) SupervisorConfig { t.Helper() return SupervisorConfig{ Enabled: true, Interface: existingInterface(t), Subnet: "192.168.1.0/24", OrchestratorURL: "http://192.168.1.2:8080", TFTPRoot: tftpRoot, } } func TestValidate_DisabledSkipsChecks(t *testing.T) { s := NewSupervisor(SupervisorConfig{Enabled: false}) if err := s.Validate(); err != nil { t.Fatalf("disabled supervisor should skip validation, got: %v", err) } } func TestValidate_HappyPath(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") s := NewSupervisor(goodCfg(t, tftp)) if err := s.Validate(); err != nil { t.Fatalf("happy-path validate: %v", err) } } func TestValidate_MissingIPXEBinary(t *testing.T) { tftp := t.TempDir() // Only seed one of the two required files. seedTFTP(t, tftp, "undionly.kpxe") s := NewSupervisor(goodCfg(t, tftp)) err := s.Validate() if err == nil { t.Fatalf("expected error for missing ipxe.efi") } if !strings.Contains(err.Error(), "ipxe.efi") { t.Fatalf("error should name the missing file, got: %v", err) } if !strings.Contains(err.Error(), "pxe-setup.sh") { t.Fatalf("error should point operator at pxe-setup.sh, got: %v", err) } } func TestValidate_MissingUndionly(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi") s := NewSupervisor(goodCfg(t, tftp)) err := s.Validate() if err == nil || !strings.Contains(err.Error(), "undionly.kpxe") { t.Fatalf("expected undionly.kpxe error, got: %v", err) } } func TestValidate_MissingInterface(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") cfg := goodCfg(t, tftp) cfg.Interface = "definitely-not-a-real-iface-9999" s := NewSupervisor(cfg) err := s.Validate() if err == nil || !strings.Contains(err.Error(), "pxe.interface") { t.Fatalf("expected interface error, got: %v", err) } } func TestValidate_MissingLiveImage(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") cfg := goodCfg(t, tftp) cfg.LiveDir = t.TempDir() // empty dir; vmlinuz + initrd.img missing s := NewSupervisor(cfg) err := s.Validate() if err == nil { t.Fatalf("expected live image error") } for _, want := range []string{"vmlinuz", "initrd.img"} { if !strings.Contains(err.Error(), want) { t.Fatalf("error should name %s, got: %v", want, err) } } } func TestValidate_LiveDirEmptySkipsLiveChecks(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") cfg := goodCfg(t, tftp) cfg.LiveDir = "" // explicit opt-out; HTTP /live just 404s s := NewSupervisor(cfg) if err := s.Validate(); err != nil { t.Fatalf("empty LiveDir should not trigger live checks, got: %v", err) } } func TestValidate_MalformedSubnet(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") cases := []struct { name string subnet string }{ {"no mask", "192.168.1.0"}, {"bad ip", "hello/24"}, {"bad mask", "192.168.1.0/99"}, {"leftover dhcp_range form", "192.168.1.100,192.168.1.200,12h"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { cfg := goodCfg(t, tftp) cfg.Subnet = tc.subnet s := NewSupervisor(cfg) err := s.Validate() if err == nil || !strings.Contains(err.Error(), "pxe.subnet") { t.Fatalf("expected pxe.subnet error for %q, got: %v", tc.subnet, err) } }) } } func TestValidate_BadOrchestratorURL(t *testing.T) { tftp := t.TempDir() seedTFTP(t, tftp, "ipxe.efi", "undionly.kpxe") cases := []string{"", "not a url", "ftp://10.0.0.1", "http://"} for _, u := range cases { t.Run(u, func(t *testing.T) { cfg := goodCfg(t, tftp) cfg.OrchestratorURL = u s := NewSupervisor(cfg) err := s.Validate() if err == nil || !strings.Contains(err.Error(), "orchestrator_url") { t.Fatalf("expected orchestrator_url error for %q, got: %v", u, err) } }) } } func TestValidate_AggregatesErrors(t *testing.T) { // Multiple problems at once: Validate must report them all in // one pass so the operator sees the full picture instead of // whack-a-mole-ing one error per restart. cfg := SupervisorConfig{ Enabled: true, // Everything else zero. } s := NewSupervisor(cfg) err := s.Validate() if err == nil { t.Fatalf("expected aggregated error") } for _, want := range []string{"pxe.interface", "pxe.tftp_root", "pxe.subnet", "pxe.orchestrator_url"} { if !strings.Contains(err.Error(), want) { t.Fatalf("expected %q in aggregated error, got: %v", want, err) } } }