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
 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
142func 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
178func 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
402func 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}