package hostfile_test import ( "ekdns/hostfile" "errors" "fmt" "os" "path/filepath" "strings" "testing" "time" ) // --------------------------------------------------------------------------- // fakeFileSystem — implements hostfile.FileSystem for unit tests // --------------------------------------------------------------------------- type fakeFileInfo struct { mode os.FileMode } func (f fakeFileInfo) Name() string { return "" } func (f fakeFileInfo) Size() int64 { return 0 } func (f fakeFileInfo) Mode() os.FileMode { return f.mode } func (f fakeFileInfo) ModTime() time.Time { return time.Time{} } func (f fakeFileInfo) IsDir() bool { return false } func (f fakeFileInfo) Sys() any { return nil } type writeCall struct { path string data []byte perm os.FileMode } type renameCall struct { src string dst string } type fakeFileSystem struct { readFiles map[string][]byte readErrors map[string]error statMode os.FileMode statErr error renameErr error removeErr error // tracking createTempDir string lastTempFile *os.File writeCalls []writeCall renameCalls []renameCall removedPaths []string chmodPath string chmodMode os.FileMode // real temp dir on disk for CreateTemp to use realTempDir string } func newFakeFS(t *testing.T) *fakeFileSystem { t.Helper() return &fakeFileSystem{ readFiles: make(map[string][]byte), readErrors: make(map[string]error), realTempDir: t.TempDir(), statMode: 0644, } } func (f *fakeFileSystem) ReadFile(path string) ([]byte, error) { if err, ok := f.readErrors[path]; ok { return nil, err } if data, ok := f.readFiles[path]; ok { return data, nil } return nil, fmt.Errorf("file not found in fake fs: %s", path) } func (f *fakeFileSystem) WriteFile(path string, data []byte, perm os.FileMode) error { f.writeCalls = append(f.writeCalls, writeCall{path: path, data: data, perm: perm}) return nil } func (f *fakeFileSystem) Stat(path string) (os.FileInfo, error) { if f.statErr != nil { return nil, f.statErr } return fakeFileInfo{mode: f.statMode}, nil } func (f *fakeFileSystem) CreateTemp(dir, pattern string) (*os.File, error) { f.createTempDir = dir tf, err := os.CreateTemp(f.realTempDir, pattern) if err == nil { f.lastTempFile = tf } return tf, err } func (f *fakeFileSystem) Rename(oldpath, newpath string) error { f.renameCalls = append(f.renameCalls, renameCall{src: oldpath, dst: newpath}) return f.renameErr } func (f *fakeFileSystem) Remove(path string) error { f.removedPaths = append(f.removedPaths, path) return f.removeErr } func (f *fakeFileSystem) Chmod(path string, mode os.FileMode) error { f.chmodPath = path f.chmodMode = mode return nil } // containsRemoved returns true if path was passed to Remove. func (f *fakeFileSystem) containsRemoved(path string) bool { for _, p := range f.removedPaths { if p == path { return true } } return false } // --------------------------------------------------------------------------- // Read tests // --------------------------------------------------------------------------- func TestRead_ParsesFileCorrectly(t *testing.T) { fs := newFakeFS(t) content := strings.Join([]string{ "127.0.0.1 localhost", hostfile.StartMarker, "1.1.1.1\thost1", hostfile.EndMarker, "# end", }, "\n") + "\n" hostsPath := "/fake/hosts" fs.readFiles[hostsPath] = []byte(content) mgr := hostfile.NewManager(fs) hf, err := mgr.Read(hostsPath) if err != nil { t.Fatalf("unexpected error: %v", err) } if hf.Path != hostsPath { t.Errorf("expected Path=%q, got %q", hostsPath, hf.Path) } if !hf.HasManagedBlock { t.Error("expected HasManagedBlock=true") } if len(hf.ManagedContent) != 1 || hf.ManagedContent[0] != "1.1.1.1\thost1" { t.Errorf("unexpected ManagedContent: %v", hf.ManagedContent) } if len(hf.PrefixContent) != 1 { t.Errorf("expected 1 prefix line, got %v", hf.PrefixContent) } if len(hf.PostfixContent) != 1 { t.Errorf("expected 1 postfix line, got %v", hf.PostfixContent) } } func TestRead_HandlesMissingFile(t *testing.T) { fs := newFakeFS(t) hostsPath := "/fake/hosts" fs.readErrors[hostsPath] = errors.New("no such file") mgr := hostfile.NewManager(fs) _, err := mgr.Read(hostsPath) if err == nil { t.Fatal("expected error for missing file") } if !strings.Contains(err.Error(), hostsPath) { t.Errorf("error should mention the path, got: %v", err) } } // --------------------------------------------------------------------------- // Write tests // --------------------------------------------------------------------------- // buildTestHostsFile builds a HostsFile where OriginalContent differs from // the assembled content (so Write proceeds). func buildTestHostsFile(hostsPath string) *hostfile.HostsFile { prefix := []string{"127.0.0.1 localhost"} managed := []string{"1.1.1.1\thost1"} postfix := []string{"# end"} // OriginalContent is different (e.g. empty managed block originally) original := prefix return &hostfile.HostsFile{ Path: hostsPath, OriginalContent: original, PrefixContent: prefix, ManagedContent: managed, PostfixContent: postfix, HasManagedBlock: false, } } func TestWrite_CreatesBackupBeforeWriting(t *testing.T) { fs := newFakeFS(t) hostsPath := filepath.Join(t.TempDir(), "hosts") backupDir := t.TempDir() hostsContent := "127.0.0.1 localhost\n" fs.readFiles[hostsPath] = []byte(hostsContent) mgr := hostfile.NewManager(fs) hf := buildTestHostsFile(hostsPath) if err := mgr.Write(hf, backupDir); err != nil { t.Fatalf("Write failed: %v", err) } // Verify backup was created via WriteFile found := false for _, wc := range fs.writeCalls { if strings.HasPrefix(wc.path, backupDir) && strings.Contains(wc.path, "hosts.bak.") { found = true break } } if !found { t.Errorf("expected backup WriteFile call in backupDir %q, write calls: %v", backupDir, fs.writeCalls) } } func TestWrite_UsesTempFileInSameDirectory(t *testing.T) { fs := newFakeFS(t) hostsDir := t.TempDir() hostsPath := filepath.Join(hostsDir, "hosts") backupDir := t.TempDir() fs.readFiles[hostsPath] = []byte("127.0.0.1 localhost\n") mgr := hostfile.NewManager(fs) hf := buildTestHostsFile(hostsPath) if err := mgr.Write(hf, backupDir); err != nil { t.Fatalf("Write failed: %v", err) } if fs.createTempDir != hostsDir { t.Errorf("expected CreateTemp dir=%q, got %q", hostsDir, fs.createTempDir) } } func TestWrite_RenamesTempOverHostsFile(t *testing.T) { fs := newFakeFS(t) hostsPath := filepath.Join(t.TempDir(), "hosts") backupDir := t.TempDir() fs.readFiles[hostsPath] = []byte("127.0.0.1 localhost\n") mgr := hostfile.NewManager(fs) hf := buildTestHostsFile(hostsPath) if err := mgr.Write(hf, backupDir); err != nil { t.Fatalf("Write failed: %v", err) } if len(fs.renameCalls) == 0 { t.Fatal("expected Rename to be called") } lastRename := fs.renameCalls[len(fs.renameCalls)-1] if lastRename.dst != hostsPath { t.Errorf("expected Rename dst=%q, got %q", hostsPath, lastRename.dst) } } func TestWrite_PreservesOriginalPermissions(t *testing.T) { fs := newFakeFS(t) fs.statMode = 0640 hostsPath := filepath.Join(t.TempDir(), "hosts") backupDir := t.TempDir() fs.readFiles[hostsPath] = []byte("127.0.0.1 localhost\n") mgr := hostfile.NewManager(fs) hf := buildTestHostsFile(hostsPath) if err := mgr.Write(hf, backupDir); err != nil { t.Fatalf("Write failed: %v", err) } if fs.chmodMode != 0640 { t.Errorf("expected Chmod mode=0640, got %v", fs.chmodMode) } } func TestWrite_DeletesBackupOnSuccess(t *testing.T) { fs := newFakeFS(t) hostsPath := filepath.Join(t.TempDir(), "hosts") backupDir := t.TempDir() fs.readFiles[hostsPath] = []byte("127.0.0.1 localhost\n") mgr := hostfile.NewManager(fs) hf := buildTestHostsFile(hostsPath) if err := mgr.Write(hf, backupDir); err != nil { t.Fatalf("Write failed: %v", err) } // Find the backup path from writeCalls var backupPath string for _, wc := range fs.writeCalls { if strings.HasPrefix(wc.path, backupDir) && strings.Contains(wc.path, "hosts.bak.") { backupPath = wc.path break } } if backupPath == "" { t.Fatal("backup not found in writeCalls") } if !fs.containsRemoved(backupPath) { t.Errorf("expected backup %q to be removed on success, removed paths: %v", backupPath, fs.removedPaths) } } func TestWrite_CleansUpTempOnFailure(t *testing.T) { fs := newFakeFS(t) fs.renameErr = errors.New("rename failed") hostsPath := filepath.Join(t.TempDir(), "hosts") backupDir := t.TempDir() fs.readFiles[hostsPath] = []byte("127.0.0.1 localhost\n") mgr := hostfile.NewManager(fs) hf := buildTestHostsFile(hostsPath) err := mgr.Write(hf, backupDir) if err == nil { t.Fatal("expected Write to fail when Rename fails") } if fs.lastTempFile == nil { t.Fatal("expected CreateTemp to have been called") } tempPath := fs.lastTempFile.Name() if !fs.containsRemoved(tempPath) { t.Errorf("expected temp file %q to be removed on failure, removed: %v", tempPath, fs.removedPaths) } } func TestWrite_SkipsIfContentUnchanged(t *testing.T) { fs := newFakeFS(t) hostsPath := filepath.Join(t.TempDir(), "hosts") backupDir := t.TempDir() prefix := []string{"127.0.0.1 localhost"} managed := []string{"1.1.1.1\thost1"} postfix := []string{"# end"} // OriginalContent matches assembled content exactly assembled := hostfile.AssembleContent(prefix, managed, postfix) hf := &hostfile.HostsFile{ Path: hostsPath, OriginalContent: assembled, PrefixContent: prefix, ManagedContent: managed, PostfixContent: postfix, HasManagedBlock: true, } mgr := hostfile.NewManager(fs) if err := mgr.Write(hf, backupDir); err != nil { t.Fatalf("unexpected error: %v", err) } if fs.lastTempFile != nil { t.Error("expected no CreateTemp call when content is unchanged") } if len(fs.writeCalls) != 0 { t.Errorf("expected no WriteFile calls, got: %v", fs.writeCalls) } }