package hostfile_test import ( "strings" "testing" "dns-helper/hostfile" ) // --------------------------------------------------------------------------- // ParseManagedBlock tests // --------------------------------------------------------------------------- func TestParseManagedBlock_NoManagedBlock(t *testing.T) { lines := []string{ "127.0.0.1 localhost", "# some comment", "1.2.3.4 example.com", } prefix, managed, postfix, has, err := hostfile.ParseManagedBlock(lines) if err != nil { t.Fatalf("unexpected error: %v", err) } if has { t.Fatal("expected HasManagedBlock=false") } if len(managed) != 0 { t.Errorf("expected empty managed, got %v", managed) } if len(postfix) != 0 { t.Errorf("expected empty postfix, got %v", postfix) } if len(prefix) != len(lines) { t.Errorf("expected all lines in prefix, got %v", prefix) } } func TestParseManagedBlock_ValidBlock(t *testing.T) { lines := []string{ "127.0.0.1 localhost", hostfile.StartMarker, "1.1.1.1\thost1", "2.2.2.2\thost2", hostfile.EndMarker, "# trailing comment", } prefix, managed, postfix, has, err := hostfile.ParseManagedBlock(lines) if err != nil { t.Fatalf("unexpected error: %v", err) } if !has { t.Fatal("expected HasManagedBlock=true") } if len(prefix) != 1 || prefix[0] != "127.0.0.1 localhost" { t.Errorf("unexpected prefix: %v", prefix) } if len(managed) != 2 { t.Errorf("expected 2 managed lines, got %v", managed) } if len(postfix) != 1 || postfix[0] != "# trailing comment" { t.Errorf("unexpected postfix: %v", postfix) } } func TestParseManagedBlock_StartMarkerAtLine0(t *testing.T) { lines := []string{ hostfile.StartMarker, "1.1.1.1\thost1", hostfile.EndMarker, } prefix, managed, postfix, has, err := hostfile.ParseManagedBlock(lines) if err != nil { t.Fatalf("unexpected error: %v", err) } if !has { t.Fatal("expected HasManagedBlock=true") } if len(prefix) != 0 { t.Errorf("expected empty prefix when start marker is at line 0, got %v", prefix) } if len(managed) != 1 || managed[0] != "1.1.1.1\thost1" { t.Errorf("unexpected managed: %v", managed) } if len(postfix) != 0 { t.Errorf("expected empty postfix, got %v", postfix) } } func TestParseManagedBlock_CorruptStartWithoutEnd(t *testing.T) { lines := []string{ "127.0.0.1 localhost", hostfile.StartMarker, "1.1.1.1\thost1", } _, _, _, _, err := hostfile.ParseManagedBlock(lines) if err == nil { t.Fatal("expected error for start marker without end marker") } if !strings.Contains(err.Error(), "start marker found") { t.Errorf("error should mention 'start marker found', got: %v", err) } if !strings.Contains(err.Error(), "no end marker") { t.Errorf("error should mention 'no end marker', got: %v", err) } } func TestParseManagedBlock_CorruptEndWithoutStart(t *testing.T) { lines := []string{ "127.0.0.1 localhost", "1.1.1.1\thost1", hostfile.EndMarker, } _, _, _, _, err := hostfile.ParseManagedBlock(lines) if err == nil { t.Fatal("expected error for end marker without start marker") } if !strings.Contains(err.Error(), "end marker found") { t.Errorf("error should mention 'end marker found', got: %v", err) } if !strings.Contains(err.Error(), "no start marker") { t.Errorf("error should mention 'no start marker', got: %v", err) } } func TestParseManagedBlock_CorruptEndBeforeStart(t *testing.T) { lines := []string{ "127.0.0.1 localhost", hostfile.EndMarker, "1.1.1.1\thost1", hostfile.StartMarker, } _, _, _, _, err := hostfile.ParseManagedBlock(lines) if err == nil { t.Fatal("expected error for end marker before start marker") } if !strings.Contains(err.Error(), "end marker") { t.Errorf("error should mention 'end marker', got: %v", err) } if !strings.Contains(err.Error(), "before start marker") { t.Errorf("error should mention 'before start marker', got: %v", err) } } func TestParseManagedBlock_CorruptDuplicateMarkers(t *testing.T) { lines := []string{ hostfile.StartMarker, "1.1.1.1\thost1", hostfile.EndMarker, hostfile.StartMarker, // duplicate "2.2.2.2\thost2", hostfile.EndMarker, } _, _, _, _, err := hostfile.ParseManagedBlock(lines) if err == nil { t.Fatal("expected error for duplicate markers") } if !strings.Contains(err.Error(), "duplicate") { t.Errorf("error should mention 'duplicate', got: %v", err) } } // --------------------------------------------------------------------------- // AddEntries tests // --------------------------------------------------------------------------- func TestAddEntries_AddsNewEntries(t *testing.T) { existing := []string{"1.1.1.1\thost1"} entries := []hostfile.DNSEntry{{IP: "2.2.2.2", Hostname: "host2"}} result := hostfile.AddEntries(existing, entries) if len(result) != 2 { t.Fatalf("expected 2 entries, got %d: %v", len(result), result) } if result[0] != "1.1.1.1\thost1" { t.Errorf("unexpected entry[0]: %q", result[0]) } if result[1] != "2.2.2.2\thost2" { t.Errorf("unexpected entry[1]: %q", result[1]) } } func TestAddEntries_ReplacesExistingHostname(t *testing.T) { existing := []string{"1.1.1.1\thost1", "2.2.2.2\thost2"} // Replace host1 with a new IP entries := []hostfile.DNSEntry{{IP: "9.9.9.9", Hostname: "host1"}} result := hostfile.AddEntries(existing, entries) // host1 should be replaced, host2 preserved if len(result) != 2 { t.Fatalf("expected 2 entries, got %d: %v", len(result), result) } found := false for _, line := range result { if strings.Contains(line, "1.1.1.1") && strings.Contains(line, "host1") { t.Errorf("old entry for host1 should have been replaced: %v", result) } if strings.Contains(line, "9.9.9.9") && strings.Contains(line, "host1") { found = true } } if !found { t.Errorf("new entry for host1 not found: %v", result) } } func TestAddEntries_Deduplicates(t *testing.T) { existing := []string{"1.1.1.1\thost1"} entries := []hostfile.DNSEntry{ {IP: "2.2.2.2", Hostname: "host2"}, {IP: "2.2.2.2", Hostname: "host2"}, // duplicate } result := hostfile.AddEntries(existing, entries) count := 0 for _, line := range result { if line == "2.2.2.2\thost2" { count++ } } if count != 1 { t.Errorf("expected exactly 1 host2 entry after dedup, got %d: %v", count, result) } } // --------------------------------------------------------------------------- // AssembleContent tests // --------------------------------------------------------------------------- func TestAssembleContent_WithManagedBlock(t *testing.T) { prefix := []string{"127.0.0.1 localhost"} managed := []string{"1.1.1.1\thost1"} postfix := []string{"# end"} result := hostfile.AssembleContent(prefix, managed, postfix) expected := []string{ "127.0.0.1 localhost", hostfile.StartMarker, "1.1.1.1\thost1", hostfile.EndMarker, "# end", } if len(result) != len(expected) { t.Fatalf("expected %d lines, got %d: %v", len(expected), len(result), result) } for i, line := range expected { if result[i] != line { t.Errorf("line %d: expected %q, got %q", i, line, result[i]) } } } func TestAssembleContent_EmptyManagedOmitsBlock(t *testing.T) { prefix := []string{"127.0.0.1 localhost"} postfix := []string{"# end"} result := hostfile.AssembleContent(prefix, nil, postfix) if len(result) != 2 { t.Fatalf("expected 2 lines (no managed block), got %d: %v", len(result), result) } for _, line := range result { if strings.Contains(line, "DNSHelper") { t.Errorf("managed block markers should be absent when managed is empty: %v", result) } } } // --------------------------------------------------------------------------- // RemoveByHostname tests (T018) // --------------------------------------------------------------------------- func TestRemoveByHostname_SingleHost(t *testing.T) { existing := []string{ "1.1.1.1\thost1", "2.2.2.2\thost2", } result := hostfile.RemoveByHostname(existing, []string{"host1"}) if len(result) != 1 { t.Fatalf("expected 1 entry, got %d: %v", len(result), result) } if result[0] != "2.2.2.2\thost2" { t.Errorf("expected host2 entry to remain, got %q", result[0]) } } func TestRemoveByHostname_MultipleHosts(t *testing.T) { existing := []string{ "1.1.1.1\thost1", "2.2.2.2\thost2", "3.3.3.3\thost3", } result := hostfile.RemoveByHostname(existing, []string{"host1", "host2"}) if len(result) != 1 { t.Fatalf("expected 1 entry, got %d: %v", len(result), result) } if result[0] != "3.3.3.3\thost3" { t.Errorf("expected host3 entry to remain, got %q", result[0]) } } func TestRemoveByHostname_HostNotFound(t *testing.T) { existing := []string{ "1.1.1.1\thost1", } result := hostfile.RemoveByHostname(existing, []string{"host2"}) if len(result) != 1 { t.Fatalf("expected content unchanged (1 entry), got %d: %v", len(result), result) } if result[0] != "1.1.1.1\thost1" { t.Errorf("expected host1 entry unchanged, got %q", result[0]) } } func TestRemoveByHostname_AllHostsRemoved(t *testing.T) { existing := []string{ "1.1.1.1\thost1", } result := hostfile.RemoveByHostname(existing, []string{"host1"}) if len(result) != 0 { t.Errorf("expected empty slice, got %d entries: %v", len(result), result) } } func TestRemoveByHostname_RegressionNoDuplicates(t *testing.T) { // This is the R-004 regression test. The legacy code's loop-order bug // produced duplicates when removing multiple hosts at once. existing := []string{ "1.1.1.1\thost1", "2.2.2.2\thost2", "3.3.3.3\thost3", } result := hostfile.RemoveByHostname(existing, []string{"host1", "host2"}) if len(result) != 1 { t.Fatalf("expected exactly 1 entry after multi-host removal, got %d: %v", len(result), result) } if result[0] != "3.3.3.3\thost3" { t.Errorf("expected only host3 to remain, got %q", result[0]) } } // --------------------------------------------------------------------------- // RemoveAll tests (T018) // --------------------------------------------------------------------------- func TestRemoveAll_ReturnEmptySlice(t *testing.T) { result := hostfile.RemoveAll() if len(result) != 0 { t.Errorf("expected empty result, got %d entries: %v", len(result), result) } } func TestRemoveAll_AssembleContentOmitsBlock(t *testing.T) { // When RemoveAll result is passed to AssembleContent, no managed block markers // should appear in the assembled output. prefix := []string{"127.0.0.1 localhost"} postfix := []string{"# trailing"} result := hostfile.AssembleContent(prefix, hostfile.RemoveAll(), postfix) if len(result) != 2 { t.Fatalf("expected 2 lines with no managed block, got %d: %v", len(result), result) } for _, line := range result { if strings.Contains(line, "DNSHelper") { t.Errorf("managed block markers should be absent after RemoveAll: %v", result) } } }