lookup_test.go

  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}