package lanresolver import ( "context" "io" "log/slog" "os" "path/filepath" "strings" "sync" "testing" ) // recRunner records every privileged command EnsureDnsmasq would run and succeeds — nothing reaches // apt, systemctl or the root checker. type recRunner struct { mu sync.Mutex calls []string } func (r *recRunner) Run(_ context.Context, name string, args ...string) ([]byte, []byte, error) { r.mu.Lock() defer r.mu.Unlock() r.calls = append(r.calls, strings.Join(append([]string{name}, args...), " ")) return nil, nil, nil } func (r *recRunner) RunStdin(ctx context.Context, _ io.Reader, name string, args ...string) ([]byte, []byte, error) { return r.Run(ctx, name, args...) } func (r *recRunner) installed() bool { for _, c := range r.calls { if strings.HasPrefix(c, "apt-get install") && strings.HasSuffix(c, " dnsmasq") { return true } } return false } // fixtureRoot builds a fake host root holding exactly the given relative files and points the REAL // probe at it for the test's duration. func fixtureRoot(t *testing.T, files ...string) { t.Helper() root := t.TempDir() for _, f := range files { p := filepath.Join(root, f) if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { t.Fatal(err) } if err := os.WriteFile(p, nil, 0o644); err != nil { t.Fatal(err) } } prev := hostRoot hostRoot = root t.Cleanup(func() { hostRoot = prev }) } func ensure(t *testing.T) *recRunner { t.Helper() r := &recRunner{} m := NewManager(r, "192.0.2.10", []string{"1.1.1.1"}, slog.New(slog.NewTextHandler(io.Discard, nil))) if err := m.EnsureDnsmasq(context.Background()); err != nil { t.Fatalf("EnsureDnsmasq: %v", err) } return r } // R-317: a host with `dnsmasq-base` (the /usr/sbin/dnsmasq binary) but WITHOUT the `dnsmasq` package // (the service unit) must get the package installed — else the following `systemctl enable --now // dnsmasq` hits a unit that does not exist and LAN name resolution silently never comes up. // // RED-PROOF: probe "usr/sbin/dnsmasq" instead of the unit paths in dnsmasqUnitInstalled → this fails // with "install was skipped". func TestEnsureDnsmasq_BinaryWithoutUnitInstalls(t *testing.T) { fixtureRoot(t, "usr/sbin/dnsmasq") r := ensure(t) if !r.installed() { t.Fatalf("install was skipped on a dnsmasq-base-only host (binary present, unit absent) — "+ "the enable that follows targets a missing unit (R-317). calls: %q", r.calls) } } func TestEnsureDnsmasq_UnitPresentSkipsInstall(t *testing.T) { for _, unit := range []string{"usr/lib/systemd/system/dnsmasq.service", "lib/systemd/system/dnsmasq.service"} { t.Run(unit, func(t *testing.T) { fixtureRoot(t, "usr/sbin/dnsmasq", unit) if r := ensure(t); r.installed() { t.Fatalf("apt-get install ran although the dnsmasq unit is present at %s: %q", unit, r.calls) } }) } } func TestEnsureDnsmasq_NothingPresentInstalls(t *testing.T) { fixtureRoot(t) if r := ensure(t); !r.installed() { t.Fatalf("install skipped on a host with no dnsmasq at all: %q", r.calls) } } // Production wiring for the hostRoot seam: the shipped probe resolves against the real root and asks // about the unit the `dnsmasq` package owns — never the dnsmasq-base binary. func TestEnsureDnsmasq_ProductionProbeIsTheUnit(t *testing.T) { if hostRoot != "/" { t.Fatalf("hostRoot default = %q, want \"/\" — the production probe would look in the wrong tree", hostRoot) } var sawUsrLib bool for _, p := range dnsmasqUnitPaths { full := filepath.Join(hostRoot, p) if strings.HasSuffix(full, "/sbin/dnsmasq") || strings.HasSuffix(full, "/bin/dnsmasq") { t.Errorf("probe path %s is the dnsmasq-base binary, not the dnsmasq unit (R-317)", full) } if full == "/usr/lib/systemd/system/dnsmasq.service" { sawUsrLib = true } } if !sawUsrLib { t.Errorf("probe paths %q miss /usr/lib/systemd/system/dnsmasq.service (dpkg -S: owned by dnsmasq)", dnsmasqUnitPaths) } }