feat: Implement platform-specific hosts file path retrieval
- Created a new `platform` package to handle OS-specific hosts file paths. - Added implementations for macOS, Linux, and Windows to retrieve the hosts file path. - Introduced tests for the `GetHostsFilePath` function to ensure correct behavior across platforms. feat: Add DNS resolver for IP lookups - Implemented a `resolver` package for performing DNS lookups. - Created a `DNSResolver` struct with a `LookupIP` method to handle A-record lookups and CNAME resolution. - Added comprehensive tests for various DNS scenarios, including CNAME chains and error handling. chore: Refactor project structure and update dependencies - Restructured the project to follow Go's standard package layout. - Updated `go.mod` to Go 1.20 and added `golang.org/x/net` dependency. - Removed old source files and ensured a clean build with all tests passing.
This commit is contained in:
@@ -0,0 +1,360 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user