- 重构 sysconf 为文档模型 INI Parser 与 Config Framework - 强化 hosts 解析、插入校验、写回与异常输入处理 - 完善 StarCmd 生命周期、等待 API、流式输出与 IO 重定向 - 扩展跨平台文件时间、文件锁、内存、进程与网络能力 - 将 Windows 进程适配更新到 b612.me/wincmd v0.1.0 - 移除本地 wincmd/win32api replace,改用发布版依赖 - 将最低 Go 版本提升到 1.18 - 补充 hosts、sysconf、FileLock、StarCmd 与平台适配回归测试
733 lines
20 KiB
Go
733 lines
20 KiB
Go
package hosts
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func Test_Hosts(t *testing.T) {
|
|
var h = NewHosts()
|
|
tmpDir := t.TempDir()
|
|
err := h.Parse("./test_hosts.txt")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
next := h.firstUid
|
|
for next != 0 {
|
|
node, _ := h.GetNode(next)
|
|
fmt.Printf("Last %d, Next: %d, IP: %s, Hosts: %s, Comment: %s\n", node.LastUID(), node.NextUID(), node.IP(), node.Hosts(), node.Comment())
|
|
next = node.NextUID()
|
|
}
|
|
data := h.ListHostsByIP("11.22.33.44")
|
|
if len(data) != 2 {
|
|
t.Error("Expected 2, got ", len(data))
|
|
} else {
|
|
t.Log(data)
|
|
}
|
|
|
|
data = h.ListIPsByHost("dns.b612.me")
|
|
if len(data) < 1 || data[0] != "4.5.6.7" {
|
|
t.Error("Expected 4.5.6.7, got ", data)
|
|
} else {
|
|
t.Log(data)
|
|
}
|
|
|
|
err = h.RemoveHosts("dns.b612.me")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
data = h.ListIPsByHost("dns.b612.me")
|
|
if len(data) > 0 {
|
|
t.Error("Expected 0, got ", len(data))
|
|
} else {
|
|
t.Log(data)
|
|
}
|
|
|
|
err = h.RemoveHosts("test.dns.set.b612.me")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
data = h.ListIPsByHost("remove.b612.me")
|
|
if len(data) < 1 || data[0] != "11.22.33.44" {
|
|
t.Error("Expected 11.22.33.44, got ", data)
|
|
} else {
|
|
t.Log(data)
|
|
}
|
|
|
|
nodes := h.ListByIP("11.22.33.44")
|
|
if nodes == nil {
|
|
t.Error("Expected not nil, got ", nodes)
|
|
} else {
|
|
t.Log(nodes)
|
|
}
|
|
|
|
nodes[0].AddHosts("hello.b612.me")
|
|
err = h.UpdateNode(nodes[0])
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
data = h.ListIPsByHost("hello.b612.me")
|
|
if len(data) < 1 || data[0] != "11.22.33.44" {
|
|
t.Error("Not Expected Data", data)
|
|
} else {
|
|
t.Log(data)
|
|
}
|
|
insertNode := new(HostNode)
|
|
insertNode.SetIP("11.11.11.11")
|
|
insertNode.SetHosts("insert.b612.me")
|
|
insertNode.SetComment("Insert Node")
|
|
insertNode.SetNextUID(nodes[0].UID())
|
|
insertNode.SetLastUID(nodes[0].LastUID())
|
|
err = h.InsertNode(insertNode)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
data = h.ListIPsByHost("insert.b612.me")
|
|
if len(data) < 1 || data[0] != "11.11.11.11" {
|
|
t.Error("Expected 11.11.11.11 got ", data)
|
|
} else {
|
|
t.Log(data)
|
|
}
|
|
|
|
err = h.SaveAs(filepath.Join(tmpDir, "test_hosts_01.txt"))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
err = h.DeleteNode(insertNode)
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
data = h.ListIPsByHost("insert.b612.me")
|
|
if len(data) > 0 {
|
|
t.Error("Expected 0 got ", data)
|
|
} else {
|
|
t.Log(data)
|
|
}
|
|
|
|
for i := 0; i < 100; i++ {
|
|
err = h.RemoveHosts("release-ftpd")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
err = h.AddHosts("2.3.4.9", "release-ftpd")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
err = h.SetHostIPs("ssh.b612.me", "9.9.9.9")
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
|
|
data = h.ListIPsByHost("ssh.b612.me")
|
|
if len(data) == 0 {
|
|
t.Error("Expected 1 got ", data)
|
|
} else {
|
|
t.Log(data)
|
|
}
|
|
|
|
err = h.SetIPHosts("10.10.10.10", "ssh.b612.me", "ssr.b612.me")
|
|
if len(data) == 0 {
|
|
t.Error("Expected 1 got ", data)
|
|
}
|
|
|
|
err = h.SaveAs(filepath.Join(tmpDir, "test_hosts_02.txt"))
|
|
if err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func BenchmarkAddHosts(b *testing.B) {
|
|
var h = NewHosts()
|
|
err := h.Parse("./test_hosts.txt")
|
|
if err != nil {
|
|
b.Error(err)
|
|
}
|
|
for i := 0; i < b.N; i++ {
|
|
err = h.AddHosts("1.3.4.5", "test.b612.me")
|
|
if err != nil {
|
|
b.Error(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestParseHandlesEmptyAndNoTrailingNewline(t *testing.T) {
|
|
t.Run("empty file", func(t *testing.T) {
|
|
h := NewHosts()
|
|
path := filepath.Join(t.TempDir(), "hosts.empty")
|
|
if err := os.WriteFile(path, nil, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.Parse(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := h.List(); len(got) != 0 {
|
|
t.Fatalf("expected empty hosts list, got %d entries", len(got))
|
|
}
|
|
})
|
|
|
|
t.Run("last line without newline", func(t *testing.T) {
|
|
h := NewHosts()
|
|
path := filepath.Join(t.TempDir(), "hosts.nonewline")
|
|
if err := os.WriteFile(path, []byte("1.2.3.4 example.test"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.Parse(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := h.ListIPsByHost("example.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
|
t.Fatalf("expected last line to be parsed, got %v", got)
|
|
}
|
|
node, err := h.GetLatestNode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if node.NextUID() != 0 {
|
|
t.Fatalf("expected last node next uid 0, got %d", node.NextUID())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAddHostsAndAddNodeWorkOnEmptyModel(t *testing.T) {
|
|
t.Run("add hosts", func(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := h.ListFirstIPByHost("example.test"); got != "1.2.3.4" {
|
|
t.Fatalf("expected inserted host ip, got %q", got)
|
|
}
|
|
out, err := h.Build()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(out), "1.2.3.4 example.test") {
|
|
t.Fatalf("unexpected build output: %q", out)
|
|
}
|
|
})
|
|
|
|
t.Run("add node", func(t *testing.T) {
|
|
h := NewHosts()
|
|
node := &HostNode{}
|
|
node.SetIP("5.6.7.8")
|
|
node.SetHosts("node.test")
|
|
if err := h.AddNode(node); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if node.UID() == 0 {
|
|
t.Fatal("expected node uid to be assigned")
|
|
}
|
|
if got := h.ListFirstIPByHost("node.test"); got != "5.6.7.8" {
|
|
t.Fatalf("expected inserted node ip, got %q", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestInsertNodeByDataInsertsAndLinksNode(t *testing.T) {
|
|
h := NewHosts()
|
|
path := filepath.Join(t.TempDir(), "hosts.insert")
|
|
if err := os.WriteFile(path, []byte("2.2.2.2 anchor.test\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.Parse(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
anchor, err := h.GetFirstNode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.InsertNodeByData(anchor, true, "before", "1.1.1.1", "before.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.InsertNodeByData(anchor, false, "after", "3.3.3.3", "after.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
nodes := h.List()
|
|
if len(nodes) != 3 {
|
|
t.Fatalf("expected 3 nodes after insert, got %d", len(nodes))
|
|
}
|
|
if nodes[0].IP() != "1.1.1.1" || nodes[1].IP() != "2.2.2.2" || nodes[2].IP() != "3.3.3.3" {
|
|
t.Fatalf("unexpected node order: %q, %q, %q", nodes[0].IP(), nodes[1].IP(), nodes[2].IP())
|
|
}
|
|
if got := h.ListFirstIPByHost("before.test"); got != "1.1.1.1" {
|
|
t.Fatalf("expected before node to be indexed, got %q", got)
|
|
}
|
|
if got := h.ListFirstIPByHost("after.test"); got != "3.3.3.3" {
|
|
t.Fatalf("expected after node to be indexed, got %q", got)
|
|
}
|
|
if nodes[0].NextUID() != nodes[1].UID() || nodes[1].LastUID() != nodes[0].UID() {
|
|
t.Fatalf("before/anchor linkage broken: before.next=%d anchor.uid=%d anchor.last=%d", nodes[0].NextUID(), nodes[1].UID(), nodes[1].LastUID())
|
|
}
|
|
if nodes[1].NextUID() != nodes[2].UID() || nodes[2].LastUID() != nodes[1].UID() {
|
|
t.Fatalf("anchor/after linkage broken: anchor.next=%d after.uid=%d after.last=%d", nodes[1].NextUID(), nodes[2].UID(), nodes[2].LastUID())
|
|
}
|
|
}
|
|
|
|
func TestInsertNodeByDataRejectsNilAnchor(t *testing.T) {
|
|
h := NewHosts()
|
|
path := filepath.Join(t.TempDir(), "hosts.insert.nil")
|
|
if err := os.WriteFile(path, []byte("2.2.2.2 anchor.test\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.Parse(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.InsertNodeByData(nil, true, "before", "1.1.1.1", "before.test"); err == nil {
|
|
t.Fatal("expected nil anchor error")
|
|
}
|
|
}
|
|
|
|
func TestSetIPHostsUpdatesReverseIndex(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.SetIPHosts("1.2.3.4", "new.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := h.ListIPsByHost("new.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
|
t.Fatalf("expected new reverse index, got %v", got)
|
|
}
|
|
if got := h.ListIPsByHost("old.test"); len(got) != 0 {
|
|
t.Fatalf("expected old reverse index to be removed, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestSetIPHostsDeduplicatesHosts(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.SetIPHosts("1.2.3.4", "new.test", "new.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "new.test" {
|
|
t.Fatalf("expected deduplicated ip mapping, got %v", got)
|
|
}
|
|
if got := h.ListIPsByHost("new.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
|
t.Fatalf("expected deduplicated reverse index, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestSetIPHostsReplacesMultipleSameIPNodes(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.AddHosts("5.6.7.8", "tail.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.SetIPHosts("1.2.3.4", "new.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "new.test" {
|
|
t.Fatalf("expected replaced same-ip mappings, got %v", got)
|
|
}
|
|
if got := h.ListIPsByHost("first.test"); len(got) != 0 {
|
|
t.Fatalf("expected first old host to disappear, got %v", got)
|
|
}
|
|
if got := h.ListIPsByHost("second.test"); len(got) != 0 {
|
|
t.Fatalf("expected second old host to disappear, got %v", got)
|
|
}
|
|
if got := h.ListFirstIPByHost("tail.test"); got != "5.6.7.8" {
|
|
t.Fatalf("expected tail node to remain linked, got %q", got)
|
|
}
|
|
nodes := h.List()
|
|
if len(nodes) != 2 || nodes[0].IP() != "5.6.7.8" || nodes[1].IP() != "1.2.3.4" {
|
|
t.Fatalf("unexpected node list after SetIPHosts: %#v", nodes)
|
|
}
|
|
if nodes[0].NextUID() != nodes[1].UID() || nodes[1].LastUID() != nodes[0].UID() {
|
|
t.Fatalf("remaining node linkage broken: first.next=%d second.uid=%d second.last=%d", nodes[0].NextUID(), nodes[1].UID(), nodes[1].LastUID())
|
|
}
|
|
}
|
|
|
|
func TestSetHostIPsReplacesMappingsInOneOperation(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.SetHostIPs("old.test", "2.2.2.2", "3.3.3.3"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := h.ListIPsByHost("old.test"); len(got) != 2 || got[0] != "2.2.2.2" || got[1] != "3.3.3.3" {
|
|
t.Fatalf("expected replaced host ip mappings, got %v", got)
|
|
}
|
|
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 0 {
|
|
t.Fatalf("expected old ip mapping to be removed, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestSetIPHostsRejectsInvalidInputWithoutMutating(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
ip string
|
|
hosts []string
|
|
}{
|
|
{name: "bad ip", ip: "bad-ip", hosts: []string{"new.test"}},
|
|
{name: "empty host", ip: "1.2.3.4", hosts: []string{""}},
|
|
{name: "comment host", ip: "1.2.3.4", hosts: []string{"#bad.test"}},
|
|
{name: "missing host", ip: "1.2.3.4"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.SetIPHosts(tt.ip, tt.hosts...); err == nil {
|
|
t.Fatal("expected invalid SetIPHosts input to fail")
|
|
}
|
|
if got := h.ListIPsByHost("old.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
|
t.Fatalf("old host mapping should remain after failed SetIPHosts, got %v", got)
|
|
}
|
|
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "old.test" {
|
|
t.Fatalf("ip index should remain after failed SetIPHosts, got %v", got)
|
|
}
|
|
if got := h.ListIPsByHost("new.test"); len(got) != 0 {
|
|
t.Fatalf("failed SetIPHosts should not add new host, got %v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSetHostIPsRejectsInvalidInputWithoutMutating(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
host string
|
|
ips []string
|
|
}{
|
|
{name: "empty host", host: "", ips: []string{"2.2.2.2"}},
|
|
{name: "comment host", host: "#old.test", ips: []string{"2.2.2.2"}},
|
|
{name: "bad ip", host: "old.test", ips: []string{"2.2.2.2", "bad-ip"}},
|
|
{name: "missing ip", host: "old.test"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "old.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.SetHostIPs(tt.host, tt.ips...); err == nil {
|
|
t.Fatal("expected invalid SetHostIPs input to fail")
|
|
}
|
|
if got := h.ListIPsByHost("old.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
|
t.Fatalf("old host mapping should remain after failed SetHostIPs, got %v", got)
|
|
}
|
|
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "old.test" {
|
|
t.Fatalf("ip index should remain after failed SetHostIPs, got %v", got)
|
|
}
|
|
if got := h.ListHostsByIP("2.2.2.2"); len(got) != 0 {
|
|
t.Fatalf("failed SetHostIPs should not add partial ip mapping, got %v", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRemoveIPHostsKeepsSameIPOtherNodes(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.RemoveIPHosts("1.2.3.4", "first.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := h.ListIPsByHost("first.test"); len(got) != 0 {
|
|
t.Fatalf("expected removed host to disappear, got %v", got)
|
|
}
|
|
if got := h.ListIPsByHost("second.test"); len(got) != 1 || got[0] != "1.2.3.4" {
|
|
t.Fatalf("expected same-ip sibling node to stay indexed, got %v", got)
|
|
}
|
|
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "second.test" {
|
|
t.Fatalf("expected ip index to keep sibling host, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestRemoveHostsKeepsSameIPOtherNodes(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.RemoveHosts("first.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 1 || got[0] != "second.test" {
|
|
t.Fatalf("expected ip index to keep sibling host after RemoveHosts, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestRemoveIPsUnlinksAdjacentNodes(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "first.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.AddHosts("1.2.3.4", "second.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.AddHosts("5.6.7.8", "tail.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.RemoveIPs("1.2.3.4"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
nodes := h.List()
|
|
if len(nodes) != 1 || nodes[0].IP() != "5.6.7.8" || nodes[0].LastUID() != 0 || nodes[0].NextUID() != 0 {
|
|
t.Fatalf("expected only tail node with clean links, got %#v", nodes)
|
|
}
|
|
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 0 {
|
|
t.Fatalf("expected removed ip index to be empty, got %v", got)
|
|
}
|
|
if got := h.ListFirstIPByHost("tail.test"); got != "5.6.7.8" {
|
|
t.Fatalf("expected tail reverse index to remain, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAddHostsRejectsInvalidInput(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("not-an-ip", "bad.test"); err == nil {
|
|
t.Fatal("expected invalid ip error")
|
|
}
|
|
if got := h.ListHostsByIP("not-an-ip"); len(got) != 0 {
|
|
t.Fatalf("invalid ip should not be indexed, got %v", got)
|
|
}
|
|
if err := h.AddHosts("1.2.3.4", ""); err == nil {
|
|
t.Fatal("expected empty host error")
|
|
}
|
|
if got := h.ListHostsByIP("1.2.3.4"); len(got) != 0 {
|
|
t.Fatalf("empty host should not be indexed, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestInsertNodeByDataRejectsInvalidHostDataWithoutMutating(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("2.2.2.2", "anchor.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
anchor, err := h.GetFirstNode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
ip string
|
|
hosts []string
|
|
}{
|
|
{name: "bad ip", ip: "not-an-ip", hosts: []string{"bad.test"}},
|
|
{name: "empty host", ip: "1.1.1.1", hosts: []string{""}},
|
|
{name: "comment host", ip: "1.1.1.1", hosts: []string{"#bad.test"}},
|
|
{name: "missing host", ip: "1.1.1.1"},
|
|
}
|
|
for _, tt := range tests {
|
|
if err := h.InsertNodeByData(anchor, false, "", tt.ip, tt.hosts...); err == nil {
|
|
t.Fatalf("%s: expected error", tt.name)
|
|
}
|
|
}
|
|
nodes := h.List()
|
|
if len(nodes) != 1 || nodes[0].IP() != "2.2.2.2" {
|
|
t.Fatalf("invalid insert should not mutate node list: %#v", nodes)
|
|
}
|
|
if got := h.ListHostsByIP("1.1.1.1"); len(got) != 0 {
|
|
t.Fatalf("invalid insert should not mutate ip index: %v", got)
|
|
}
|
|
|
|
if err := h.InsertNodeByData(anchor, true, "comment-only", ""); err != nil {
|
|
t.Fatalf("comment-only insert should remain valid: %v", err)
|
|
}
|
|
nodes = h.List()
|
|
if len(nodes) != 2 || !nodes[0].OnlyComment() || nodes[1].IP() != "2.2.2.2" {
|
|
t.Fatalf("comment-only insert mismatch: %#v", nodes)
|
|
}
|
|
}
|
|
|
|
func TestInsertNodeByDataRejectsEmptyNodeWithoutMutating(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("2.2.2.2", "anchor.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
anchor, err := h.GetFirstNode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := h.InsertNodeByData(anchor, true, "", ""); err == nil {
|
|
t.Fatal("expected empty insert to fail")
|
|
}
|
|
nodes := h.List()
|
|
if len(nodes) != 1 || nodes[0].IP() != "2.2.2.2" {
|
|
t.Fatalf("empty insert should not mutate node list: %#v", nodes)
|
|
}
|
|
out, err := h.Build()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got, want := string(out), "2.2.2.2 anchor.test"+lineBreaker; got != want {
|
|
t.Fatalf("empty insert should not change output: got %q want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestEmptyHostsBuildAndSaveAs(t *testing.T) {
|
|
h := NewHosts()
|
|
path := filepath.Join(t.TempDir(), "hosts.empty")
|
|
if err := os.WriteFile(path, nil, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.Parse(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := h.Build()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(out) != 0 {
|
|
t.Fatalf("expected empty build output, got %q", out)
|
|
}
|
|
outPath := filepath.Join(t.TempDir(), "hosts.out")
|
|
if err := h.SaveAs(outPath); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
saved, err := os.ReadFile(outPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(saved) != 0 {
|
|
t.Fatalf("expected empty saved file, got %q", saved)
|
|
}
|
|
}
|
|
|
|
func TestParsePreservesBlankAndRawLines(t *testing.T) {
|
|
h := NewHosts()
|
|
path := filepath.Join(t.TempDir(), "hosts.raw")
|
|
input := []byte("127.0.0.1 localhost\n\nbadline\n# tail comment\n")
|
|
if err := os.WriteFile(path, input, 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := h.Parse(path); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
out, err := h.Build()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
got := string(out)
|
|
if !strings.Contains(got, "127.0.0.1 localhost"+lineBreaker+lineBreaker+"badline"+lineBreaker) {
|
|
t.Fatalf("expected blank/raw lines to be preserved, got %q", got)
|
|
}
|
|
if !strings.Contains(got, "# tail comment"+lineBreaker) {
|
|
t.Fatalf("expected comment line to be preserved, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestHostAccessorsReturnDetachedCopies(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
node, err := h.GetFirstNode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
node.SetIP("9.9.9.9")
|
|
node.SetHosts("mutated.test")
|
|
|
|
if got := h.ListFirstIPByHost("example.test"); got != "1.2.3.4" {
|
|
t.Fatalf("detached copy mutated internal host index: %q", got)
|
|
}
|
|
if got := h.ListFirstIPByHost("mutated.test"); got != "" {
|
|
t.Fatalf("detached copy should not create new host index: %q", got)
|
|
}
|
|
out, err := h.Build()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if strings.Contains(string(out), "9.9.9.9 mutated.test") {
|
|
t.Fatalf("detached copy leaked into build output: %q", out)
|
|
}
|
|
}
|
|
|
|
func TestUpdateNodeRejectsInvalidMutationAndPreservesState(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
node, err := h.GetFirstNode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
node.SetIP("bad-ip")
|
|
if err := h.UpdateNode(node); err == nil {
|
|
t.Fatal("expected invalid update to fail")
|
|
}
|
|
if got := h.ListFirstIPByHost("example.test"); got != "1.2.3.4" {
|
|
t.Fatalf("failed update should preserve previous index, got %q", got)
|
|
}
|
|
out, err := h.Build()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(out), "1.2.3.4 example.test") || strings.Contains(string(out), "bad-ip") {
|
|
t.Fatalf("failed update should preserve previous output, got %q", out)
|
|
}
|
|
}
|
|
|
|
func TestUpdateNodeCommentOnlyStateReindexesCleanly(t *testing.T) {
|
|
h := NewHosts()
|
|
if err := h.AddHosts("1.2.3.4", "example.test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
node, err := h.GetFirstNode()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
node.SetIP("")
|
|
node.SetHosts()
|
|
node.SetComment("note")
|
|
if err := h.UpdateNode(node); err != nil {
|
|
t.Fatalf("comment-only update failed: %v", err)
|
|
}
|
|
if got := h.ListByIP(""); len(got) != 0 {
|
|
t.Fatalf("comment-only node should not be indexed under empty ip: %#v", got)
|
|
}
|
|
updated, err := h.GetNode(node.UID())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !updated.OnlyComment() {
|
|
t.Fatalf("comment-only node should keep onlyComment state: %#v", updated)
|
|
}
|
|
node = updated
|
|
node.SetIP("2.2.2.2")
|
|
node.SetHosts("restored.test")
|
|
if err := h.UpdateNode(node); err != nil {
|
|
t.Fatalf("restoring host entry failed: %v", err)
|
|
}
|
|
updated, err = h.GetNode(node.UID())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if updated.OnlyComment() {
|
|
t.Fatalf("host entry should clear onlyComment after restore: %#v", updated)
|
|
}
|
|
if got := h.ListFirstIPByHost("restored.test"); got != "2.2.2.2" {
|
|
t.Fatalf("restored host index mismatch: %q", got)
|
|
}
|
|
if got := h.ListByIP(""); len(got) != 0 {
|
|
t.Fatalf("restored host should still avoid empty ip index: %#v", got)
|
|
}
|
|
}
|