package tests import ( "os" "path/filepath" "strings" "testing" "hosts-go/internal/core" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestParseHostsFile_StandardEntries(t *testing.T) { tests := []struct { name string content string expectedLen int checks func(t *testing.T, hostsFile *core.HostsFile, warnings []core.ParseWarning) }{ { name: "basic IPv4 entry", content: `127.0.0.1 localhost`, expectedLen: 1, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Len(t, warnings, 0) entry := hf.Entries[0] assert.Equal(t, "127.0.0.1", entry.IP) assert.Equal(t, "localhost", entry.Hostname) assert.Empty(t, entry.Aliases) assert.True(t, entry.Active) assert.Empty(t, entry.Comment) }, }, { name: "entry with multiple aliases", content: `192.168.1.100 example.com www.example.com api.example.com`, expectedLen: 1, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Len(t, warnings, 0) entry := hf.Entries[0] assert.Equal(t, "192.168.1.100", entry.IP) assert.Equal(t, "example.com", entry.Hostname) assert.Equal(t, []string{"www.example.com", "api.example.com"}, entry.Aliases) }, }, { name: "entry with inline comment", content: `127.0.0.1 localhost # Local loopback`, expectedLen: 1, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Len(t, warnings, 0) entry := hf.Entries[0] assert.Equal(t, "127.0.0.1", entry.IP) assert.Equal(t, "localhost", entry.Hostname) assert.Equal(t, "Local loopback", entry.Comment) }, }, { name: "IPv6 entry", content: `::1 localhost`, expectedLen: 1, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Len(t, warnings, 0) entry := hf.Entries[0] assert.Equal(t, "::1", entry.IP) assert.Equal(t, "localhost", entry.Hostname) }, }, { name: "multiple entries", content: `127.0.0.1 localhost 192.168.1.100 example.com ::1 ip6-localhost`, expectedLen: 3, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Len(t, warnings, 0) assert.Equal(t, "127.0.0.1", hf.Entries[0].IP) assert.Equal(t, "192.168.1.100", hf.Entries[1].IP) assert.Equal(t, "::1", hf.Entries[2].IP) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hostsFile, warnings, err := parseHostsContent(tt.content) require.NoError(t, err) assert.Len(t, hostsFile.Entries, tt.expectedLen) tt.checks(t, hostsFile, warnings) }) } } func TestParseHostsFile_CommentsAndDisabled(t *testing.T) { tests := []struct { name string content string expectedEntries int expectedComments int checks func(t *testing.T, hostsFile *core.HostsFile, warnings []core.ParseWarning) }{ { name: "disabled entry (commented out)", content: `# 192.168.1.100 disabled.com`, expectedEntries: 1, expectedComments: 0, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Len(t, warnings, 0) entry := hf.Entries[0] assert.Equal(t, "192.168.1.100", entry.IP) assert.Equal(t, "disabled.com", entry.Hostname) assert.False(t, entry.Active) }, }, { name: "standalone comment", content: `# This is a comment line`, expectedEntries: 0, expectedComments: 1, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Len(t, warnings, 0) assert.Contains(t, hf.Comments, "This is a comment line") }, }, { name: "mixed active, disabled, and comments", content: `# Header comment 127.0.0.1 localhost # 192.168.1.100 disabled.com # disabled server 192.168.1.101 active.com # Another comment`, expectedEntries: 3, expectedComments: 2, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Len(t, warnings, 0) // Check entries assert.True(t, hf.Entries[0].Active) // localhost assert.False(t, hf.Entries[1].Active) // disabled.com assert.True(t, hf.Entries[2].Active) // active.com // Check comments assert.Contains(t, hf.Comments, "Header comment") assert.Contains(t, hf.Comments, "Another comment") // Check disabled entry has comment assert.Equal(t, "disabled server", hf.Entries[1].Comment) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hostsFile, warnings, err := parseHostsContent(tt.content) require.NoError(t, err) assert.Len(t, hostsFile.Entries, tt.expectedEntries) assert.Len(t, hostsFile.Comments, tt.expectedComments) tt.checks(t, hostsFile, warnings) }) } } func TestParseHostsFile_MalformedLines(t *testing.T) { tests := []struct { name string content string expectedEntries int expectedWarnings int checks func(t *testing.T, hostsFile *core.HostsFile, warnings []core.ParseWarning) }{ { name: "invalid IP address", content: `999.999.999.999 invalid-ip.com`, expectedEntries: 0, expectedWarnings: 1, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Contains(t, warnings[0].Message, "invalid IP address") }, }, { name: "missing hostname", content: `192.168.1.100`, expectedEntries: 0, expectedWarnings: 1, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Contains(t, warnings[0].Message, "missing hostname") }, }, { name: "invalid hostname format", content: `192.168.1.100 -invalid-hostname.com`, expectedEntries: 0, expectedWarnings: 1, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Contains(t, warnings[0].Message, "invalid hostname") }, }, { name: "mixed valid and invalid entries", content: `127.0.0.1 localhost 999.999.999.999 invalid.com 192.168.1.100 valid.com`, expectedEntries: 2, expectedWarnings: 1, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Equal(t, "localhost", hf.Entries[0].Hostname) assert.Equal(t, "valid.com", hf.Entries[1].Hostname) assert.Contains(t, warnings[0].Message, "invalid IP address") }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hostsFile, warnings, err := parseHostsContent(tt.content) require.NoError(t, err) assert.Len(t, hostsFile.Entries, tt.expectedEntries) assert.Len(t, warnings, tt.expectedWarnings) if len(warnings) > 0 { tt.checks(t, hostsFile, warnings) } }) } } func TestParseHostsFile_WhitespaceVariations(t *testing.T) { tests := []struct { name string content string checks func(t *testing.T, hostsFile *core.HostsFile) }{ { name: "tabs and spaces mixed", content: "127.0.0.1\tlocalhost \t# comment", checks: func(t *testing.T, hf *core.HostsFile) { entry := hf.Entries[0] assert.Equal(t, "127.0.0.1", entry.IP) assert.Equal(t, "localhost", entry.Hostname) assert.Equal(t, "comment", entry.Comment) }, }, { name: "leading and trailing whitespace", content: " 127.0.0.1 localhost ", checks: func(t *testing.T, hf *core.HostsFile) { entry := hf.Entries[0] assert.Equal(t, "127.0.0.1", entry.IP) assert.Equal(t, "localhost", entry.Hostname) }, }, { name: "empty lines", content: `127.0.0.1 localhost 192.168.1.100 example.com `, checks: func(t *testing.T, hf *core.HostsFile) { assert.Len(t, hf.Entries, 2) assert.Equal(t, "localhost", hf.Entries[0].Hostname) assert.Equal(t, "example.com", hf.Entries[1].Hostname) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hostsFile, warnings, err := parseHostsContent(tt.content) require.NoError(t, err) assert.Len(t, warnings, 0) tt.checks(t, hostsFile) }) } } func TestDetectFormattingStyle(t *testing.T) { tests := []struct { name string content string expectedUseTabs bool expectedSpaces int }{ { name: "tab-separated content", content: `127.0.0.1 localhost 192.168.1.100 example.com`, expectedUseTabs: true, expectedSpaces: 0, }, { name: "space-separated content", content: `127.0.0.1 localhost 192.168.1.100 example.com`, expectedUseTabs: false, expectedSpaces: 4, }, { name: "mixed content (should default to tabs)", content: `127.0.0.1 localhost 192.168.1.100 example.com`, expectedUseTabs: true, expectedSpaces: 0, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { style := core.DetectFormattingStyle(strings.Split(tt.content, "\n")) assert.Equal(t, tt.expectedUseTabs, style.UseTabs) if !tt.expectedUseTabs { assert.Equal(t, tt.expectedSpaces, style.SpacesPerTab) } }) } } func TestWriteHostsFile_RoundTrip(t *testing.T) { originalContent := `# Header comment 127.0.0.1 localhost # Local loopback 192.168.1.100 example.com www.example.com # Development server # 10.0.0.50 staging.com # Disabled staging server # Another comment` // Parse the content hostsFile, warnings, err := parseHostsContent(originalContent) require.NoError(t, err) assert.Len(t, warnings, 0) // Write it back and verify structure is preserved lines := core.FormatHostsFile(hostsFile) reformattedContent := strings.Join(lines, "\n") // Parse again to verify round-trip hostsFile2, warnings2, err2 := parseHostsContent(reformattedContent) require.NoError(t, err2) assert.Len(t, warnings2, 0) // Verify same number of entries and comments assert.Len(t, hostsFile2.Entries, len(hostsFile.Entries)) assert.Len(t, hostsFile2.Comments, len(hostsFile.Comments)) // Verify entry content matches for i, entry := range hostsFile.Entries { entry2 := hostsFile2.Entries[i] assert.Equal(t, entry.IP, entry2.IP) assert.Equal(t, entry.Hostname, entry2.Hostname) assert.Equal(t, entry.Aliases, entry2.Aliases) assert.Equal(t, entry.Comment, entry2.Comment) assert.Equal(t, entry.Active, entry2.Active) } } func TestParseHostsFile_EmptyAndCommentOnlyFiles(t *testing.T) { tests := []struct { name string content string checks func(t *testing.T, hostsFile *core.HostsFile, warnings []core.ParseWarning) }{ { name: "completely empty file", content: "", checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Len(t, hf.Entries, 0) assert.Len(t, hf.Comments, 0) assert.Len(t, warnings, 0) }, }, { name: "only whitespace", content: ` `, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Len(t, hf.Entries, 0) assert.Len(t, hf.Comments, 0) assert.Len(t, warnings, 0) }, }, { name: "only comments", content: `# First comment # Second comment # Third comment`, checks: func(t *testing.T, hf *core.HostsFile, warnings []core.ParseWarning) { assert.Len(t, hf.Entries, 0) assert.Len(t, hf.Comments, 3) assert.Contains(t, hf.Comments, "First comment") assert.Contains(t, hf.Comments, "Second comment") assert.Contains(t, hf.Comments, "Third comment") assert.Len(t, warnings, 0) }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { hostsFile, warnings, err := parseHostsContent(tt.content) require.NoError(t, err) tt.checks(t, hostsFile, warnings) }) } } func TestBackupHostsFile(t *testing.T) { // Create a temporary file to simulate /etc/hosts tmpDir := t.TempDir() hostsPath := filepath.Join(tmpDir, "hosts") hostsContent := `127.0.0.1 localhost 192.168.1.100 example.com` err := os.WriteFile(hostsPath, []byte(hostsContent), 0644) require.NoError(t, err) // Test backup functionality backupPath, err := core.BackupHostsFile(hostsPath) require.NoError(t, err) assert.True(t, strings.Contains(backupPath, "hosts.backup")) // Verify backup file exists and has same content backupContent, err := os.ReadFile(backupPath) require.NoError(t, err) assert.Equal(t, hostsContent, string(backupContent)) // Cleanup os.Remove(backupPath) } // Helper function to parse hosts content from string (for testing) func parseHostsContent(content string) (*core.HostsFile, []core.ParseWarning, error) { lines := strings.Split(content, "\n") return core.ParseHostsContent(lines) }