package peer import ( "net/netip" "os" "path/filepath" "sort" "strings" "testing" ) // writeTempHosts creates a temp hosts file with the given content and returns // its path. func writeTempHosts(t *testing.T, content string) string { t.Helper() path := filepath.Join(t.TempDir(), "hosts") if err := os.WriteFile(path, []byte(content), 0600); err != nil { t.Fatal(err) } return path } // readManagedSection returns the lines between the begin/end markers for the // given localDomain, plus everything outside the section ("outside"). func readManagedSection(t *testing.T, path, localDomain string) (inside, outside []string) { t.Helper() raw, err := os.ReadFile(path) if err != nil { t.Fatal(err) } begin, end := hostMarkers(localDomain) inSection := false for _, line := range strings.Split(string(raw), "\n") { switch { case strings.HasPrefix(line, begin): inSection = true case strings.HasPrefix(line, end): inSection = false case inSection: if f := strings.Join(strings.Fields(line), " "); f != "" { inside = append(inside, f) } default: if f := strings.Join(strings.Fields(line), " "); f != "" { outside = append(outside, f) } } } return inside, outside } func peer(name string) *Peer { return &Peer{Name: name} } func TestUpdateHosts_AddsSection(t *testing.T) { path := writeTempHosts(t, "127.0.0.1 localhost\n") peers := map[netip.Addr]*Peer{ netip.MustParseAddr("10.11.12.1"): peer("hub"), netip.MustParseAddr("10.11.12.10"): peer("laptop"), } if err := updateHosts(path, "mynet.local", peers); err != nil { t.Fatal(err) } inside, outside := readManagedSection(t, path, "mynet.local") sort.Strings(inside) want := []string{ "10.11.12.1 hub.mynet.local", "10.11.12.10 laptop.mynet.local", } if strings.Join(inside, "\n") != strings.Join(want, "\n") { t.Errorf("managed section = %v, want %v", inside, want) } if !contains(outside, "127.0.0.1 localhost") { t.Errorf("original content lost; outside = %v", outside) } } func TestUpdateHosts_ReplacesExistingSection(t *testing.T) { path := writeTempHosts(t, "127.0.0.1 localhost\n") // First write. first := map[netip.Addr]*Peer{ netip.MustParseAddr("10.11.12.1"): peer("hub"), } if err := updateHosts(path, "mynet.local", first); err != nil { t.Fatal(err) } // Second write with a different set of peers. second := map[netip.Addr]*Peer{ netip.MustParseAddr("10.11.12.20"): peer("phone"), } if err := updateHosts(path, "mynet.local", second); err != nil { t.Fatal(err) } inside, outside := readManagedSection(t, path, "mynet.local") if len(inside) != 1 || inside[0] != "10.11.12.20 phone.mynet.local" { t.Errorf("section not replaced; inside = %v", inside) } if contains(inside, "10.11.12.1 hub.mynet.local") { t.Errorf("stale entry remained; inside = %v", inside) } if !contains(outside, "127.0.0.1 localhost") { t.Errorf("original content lost; outside = %v", outside) } } func TestUpdateHosts_SkipsEmptyNames(t *testing.T) { path := writeTempHosts(t, "127.0.0.1 localhost\n") peers := map[netip.Addr]*Peer{ netip.MustParseAddr("10.11.12.1"): peer("hub"), netip.MustParseAddr("10.11.12.99"): peer(""), // no name } if err := updateHosts(path, "mynet.local", peers); err != nil { t.Fatal(err) } inside, _ := readManagedSection(t, path, "mynet.local") if len(inside) != 1 || inside[0] != "10.11.12.1 hub.mynet.local" { t.Errorf("expected only named peer; inside = %v", inside) } } func TestUpdateHosts_Idempotent(t *testing.T) { path := writeTempHosts(t, "127.0.0.1 localhost\n") peers := map[netip.Addr]*Peer{ netip.MustParseAddr("10.11.12.1"): peer("hub"), } if err := updateHosts(path, "mynet.local", peers); err != nil { t.Fatal(err) } first, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if err := updateHosts(path, "mynet.local", peers); err != nil { t.Fatal(err) } second, err := os.ReadFile(path) if err != nil { t.Fatal(err) } if string(first) != string(second) { t.Errorf("repeated update changed file:\nfirst:\n%s\nsecond:\n%s", first, second) } } // TestUpdateHosts_PrefixDomainsCoexist guards finding 4.4: two domains where // one label is a prefix of the other ("net" vs "net2") must each manage their // own section without clobbering the other's, even sharing one hosts file. func TestUpdateHosts_PrefixDomainsCoexist(t *testing.T) { path := writeTempHosts(t, "127.0.0.1 localhost\n") if err := updateHosts(path, "net2.local", map[netip.Addr]*Peer{ netip.MustParseAddr("10.0.2.1"): peer("a"), }); err != nil { t.Fatal(err) } if err := updateHosts(path, "net.local", map[netip.Addr]*Peer{ netip.MustParseAddr("10.0.1.1"): peer("b"), }); err != nil { t.Fatal(err) } // Both sections coexist after writing the prefix domain. if in, _ := readManagedSection(t, path, "net2.local"); len(in) != 1 || in[0] != "10.0.2.1 a.net2.local" { t.Errorf("net2 section clobbered: %v", in) } if in, _ := readManagedSection(t, path, "net.local"); len(in) != 1 || in[0] != "10.0.1.1 b.net.local" { t.Errorf("net section wrong: %v", in) } // Re-updating net2 must not disturb the net section. if err := updateHosts(path, "net2.local", map[netip.Addr]*Peer{ netip.MustParseAddr("10.0.2.2"): peer("c"), }); err != nil { t.Fatal(err) } if in, _ := readManagedSection(t, path, "net.local"); len(in) != 1 || in[0] != "10.0.1.1 b.net.local" { t.Errorf("net section disturbed by net2 update: %v", in) } if in, _ := readManagedSection(t, path, "net2.local"); len(in) != 1 || in[0] != "10.0.2.2 c.net2.local" { t.Errorf("net2 section not updated: %v", in) } } func contains(ss []string, s string) bool { for _, x := range ss { if x == s { return true } } return false }