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 TestLookupClosestBounded(t *testing.T) {
357 t.Run("found in starting directory", func(t *testing.T) {
358 testDir := t.TempDir()
359
360 targetFile := filepath.Join(testDir, "target.txt")
361 require.NoError(t, os.WriteFile(targetFile, []byte("test"), 0o644))
362
363 foundPath, found := LookupClosestBounded(testDir, testDir, "target.txt")
364 require.True(t, found)
365 require.Equal(t, targetFile, foundPath)
366 })
367
368 t.Run("found at boundary directory", func(t *testing.T) {
369 boundary := t.TempDir()
370
371 subDir := filepath.Join(boundary, "subdir")
372 require.NoError(t, os.Mkdir(subDir, 0o755))
373
374 targetFile := filepath.Join(boundary, "target.txt")
375 require.NoError(t, os.WriteFile(targetFile, []byte("test"), 0o644))
376
377 foundPath, found := LookupClosestBounded(subDir, boundary, "target.txt")
378 require.True(t, found)
379 require.Equal(t, targetFile, foundPath)
380 })
381
382 t.Run("does not climb past boundary", func(t *testing.T) {
383 parent := t.TempDir()
384
385 // Target lives above the boundary.
386 require.NoError(t, os.WriteFile(filepath.Join(parent, "target.txt"), []byte("test"), 0o644))
387
388 boundary := filepath.Join(parent, "project")
389 require.NoError(t, os.Mkdir(boundary, 0o755))
390
391 subDir := filepath.Join(boundary, "subdir")
392 require.NoError(t, os.Mkdir(subDir, 0o755))
393
394 foundPath, found := LookupClosestBounded(subDir, boundary, "target.txt")
395 require.False(t, found)
396 require.Empty(t, foundPath)
397 })
398
399 t.Run("empty boundary searches only starting directory", func(t *testing.T) {
400 parent := t.TempDir()
401
402 require.NoError(t, os.WriteFile(filepath.Join(parent, "target.txt"), []byte("test"), 0o644))
403
404 subDir := filepath.Join(parent, "subdir")
405 require.NoError(t, os.Mkdir(subDir, 0o755))
406
407 foundPath, found := LookupClosestBounded(subDir, "", "target.txt")
408 require.False(t, found)
409 require.Empty(t, foundPath)
410 })
411
412 t.Run("empty boundary still finds in starting directory", func(t *testing.T) {
413 testDir := t.TempDir()
414
415 targetFile := filepath.Join(testDir, "target.txt")
416 require.NoError(t, os.WriteFile(targetFile, []byte("test"), 0o644))
417
418 foundPath, found := LookupClosestBounded(testDir, "", "target.txt")
419 require.True(t, found)
420 require.Equal(t, targetFile, foundPath)
421 })
422}
423
424func TestLookupBounded(t *testing.T) {
425 t.Run("returns matches at and below boundary", func(t *testing.T) {
426 boundary := t.TempDir()
427
428 subDir := filepath.Join(boundary, "subdir")
429 require.NoError(t, os.Mkdir(subDir, 0o755))
430
431 atBoundary := filepath.Join(boundary, "target.txt")
432 atSub := filepath.Join(subDir, "target.txt")
433 require.NoError(t, os.WriteFile(atBoundary, []byte("a"), 0o644))
434 require.NoError(t, os.WriteFile(atSub, []byte("b"), 0o644))
435
436 found, err := LookupBounded(subDir, boundary, "target.txt")
437 require.NoError(t, err)
438 require.Len(t, found, 2)
439 require.Contains(t, found, atBoundary)
440 require.Contains(t, found, atSub)
441 })
442
443 t.Run("ignores matches above boundary", func(t *testing.T) {
444 parent := t.TempDir()
445
446 require.NoError(t, os.WriteFile(filepath.Join(parent, "target.txt"), []byte("nope"), 0o644))
447
448 boundary := filepath.Join(parent, "project")
449 require.NoError(t, os.Mkdir(boundary, 0o755))
450
451 subDir := filepath.Join(boundary, "subdir")
452 require.NoError(t, os.Mkdir(subDir, 0o755))
453
454 // Target lives only above the boundary.
455 found, err := LookupBounded(subDir, boundary, "target.txt")
456 require.NoError(t, err)
457 require.Empty(t, found)
458 })
459
460 t.Run("empty boundary searches only starting directory", func(t *testing.T) {
461 parent := t.TempDir()
462
463 require.NoError(t, os.WriteFile(filepath.Join(parent, "target.txt"), []byte("nope"), 0o644))
464
465 subDir := filepath.Join(parent, "subdir")
466 require.NoError(t, os.Mkdir(subDir, 0o755))
467
468 found, err := LookupBounded(subDir, "", "target.txt")
469 require.NoError(t, err)
470 require.Empty(t, found)
471 })
472
473 t.Run("no targets returns nil", func(t *testing.T) {
474 dir := t.TempDir()
475 found, err := LookupBounded(dir, dir)
476 require.NoError(t, err)
477 require.Empty(t, found)
478 })
479}
480
481func TestProbeEnt(t *testing.T) {
482 t.Run("existing file with correct owner", func(t *testing.T) {
483 tempDir := t.TempDir()
484
485 // Create test file
486 testFile := filepath.Join(tempDir, "test.txt")
487 err := os.WriteFile(testFile, []byte("test"), 0o644)
488 require.NoError(t, err)
489
490 // Get owner of temp directory
491 owner, err := Owner(tempDir)
492 require.NoError(t, err)
493
494 // Test probeEnt with correct owner
495 err = probeEnt(testFile, owner)
496 require.NoError(t, err)
497 })
498
499 t.Run("existing directory with correct owner", func(t *testing.T) {
500 tempDir := t.TempDir()
501
502 // Create test directory
503 testDir := filepath.Join(tempDir, "testdir")
504 err := os.Mkdir(testDir, 0o755)
505 require.NoError(t, err)
506
507 // Get owner of temp directory
508 owner, err := Owner(tempDir)
509 require.NoError(t, err)
510
511 // Test probeEnt with correct owner
512 err = probeEnt(testDir, owner)
513 require.NoError(t, err)
514 })
515
516 t.Run("nonexistent file", func(t *testing.T) {
517 tempDir := t.TempDir()
518
519 nonexistentFile := filepath.Join(tempDir, "nonexistent.txt")
520 owner, err := Owner(tempDir)
521 require.NoError(t, err)
522
523 err = probeEnt(nonexistentFile, owner)
524 require.Error(t, err)
525 require.True(t, errors.Is(err, os.ErrNotExist))
526 })
527
528 t.Run("nonexistent file in nonexistent directory", func(t *testing.T) {
529 nonexistentFile := "/this/directory/does/not/exists/nonexistent.txt"
530
531 err := probeEnt(nonexistentFile, -1)
532 require.Error(t, err)
533 require.True(t, errors.Is(err, os.ErrNotExist))
534 })
535
536 t.Run("ownership bypass with -1", func(t *testing.T) {
537 tempDir := t.TempDir()
538
539 // Create test file
540 testFile := filepath.Join(tempDir, "test.txt")
541 err := os.WriteFile(testFile, []byte("test"), 0o644)
542 require.NoError(t, err)
543
544 // Test probeEnt with -1 (bypass ownership check)
545 err = probeEnt(testFile, -1)
546 require.NoError(t, err)
547 })
548
549 t.Run("ownership mismatch returns permission error", func(t *testing.T) {
550 tempDir := t.TempDir()
551
552 // Create test file
553 testFile := filepath.Join(tempDir, "test.txt")
554 err := os.WriteFile(testFile, []byte("test"), 0o644)
555 require.NoError(t, err)
556
557 // Test probeEnt with different owner (use 9999 which is unlikely to be the actual owner)
558 err = probeEnt(testFile, 9999)
559 require.Error(t, err)
560 require.True(t, errors.Is(err, os.ErrPermission))
561 })
562}