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}