package main // The hosts file's verbs over files shaped like the workstation's on 2026-10-03 (novox/hq ADR 0199): // distribution lines, an operator's development names, the mesh's block and another tool's. import ( "context" "encoding/json" "os" "os/exec" "path/filepath" "reflect" "strings" "testing" ) const file = "# Static table lookup for hostnames.\n" + "127.0.0.1\tlocaldev.example.com\n" + "127.0.0.1 a.example.com b.example.com\n" + "# BEGIN mesh hosts.own\n" + "127.0.0.1\tlocalhost\n" + "::1\tlocalhost\n" + "# END mesh hosts.own\n" + "# BEGIN other-tool\n" + "192.0.2.7\tproject.test\n" + "# END other-tool\n" func blocks(text string) []string { var out []string for _, l := range Parse(text) { if l.Owner != Operator { out = append(out, l.Text) } } return out } func TestEveryLineSaysWhoseItIs(t *testing.T) { lines := Parse(file) if len(lines) != 10 { t.Fatalf("%d lines: %+v", len(lines), lines) } want := Line{Text: "127.0.0.1\tlocaldev.example.com", Owner: Operator, Address: "127.0.0.1", Names: []string{"localdev.example.com"}} if !reflect.DeepEqual(lines[1], want) { t.Errorf("%+v", lines[1]) } if lines[0].Address != "" || lines[0].Owner != Operator { t.Errorf("a comment is the operator's and no entry: %+v", lines[0]) } if lines[3].Owner != "mesh hosts.own" || lines[4].Owner != "mesh hosts.own" || lines[6].Owner != "mesh hosts.own" { t.Errorf("the mesh's block, its markers included: %+v", lines[3:7]) } if lines[8].Owner != "other-tool" || !reflect.DeepEqual(lines[8].Names, []string{"project.test"}) { t.Errorf("%+v", lines[8]) } if lines[5].Address != "::1" { t.Errorf("an IPv6 entry: %+v", lines[5]) } } func TestAnEntryWithATrailingCommentKeepsItsNames(t *testing.T) { l := Parse("10.0.0.1 nas.lan # the box upstairs")[0] if l.Address != "10.0.0.1" || !reflect.DeepEqual(l.Names, []string{"nas.lan"}) { t.Errorf("%+v", l) } } func TestAnUnclosedBlockHoldsTheRestOfTheFile(t *testing.T) { lines := Parse("# BEGIN x\n10.0.0.1 a.test\n# END y\n10.0.0.2 b.test\n") for _, l := range lines { if l.Owner != "x" { t.Errorf("%+v", l) } } } func TestAddAppendsAnOperatorLineAndIsANoOpWhenTheNamesAreThere(t *testing.T) { after, err := WithAdded(file, "192.0.2.9", []string{"lab.test", "www.lab.test"}) if err != nil { t.Fatal(err) } if !strings.HasSuffix(after, "192.0.2.9\tlab.test www.lab.test\n") { t.Errorf("%q", after) } if !reflect.DeepEqual(blocks(after), blocks(file)) { t.Errorf("blocks changed") } if same, _ := WithAdded(file, "127.0.0.1", []string{"a.example.com"}); same != file { t.Errorf("a name already there changed the file") } if some, _ := WithAdded(file, "127.0.0.1", []string{"a.example.com", "c.example.com"}); !strings.HasSuffix(some, "127.0.0.1\tc.example.com\n") { t.Errorf("%q", some) } if ended, _ := WithAdded("127.0.0.1 localhost", "192.0.2.1", []string{"x.test"}); ended != "127.0.0.1 localhost\n192.0.2.1\tx.test\n" { t.Errorf("a file without a last newline: %q", ended) } if empty, _ := WithAdded("", "192.0.2.1", []string{"x.test"}); empty != "192.0.2.1\tx.test\n" { t.Errorf("an empty file: %q", empty) } } func TestAddDoesNotCountANameOnlyABlockHasAsTheOperators(t *testing.T) { after, err := WithAdded(file, "127.0.0.1", []string{"localhost"}) if err != nil { t.Fatal(err) } if !strings.HasSuffix(after, "# END other-tool\n127.0.0.1\tlocalhost\n") { t.Errorf("%q", after) } } func TestAddRefusesWhatIsNotAnAddressOrAHostName(t *testing.T) { for _, c := range []struct { address string names []string says string }{ {"not-an-ip", []string{"x.test"}, "not an IPv4 or IPv6 address"}, {"192.0.2.9", []string{"bad name\n10.0.0.1 evil"}, "not a host name"}, {"192.0.2.9", []string{"-lead.test"}, "not a host name"}, {"192.0.2.9", []string{strings.Repeat("a", 64) + ".test"}, "not a host name"}, {"192.0.2.9", []string{strings.Repeat("abcdefgh.", 30)}, "not a host name"}, {"192.0.2.9", []string{}, "at least one name"}, } { if _, err := WithAdded(file, c.address, c.names); err == nil || !strings.Contains(err.Error(), c.says) { t.Errorf("%q %q: %v", c.address, c.names, err) } } if _, err := WithAdded(file, "2001:db8::1", []string{"v6.test."}); err != nil { t.Errorf("an IPv6 address and a rooted name: %v", err) } } func TestRemoveTakesOneNameOrOneAddressAndBlocksStayByteForByte(t *testing.T) { one, n, err := WithRemoved(file, "a.example.com") if err != nil || n != 1 { t.Fatalf("%d %v", n, err) } if !strings.Contains(one, "127.0.0.1\tb.example.com\n") || strings.Contains(one, "a.example.com") { t.Errorf("%q", one) } if !reflect.DeepEqual(blocks(one), blocks(file)) { t.Errorf("blocks changed") } all, n, err := WithRemoved(file, "127.0.0.1") if err != nil || n != 2 { t.Fatalf("%d %v", n, err) } if !strings.Contains(all, "# BEGIN mesh hosts.own\n127.0.0.1\tlocalhost\n") { t.Errorf("the mesh's own localhost is not the operator's to remove: %q", all) } if !reflect.DeepEqual(blocks(all), blocks(file)) { t.Errorf("blocks changed") } } func TestRemoveRefusesANameOnlyABlockWritesNamingWhose(t *testing.T) { if _, _, err := WithRemoved(file, "project.test"); err == nil || !strings.Contains(err.Error(), "written by other-tool") { t.Errorf("%v", err) } if _, _, err := WithRemoved(file, "::1"); err == nil || !strings.Contains(err.Error(), "written by mesh hosts.own") { t.Errorf("%v", err) } if _, n, err := WithRemoved(file, "nowhere.test"); err != nil || n != 0 { t.Errorf("%d %v", n, err) } if _, _, err := WithRemoved(file, "bad name"); err == nil { t.Errorf("a name that is neither an address nor a host name is refused") } } func TestTheFileIsWrittenAsRootThroughSudoWhereTheAccountIsNotRoot(t *testing.T) { if p, a := escalated(1000, "install", []string{"x"}); p != "sudo" || !reflect.DeepEqual(a, []string{"-n", "install", "x"}) { t.Errorf("%s %v", p, a) } if p, a := escalated(0, "install", []string{"x"}); p != "install" || !reflect.DeepEqual(a, []string{"x"}) { t.Errorf("%s %v", p, a) } } // runPlain runs a command as given, unescalated: the test's file is the test's own. func runPlain(ctx context.Context, name string, args ...string) (string, error) { out, err := exec.CommandContext(ctx, name, args...).CombinedOutput() return string(out), err } func TestTheVerbsWriteTheFileWholeBesideItAndRenameItOver(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "hosts") if err := os.WriteFile(path, []byte(file), 0o644); err != nil { t.Fatal(err) } var calls [][]string h := HostsFile{Path: path, Run: func(ctx context.Context, name string, args ...string) (string, error) { calls = append(calls, append([]string{name}, args...)) return runPlain(ctx, name, args...) }} ctx := context.Background() added, err := h.Add(ctx, "192.0.2.9", []string{"lab.test"}) if err != nil || !added.Added || added.Line != "192.0.2.9\tlab.test" { t.Fatalf("%+v %v", added, err) } beside := filepath.Join(dir, ".hosts.hosts-tools") if len(calls) != 2 || calls[0][0] != "install" || calls[0][len(calls[0])-1] != beside || !reflect.DeepEqual(calls[1], []string{"mv", "-f", beside, path}) { t.Errorf("%v", calls) } if again, _ := h.Add(ctx, "192.0.2.9", []string{"lab.test"}); again.Added || len(calls) != 2 { t.Errorf("an add already there wrote the file: %+v %v", again, calls) } got, err := h.Entries() if err != nil { t.Fatal(err) } last := got.Lines[len(got.Lines)-1] if last.Owner != Operator || last.Address != "192.0.2.9" { t.Errorf("add then entries shows the line as the operator's: %+v", last) } removed, err := h.Remove(ctx, "lab.test") if err != nil || removed.Removed != 1 { t.Fatalf("%+v %v", removed, err) } if b, _ := os.ReadFile(path); string(b) != file { t.Errorf("add then remove gives the file back: %q", b) } if _, err := os.Stat(beside); !os.IsNotExist(err) { t.Errorf("the staged copy beside the file is left: %v", err) } if info, _ := os.Stat(path); info.Mode().Perm() != 0o644 { t.Errorf("mode %v", info.Mode()) } } func TestThePathTheCodeWritesIsThePathTheManifestsResourceDeclares(t *testing.T) { raw, err := os.ReadFile("../../module.json") if err != nil { t.Fatal(err) } var m struct { Claims []struct { Name string `json:"name"` Serves []string `json:"serves"` } `json:"claims"` Resources []struct { ID string `json:"id"` Path string `json:"path"` } `json:"resources"` } if err := json.Unmarshal(raw, &m); err != nil { t.Fatal(err) } found := false for _, r := range m.Resources { if r.ID == "own" { found = true if r.Path != HostsPath { t.Errorf("the manifest writes %s, the code %s", r.Path, HostsPath) } } } if !found { t.Errorf("no resource own") } var served []string for _, tool := range tools(HostsFile{}) { served = append(served, strings.TrimPrefix(tool.Name, Seat+".")) if tool.Description == "" || tool.Run == nil { t.Errorf("%s", tool.Name) } } if len(m.Claims) != 1 || m.Claims[0].Name != Seat || !reflect.DeepEqual(m.Claims[0].Serves, served) { t.Errorf("the manifest serves %+v, the binary %v", m.Claims, served) } } func TestNamesAreSplitOnSpacesAndCommasOrTakenAsAList(t *testing.T) { if got := namesArg(map[string]any{"names": " a.test, b.test c.test"}); !reflect.DeepEqual(got, []string{"a.test", "b.test", "c.test"}) { t.Errorf("%v", got) } if got := namesArg(map[string]any{"names": []any{"a.test", "b.test"}}); !reflect.DeepEqual(got, []string{"a.test", "b.test"}) { t.Errorf("%v", got) } if got := namesArg(map[string]any{}); len(got) != 0 { t.Errorf("%v", got) } }