package core import ( "os" "path/filepath" "regexp" "testing" ) func TestLinkFile(t *testing.T) { tmp := t.TempDir() src := filepath.Join(tmp, "source.txt") dst := filepath.Join(tmp, "link.txt") // Create source file if err := os.WriteFile(src, []byte("hello"), 0o644); err != nil { t.Fatalf("failed to create source: %v", err) } // Test: create symlink if err := LinkFile(src, dst); err != nil { t.Errorf("LinkFile failed: %v", err) } // Verify symlink info, err := os.Lstat(dst) if err != nil { t.Fatalf("failed to stat link: %v", err) } if info.Mode()&os.ModeSymlink == 0 { t.Error("dst is not a symlink") } // Test: idempotent - same symlink again should succeed if err := LinkFile(src, dst); err != nil { t.Errorf("LinkFile second time failed: %v", err) } // Test: conflict - different source should error src2 := filepath.Join(tmp, "source2.txt") if err := os.WriteFile(src2, []byte("world"), 0o644); err != nil { t.Fatalf("failed to create source2: %v", err) } if err := LinkFile(src2, dst); err == nil { t.Error("LinkFile should fail with conflicting symlink") } // Test: conflict - non-symlink file should error dst2 := filepath.Join(tmp, "regular.txt") if err := os.WriteFile(dst2, []byte("content"), 0o644); err != nil { t.Fatalf("failed to create regular file: %v", err) } if err := LinkFile(src, dst2); err == nil { t.Error("LinkFile should fail when target exists and is not a symlink") } } func TestFindStaleLinks(t *testing.T) { tmp := t.TempDir() filesDir := filepath.Join(tmp, "files") targetDir := filepath.Join(tmp, "target") if err := os.MkdirAll(filesDir, 0o755); err != nil { t.Fatalf("failed to create files dir: %v", err) } if err := os.MkdirAll(targetDir, 0o755); err != nil { t.Fatalf("failed to create target dir: %v", err) } // Create a file in files/ src := filepath.Join(filesDir, "test.txt") if err := os.WriteFile(src, []byte("content"), 0o644); err != nil { t.Fatalf("failed to create source: %v", err) } // Create a subdirectory with a file subDir := filepath.Join(filesDir, "subdir") if err := os.MkdirAll(subDir, 0o755); err != nil { t.Fatalf("failed to create subdir: %v", err) } src2 := filepath.Join(subDir, "nested.txt") if err := os.WriteFile(src2, []byte("nested"), 0o644); err != nil { t.Fatalf("failed to create nested source: %v", err) } // Create symlinks dst := filepath.Join(targetDir, "test.txt") if err := os.Symlink(src, dst); err != nil { t.Fatalf("failed to create symlink: %v", err) } targetSubDir := filepath.Join(targetDir, "subdir") if err := os.MkdirAll(targetSubDir, 0o755); err != nil { t.Fatalf("failed to create target subdir: %v", err) } dst2 := filepath.Join(targetSubDir, "nested.txt") if err := os.Symlink(src2, dst2); err != nil { t.Fatalf("failed to create nested symlink: %v", err) } // Test: no stale links when sources exist stale, err := FindStaleLinks(filesDir, targetDir) if err != nil { t.Errorf("FindStaleLinks failed: %v", err) } if len(stale) != 0 { t.Errorf("expected 0 stale links, got %d", len(stale)) } // Note: The current implementation walks filesDir, so it can't detect // stale links for files that have been deleted from the repo. // This is a known limitation - we'd need to walk the target directory // to find orphaned symlinks, but that has performance issues with // large target directories (like ~). } func TestFindStaleLinks_IgnoresNonRepoSymlinks(t *testing.T) { tmp := t.TempDir() filesDir := filepath.Join(tmp, "files") targetDir := filepath.Join(tmp, "target") if err := os.MkdirAll(filesDir, 0o755); err != nil { t.Fatalf("failed to create files dir: %v", err) } if err := os.MkdirAll(targetDir, 0o755); err != nil { t.Fatalf("failed to create target dir: %v", err) } // Create a file outside the repo external := filepath.Join(tmp, "external.txt") if err := os.WriteFile(external, []byte("external"), 0o644); err != nil { t.Fatalf("failed to create external file: %v", err) } // Create a symlink to external file (should be ignored by FindStaleLinks) dst := filepath.Join(targetDir, "link.txt") if err := os.Symlink(external, dst); err != nil { t.Fatalf("failed to create symlink: %v", err) } // Test: should not report external symlinks as stale stale, err := FindStaleLinks(filesDir, targetDir) if err != nil { t.Errorf("FindStaleLinks failed: %v", err) } if len(stale) != 0 { t.Errorf("expected 0 stale links (external symlinks ignored), got %d", len(stale)) } } func TestApplyPackage(t *testing.T) { tmp := t.TempDir() filesDir := filepath.Join(tmp, "files") targetDir := filepath.Join(tmp, "target") // Create files structure subDir := filepath.Join(filesDir, "config") if err := os.MkdirAll(subDir, 0o755); err != nil { t.Fatalf("failed to create subdir: %v", err) } file1 := filepath.Join(filesDir, "readme.txt") if err := os.WriteFile(file1, []byte("readme"), 0o644); err != nil { t.Fatalf("failed to create file1: %v", err) } file2 := filepath.Join(subDir, "app.conf") if err := os.WriteFile(file2, []byte("config"), 0o644); err != nil { t.Fatalf("failed to create file2: %v", err) } cfg := &packageConfig{} // empty config, no ignores // Apply package if err := ApplyPackage(filesDir, targetDir, cfg); err != nil { t.Errorf("ApplyPackage failed: %v", err) } // Verify symlinks exist targetFile1 := filepath.Join(targetDir, "readme.txt") info, err := os.Lstat(targetFile1) if err != nil { t.Errorf("target file1 not found: %v", err) } else if info.Mode()&os.ModeSymlink == 0 { t.Error("target file1 is not a symlink") } targetFile2 := filepath.Join(targetDir, "config", "app.conf") info, err = os.Lstat(targetFile2) if err != nil { t.Errorf("target file2 not found: %v", err) } else if info.Mode()&os.ModeSymlink == 0 { t.Error("target file2 is not a symlink") } // Verify content is accessible through symlink content, err := os.ReadFile(targetFile1) if err != nil { t.Errorf("failed to read through symlink: %v", err) } if string(content) != "readme" { t.Errorf("wrong content through symlink: %s", string(content)) } } func TestApplyPackage_WithIgnores(t *testing.T) { tmp := t.TempDir() filesDir := filepath.Join(tmp, "files") targetDir := filepath.Join(tmp, "target") if err := os.MkdirAll(filesDir, 0o755); err != nil { t.Fatalf("failed to create files dir: %v", err) } // Create files, some to be ignored mainFile := filepath.Join(filesDir, "main.txt") if err := os.WriteFile(mainFile, []byte("main"), 0o644); err != nil { t.Fatalf("failed to create main file: %v", err) } cacheDir := filepath.Join(filesDir, "cache") if err := os.MkdirAll(cacheDir, 0o755); err != nil { t.Fatalf("failed to create cache dir: %v", err) } cacheFile := filepath.Join(cacheDir, "data.tmp") if err := os.WriteFile(cacheFile, []byte("temp"), 0o644); err != nil { t.Fatalf("failed to create cache file: %v", err) } // Create config with ignore pattern cfg := &packageConfig{ compiledIgnores: []*regexp.Regexp{}, } re, _ := GlobToRegexp("cache/**") cfg.compiledIgnores = append(cfg.compiledIgnores, re) // Apply package if err := ApplyPackage(filesDir, targetDir, cfg); err != nil { t.Errorf("ApplyPackage failed: %v", err) } // Verify main file is linked targetMain := filepath.Join(targetDir, "main.txt") if _, err := os.Lstat(targetMain); err != nil { t.Errorf("main file should be linked: %v", err) } // Verify cache file is not linked (the directory may exist since we walk it // before checking ignore patterns, but it should be empty) targetCacheFile := filepath.Join(targetDir, "cache", "data.tmp") if _, err := os.Lstat(targetCacheFile); !os.IsNotExist(err) { t.Error("cache/data.tmp should not exist in target") } } func TestRestoreOne(t *testing.T) { tmp := t.TempDir() filesDir := filepath.Join(tmp, "files") targetDir := filepath.Join(tmp, "target") if err := os.MkdirAll(filesDir, 0o755); err != nil { t.Fatalf("failed to create files dir: %v", err) } if err := os.MkdirAll(targetDir, 0o755); err != nil { t.Fatalf("failed to create target dir: %v", err) } // Create source file in repo src := filepath.Join(filesDir, "test.txt") if err := os.WriteFile(src, []byte("original"), 0o644); err != nil { t.Fatalf("failed to create source: %v", err) } // Create symlink in target dst := filepath.Join(targetDir, "test.txt") if err := os.Symlink(src, dst); err != nil { t.Fatalf("failed to create symlink: %v", err) } // Restore (removes symlink, copies file) filesAbs, _ := filepath.Abs(filesDir) if err := RestoreOne(src, dst, filesAbs, false); err != nil { t.Errorf("RestoreOne failed: %v", err) } // Verify symlink is gone info, err := os.Lstat(dst) if err != nil { t.Fatalf("failed to stat restored file: %v", err) } if info.Mode()&os.ModeSymlink != 0 { t.Error("file should not be a symlink after restore") } // Verify content is preserved content, err := os.ReadFile(dst) if err != nil { t.Errorf("failed to read restored file: %v", err) } if string(content) != "original" { t.Errorf("wrong content after restore: %s", string(content)) } }