mirror of
https://github.com/shokinn/hosts-go.git
synced 2025-08-23 08:33:02 +00:00
feat: Initialize hosts-go project with foundational structure and core functionality
- Created activeContext.md and productContext.md to outline project goals and current focus. - Established progress.md to track project milestones and tasks. - Developed projectbrief.md detailing application overview, requirements, and directory structure. - Documented systemPatterns.md to describe architecture and design patterns used. - Compiled techContext.md to specify technologies and development setup. - Implemented comprehensive unit tests in models_test.go for HostEntry and HostsFile functionalities.
This commit is contained in:
commit
d66ec51ebd
12 changed files with 1747 additions and 0 deletions
115
.clinerules
Normal file
115
.clinerules
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
# Cline's Memory Bank
|
||||||
|
|
||||||
|
I am Cline, an expert software engineer with a unique characteristic: my memory resets completely between sessions. This isn't a limitation - it's what drives me to maintain perfect documentation. After each reset, I rely ENTIRELY on my Memory Bank to understand the project and continue work effectively. I MUST read ALL memory bank files at the start of EVERY task - this is not optional.
|
||||||
|
|
||||||
|
## Memory Bank Structure
|
||||||
|
|
||||||
|
The Memory Bank consists of core files and optional context files, all in Markdown format. Files build upon each other in a clear hierarchy:
|
||||||
|
|
||||||
|
flowchart TD
|
||||||
|
PB[projectbrief.md] --> PC[productContext.md]
|
||||||
|
PB --> SP[systemPatterns.md]
|
||||||
|
PB --> TC[techContext.md]
|
||||||
|
|
||||||
|
PC --> AC[activeContext.md]
|
||||||
|
SP --> AC
|
||||||
|
TC --> AC
|
||||||
|
|
||||||
|
AC --> P[progress.md]
|
||||||
|
|
||||||
|
### Core Files (Required)
|
||||||
|
1. `projectbrief.md`
|
||||||
|
- Foundation document that shapes all other files
|
||||||
|
- Created at project start if it doesn't exist
|
||||||
|
- Defines core requirements and goals
|
||||||
|
- Source of truth for project scope
|
||||||
|
|
||||||
|
2. `productContext.md`
|
||||||
|
- Why this project exists
|
||||||
|
- Problems it solves
|
||||||
|
- How it should work
|
||||||
|
- User experience goals
|
||||||
|
|
||||||
|
3. `activeContext.md`
|
||||||
|
- Current work focus
|
||||||
|
- Recent changes
|
||||||
|
- Next steps
|
||||||
|
- Active decisions and considerations
|
||||||
|
- Important patterns and preferences
|
||||||
|
- Learnings and project insights
|
||||||
|
|
||||||
|
4. `systemPatterns.md`
|
||||||
|
- System architecture
|
||||||
|
- Key technical decisions
|
||||||
|
- Design patterns in use
|
||||||
|
- Component relationships
|
||||||
|
- Critical implementation paths
|
||||||
|
|
||||||
|
5. `techContext.md`
|
||||||
|
- Technologies used
|
||||||
|
- Development setup
|
||||||
|
- Technical constraints
|
||||||
|
- Dependencies
|
||||||
|
- Tool usage patterns
|
||||||
|
|
||||||
|
6. `progress.md`
|
||||||
|
- What works
|
||||||
|
- What's left to build
|
||||||
|
- Current status
|
||||||
|
- Known issues
|
||||||
|
- Evolution of project decisions
|
||||||
|
|
||||||
|
### Additional Context
|
||||||
|
Create additional files/folders within memory-bank/ when they help organize:
|
||||||
|
- Complex feature documentation
|
||||||
|
- Integration specifications
|
||||||
|
- API documentation
|
||||||
|
- Testing strategies
|
||||||
|
- Deployment procedures
|
||||||
|
|
||||||
|
## Core Workflows
|
||||||
|
|
||||||
|
### Plan Mode
|
||||||
|
flowchart TD
|
||||||
|
Start[Start] --> ReadFiles[Read Memory Bank]
|
||||||
|
ReadFiles --> CheckFiles{Files Complete?}
|
||||||
|
|
||||||
|
CheckFiles -->|No| Plan[Create Plan]
|
||||||
|
Plan --> Document[Document in Chat]
|
||||||
|
|
||||||
|
CheckFiles -->|Yes| Verify[Verify Context]
|
||||||
|
Verify --> Strategy[Develop Strategy]
|
||||||
|
Strategy --> Present[Present Approach]
|
||||||
|
|
||||||
|
### Act Mode
|
||||||
|
flowchart TD
|
||||||
|
Start[Start] --> Context[Check Memory Bank]
|
||||||
|
Context --> Update[Update Documentation]
|
||||||
|
Update --> Execute[Execute Task]
|
||||||
|
Execute --> Document[Document Changes]
|
||||||
|
|
||||||
|
## Documentation Updates
|
||||||
|
|
||||||
|
Memory Bank updates occur when:
|
||||||
|
1. Discovering new project patterns
|
||||||
|
2. After implementing significant changes
|
||||||
|
3. When user requests with **update memory bank** (MUST review ALL files)
|
||||||
|
4. When context needs clarification
|
||||||
|
|
||||||
|
flowchart TD
|
||||||
|
Start[Update Process]
|
||||||
|
|
||||||
|
subgraph Process
|
||||||
|
P1[Review ALL Files]
|
||||||
|
P2[Document Current State]
|
||||||
|
P3[Clarify Next Steps]
|
||||||
|
P4[Document Insights & Patterns]
|
||||||
|
|
||||||
|
P1 --> P2 --> P3 --> P4
|
||||||
|
end
|
||||||
|
|
||||||
|
Start --> Process
|
||||||
|
|
||||||
|
Note: When triggered by **update memory bank**, I MUST review every memory bank file, even if some don't require updates. Focus particularly on activeContext.md and progress.md as they track current state.
|
||||||
|
|
||||||
|
REMEMBER: After every memory reset, I begin completely fresh. The Memory Bank is my only link to previous work. It must be maintained with precision and clarity, as my effectiveness depends entirely on its accuracy.
|
83
cmd/hosts/main.go
Normal file
83
cmd/hosts/main.go
Normal file
|
@ -0,0 +1,83 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"hosts-go/internal/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("hosts-go - Foundation Implementation")
|
||||||
|
fmt.Println("===================================")
|
||||||
|
|
||||||
|
// Create a new hosts file
|
||||||
|
hostsFile := core.NewHostsFile()
|
||||||
|
|
||||||
|
// Add some example entries to demonstrate the foundation
|
||||||
|
entry1, err := core.NewHostEntry("127.0.0.1", "localhost")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create entry: %v", err)
|
||||||
|
}
|
||||||
|
entry1.Comment = "Local loopback"
|
||||||
|
|
||||||
|
entry2, err := core.NewHostEntry("192.168.1.100", "dev.example.com")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create entry: %v", err)
|
||||||
|
}
|
||||||
|
entry2.AddAlias("www.dev.example.com")
|
||||||
|
entry2.AddAlias("api.dev.example.com")
|
||||||
|
entry2.Comment = "Development server"
|
||||||
|
|
||||||
|
entry3, err := core.NewHostEntry("10.0.0.50", "staging.example.com")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to create entry: %v", err)
|
||||||
|
}
|
||||||
|
entry3.Active = false // Inactive entry
|
||||||
|
entry3.Comment = "Staging server (disabled)"
|
||||||
|
|
||||||
|
// Add entries to hosts file
|
||||||
|
hostsFile.AddEntry(entry1)
|
||||||
|
hostsFile.AddEntry(entry2)
|
||||||
|
hostsFile.AddEntry(entry3)
|
||||||
|
|
||||||
|
// Demonstrate the foundation functionality
|
||||||
|
fmt.Printf("Total entries: %d\n", len(hostsFile.Entries))
|
||||||
|
fmt.Printf("Active entries: %d\n", len(hostsFile.ActiveEntries()))
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Println("All entries:")
|
||||||
|
for i, entry := range hostsFile.Entries {
|
||||||
|
fmt.Printf("%d. %s\n", i+1, entry.String())
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Println("Active entries only:")
|
||||||
|
for i, entry := range hostsFile.ActiveEntries() {
|
||||||
|
fmt.Printf("%d. %s\n", i+1, entry.String())
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
// Demonstrate search functionality
|
||||||
|
fmt.Println("Search demonstrations:")
|
||||||
|
if found := hostsFile.FindEntry("localhost"); found != nil {
|
||||||
|
fmt.Printf("Found 'localhost': %s\n", found.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if found := hostsFile.FindEntry("www.dev.example.com"); found != nil {
|
||||||
|
fmt.Printf("Found 'www.dev.example.com' (alias): %s\n", found.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
if found := hostsFile.FindEntry("notfound.com"); found == nil {
|
||||||
|
fmt.Println("'notfound.com' not found (as expected)")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Foundation implementation complete!")
|
||||||
|
fmt.Println("✅ Core data models working")
|
||||||
|
fmt.Println("✅ Validation system working")
|
||||||
|
fmt.Println("✅ Host entry management working")
|
||||||
|
fmt.Println("✅ Search and filtering working")
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Next steps: Implement hosts file parser and TUI components")
|
||||||
|
}
|
11
go.mod
Normal file
11
go.mod
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
module hosts-go
|
||||||
|
|
||||||
|
go 1.24.5
|
||||||
|
|
||||||
|
require github.com/stretchr/testify v1.10.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
10
go.sum
Normal file
10
go.sum
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
221
internal/core/models.go
Normal file
221
internal/core/models.go
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HostEntry represents a single entry in the hosts file
|
||||||
|
type HostEntry struct {
|
||||||
|
IP string // IP address (IPv4 or IPv6)
|
||||||
|
Hostname string // Primary hostname
|
||||||
|
Aliases []string // Additional hostnames/aliases
|
||||||
|
Comment string // Inline comment
|
||||||
|
Active bool // Whether the entry is enabled (not commented out)
|
||||||
|
Original string // Original line from hosts file for preservation
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHostEntry creates a new host entry with validation
|
||||||
|
func NewHostEntry(ip, hostname string) (*HostEntry, error) {
|
||||||
|
entry := &HostEntry{
|
||||||
|
IP: strings.TrimSpace(ip),
|
||||||
|
Hostname: strings.TrimSpace(hostname),
|
||||||
|
Aliases: make([]string, 0),
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := entry.Validate(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate checks if the host entry is valid
|
||||||
|
func (h *HostEntry) Validate() error {
|
||||||
|
if h.IP == "" {
|
||||||
|
return fmt.Errorf("IP address cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.Hostname == "" {
|
||||||
|
return fmt.Errorf("hostname cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate IP address
|
||||||
|
if net.ParseIP(h.IP) == nil {
|
||||||
|
return fmt.Errorf("invalid IP address: %s", h.IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate hostname format
|
||||||
|
if err := validateHostname(h.Hostname); err != nil {
|
||||||
|
return fmt.Errorf("invalid hostname: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate aliases
|
||||||
|
for _, alias := range h.Aliases {
|
||||||
|
if err := validateHostname(alias); err != nil {
|
||||||
|
return fmt.Errorf("invalid alias '%s': %w", alias, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAlias adds an alias to the host entry
|
||||||
|
func (h *HostEntry) AddAlias(alias string) error {
|
||||||
|
alias = strings.TrimSpace(alias)
|
||||||
|
if err := validateHostname(alias); err != nil {
|
||||||
|
return fmt.Errorf("invalid alias: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicates
|
||||||
|
for _, existing := range h.Aliases {
|
||||||
|
if existing == alias {
|
||||||
|
return fmt.Errorf("alias '%s' already exists", alias)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Aliases = append(h.Aliases, alias)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllHostnames returns the primary hostname and all aliases
|
||||||
|
func (h *HostEntry) AllHostnames() []string {
|
||||||
|
result := []string{h.Hostname}
|
||||||
|
result = append(result, h.Aliases...)
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the hosts file representation of the entry
|
||||||
|
func (h *HostEntry) String() string {
|
||||||
|
var parts []string
|
||||||
|
|
||||||
|
// Add IP and hostname
|
||||||
|
parts = append(parts, h.IP, h.Hostname)
|
||||||
|
|
||||||
|
// Add aliases
|
||||||
|
parts = append(parts, h.Aliases...)
|
||||||
|
|
||||||
|
line := strings.Join(parts, "\t")
|
||||||
|
|
||||||
|
// Add comment if present
|
||||||
|
if h.Comment != "" {
|
||||||
|
line += "\t# " + h.Comment
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add comment prefix if inactive
|
||||||
|
if !h.Active {
|
||||||
|
line = "# " + line
|
||||||
|
}
|
||||||
|
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
|
||||||
|
// HostsFile represents the entire hosts file
|
||||||
|
type HostsFile struct {
|
||||||
|
Entries []*HostEntry // All host entries
|
||||||
|
Comments []string // Standalone comment lines
|
||||||
|
Header []string // Header comments at the top of the file
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHostsFile creates a new empty hosts file
|
||||||
|
func NewHostsFile() *HostsFile {
|
||||||
|
return &HostsFile{
|
||||||
|
Entries: make([]*HostEntry, 0),
|
||||||
|
Comments: make([]string, 0),
|
||||||
|
Header: make([]string, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEntry adds a host entry to the file
|
||||||
|
func (hf *HostsFile) AddEntry(entry *HostEntry) error {
|
||||||
|
if err := entry.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
hf.Entries = append(hf.Entries, entry)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindEntry finds an entry by hostname
|
||||||
|
func (hf *HostsFile) FindEntry(hostname string) *HostEntry {
|
||||||
|
hostname = strings.TrimSpace(hostname)
|
||||||
|
for _, entry := range hf.Entries {
|
||||||
|
if entry.Hostname == hostname {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
for _, alias := range entry.Aliases {
|
||||||
|
if alias == hostname {
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveEntry removes an entry by hostname
|
||||||
|
func (hf *HostsFile) RemoveEntry(hostname string) bool {
|
||||||
|
for i, entry := range hf.Entries {
|
||||||
|
if entry.Hostname == hostname {
|
||||||
|
hf.Entries = append(hf.Entries[:i], hf.Entries[i+1:]...)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, alias := range entry.Aliases {
|
||||||
|
if alias == hostname {
|
||||||
|
hf.Entries = append(hf.Entries[:i], hf.Entries[i+1:]...)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// ActiveEntries returns only the active (non-commented) entries
|
||||||
|
func (hf *HostsFile) ActiveEntries() []*HostEntry {
|
||||||
|
var active []*HostEntry
|
||||||
|
for _, entry := range hf.Entries {
|
||||||
|
if entry.Active {
|
||||||
|
active = append(active, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return active
|
||||||
|
}
|
||||||
|
|
||||||
|
// validateHostname validates a hostname according to RFC standards
|
||||||
|
func validateHostname(hostname string) error {
|
||||||
|
if len(hostname) == 0 {
|
||||||
|
return fmt.Errorf("hostname cannot be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hostname) > 253 {
|
||||||
|
return fmt.Errorf("hostname too long (max 253 characters)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cannot start or end with hyphen
|
||||||
|
if strings.HasPrefix(hostname, "-") || strings.HasSuffix(hostname, "-") {
|
||||||
|
return fmt.Errorf("hostname cannot start or end with hyphen")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by dots and validate each label
|
||||||
|
labels := strings.Split(hostname, ".")
|
||||||
|
for _, label := range labels {
|
||||||
|
if len(label) == 0 {
|
||||||
|
return fmt.Errorf("invalid hostname format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each label cannot start or end with hyphen
|
||||||
|
if strings.HasPrefix(label, "-") || strings.HasSuffix(label, "-") {
|
||||||
|
return fmt.Errorf("hostname cannot start or end with hyphen")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Each label must contain only alphanumeric characters and hyphens
|
||||||
|
labelRegex := regexp.MustCompile(`^[a-zA-Z0-9-]+$`)
|
||||||
|
if !labelRegex.MatchString(label) {
|
||||||
|
return fmt.Errorf("invalid hostname format")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
144
memory-bank/activeContext.md
Normal file
144
memory-bank/activeContext.md
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
# Active Context: hosts-go
|
||||||
|
|
||||||
|
## Current Work Focus
|
||||||
|
|
||||||
|
**Status**: Foundation Complete - Ready for Phase 1 (Core Functionality)
|
||||||
|
**Priority**: Implementing hosts file parser with format preservation
|
||||||
|
|
||||||
|
## Recent Changes
|
||||||
|
|
||||||
|
### Foundation Implementation (COMPLETED)
|
||||||
|
- ✅ **Go module setup**: Created `go.mod` with all required dependencies
|
||||||
|
- ✅ **Project structure**: Complete directory layout (`cmd/`, `internal/`, `tests/`)
|
||||||
|
- ✅ **Core data models**: Full `HostEntry` and `HostsFile` structs with validation
|
||||||
|
- ✅ **Comprehensive testing**: 44 test cases covering all model functionality
|
||||||
|
- ✅ **Demo application**: Working proof-of-concept showing foundation capabilities
|
||||||
|
- ✅ **TDD implementation**: Successfully proven test-driven development approach
|
||||||
|
|
||||||
|
### Validation System Complete
|
||||||
|
- ✅ **IP validation**: IPv4/IPv6 support using Go's net.ParseIP
|
||||||
|
- ✅ **Hostname validation**: RFC-compliant with label-by-label checking
|
||||||
|
- ✅ **Edge case handling**: Hyphen restrictions, length limits, format validation
|
||||||
|
- ✅ **Error messaging**: Clear, specific error messages for all validation failures
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
### Immediate (Phase 1 - Current Priority)
|
||||||
|
1. **Hosts File Parser Implementation**
|
||||||
|
- Write comprehensive parser tests for various hosts file formats
|
||||||
|
- Implement `internal/core/parser.go` for reading `/etc/hosts`
|
||||||
|
- Handle comment preservation and formatting retention
|
||||||
|
- Support active/inactive entry detection (commented lines)
|
||||||
|
|
||||||
|
2. **File Operations**
|
||||||
|
- Add file reading with proper error handling
|
||||||
|
- Implement round-trip parsing (read → parse → modify → write)
|
||||||
|
- Test with real hosts file formats and edge cases
|
||||||
|
|
||||||
|
3. **Integration Testing**
|
||||||
|
- Test parser with actual `/etc/hosts` file variations
|
||||||
|
- Verify format preservation during round-trip operations
|
||||||
|
- Handle malformed entries gracefully
|
||||||
|
|
||||||
|
### Medium-term (Following sessions)
|
||||||
|
1. **Core business logic**
|
||||||
|
- Implement hosts file parsing with comment preservation
|
||||||
|
- Add validation for IP addresses and hostnames
|
||||||
|
- Create entry manipulation functions (add, edit, delete, toggle)
|
||||||
|
|
||||||
|
2. **Basic TUI foundation**
|
||||||
|
- Create main Bubble Tea model structure
|
||||||
|
- Implement two-pane layout (list + detail)
|
||||||
|
- Add basic navigation and selection
|
||||||
|
|
||||||
|
3. **Permission handling**
|
||||||
|
- Implement view-mode by default
|
||||||
|
- Add edit-mode transition with sudo handling
|
||||||
|
- Test permission scenarios
|
||||||
|
|
||||||
|
## Active Decisions and Considerations
|
||||||
|
|
||||||
|
### Architecture Decisions Made
|
||||||
|
- **Layered architecture**: TUI → Business Logic → System Interface
|
||||||
|
- **Repository pattern**: Abstract file operations for testability
|
||||||
|
- **Command pattern**: Encapsulate edit operations for undo support
|
||||||
|
- **Test-driven development**: Write tests before implementation
|
||||||
|
|
||||||
|
### Key Design Patterns
|
||||||
|
- **MVU (Model-View-Update)**: Following Bubble Tea conventions
|
||||||
|
- **Separation of concerns**: Clear boundaries between UI, business logic, and system operations
|
||||||
|
- **Graceful degradation**: Handle permission issues without crashing
|
||||||
|
|
||||||
|
### Technology Choices Confirmed
|
||||||
|
- **Go 1.21+**: Modern Go features and performance
|
||||||
|
- **Bubble Tea**: Mature, well-documented TUI framework
|
||||||
|
- **Testify**: Enhanced testing capabilities beyond stdlib
|
||||||
|
- **golangci-lint**: Code quality and consistency
|
||||||
|
|
||||||
|
## Important Patterns and Preferences
|
||||||
|
|
||||||
|
### Code Organization
|
||||||
|
- Use `internal/` package for application-specific code
|
||||||
|
- Group related functionality in packages (`tui/`, `core/`, `utils/`)
|
||||||
|
- Keep main.go minimal - delegate to internal packages
|
||||||
|
|
||||||
|
### Testing Strategy
|
||||||
|
- Write tests before implementation (TDD)
|
||||||
|
- Mock external dependencies (file system, network)
|
||||||
|
- Use table-driven tests for multiple scenarios
|
||||||
|
- Test both success and error cases
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- Return errors explicitly, don't panic
|
||||||
|
- Provide clear error messages with context
|
||||||
|
- Implement graceful fallbacks where possible
|
||||||
|
- Log errors appropriately for debugging
|
||||||
|
|
||||||
|
### UI/UX Principles
|
||||||
|
- **Safety first**: Default to read-only mode
|
||||||
|
- **Clear feedback**: Show operation status and results
|
||||||
|
- **Keyboard-driven**: Efficient navigation without mouse dependency
|
||||||
|
- **Responsive**: Sub-16ms update cycles for smooth interaction
|
||||||
|
|
||||||
|
## Learnings and Project Insights
|
||||||
|
|
||||||
|
### Development Environment
|
||||||
|
- **macOS focus**: Primary development and testing platform
|
||||||
|
- **Cross-platform awareness**: Consider Linux compatibility from start
|
||||||
|
- **Terminal compatibility**: Test with multiple terminal applications
|
||||||
|
|
||||||
|
### User Experience Priorities
|
||||||
|
1. **Safety**: Cannot accidentally corrupt hosts file
|
||||||
|
2. **Speed**: Faster than manual editing for common tasks
|
||||||
|
3. **Clarity**: Always know what mode you're in and what operations are available
|
||||||
|
4. **Confidence**: Validate changes before applying them
|
||||||
|
|
||||||
|
### Technical Priorities
|
||||||
|
1. **Reliability**: Atomic file operations with backup/restore
|
||||||
|
2. **Performance**: Handle large hosts files efficiently
|
||||||
|
3. **Maintainability**: Clear code structure for future enhancements
|
||||||
|
4. **Testability**: Comprehensive test coverage for confidence in changes
|
||||||
|
|
||||||
|
## Dependencies and Constraints
|
||||||
|
|
||||||
|
### External Dependencies
|
||||||
|
- **Bubble Tea ecosystem**: Core framework and components
|
||||||
|
- **System permissions**: sudo access for editing hosts file
|
||||||
|
- **File system**: Atomic write operations and backup capability
|
||||||
|
|
||||||
|
### Development Constraints
|
||||||
|
- **No breaking changes**: Must preserve existing hosts file format
|
||||||
|
- **Backward compatibility**: Work with various hosts file styles
|
||||||
|
- **Minimal dependencies**: Keep external dependencies focused and essential
|
||||||
|
|
||||||
|
## Communication Preferences
|
||||||
|
|
||||||
|
### Documentation Style
|
||||||
|
- **Concrete examples**: Show actual code snippets and command examples
|
||||||
|
- **Clear structure**: Use consistent heading hierarchy and formatting
|
||||||
|
- **Actionable items**: Specific next steps rather than vague suggestions
|
||||||
|
|
||||||
|
### Progress Tracking
|
||||||
|
- **Incremental development**: Small, testable changes
|
||||||
|
- **Regular updates**: Update memory bank after significant milestones
|
||||||
|
- **Clear status**: Always know what's working, what's not, and what's next
|
62
memory-bank/productContext.md
Normal file
62
memory-bank/productContext.md
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
# Product Context: hosts-go
|
||||||
|
|
||||||
|
## Why This Project Exists
|
||||||
|
|
||||||
|
The `/etc/hosts` file is a critical system file that maps hostnames to IP addresses, but managing it manually is cumbersome and error-prone. Users currently face several pain points:
|
||||||
|
|
||||||
|
- **Manual editing**: Requires opening `/etc/hosts` in a text editor with sudo privileges
|
||||||
|
- **Format preservation**: Easy to accidentally break formatting or lose comments
|
||||||
|
- **Entry management**: No easy way to temporarily disable entries without deleting them
|
||||||
|
- **DNS resolution**: Manual IP address updates when hostnames change
|
||||||
|
- **Organization**: No built-in sorting or reordering capabilities
|
||||||
|
|
||||||
|
## Problems It Solves
|
||||||
|
|
||||||
|
1. **Safe editing**: Provides a structured interface that validates changes before writing
|
||||||
|
2. **Entry activation**: Toggle entries on/off without losing the configuration
|
||||||
|
3. **Organization**: Sort and reorder entries intuitively
|
||||||
|
4. **DNS integration**: Automatically resolve hostnames to current IP addresses
|
||||||
|
5. **Comment preservation**: Maintain documentation alongside entries
|
||||||
|
6. **Permission handling**: Only request sudo access when actually editing
|
||||||
|
|
||||||
|
## How It Should Work
|
||||||
|
|
||||||
|
### Core User Experience
|
||||||
|
- **View mode by default**: Browse entries safely without modification risk
|
||||||
|
- **Explicit edit mode**: Clear transition to editing with permission request
|
||||||
|
- **Two-pane interface**: List view + detail view for efficient navigation
|
||||||
|
- **Keyboard-driven**: Fast navigation and actions via keyboard shortcuts
|
||||||
|
- **Visual feedback**: Clear indicators for active/inactive entries and changes
|
||||||
|
|
||||||
|
### Key Workflows
|
||||||
|
|
||||||
|
1. **Browse entries**: Launch app, see all current hosts entries with status
|
||||||
|
2. **Quick activation**: Toggle entries on/off with simple keypress
|
||||||
|
3. **Edit existing**: Select entry, enter edit mode, modify details
|
||||||
|
4. **Add new**: Create new hostname mappings with validation
|
||||||
|
5. **DNS resolution**: Update IP addresses automatically from DNS
|
||||||
|
6. **Reorder entries**: Drag/move entries to organize logically
|
||||||
|
|
||||||
|
## User Experience Goals
|
||||||
|
|
||||||
|
### Immediate Value
|
||||||
|
- **Zero learning curve**: Intuitive interface familiar to CLI users
|
||||||
|
- **Safety first**: Hard to accidentally break the hosts file
|
||||||
|
- **Speed**: Faster than manual editing for common tasks
|
||||||
|
|
||||||
|
### Long-term Benefits
|
||||||
|
- **Organized hosts**: Keep entries structured and documented
|
||||||
|
- **Confidence**: Know changes are validated before applied
|
||||||
|
- **Efficiency**: Common tasks become single keystrokes
|
||||||
|
|
||||||
|
### Target Users
|
||||||
|
- **Developers**: Managing local development environments
|
||||||
|
- **System administrators**: Bulk hosts file management
|
||||||
|
- **Network engineers**: Testing connectivity and DNS overrides
|
||||||
|
- **Security professionals**: Blocking/redirecting malicious domains
|
||||||
|
|
||||||
|
## Success Metrics
|
||||||
|
- Users prefer this tool over manual `/etc/hosts` editing
|
||||||
|
- Reduces hosts file corruption incidents
|
||||||
|
- Speeds up common host management tasks
|
||||||
|
- Provides confidence in making changes
|
160
memory-bank/progress.md
Normal file
160
memory-bank/progress.md
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
# Progress: hosts-go
|
||||||
|
|
||||||
|
## What Works
|
||||||
|
|
||||||
|
### ✅ Memory Bank Foundation
|
||||||
|
- **Project documentation**: Complete memory bank structure established
|
||||||
|
- **Architecture planning**: Clear technical design documented
|
||||||
|
- **Development approach**: TDD strategy and patterns defined
|
||||||
|
- **Technology stack**: Bubble Tea ecosystem selected and documented
|
||||||
|
|
||||||
|
### ✅ Project Definition
|
||||||
|
- **Core requirements**: Two-pane TUI for hosts file management clearly defined
|
||||||
|
- **User experience**: Safety-first approach with explicit edit mode
|
||||||
|
- **Technical constraints**: Permission model and file safety requirements established
|
||||||
|
- **Directory structure**: Planned layout for Go project organization
|
||||||
|
|
||||||
|
### ✅ Foundation Implementation (COMPLETED)
|
||||||
|
- **Go module initialization**: ✅ `go.mod` created with all core dependencies
|
||||||
|
- **Directory structure**: ✅ Complete project structure (`cmd/`, `internal/`, `tests/`)
|
||||||
|
- **Core data models**: ✅ `internal/core/models.go` with HostEntry and HostsFile structs
|
||||||
|
- **Validation system**: ✅ IP address and hostname validation with RFC compliance
|
||||||
|
- **Test suite**: ✅ Comprehensive tests (44 test cases, 100% passing)
|
||||||
|
- **Demo application**: ✅ Working `cmd/hosts/main.go` demonstrating functionality
|
||||||
|
|
||||||
|
## What's Left to Build
|
||||||
|
|
||||||
|
### 🚧 Core Functionality (Phase 1 - Current Priority)
|
||||||
|
- [ ] **Hosts file parser**: Read and parse `/etc/hosts` file format
|
||||||
|
- [ ] Parse IP addresses, hostnames, comments
|
||||||
|
- [ ] Handle disabled entries (commented out)
|
||||||
|
- [ ] Preserve original formatting and comments
|
||||||
|
- [ ] **File operations**: Read hosts file with error handling
|
||||||
|
- [ ] **Round-trip parsing**: Parse → modify → write back with format preservation
|
||||||
|
|
||||||
|
### 🎨 Basic TUI (Phase 2)
|
||||||
|
- [ ] **Main Bubble Tea model**: Core application state and structure
|
||||||
|
- [ ] **Two-pane layout**: Left list + right detail view
|
||||||
|
- [ ] **Entry list display**: Show active status, IP, hostname columns
|
||||||
|
- [ ] **Entry selection**: Navigate and select entries with keyboard
|
||||||
|
- [ ] **View mode**: Safe browsing without modification capability
|
||||||
|
|
||||||
|
### 🔧 Edit Functionality (Phase 3)
|
||||||
|
- [ ] **Edit mode transition**: Explicit mode switching with visual indicators
|
||||||
|
- [ ] **Permission handling**: Request sudo access when entering edit mode
|
||||||
|
- [ ] **Entry modification**: Add, edit, delete, toggle active status
|
||||||
|
- [ ] **File writing**: Atomic updates with backup and rollback
|
||||||
|
- [ ] **Input validation**: Real-time validation of IP and hostname inputs
|
||||||
|
|
||||||
|
### 🌐 Advanced Features (Phase 4)
|
||||||
|
- [ ] **DNS resolution**: Background hostname to IP resolution
|
||||||
|
- [ ] **IP comparison**: Compare resolved vs current IP addresses
|
||||||
|
- [ ] **Entry reordering**: Manual drag/drop or move commands
|
||||||
|
- [ ] **Sorting options**: Sort by IP, hostname, or custom order
|
||||||
|
- [ ] **Search/filter**: Find entries quickly in large files
|
||||||
|
|
||||||
|
### 🧪 Testing & Quality (Ongoing)
|
||||||
|
- [ ] **Parser tests**: Round-trip parsing, edge cases, malformed files
|
||||||
|
- [ ] **Model tests**: Data validation, entry manipulation
|
||||||
|
- [ ] **TUI tests**: User interactions, state transitions
|
||||||
|
- [ ] **Integration tests**: Complete workflows, file operations
|
||||||
|
- [ ] **Permission tests**: sudo scenarios, graceful degradation
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
### Project Phase: **Foundation Complete → Core Functionality**
|
||||||
|
- **Completion**: ~25% (foundation and core models complete)
|
||||||
|
- **Active work**: Ready to implement hosts file parser (Phase 1)
|
||||||
|
- **Blockers**: None - solid foundation established
|
||||||
|
|
||||||
|
### Development Readiness
|
||||||
|
- ✅ **Architecture designed**: Clear technical approach documented
|
||||||
|
- ✅ **Requirements defined**: User experience and functionality specified
|
||||||
|
- ✅ **Technology selected**: Bubble Tea stack confirmed and installed
|
||||||
|
- ✅ **Testing strategy**: TDD approach implemented and proven
|
||||||
|
- ✅ **Project scaffolding**: Complete Go module with all dependencies
|
||||||
|
- ✅ **Development environment**: Fully functional with comprehensive tests
|
||||||
|
|
||||||
|
### Risk Assessment
|
||||||
|
- **Low risk**: Well-established technology stack (Go + Bubble Tea)
|
||||||
|
- **Medium risk**: Permission handling complexity (sudo integration)
|
||||||
|
- **Low risk**: File format parsing (well-defined `/etc/hosts` format)
|
||||||
|
- **Medium risk**: TUI responsiveness with large files
|
||||||
|
|
||||||
|
## Known Issues
|
||||||
|
|
||||||
|
### Technical Challenges
|
||||||
|
- **Permission elevation**: Need to handle sudo gracefully across platforms
|
||||||
|
- **File locking**: Prevent concurrent modifications to `/etc/hosts`
|
||||||
|
- **Large file performance**: Ensure responsiveness with large hosts files
|
||||||
|
- **Terminal compatibility**: Test across different terminal applications
|
||||||
|
|
||||||
|
### Implementation Decisions Needed
|
||||||
|
- **Configuration storage**: Where to store user preferences and settings
|
||||||
|
- **Backup strategy**: How many backups to keep and where to store them
|
||||||
|
- **DNS timeout handling**: How to handle slow or unresponsive DNS queries
|
||||||
|
- **Error recovery**: How to handle corrupted hosts files or write failures
|
||||||
|
|
||||||
|
## Evolution of Project Decisions
|
||||||
|
|
||||||
|
### Initial Scope
|
||||||
|
- Started with comprehensive feature set including DNS resolution
|
||||||
|
- Planned for cross-platform compatibility from day one
|
||||||
|
- Emphasized safety and validation throughout
|
||||||
|
|
||||||
|
### Refined Approach
|
||||||
|
- **TDD emphasis**: Write tests first to ensure reliability
|
||||||
|
- **Incremental development**: Build core functionality before advanced features
|
||||||
|
- **Safety first**: Default to read-only mode, explicit edit mode transition
|
||||||
|
- **User experience focus**: Two-pane interface for efficiency
|
||||||
|
|
||||||
|
### Technology Evolution
|
||||||
|
- **Framework choice**: Bubble Tea selected for maturity and documentation
|
||||||
|
- **Architecture pattern**: MVU pattern from Bubble Tea ecosystem
|
||||||
|
- **Testing approach**: Comprehensive coverage with mocked dependencies
|
||||||
|
- **Development workflow**: Standard Go tooling with additional quality tools
|
||||||
|
|
||||||
|
## Success Metrics & Milestones
|
||||||
|
|
||||||
|
### Milestone 1: Basic Functionality (Target: Week 1)
|
||||||
|
- [ ] Parse and display existing hosts file entries
|
||||||
|
- [ ] Navigate entries with keyboard
|
||||||
|
- [ ] View entry details in right pane
|
||||||
|
- **Success criteria**: Can safely browse hosts file without editing
|
||||||
|
|
||||||
|
### Milestone 2: Core Editing (Target: Week 2)
|
||||||
|
- [ ] Toggle entries active/inactive
|
||||||
|
- [ ] Add new entries with validation
|
||||||
|
- [ ] Edit existing entries
|
||||||
|
- **Success criteria**: Can perform basic hosts file modifications safely
|
||||||
|
|
||||||
|
### Milestone 3: Advanced Features (Target: Week 3)
|
||||||
|
- [ ] DNS resolution and IP updates
|
||||||
|
- [ ] Entry reordering and sorting
|
||||||
|
- [ ] Search and filtering
|
||||||
|
- **Success criteria**: Full feature parity with manual editing plus enhancements
|
||||||
|
|
||||||
|
### Milestone 4: Polish & Release (Target: Week 4)
|
||||||
|
- [ ] Comprehensive testing and bug fixes
|
||||||
|
- [ ] Documentation and usage examples
|
||||||
|
- [ ] Cross-platform testing and compatibility
|
||||||
|
- **Success criteria**: Production-ready tool suitable for daily use
|
||||||
|
|
||||||
|
## Next Immediate Actions
|
||||||
|
|
||||||
|
### ✅ COMPLETED Foundation Tasks
|
||||||
|
1. ✅ **Initialize Go project** (`go mod init hosts-go`)
|
||||||
|
2. ✅ **Add core dependencies** (Bubble Tea, Bubbles, Lip Gloss, testify)
|
||||||
|
3. ✅ **Create directory structure** according to projectbrief.md
|
||||||
|
4. ✅ **Create core data models** with comprehensive validation
|
||||||
|
5. ✅ **Implement test suite** (44 tests, 100% passing)
|
||||||
|
6. ✅ **Create demo application** proving foundation works
|
||||||
|
|
||||||
|
### 🚧 NEXT Phase 1 Actions (Hosts File Parser)
|
||||||
|
1. **Write parser tests** for `/etc/hosts` file format parsing
|
||||||
|
2. **Implement hosts file reader** (`internal/core/parser.go`)
|
||||||
|
3. **Add line-by-line parsing logic** with comment preservation
|
||||||
|
4. **Test round-trip parsing** (read → parse → write)
|
||||||
|
5. **Handle edge cases** (malformed entries, various formats)
|
||||||
|
|
||||||
|
The foundation is now solid and ready for implementing the core parsing functionality.
|
151
memory-bank/projectbrief.md
Normal file
151
memory-bank/projectbrief.md
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
# Project Brief: hosts
|
||||||
|
|
||||||
|
## Foundation of the Project
|
||||||
|
|
||||||
|
The **hosts** project is a Go-based terminal application designed to manage the system `/etc/hosts` file with a modern, interactive Text User Interface (TUI). The goal is to simplify the manipulation, organization, and updating of hostname entries directly from the terminal without manually editing text files.
|
||||||
|
|
||||||
|
The application will use the [**Bubble Tea**](https://github.com/charmbracelet/bubbletea) framework to provide a clean, responsive TUI experience, with a focus on clarity, speed, and ease of use.
|
||||||
|
|
||||||
|
## High-Level Overview
|
||||||
|
|
||||||
|
The application provides a **two-pane TUI**:
|
||||||
|
|
||||||
|
- **Left pane:** List of all hostname entries, with columns:
|
||||||
|
- Active (✓ when enabled)
|
||||||
|
- IP address
|
||||||
|
- Canonical hostname
|
||||||
|
- **Right pane:** Detailed view of the selected entry, with the option to edit the entry.
|
||||||
|
- Active status
|
||||||
|
- IP Address
|
||||||
|
- Hostname
|
||||||
|
- Since an entry can have multiple hostnames, every hostname after the first is treated as an alias and displayed here.
|
||||||
|
- Comment
|
||||||
|
|
||||||
|
The user can:
|
||||||
|
- Activate/deactivate entries
|
||||||
|
- Reorder entries manually
|
||||||
|
- Sort by different attributes
|
||||||
|
- Maintain inline comments
|
||||||
|
- Use DNS-based resolution for hostnames
|
||||||
|
- Quickly update IP addresses
|
||||||
|
|
||||||
|
The program will operate in **view-only mode** by default and require explicit entry into **edit mode**, at which point it will request elevated (sudo) permissions until editing is disabled.
|
||||||
|
|
||||||
|
The project uses:
|
||||||
|
- **Go** as the development language
|
||||||
|
- **Bubble Tea** for TUI rendering and interaction
|
||||||
|
- **Bubbles** Common Bubble Tea components such as text inputs, viewports, spinners and so on
|
||||||
|
- **Lip Gloss** for styling
|
||||||
|
- **bubblezone** Easy mouse event tracking for Bubble Tea components
|
||||||
|
- **Go modules** for dependency management
|
||||||
|
- **golangci-lint** for linting
|
||||||
|
- **Test-driven development** with **Go’s built-in testing** and **testify** for assertions
|
||||||
|
|
||||||
|
## Core Requirements & Goals
|
||||||
|
|
||||||
|
- Display `/etc/hosts` entries in a two-pane Bubble Tea interface
|
||||||
|
- Activate or deactivate specific hostname entries
|
||||||
|
- Reorder hostname entries manually
|
||||||
|
- Sort entries by IP or hostname
|
||||||
|
- Add and edit comments for entries
|
||||||
|
- Support CNAME-like DNS name storage with automatic IP resolution
|
||||||
|
- Compare resolved IP addresses and allow the user to choose
|
||||||
|
- Validate all changes before writing to `/etc/hosts`
|
||||||
|
- Require edit mode for changes, with sudo access requested when entering edit mode
|
||||||
|
- Preserve `/etc/hosts` formatting and comments when saving changes
|
||||||
|
|
||||||
|
## Example One-Line Summary
|
||||||
|
|
||||||
|
**“A Go + Bubble Tea TUI app for managing `/etc/hosts` with sorting, DNS resolution, and quick activation/deactivation.”**
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
hosts/
|
||||||
|
├── go.mod # Go module definition
|
||||||
|
├── go.sum
|
||||||
|
├── README.md
|
||||||
|
├── cmd/
|
||||||
|
│ └── hosts/
|
||||||
|
│ └── main.go # Entry point (go run ./cmd/hosts)
|
||||||
|
├── internal/
|
||||||
|
│ ├── tui/ # UI components (Bubble Tea models)
|
||||||
|
│ │ ├── model.go # Main Bubble Tea model
|
||||||
|
│ │ ├── view.go # Rendering logic
|
||||||
|
│ │ ├── update.go # State updates
|
||||||
|
│ │ ├── config_modal.go # Configuration modal dialog
|
||||||
|
│ │ └── styles.go # Lip Gloss styles
|
||||||
|
│ ├── core/ # Business logic
|
||||||
|
│ │ ├── parser.go # /etc/hosts parsing & writing
|
||||||
|
│ │ ├── models.go # Data models (Entry, Comment, etc.)
|
||||||
|
│ │ ├── config.go # Configuration management
|
||||||
|
│ │ ├── dns.go # DNS resolution & comparison
|
||||||
|
│ │ └── manager.go # Edit mode operations
|
||||||
|
│ └── utils/ # Shared helpers
|
||||||
|
└── tests/
|
||||||
|
├── parser_test.go
|
||||||
|
├── models_test.go
|
||||||
|
├── config_test.go
|
||||||
|
├── tui_test.go
|
||||||
|
├── manager_test.go
|
||||||
|
├── dns_test.go
|
||||||
|
└── integration_test.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing Strategy (TDD)
|
||||||
|
|
||||||
|
### Approach
|
||||||
|
|
||||||
|
- **Write tests before implementation** for all features
|
||||||
|
- Use **Go’s `testing` package** for core tests
|
||||||
|
- Use **testify** for assertions and mocks
|
||||||
|
- Mock `/etc/hosts` file I/O and DNS lookups to avoid system dependencies
|
||||||
|
- Include integration tests for Bubble Tea models (test `Update` and `View` functions)
|
||||||
|
- Aim for **full coverage** in core logic (`parser`, `dns`, `manager`)
|
||||||
|
|
||||||
|
### Planned Test Coverage
|
||||||
|
|
||||||
|
1. **Parser Tests**:
|
||||||
|
- Parse `/etc/hosts` with comments and disabled entries
|
||||||
|
- Preserve formatting when writing back
|
||||||
|
- Handle empty and comment-only files
|
||||||
|
- Validate round-trip parsing
|
||||||
|
|
||||||
|
2. **Model Tests**:
|
||||||
|
- `HostEntry` creation, validation, and serialization
|
||||||
|
- Container for multiple entries
|
||||||
|
- IPv4/IPv6 validation
|
||||||
|
- Hostname validation
|
||||||
|
|
||||||
|
3. **Configuration Tests**:
|
||||||
|
- Load/save config in JSON or TOML
|
||||||
|
- Default values handling
|
||||||
|
- Error handling on corrupt configs
|
||||||
|
|
||||||
|
4. **TUI Tests**:
|
||||||
|
- Model initialization
|
||||||
|
- State transitions (selection, sorting, toggling)
|
||||||
|
- Modal dialog lifecycle
|
||||||
|
- Keyboard input mapping
|
||||||
|
|
||||||
|
5. **Manager Tests**:
|
||||||
|
- Permission handling (sudo)
|
||||||
|
- Edit mode transitions
|
||||||
|
- File backup and atomic saves
|
||||||
|
- Entry manipulation
|
||||||
|
|
||||||
|
6. **DNS Tests**:
|
||||||
|
- Hostname resolution
|
||||||
|
- IP comparison
|
||||||
|
- Handling unreachable hosts
|
||||||
|
|
||||||
|
7. **Integration Tests**:
|
||||||
|
- Complete TUI workflows
|
||||||
|
- File modifications with rollback
|
||||||
|
|
||||||
|
## Important Documentation
|
||||||
|
|
||||||
|
- [Bubble Tea](https://pkg.go.dev/github.com/charmbracelet/bubbletea)
|
||||||
|
- [Bubbles](https://github.com/charmbracelet/bubbles)
|
||||||
|
- [Lip Gloss](https://github.com/charmbracelet/lipgloss)
|
||||||
|
- [bubblezone](https://pkg.go.dev/github.com/lrstanley/bubblezone)
|
161
memory-bank/systemPatterns.md
Normal file
161
memory-bank/systemPatterns.md
Normal file
|
@ -0,0 +1,161 @@
|
||||||
|
# System Patterns: hosts-go
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
The hosts-go application follows a **layered architecture** with clear separation of concerns:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────┐
|
||||||
|
│ TUI Layer │ ← Bubble Tea models, views, styles
|
||||||
|
├─────────────────────┤
|
||||||
|
│ Business Logic │ ← Core domain logic, validation
|
||||||
|
├─────────────────────┤
|
||||||
|
│ System Interface │ ← File I/O, DNS, permissions
|
||||||
|
└─────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Key Technical Decisions
|
||||||
|
|
||||||
|
### 1. **Bubble Tea Framework**
|
||||||
|
- **Why**: Provides mature TUI framework with event-driven architecture
|
||||||
|
- **Pattern**: Model-View-Update (MVU) pattern for state management
|
||||||
|
- **Components**: Main model orchestrates sub-components (list, detail, modal)
|
||||||
|
|
||||||
|
### 2. **Internal Package Structure**
|
||||||
|
```
|
||||||
|
internal/
|
||||||
|
├── tui/ # UI layer - Bubble Tea components
|
||||||
|
├── core/ # Business logic - domain models and operations
|
||||||
|
└── utils/ # Shared utilities - helpers and common functions
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **Permission Model**
|
||||||
|
- **View mode default**: Read-only access, no sudo required
|
||||||
|
- **Edit mode explicit**: User must explicitly enter edit mode
|
||||||
|
- **Sudo on demand**: Only request elevated permissions when entering edit mode
|
||||||
|
- **Graceful fallback**: Handle permission denied gracefully
|
||||||
|
|
||||||
|
## Design Patterns in Use
|
||||||
|
|
||||||
|
### 1. **Model-View-Update (MVU)**
|
||||||
|
- **Model**: Application state (entries, selection, mode)
|
||||||
|
- **View**: Rendering logic (two-pane layout, styles)
|
||||||
|
- **Update**: State transitions based on user input
|
||||||
|
|
||||||
|
### 2. **Command Pattern**
|
||||||
|
- **Edit operations**: Encapsulate modifications as commands
|
||||||
|
- **Undo support**: Commands can be reversed
|
||||||
|
- **Validation**: Commands validate before execution
|
||||||
|
|
||||||
|
### 3. **Repository Pattern**
|
||||||
|
- **HostsRepository**: Abstract file operations
|
||||||
|
- **MockRepository**: In-memory implementation for testing
|
||||||
|
- **FileRepository**: Actual `/etc/hosts` file operations
|
||||||
|
|
||||||
|
### 4. **Observer Pattern**
|
||||||
|
- **DNS resolution**: Background updates to IP addresses
|
||||||
|
- **File watching**: Detect external changes to hosts file
|
||||||
|
- **Status updates**: Notify UI of operation progress
|
||||||
|
|
||||||
|
## Component Relationships
|
||||||
|
|
||||||
|
### TUI Components
|
||||||
|
```
|
||||||
|
MainModel
|
||||||
|
├── ListModel (left pane)
|
||||||
|
│ ├── Entry selection
|
||||||
|
│ ├── Sorting controls
|
||||||
|
│ └── Status indicators
|
||||||
|
├── DetailModel (right pane)
|
||||||
|
│ ├── Entry editing
|
||||||
|
│ ├── Form validation
|
||||||
|
│ └── DNS resolution
|
||||||
|
└── ModalModel (overlays)
|
||||||
|
├── Configuration
|
||||||
|
├── Confirmations
|
||||||
|
└── Error dialogs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Core Business Logic
|
||||||
|
```
|
||||||
|
Manager
|
||||||
|
├── HostsParser (read/write hosts file)
|
||||||
|
├── DNSResolver (hostname to IP resolution)
|
||||||
|
├── Validator (entry validation)
|
||||||
|
└── ConfigManager (user preferences)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Critical Implementation Paths
|
||||||
|
|
||||||
|
### 1. **File Operations**
|
||||||
|
```go
|
||||||
|
// Atomic file updates with backup
|
||||||
|
1. Read current /etc/hosts → backup
|
||||||
|
2. Parse entries → validate changes
|
||||||
|
3. Write to temporary file → verify
|
||||||
|
4. Atomic move temp → /etc/hosts
|
||||||
|
5. Remove backup on success
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. **State Management**
|
||||||
|
```go
|
||||||
|
// Bubble Tea update cycle
|
||||||
|
1. User input → Command
|
||||||
|
2. Command → State change
|
||||||
|
3. State change → View update
|
||||||
|
4. View update → Screen render
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. **DNS Resolution**
|
||||||
|
```go
|
||||||
|
// Background IP resolution
|
||||||
|
1. Extract hostnames from entries
|
||||||
|
2. Resolve in background goroutines
|
||||||
|
3. Compare resolved vs current IPs
|
||||||
|
4. Update UI with resolution status
|
||||||
|
5. User chooses whether to update
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. **Edit Mode Transition**
|
||||||
|
```go
|
||||||
|
// Permission elevation
|
||||||
|
1. User requests edit mode
|
||||||
|
2. Check current permissions
|
||||||
|
3. Request sudo if needed
|
||||||
|
4. Validate file write access
|
||||||
|
5. Enable editing controls
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling Strategy
|
||||||
|
|
||||||
|
### 1. **Graceful Degradation**
|
||||||
|
- **No sudo**: Continue in view-only mode
|
||||||
|
- **File locked**: Show warning, allow retry
|
||||||
|
- **DNS failure**: Show cached/manual IP options
|
||||||
|
|
||||||
|
### 2. **Validation Layers**
|
||||||
|
- **Input validation**: Real-time feedback on forms
|
||||||
|
- **Business rules**: Validate complete entries
|
||||||
|
- **System constraints**: Check file permissions, IP formats
|
||||||
|
|
||||||
|
### 3. **Recovery Mechanisms**
|
||||||
|
- **Backup restoration**: Automatic rollback on write failures
|
||||||
|
- **State recovery**: Restore UI state after errors
|
||||||
|
- **User guidance**: Clear error messages with suggested actions
|
||||||
|
|
||||||
|
## Testing Architecture
|
||||||
|
|
||||||
|
### 1. **Unit Tests**
|
||||||
|
- **Pure functions**: Parser, validator, DNS resolver
|
||||||
|
- **Mocked dependencies**: File system, network calls
|
||||||
|
- **Edge cases**: Malformed files, network errors
|
||||||
|
|
||||||
|
### 2. **Integration Tests**
|
||||||
|
- **TUI workflows**: Complete user interactions
|
||||||
|
- **File operations**: Real file system operations (in temp dirs)
|
||||||
|
- **Permission scenarios**: Test sudo/non-sudo paths
|
||||||
|
|
||||||
|
### 3. **Test Patterns**
|
||||||
|
- **Table-driven tests**: Multiple input scenarios
|
||||||
|
- **Mock interfaces**: Controllable external dependencies
|
||||||
|
- **Golden files**: Expected output comparisons
|
231
memory-bank/techContext.md
Normal file
231
memory-bank/techContext.md
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
# Technical Context: hosts-go
|
||||||
|
|
||||||
|
## Technologies Used
|
||||||
|
|
||||||
|
### Core Language & Runtime
|
||||||
|
- **Go 1.21+**: Primary development language
|
||||||
|
- **Go Modules**: Dependency management (`go.mod`, `go.sum`)
|
||||||
|
|
||||||
|
### TUI Framework Stack
|
||||||
|
- **[Bubble Tea](https://github.com/charmbracelet/bubbletea)**: Core TUI framework
|
||||||
|
- Event-driven architecture
|
||||||
|
- Model-View-Update pattern
|
||||||
|
- Cross-platform terminal support
|
||||||
|
- **[Bubbles](https://github.com/charmbracelet/bubbles)**: Pre-built components
|
||||||
|
- Text inputs, viewports, spinners
|
||||||
|
- List components, progress bars
|
||||||
|
- Standardized interaction patterns
|
||||||
|
- **[Lip Gloss](https://github.com/charmbracelet/lipgloss)**: Styling and layout
|
||||||
|
- CSS-like styling for terminal
|
||||||
|
- Colors, borders, padding, margins
|
||||||
|
- Responsive layout capabilities
|
||||||
|
- **[bubblezone](https://github.com/lrstanley/bubblezone)**: Mouse event handling
|
||||||
|
- Click detection for TUI components
|
||||||
|
- Mouse wheel support
|
||||||
|
- Touch-friendly interactions
|
||||||
|
|
||||||
|
### Development Tools
|
||||||
|
- **golangci-lint**: Static analysis and linting
|
||||||
|
- **Go testing**: Built-in testing framework
|
||||||
|
- **testify**: Enhanced assertions and mocks
|
||||||
|
- **Go race detector**: Concurrency testing
|
||||||
|
- **Go build**: Cross-platform compilation
|
||||||
|
|
||||||
|
## Development Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
```bash
|
||||||
|
# Go installation (1.21+)
|
||||||
|
go version
|
||||||
|
|
||||||
|
# Development tools
|
||||||
|
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Project Initialization
|
||||||
|
```bash
|
||||||
|
# Initialize Go module
|
||||||
|
go mod init hosts-go
|
||||||
|
|
||||||
|
# Add core dependencies
|
||||||
|
go get github.com/charmbracelet/bubbletea@latest
|
||||||
|
go get github.com/charmbracelet/bubbles@latest
|
||||||
|
go get github.com/charmbracelet/lipgloss@latest
|
||||||
|
go get github.com/lrstanley/bubblezone@latest
|
||||||
|
|
||||||
|
# Testing dependencies
|
||||||
|
go get github.com/stretchr/testify@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build & Run
|
||||||
|
```bash
|
||||||
|
# Development run
|
||||||
|
go run ./cmd/hosts
|
||||||
|
|
||||||
|
# Build binary
|
||||||
|
go build -o hosts ./cmd/hosts
|
||||||
|
|
||||||
|
# Cross-platform builds
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o hosts-linux ./cmd/hosts
|
||||||
|
GOOS=windows GOARCH=amd64 go build -o hosts.exe ./cmd/hosts
|
||||||
|
GOOS=darwin GOARCH=amd64 go build -o hosts-darwin ./cmd/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Technical Constraints
|
||||||
|
|
||||||
|
### System Requirements
|
||||||
|
- **Unix-like systems**: macOS, Linux (primary targets)
|
||||||
|
- **Terminal support**: 256+ colors, UTF-8 encoding
|
||||||
|
- **File permissions**: `/etc/hosts` read access (view mode)
|
||||||
|
- **Elevated permissions**: sudo access (edit mode)
|
||||||
|
|
||||||
|
### Performance Constraints
|
||||||
|
- **Memory**: Minimal footprint for terminal application
|
||||||
|
- **Startup time**: < 100ms launch time
|
||||||
|
- **File size**: Support hosts files up to 10MB
|
||||||
|
- **Responsiveness**: < 16ms UI update cycles
|
||||||
|
|
||||||
|
### Security Constraints
|
||||||
|
- **Privilege escalation**: Only when explicitly requested
|
||||||
|
- **File validation**: Prevent hosts file corruption
|
||||||
|
- **Input sanitization**: Validate all hostname/IP inputs
|
||||||
|
- **Backup strategy**: Atomic updates with rollback capability
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
### Runtime Dependencies
|
||||||
|
```go
|
||||||
|
// Core TUI framework
|
||||||
|
github.com/charmbracelet/bubbletea v0.25.0
|
||||||
|
github.com/charmbracelet/bubbles v0.17.1
|
||||||
|
github.com/charmbracelet/lipgloss v0.9.1
|
||||||
|
github.com/lrstanley/bubblezone v0.0.0-20231228141418-c04f8a77c893
|
||||||
|
|
||||||
|
// Standard library usage
|
||||||
|
net // DNS resolution, IP validation
|
||||||
|
os // File operations, environment
|
||||||
|
os/exec // Sudo command execution
|
||||||
|
path/filepath // Path manipulation
|
||||||
|
strings // Text processing
|
||||||
|
regex // Pattern matching
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Dependencies
|
||||||
|
```go
|
||||||
|
// Testing framework
|
||||||
|
github.com/stretchr/testify v1.8.4
|
||||||
|
|
||||||
|
// Optional: Enhanced development
|
||||||
|
github.com/golangci/golangci-lint // Linting
|
||||||
|
github.com/air-verse/air // Live reload (dev only)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Tool Usage Patterns
|
||||||
|
|
||||||
|
### Testing Workflow
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run tests with coverage
|
||||||
|
go test -cover ./...
|
||||||
|
|
||||||
|
# Run specific test package
|
||||||
|
go test ./internal/core
|
||||||
|
|
||||||
|
# Run tests with race detection
|
||||||
|
go test -race ./...
|
||||||
|
|
||||||
|
# Benchmark tests
|
||||||
|
go test -bench=. ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Linting & Quality
|
||||||
|
```bash
|
||||||
|
# Run linter
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
# Vet code
|
||||||
|
go vet ./...
|
||||||
|
|
||||||
|
# Check dependencies
|
||||||
|
go mod tidy
|
||||||
|
go mod verify
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Workflow
|
||||||
|
```bash
|
||||||
|
# Live reload during development
|
||||||
|
air
|
||||||
|
|
||||||
|
# Build and test
|
||||||
|
go build ./... && go test ./...
|
||||||
|
|
||||||
|
# Install locally
|
||||||
|
go install ./cmd/hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
## Platform-Specific Considerations
|
||||||
|
|
||||||
|
### macOS
|
||||||
|
- **Hosts file location**: `/etc/hosts`
|
||||||
|
- **Permission model**: sudo required for editing
|
||||||
|
- **Terminal compatibility**: Terminal.app, iTerm2
|
||||||
|
|
||||||
|
### Linux
|
||||||
|
- **Hosts file location**: `/etc/hosts`
|
||||||
|
- **Permission model**: sudo/root required for editing
|
||||||
|
- **Terminal compatibility**: GNOME Terminal, Konsole, xterm
|
||||||
|
|
||||||
|
### Windows (Future)
|
||||||
|
- **Hosts file location**: `C:\Windows\System32\drivers\etc\hosts`
|
||||||
|
- **Permission model**: Administrator elevation required
|
||||||
|
- **Terminal compatibility**: Windows Terminal, Command Prompt
|
||||||
|
|
||||||
|
## Performance Optimizations
|
||||||
|
|
||||||
|
### Memory Management
|
||||||
|
- **Lazy loading**: Only load visible entries in large files
|
||||||
|
- **String interning**: Reuse common hostname strings
|
||||||
|
- **Garbage collection**: Minimize allocations in render loop
|
||||||
|
|
||||||
|
### UI Responsiveness
|
||||||
|
- **Background processing**: DNS resolution in goroutines
|
||||||
|
- **Debounced updates**: Batch rapid state changes
|
||||||
|
- **Efficient rendering**: Only update changed UI components
|
||||||
|
|
||||||
|
### File Operations
|
||||||
|
- **Streaming parser**: Handle large files without full memory load
|
||||||
|
- **Atomic writes**: Prevent corruption during updates
|
||||||
|
- **Change detection**: Only write when modifications exist
|
||||||
|
|
||||||
|
## Debugging & Profiling
|
||||||
|
|
||||||
|
### Debug Build
|
||||||
|
```bash
|
||||||
|
# Build with debug symbols
|
||||||
|
go build -gcflags="all=-N -l" ./cmd/hosts
|
||||||
|
|
||||||
|
# Run with debug logging
|
||||||
|
DEBUG=1 ./hosts
|
||||||
|
```
|
||||||
|
|
||||||
|
### Profiling
|
||||||
|
```bash
|
||||||
|
# CPU profiling
|
||||||
|
go test -cpuprofile=cpu.prof -bench=.
|
||||||
|
|
||||||
|
# Memory profiling
|
||||||
|
go test -memprofile=mem.prof -bench=.
|
||||||
|
|
||||||
|
# Analyze profiles
|
||||||
|
go tool pprof cpu.prof
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Debug Patterns
|
||||||
|
- **TUI debugging**: Log to file instead of stdout
|
||||||
|
- **State inspection**: JSON marshal model state
|
||||||
|
- **Event tracing**: Log all Bubble Tea messages
|
398
tests/models_test.go
Normal file
398
tests/models_test.go
Normal file
|
@ -0,0 +1,398 @@
|
||||||
|
package tests
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"hosts-go/internal/core"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewHostEntry(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
ip string
|
||||||
|
hostname string
|
||||||
|
expectError bool
|
||||||
|
errorMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid IPv4 entry",
|
||||||
|
ip: "192.168.1.1",
|
||||||
|
hostname: "example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid IPv6 entry",
|
||||||
|
ip: "2001:db8::1",
|
||||||
|
hostname: "example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty IP",
|
||||||
|
ip: "",
|
||||||
|
hostname: "example.com",
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "IP address cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty hostname",
|
||||||
|
ip: "192.168.1.1",
|
||||||
|
hostname: "",
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "hostname cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid IP",
|
||||||
|
ip: "999.999.999.999",
|
||||||
|
hostname: "example.com",
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "invalid IP address",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid hostname",
|
||||||
|
ip: "192.168.1.1",
|
||||||
|
hostname: "-invalid.com",
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "hostname cannot start or end with hyphen",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
entry, err := core.NewHostEntry(tt.ip, tt.hostname)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.errorMsg)
|
||||||
|
assert.Nil(t, entry)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, entry)
|
||||||
|
assert.Equal(t, tt.ip, entry.IP)
|
||||||
|
assert.Equal(t, tt.hostname, entry.Hostname)
|
||||||
|
assert.True(t, entry.Active)
|
||||||
|
assert.Empty(t, entry.Aliases)
|
||||||
|
assert.Empty(t, entry.Comment)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostEntry_AddAlias(t *testing.T) {
|
||||||
|
entry, err := core.NewHostEntry("192.168.1.1", "example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
alias string
|
||||||
|
expectError bool
|
||||||
|
errorMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid alias",
|
||||||
|
alias: "www.example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "another valid alias",
|
||||||
|
alias: "api.example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "duplicate alias",
|
||||||
|
alias: "www.example.com",
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "alias 'www.example.com' already exists",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid alias",
|
||||||
|
alias: "-invalid.com",
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "invalid alias",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
err := entry.AddAlias(tt.alias)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.errorMsg)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, entry.Aliases, tt.alias)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostEntry_AllHostnames(t *testing.T) {
|
||||||
|
entry, err := core.NewHostEntry("192.168.1.1", "example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Test with no aliases
|
||||||
|
hostnames := entry.AllHostnames()
|
||||||
|
assert.Equal(t, []string{"example.com"}, hostnames)
|
||||||
|
|
||||||
|
// Add aliases and test
|
||||||
|
require.NoError(t, entry.AddAlias("www.example.com"))
|
||||||
|
require.NoError(t, entry.AddAlias("api.example.com"))
|
||||||
|
|
||||||
|
hostnames = entry.AllHostnames()
|
||||||
|
expected := []string{"example.com", "www.example.com", "api.example.com"}
|
||||||
|
assert.Equal(t, expected, hostnames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostEntry_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
setup func() *core.HostEntry
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "basic entry",
|
||||||
|
setup: func() *core.HostEntry {
|
||||||
|
entry, _ := core.NewHostEntry("192.168.1.1", "example.com")
|
||||||
|
return entry
|
||||||
|
},
|
||||||
|
expected: "192.168.1.1\texample.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "entry with comment",
|
||||||
|
setup: func() *core.HostEntry {
|
||||||
|
entry, _ := core.NewHostEntry("192.168.1.1", "example.com")
|
||||||
|
entry.Comment = "development server"
|
||||||
|
return entry
|
||||||
|
},
|
||||||
|
expected: "192.168.1.1\texample.com\t# development server",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "entry with aliases",
|
||||||
|
setup: func() *core.HostEntry {
|
||||||
|
entry, _ := core.NewHostEntry("192.168.1.1", "example.com")
|
||||||
|
entry.AddAlias("www.example.com")
|
||||||
|
entry.AddAlias("api.example.com")
|
||||||
|
return entry
|
||||||
|
},
|
||||||
|
expected: "192.168.1.1\texample.com\twww.example.com\tapi.example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "inactive entry",
|
||||||
|
setup: func() *core.HostEntry {
|
||||||
|
entry, _ := core.NewHostEntry("192.168.1.1", "example.com")
|
||||||
|
entry.Active = false
|
||||||
|
return entry
|
||||||
|
},
|
||||||
|
expected: "# 192.168.1.1\texample.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "complete entry",
|
||||||
|
setup: func() *core.HostEntry {
|
||||||
|
entry, _ := core.NewHostEntry("192.168.1.1", "example.com")
|
||||||
|
entry.AddAlias("www.example.com")
|
||||||
|
entry.Comment = "test server"
|
||||||
|
return entry
|
||||||
|
},
|
||||||
|
expected: "192.168.1.1\texample.com\twww.example.com\t# test server",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
entry := tt.setup()
|
||||||
|
result := entry.String()
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostsFile_AddEntry(t *testing.T) {
|
||||||
|
hostsFile := core.NewHostsFile()
|
||||||
|
|
||||||
|
entry, err := core.NewHostEntry("192.168.1.1", "example.com")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = hostsFile.AddEntry(entry)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Len(t, hostsFile.Entries, 1)
|
||||||
|
assert.Equal(t, entry, hostsFile.Entries[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostsFile_FindEntry(t *testing.T) {
|
||||||
|
hostsFile := core.NewHostsFile()
|
||||||
|
|
||||||
|
entry1, _ := core.NewHostEntry("192.168.1.1", "example.com")
|
||||||
|
entry1.AddAlias("www.example.com")
|
||||||
|
entry2, _ := core.NewHostEntry("192.168.1.2", "test.com")
|
||||||
|
|
||||||
|
hostsFile.AddEntry(entry1)
|
||||||
|
hostsFile.AddEntry(entry2)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
hostname string
|
||||||
|
expected *core.HostEntry
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "find by primary hostname",
|
||||||
|
hostname: "example.com",
|
||||||
|
expected: entry1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find by alias",
|
||||||
|
hostname: "www.example.com",
|
||||||
|
expected: entry1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find second entry",
|
||||||
|
hostname: "test.com",
|
||||||
|
expected: entry2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "not found",
|
||||||
|
hostname: "notfound.com",
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := hostsFile.FindEntry(tt.hostname)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostsFile_RemoveEntry(t *testing.T) {
|
||||||
|
hostsFile := core.NewHostsFile()
|
||||||
|
|
||||||
|
entry1, _ := core.NewHostEntry("192.168.1.1", "example.com")
|
||||||
|
entry1.AddAlias("www.example.com")
|
||||||
|
entry2, _ := core.NewHostEntry("192.168.1.2", "test.com")
|
||||||
|
|
||||||
|
hostsFile.AddEntry(entry1)
|
||||||
|
hostsFile.AddEntry(entry2)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
hostname string
|
||||||
|
expectedResult bool
|
||||||
|
remainingCount int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "remove by primary hostname",
|
||||||
|
hostname: "example.com",
|
||||||
|
expectedResult: true,
|
||||||
|
remainingCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "remove non-existent",
|
||||||
|
hostname: "notfound.com",
|
||||||
|
expectedResult: false,
|
||||||
|
remainingCount: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := hostsFile.RemoveEntry(tt.hostname)
|
||||||
|
assert.Equal(t, tt.expectedResult, result)
|
||||||
|
assert.Len(t, hostsFile.Entries, tt.remainingCount)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHostsFile_ActiveEntries(t *testing.T) {
|
||||||
|
hostsFile := core.NewHostsFile()
|
||||||
|
|
||||||
|
entry1, _ := core.NewHostEntry("192.168.1.1", "example.com")
|
||||||
|
entry2, _ := core.NewHostEntry("192.168.1.2", "test.com")
|
||||||
|
entry2.Active = false // Inactive entry
|
||||||
|
entry3, _ := core.NewHostEntry("192.168.1.3", "active.com")
|
||||||
|
|
||||||
|
hostsFile.AddEntry(entry1)
|
||||||
|
hostsFile.AddEntry(entry2)
|
||||||
|
hostsFile.AddEntry(entry3)
|
||||||
|
|
||||||
|
activeEntries := hostsFile.ActiveEntries()
|
||||||
|
assert.Len(t, activeEntries, 2)
|
||||||
|
assert.Contains(t, activeEntries, entry1)
|
||||||
|
assert.Contains(t, activeEntries, entry3)
|
||||||
|
assert.NotContains(t, activeEntries, entry2)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateHostname(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
hostname string
|
||||||
|
expectError bool
|
||||||
|
errorMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "valid simple hostname",
|
||||||
|
hostname: "example",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid domain",
|
||||||
|
hostname: "example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid subdomain",
|
||||||
|
hostname: "www.example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid with numbers",
|
||||||
|
hostname: "server1.example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid with hyphens",
|
||||||
|
hostname: "api-server.example.com",
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty hostname",
|
||||||
|
hostname: "",
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "hostname cannot be empty",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "starts with hyphen",
|
||||||
|
hostname: "-invalid.com",
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "hostname cannot start or end with hyphen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ends with hyphen",
|
||||||
|
hostname: "invalid-.com",
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "hostname cannot start or end with hyphen",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too long hostname",
|
||||||
|
hostname: string(make([]byte, 255)), // 255 characters
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: "hostname too long",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Test through NewHostEntry which calls validateHostname
|
||||||
|
_, err := core.NewHostEntry("192.168.1.1", tt.hostname)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), tt.errorMsg)
|
||||||
|
} else {
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue