package overlay import ( "strings" "testing" ) func hostsFor(t *testing.T, self string, nodes ...Node) string { t.Helper() out, err := Hosts(nodes, self) if err != nil { t.Fatal(err) } return out } func TestEveryNodeWithAPlaceGetsAName(t *testing.T) { got := hostsFor(t, "laptop", Node{Name: "anchor", Address: "10.42.0.1"}, Node{Name: "laptop", Address: "10.42.0.2"}) for _, want := range []string{"10.42.0.1\tanchor.internal\tanchor", "10.42.0.2\tlaptop.internal\tlaptop"} { if !strings.Contains(got, want) { t.Errorf("no entry for %q in:\n%s", want, got) } } } func TestANodeSeesItselfAtItsOverlayAddress(t *testing.T) { // Not at a loopback. A service that binds to the name the machine was given would otherwise // listen somewhere nothing else can reach, and the failure appears on every other node rather // than this one. got := hostsFor(t, "laptop", Node{Name: "laptop", Address: "10.42.0.2"}) if !strings.Contains(got, "10.42.0.2\tlaptop.internal") { t.Error("a node does not resolve its own mesh name to its overlay address") } } func TestTheMachinesOwnLoopbackSurvives(t *testing.T) { // This file is generated whole, so anything left out is removed. Dropping localhost would // break things that have nothing to do with the mesh, on a machine the mesh was asked to // improve. got := hostsFor(t, "laptop", Node{Name: "laptop", Address: "10.42.0.2"}) if !strings.Contains(got, "127.0.0.1\tlocalhost") { t.Error("localhost is missing; this file replaces the machine's own") } if !strings.Contains(got, "::1") { t.Error("the IPv6 loopback is missing") } } func TestANodeWithNoAddressGetsNoName(t *testing.T) { // A name resolving to nothing is worse than no name: a connection to an address that does not // answer hangs, where a name that does not resolve fails at once and says which name it was. got := hostsFor(t, "laptop", Node{Name: "laptop", Address: "10.42.0.2"}, Node{Name: "newcomer", Address: ""}) if strings.Contains(got, "newcomer") { t.Error("a node with no address on the network was given a name") } } func TestANameThatCannotBeAHostnameIsRefused(t *testing.T) { // Refused here, where a person is looking, rather than written into a file that every // machine then reads and disagrees about. for _, bad := range []string{"Anchor", "my node", "under_score", "-leading", "trailing-"} { if _, err := Hosts([]Node{{Name: bad, Address: "10.42.0.1"}}, ""); err == nil { t.Errorf("%q was accepted as a mesh name", bad) } } } func TestTheOrderIsStable(t *testing.T) { // The file is rewritten whenever anything changes, and a file whose lines move for no reason // makes every reconcile look like a change — which means a service that reflects it restarts // for ever. a := hostsFor(t, "", Node{Name: "b", Address: "10.42.0.2"}, Node{Name: "a", Address: "10.42.0.1"}) b := hostsFor(t, "", Node{Name: "a", Address: "10.42.0.1"}, Node{Name: "b", Address: "10.42.0.2"}) if a != b { t.Error("the same mesh produced two different files depending on the order it was read in") } } func TestTheSuffixIsReservedForThis(t *testing.T) { // `.internal` was reserved by IANA in 2024 for exactly this. A name under it can never // collide with a public one, so an internal name that leaks into a public resolver fails // rather than reaching a stranger's machine. if InternalName("anchor") != "anchor.internal" { t.Errorf("internal names end in %q", Suffix()) } } func TestTheSuffixCanBeChosen(t *testing.T) { t.Setenv(SuffixVar, ".mesh") if InternalName("anchor") != "anchor.mesh" { t.Errorf("got %q", InternalName("anchor")) } } func TestTheFileSaysItIsGenerated(t *testing.T) { got := hostsFor(t, "laptop", Node{Name: "laptop", Address: "10.42.0.2"}) if !strings.Contains(got, "Do not edit") { t.Error("a generated file does not say so") } }