package utils import ( "os" "path/filepath" "runtime" "strings" "testing" "github.com/stretchr/testify/assert" ) func TestResolvePath(t *testing.T) { // Save original working directory origDir, _ := os.Getwd() defer os.Chdir(origDir) // Create a temporary directory for testing tmpDir, err := os.MkdirTemp("", "path_test") assert.NoError(t, err) defer os.RemoveAll(tmpDir) tests := []struct { name string input string expected string setup func() // Optional setup function }{ { name: "Empty path", input: "", expected: "", }, { name: "Already absolute path", input: func() string { if runtime.GOOS == "windows" { return "C:/absolute/path/file.txt" } return "/absolute/path/file.txt" }(), expected: func() string { if runtime.GOOS == "windows" { return "C:/absolute/path/file.txt" } return "/absolute/path/file.txt" }(), }, { name: "Relative path", input: "relative/file.txt", expected: func() string { abs, _ := filepath.Abs("relative/file.txt") return strings.ReplaceAll(abs, "\\", "/") }(), }, { name: "Tilde expansion - home only", input: "~", expected: func() string { home := os.Getenv("HOME") if home == "" && runtime.GOOS == "windows" { home = os.Getenv("USERPROFILE") } return strings.ReplaceAll(filepath.Clean(home), "\\", "/") }(), }, { name: "Tilde expansion - with subpath", input: "~/Documents/file.txt", expected: func() string { home := os.Getenv("HOME") if home == "" && runtime.GOOS == "windows" { home = os.Getenv("USERPROFILE") } expected := filepath.Join(home, "Documents", "file.txt") return strings.ReplaceAll(filepath.Clean(expected), "\\", "/") }(), }, { name: "Path normalization - double slashes", input: "path//to//file.txt", expected: func() string { abs, _ := filepath.Abs("path/to/file.txt") return strings.ReplaceAll(abs, "\\", "/") }(), }, { name: "Path normalization - . and ..", input: "path/./to/../file.txt", expected: func() string { abs, _ := filepath.Abs("path/file.txt") return strings.ReplaceAll(abs, "\\", "/") }(), }, { name: "Windows backslash normalization", input: "path\\to\\file.txt", expected: func() string { abs, _ := filepath.Abs("path/to/file.txt") return strings.ReplaceAll(abs, "\\", "/") }(), }, { name: "Mixed separators with tilde", input: "~/Documents\\file.txt", expected: func() string { home := os.Getenv("HOME") if home == "" && runtime.GOOS == "windows" { home = os.Getenv("USERPROFILE") } expected := filepath.Join(home, "Documents", "file.txt") return strings.ReplaceAll(filepath.Clean(expected), "\\", "/") }(), }, { name: "Relative path from current directory", input: "./file.txt", expected: func() string { abs, _ := filepath.Abs("file.txt") return strings.ReplaceAll(abs, "\\", "/") }(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.setup != nil { tt.setup() } result := ResolvePath(tt.input) assert.Equal(t, tt.expected, result, "ResolvePath(%q) = %q, want %q", tt.input, result, tt.expected) }) } } func TestResolvePathWithWorkingDirectoryChange(t *testing.T) { // Save original working directory origDir, _ := os.Getwd() defer os.Chdir(origDir) // Create temporary directories tmpDir, err := os.MkdirTemp("", "path_test") assert.NoError(t, err) defer os.RemoveAll(tmpDir) subDir := filepath.Join(tmpDir, "subdir") err = os.MkdirAll(subDir, 0755) assert.NoError(t, err) // Change to subdirectory err = os.Chdir(subDir) assert.NoError(t, err) // Test relative path resolution from new working directory result := ResolvePath("../test.txt") expected := filepath.Join(tmpDir, "test.txt") expected = strings.ReplaceAll(filepath.Clean(expected), "\\", "/") assert.Equal(t, expected, result) } func TestResolvePathComplexTilde(t *testing.T) { // Test complex tilde patterns home := os.Getenv("HOME") if home == "" && runtime.GOOS == "windows" { home = os.Getenv("USERPROFILE") } if home == "" { t.Skip("Cannot determine home directory for tilde expansion tests") } tests := []struct { input string expected string }{ { input: "~", expected: strings.ReplaceAll(filepath.Clean(home), "\\", "/"), }, { input: "~/", expected: strings.ReplaceAll(filepath.Clean(home), "\\", "/"), }, { input: "~~", expected: func() string { // ~~ should be treated as ~ followed by ~ (tilde expansion) home := os.Getenv("HOME") if home == "" && runtime.GOOS == "windows" { home = os.Getenv("USERPROFILE") } if home != "" { // First ~ gets expanded, second ~ remains return strings.ReplaceAll(filepath.Clean(home+"~"), "\\", "/") } abs, _ := filepath.Abs("~~") return strings.ReplaceAll(abs, "\\", "/") }(), }, { input: func() string { if runtime.GOOS == "windows" { return "C:/not/tilde/path" } return "/not/tilde/path" }(), expected: func() string { if runtime.GOOS == "windows" { return "C:/not/tilde/path" } return "/not/tilde/path" }(), }, } for _, tt := range tests { t.Run("Complex tilde: "+tt.input, func(t *testing.T) { result := ResolvePath(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestIsAbsolutePath(t *testing.T) { tests := []struct { name string input string expected bool }{ { name: "Empty path", input: "", expected: false, }, { name: "Absolute Unix path", input: "/absolute/path", expected: func() bool { if runtime.GOOS == "windows" { // On Windows, paths starting with / are not considered absolute return false } return true }(), }, { name: "Relative path", input: "relative/path", expected: false, }, { name: "Tilde expansion (becomes absolute)", input: "~/path", expected: true, }, { name: "Windows absolute path", input: "C:\\Windows\\System32", expected: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := IsAbsolutePath(tt.input) assert.Equal(t, tt.expected, result) }) } } func TestGetRelativePath(t *testing.T) { // Create temporary directories for testing tmpDir, err := os.MkdirTemp("", "relative_path_test") assert.NoError(t, err) defer os.RemoveAll(tmpDir) baseDir := filepath.Join(tmpDir, "base") targetDir := filepath.Join(tmpDir, "target") subDir := filepath.Join(targetDir, "subdir") err = os.MkdirAll(baseDir, 0755) assert.NoError(t, err) err = os.MkdirAll(subDir, 0755) assert.NoError(t, err) tests := []struct { name string base string target string expected string wantErr bool }{ { name: "Target is subdirectory of base", base: baseDir, target: filepath.Join(baseDir, "subdir"), expected: "subdir", wantErr: false, }, { name: "Target is parent of base", base: filepath.Join(baseDir, "subdir"), target: baseDir, expected: "..", wantErr: false, }, { name: "Target is sibling directory", base: baseDir, target: targetDir, expected: "../target", wantErr: false, }, { name: "Same directory", base: baseDir, target: baseDir, expected: ".", wantErr: false, }, { name: "With tilde expansion", base: baseDir, target: filepath.Join(baseDir, "file.txt"), expected: "file.txt", wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := GetRelativePath(tt.base, tt.target) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, tt.expected, result) } }) } } func TestResolvePathRegression(t *testing.T) { // This test specifically addresses the original bug: // "~ is NOT BEING FUCKING RESOLVED" home := os.Getenv("HOME") if home == "" && runtime.GOOS == "windows" { home = os.Getenv("USERPROFILE") } if home == "" { t.Skip("Cannot determine home directory for regression test") } // Test the exact pattern from the bug report testPath := "~/Seafile/activitywatch/sync.yml" result := ResolvePath(testPath) expected := filepath.Join(home, "Seafile", "activitywatch", "sync.yml") expected = strings.ReplaceAll(filepath.Clean(expected), "\\", "/") assert.Equal(t, expected, result, "Tilde expansion bug not fixed!") assert.NotContains(t, result, "~", "Tilde still present in resolved path!") // Convert both to forward slashes for comparison homeForwardSlash := strings.ReplaceAll(home, "\\", "/") assert.Contains(t, result, homeForwardSlash, "Home directory not found in resolved path!") } func TestResolvePathEdgeCases(t *testing.T) { // Save original working directory origDir, _ := os.Getwd() defer os.Chdir(origDir) tests := []struct { name string input string setup func() shouldPanic bool }{ { name: "Just dot", input: ".", }, { name: "Just double dot", input: "..", }, { name: "Triple dot", input: "...", }, { name: "Multiple leading dots", input: "./.././../file.txt", }, { name: "Path with spaces", input: "path with spaces/file.txt", }, { name: "Very long relative path", input: strings.Repeat("../", 10) + "file.txt", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if tt.setup != nil { tt.setup() } if tt.shouldPanic { assert.Panics(t, func() { ResolvePath(tt.input) }) } else { // Should not panic assert.NotPanics(t, func() { ResolvePath(tt.input) }) // Result should be a valid absolute path result := ResolvePath(tt.input) if tt.input != "" { assert.True(t, filepath.IsAbs(result) || result == "", "Result should be absolute or empty") } } }) } }