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