@@ -0,0 +1,483 @@
1+package fsext
2+
3+import (
4+ "errors"
5+ "os"
6+ "path/filepath"
7+ "testing"
8+
9+ "github.com/charmbracelet/crush/internal/home"
10+ "github.com/stretchr/testify/require"
11+)
12+
13+func TestLookupClosest(t *testing.T) {
14+ tempDir := t.TempDir()
15+
16+ // Change to temp directory
17+ oldWd, _ := os.Getwd()
18+ err := os.Chdir(tempDir)
19+ require.NoError(t, err)
20+
21+ t.Cleanup(func() {
22+ os.Chdir(oldWd)
23+ })
24+
25+ t.Run("target found in starting directory", func(t *testing.T) {
26+ testDir := t.TempDir()
27+
28+ // Create target file in current directory
29+ targetFile := filepath.Join(testDir, "target.txt")
30+ err := os.WriteFile(targetFile, []byte("test"), 0o644)
31+ require.NoError(t, err)
32+
33+ foundPath, found := LookupClosest(testDir, "target.txt")
34+ require.True(t, found)
35+ require.Equal(t, targetFile, foundPath)
36+ })
37+
38+ t.Run("target found in parent directory", func(t *testing.T) {
39+ testDir := t.TempDir()
40+
41+ // Create subdirectory
42+ subDir := filepath.Join(testDir, "subdir")
43+ err := os.Mkdir(subDir, 0o755)
44+ require.NoError(t, err)
45+
46+ // Create target file in parent directory
47+ targetFile := filepath.Join(testDir, "target.txt")
48+ err = os.WriteFile(targetFile, []byte("test"), 0o644)
49+ require.NoError(t, err)
50+
51+ foundPath, found := LookupClosest(subDir, "target.txt")
52+ require.True(t, found)
53+ require.Equal(t, targetFile, foundPath)
54+ })
55+
56+ t.Run("target found in grandparent directory", func(t *testing.T) {
57+ testDir := t.TempDir()
58+
59+ // Create nested subdirectories
60+ subDir := filepath.Join(testDir, "subdir")
61+ err := os.Mkdir(subDir, 0o755)
62+ require.NoError(t, err)
63+
64+ subSubDir := filepath.Join(subDir, "subsubdir")
65+ err = os.Mkdir(subSubDir, 0o755)
66+ require.NoError(t, err)
67+
68+ // Create target file in grandparent directory
69+ targetFile := filepath.Join(testDir, "target.txt")
70+ err = os.WriteFile(targetFile, []byte("test"), 0o644)
71+ require.NoError(t, err)
72+
73+ foundPath, found := LookupClosest(subSubDir, "target.txt")
74+ require.True(t, found)
75+ require.Equal(t, targetFile, foundPath)
76+ })
77+
78+ t.Run("target not found", func(t *testing.T) {
79+ testDir := t.TempDir()
80+
81+ foundPath, found := LookupClosest(testDir, "nonexistent.txt")
82+ require.False(t, found)
83+ require.Empty(t, foundPath)
84+ })
85+
86+ t.Run("target directory found", func(t *testing.T) {
87+ testDir := t.TempDir()
88+
89+ // Create target directory in current directory
90+ targetDir := filepath.Join(testDir, "targetdir")
91+ err := os.Mkdir(targetDir, 0o755)
92+ require.NoError(t, err)
93+
94+ foundPath, found := LookupClosest(testDir, "targetdir")
95+ require.True(t, found)
96+ require.Equal(t, targetDir, foundPath)
97+ })
98+
99+ t.Run("stops at home directory", func(t *testing.T) {
100+ // This test is limited as we can't easily create files above home directory
101+ // but we can test the behavior by searching from home directory itself
102+ homeDir := home.Dir()
103+
104+ // Search for a file that doesn't exist from home directory
105+ foundPath, found := LookupClosest(homeDir, "nonexistent_file_12345.txt")
106+ require.False(t, found)
107+ require.Empty(t, foundPath)
108+ })
109+
110+ t.Run("invalid starting directory", func(t *testing.T) {
111+ foundPath, found := LookupClosest("/invalid/path/that/does/not/exist", "target.txt")
112+ require.False(t, found)
113+ require.Empty(t, foundPath)
114+ })
115+
116+ t.Run("relative path handling", func(t *testing.T) {
117+ testDir := t.TempDir()
118+
119+ // Change to test directory
120+ oldWd, _ := os.Getwd()
121+ err := os.Chdir(testDir)
122+ require.NoError(t, err)
123+ defer os.Chdir(oldWd)
124+
125+ // Create target file in current directory
126+ err = os.WriteFile("target.txt", []byte("test"), 0o644)
127+ require.NoError(t, err)
128+
129+ // Search using relative path
130+ foundPath, found := LookupClosest(".", "target.txt")
131+ require.True(t, found)
132+
133+ // Resolve symlinks to handle macOS /private/var vs /var discrepancy
134+ expectedPath, err := filepath.EvalSymlinks(filepath.Join(testDir, "target.txt"))
135+ require.NoError(t, err)
136+ actualPath, err := filepath.EvalSymlinks(foundPath)
137+ require.NoError(t, err)
138+ require.Equal(t, expectedPath, actualPath)
139+ })
140+}
141+
142+func TestLookupClosestWithOwnership(t *testing.T) {
143+ // Note: Testing ownership boundaries is difficult in a cross-platform way
144+ // without creating complex directory structures with different owners.
145+ // This test focuses on the basic functionality when ownership checks pass.
146+
147+ tempDir := t.TempDir()
148+
149+ // Change to temp directory
150+ oldWd, _ := os.Getwd()
151+ err := os.Chdir(tempDir)
152+ require.NoError(t, err)
153+
154+ t.Cleanup(func() {
155+ os.Chdir(oldWd)
156+ })
157+
158+ t.Run("search respects same ownership", func(t *testing.T) {
159+ testDir := t.TempDir()
160+
161+ // Create subdirectory structure
162+ subDir := filepath.Join(testDir, "subdir")
163+ err := os.Mkdir(subDir, 0o755)
164+ require.NoError(t, err)
165+
166+ // Create target file in parent directory
167+ targetFile := filepath.Join(testDir, "target.txt")
168+ err = os.WriteFile(targetFile, []byte("test"), 0o644)
169+ require.NoError(t, err)
170+
171+ // Search should find the target assuming same ownership
172+ foundPath, found := LookupClosest(subDir, "target.txt")
173+ require.True(t, found)
174+ require.Equal(t, targetFile, foundPath)
175+ })
176+}
177+
178+func TestLookup(t *testing.T) {
179+ tempDir := t.TempDir()
180+
181+ // Change to temp directory
182+ oldWd, _ := os.Getwd()
183+ err := os.Chdir(tempDir)
184+ require.NoError(t, err)
185+
186+ t.Cleanup(func() {
187+ os.Chdir(oldWd)
188+ })
189+
190+ t.Run("no targets returns empty slice", func(t *testing.T) {
191+ testDir := t.TempDir()
192+
193+ found, err := Lookup(testDir)
194+ require.NoError(t, err)
195+ require.Empty(t, found)
196+ })
197+
198+ t.Run("single target found in starting directory", func(t *testing.T) {
199+ testDir := t.TempDir()
200+
201+ // Create target file in current directory
202+ targetFile := filepath.Join(testDir, "target.txt")
203+ err := os.WriteFile(targetFile, []byte("test"), 0o644)
204+ require.NoError(t, err)
205+
206+ found, err := Lookup(testDir, "target.txt")
207+ require.NoError(t, err)
208+ require.Len(t, found, 1)
209+ require.Equal(t, targetFile, found[0])
210+ })
211+
212+ t.Run("multiple targets found in starting directory", func(t *testing.T) {
213+ testDir := t.TempDir()
214+
215+ // Create multiple target files in current directory
216+ targetFile1 := filepath.Join(testDir, "target1.txt")
217+ targetFile2 := filepath.Join(testDir, "target2.txt")
218+ targetFile3 := filepath.Join(testDir, "target3.txt")
219+
220+ err := os.WriteFile(targetFile1, []byte("test1"), 0o644)
221+ require.NoError(t, err)
222+ err = os.WriteFile(targetFile2, []byte("test2"), 0o644)
223+ require.NoError(t, err)
224+ err = os.WriteFile(targetFile3, []byte("test3"), 0o644)
225+ require.NoError(t, err)
226+
227+ found, err := Lookup(testDir, "target1.txt", "target2.txt", "target3.txt")
228+ require.NoError(t, err)
229+ require.Len(t, found, 3)
230+ require.Contains(t, found, targetFile1)
231+ require.Contains(t, found, targetFile2)
232+ require.Contains(t, found, targetFile3)
233+ })
234+
235+ t.Run("targets found in parent directories", func(t *testing.T) {
236+ testDir := t.TempDir()
237+
238+ // Create subdirectory
239+ subDir := filepath.Join(testDir, "subdir")
240+ err := os.Mkdir(subDir, 0o755)
241+ require.NoError(t, err)
242+
243+ // Create target files in parent directory
244+ targetFile1 := filepath.Join(testDir, "target1.txt")
245+ targetFile2 := filepath.Join(testDir, "target2.txt")
246+ err = os.WriteFile(targetFile1, []byte("test1"), 0o644)
247+ require.NoError(t, err)
248+ err = os.WriteFile(targetFile2, []byte("test2"), 0o644)
249+ require.NoError(t, err)
250+
251+ found, err := Lookup(subDir, "target1.txt", "target2.txt")
252+ require.NoError(t, err)
253+ require.Len(t, found, 2)
254+ require.Contains(t, found, targetFile1)
255+ require.Contains(t, found, targetFile2)
256+ })
257+
258+ t.Run("targets found across multiple directory levels", func(t *testing.T) {
259+ testDir := t.TempDir()
260+
261+ // Create nested subdirectories
262+ subDir := filepath.Join(testDir, "subdir")
263+ err := os.Mkdir(subDir, 0o755)
264+ require.NoError(t, err)
265+
266+ subSubDir := filepath.Join(subDir, "subsubdir")
267+ err = os.Mkdir(subSubDir, 0o755)
268+ require.NoError(t, err)
269+
270+ // Create target files at different levels
271+ targetFile1 := filepath.Join(testDir, "target1.txt")
272+ targetFile2 := filepath.Join(subDir, "target2.txt")
273+ targetFile3 := filepath.Join(subSubDir, "target3.txt")
274+
275+ err = os.WriteFile(targetFile1, []byte("test1"), 0o644)
276+ require.NoError(t, err)
277+ err = os.WriteFile(targetFile2, []byte("test2"), 0o644)
278+ require.NoError(t, err)
279+ err = os.WriteFile(targetFile3, []byte("test3"), 0o644)
280+ require.NoError(t, err)
281+
282+ found, err := Lookup(subSubDir, "target1.txt", "target2.txt", "target3.txt")
283+ require.NoError(t, err)
284+ require.Len(t, found, 3)
285+ require.Contains(t, found, targetFile1)
286+ require.Contains(t, found, targetFile2)
287+ require.Contains(t, found, targetFile3)
288+ })
289+
290+ t.Run("some targets not found", func(t *testing.T) {
291+ testDir := t.TempDir()
292+
293+ // Create only some target files
294+ targetFile1 := filepath.Join(testDir, "target1.txt")
295+ targetFile2 := filepath.Join(testDir, "target2.txt")
296+
297+ err := os.WriteFile(targetFile1, []byte("test1"), 0o644)
298+ require.NoError(t, err)
299+ err = os.WriteFile(targetFile2, []byte("test2"), 0o644)
300+ require.NoError(t, err)
301+
302+ // Search for existing and non-existing targets
303+ found, err := Lookup(testDir, "target1.txt", "nonexistent.txt", "target2.txt", "another_nonexistent.txt")
304+ require.NoError(t, err)
305+ require.Len(t, found, 2)
306+ require.Contains(t, found, targetFile1)
307+ require.Contains(t, found, targetFile2)
308+ })
309+
310+ t.Run("no targets found", func(t *testing.T) {
311+ testDir := t.TempDir()
312+
313+ found, err := Lookup(testDir, "nonexistent1.txt", "nonexistent2.txt", "nonexistent3.txt")
314+ require.NoError(t, err)
315+ require.Empty(t, found)
316+ })
317+
318+ t.Run("target directories found", func(t *testing.T) {
319+ testDir := t.TempDir()
320+
321+ // Create target directories
322+ targetDir1 := filepath.Join(testDir, "targetdir1")
323+ targetDir2 := filepath.Join(testDir, "targetdir2")
324+ err := os.Mkdir(targetDir1, 0o755)
325+ require.NoError(t, err)
326+ err = os.Mkdir(targetDir2, 0o755)
327+ require.NoError(t, err)
328+
329+ found, err := Lookup(testDir, "targetdir1", "targetdir2")
330+ require.NoError(t, err)
331+ require.Len(t, found, 2)
332+ require.Contains(t, found, targetDir1)
333+ require.Contains(t, found, targetDir2)
334+ })
335+
336+ t.Run("mixed files and directories", func(t *testing.T) {
337+ testDir := t.TempDir()
338+
339+ // Create target files and directories
340+ targetFile := filepath.Join(testDir, "target.txt")
341+ targetDir := filepath.Join(testDir, "targetdir")
342+ err := os.WriteFile(targetFile, []byte("test"), 0o644)
343+ require.NoError(t, err)
344+ err = os.Mkdir(targetDir, 0o755)
345+ require.NoError(t, err)
346+
347+ found, err := Lookup(testDir, "target.txt", "targetdir")
348+ require.NoError(t, err)
349+ require.Len(t, found, 2)
350+ require.Contains(t, found, targetFile)
351+ require.Contains(t, found, targetDir)
352+ })
353+
354+ t.Run("invalid starting directory", func(t *testing.T) {
355+ found, err := Lookup("/invalid/path/that/does/not/exist", "target.txt")
356+ require.Error(t, err)
357+ require.Empty(t, found)
358+ })
359+
360+ t.Run("relative path handling", func(t *testing.T) {
361+ testDir := t.TempDir()
362+
363+ // Change to test directory
364+ oldWd, _ := os.Getwd()
365+ err := os.Chdir(testDir)
366+ require.NoError(t, err)
367+
368+ t.Cleanup(func() {
369+ os.Chdir(oldWd)
370+ })
371+
372+ // Create target files in current directory
373+ err = os.WriteFile("target1.txt", []byte("test1"), 0o644)
374+ require.NoError(t, err)
375+ err = os.WriteFile("target2.txt", []byte("test2"), 0o644)
376+ require.NoError(t, err)
377+
378+ // Search using relative path
379+ found, err := Lookup(".", "target1.txt", "target2.txt")
380+ require.NoError(t, err)
381+ require.Len(t, found, 2)
382+
383+ // Resolve symlinks to handle macOS /private/var vs /var discrepancy
384+ expectedPath1, err := filepath.EvalSymlinks(filepath.Join(testDir, "target1.txt"))
385+ require.NoError(t, err)
386+ expectedPath2, err := filepath.EvalSymlinks(filepath.Join(testDir, "target2.txt"))
387+ require.NoError(t, err)
388+
389+ // Check that found paths match expected paths (order may vary)
390+ foundEvalSymlinks := make([]string, len(found))
391+ for i, path := range found {
392+ evalPath, err := filepath.EvalSymlinks(path)
393+ require.NoError(t, err)
394+ foundEvalSymlinks[i] = evalPath
395+ }
396+
397+ require.Contains(t, foundEvalSymlinks, expectedPath1)
398+ require.Contains(t, foundEvalSymlinks, expectedPath2)
399+ })
400+}
401+
402+func TestProbeEnt(t *testing.T) {
403+ t.Run("existing file with correct owner", func(t *testing.T) {
404+ tempDir := t.TempDir()
405+
406+ // Create test file
407+ testFile := filepath.Join(tempDir, "test.txt")
408+ err := os.WriteFile(testFile, []byte("test"), 0o644)
409+ require.NoError(t, err)
410+
411+ // Get owner of temp directory
412+ owner, err := Owner(tempDir)
413+ require.NoError(t, err)
414+
415+ // Test probeEnt with correct owner
416+ err = probeEnt(testFile, owner)
417+ require.NoError(t, err)
418+ })
419+
420+ t.Run("existing directory with correct owner", func(t *testing.T) {
421+ tempDir := t.TempDir()
422+
423+ // Create test directory
424+ testDir := filepath.Join(tempDir, "testdir")
425+ err := os.Mkdir(testDir, 0o755)
426+ require.NoError(t, err)
427+
428+ // Get owner of temp directory
429+ owner, err := Owner(tempDir)
430+ require.NoError(t, err)
431+
432+ // Test probeEnt with correct owner
433+ err = probeEnt(testDir, owner)
434+ require.NoError(t, err)
435+ })
436+
437+ t.Run("nonexistent file", func(t *testing.T) {
438+ tempDir := t.TempDir()
439+
440+ nonexistentFile := filepath.Join(tempDir, "nonexistent.txt")
441+ owner, err := Owner(tempDir)
442+ require.NoError(t, err)
443+
444+ err = probeEnt(nonexistentFile, owner)
445+ require.Error(t, err)
446+ require.True(t, errors.Is(err, os.ErrNotExist))
447+ })
448+
449+ t.Run("nonexistent file in nonexistent directory", func(t *testing.T) {
450+ nonexistentFile := "/this/directory/does/not/exists/nonexistent.txt"
451+
452+ err := probeEnt(nonexistentFile, -1)
453+ require.Error(t, err)
454+ require.True(t, errors.Is(err, os.ErrNotExist))
455+ })
456+
457+ t.Run("ownership bypass with -1", func(t *testing.T) {
458+ tempDir := t.TempDir()
459+
460+ // Create test file
461+ testFile := filepath.Join(tempDir, "test.txt")
462+ err := os.WriteFile(testFile, []byte("test"), 0o644)
463+ require.NoError(t, err)
464+
465+ // Test probeEnt with -1 (bypass ownership check)
466+ err = probeEnt(testFile, -1)
467+ require.NoError(t, err)
468+ })
469+
470+ t.Run("ownership mismatch returns permission error", func(t *testing.T) {
471+ tempDir := t.TempDir()
472+
473+ // Create test file
474+ testFile := filepath.Join(tempDir, "test.txt")
475+ err := os.WriteFile(testFile, []byte("test"), 0o644)
476+ require.NoError(t, err)
477+
478+ // Test probeEnt with different owner (use 9999 which is unlikely to be the actual owner)
479+ err = probeEnt(testFile, 9999)
480+ require.Error(t, err)
481+ require.True(t, errors.Is(err, os.ErrPermission))
482+ })
483+}