summaryrefslogtreecommitdiff
path: root/internal/resource/link/link_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/resource/link/link_test.go')
-rw-r--r--internal/resource/link/link_test.go189
1 files changed, 189 insertions, 0 deletions
diff --git a/internal/resource/link/link_test.go b/internal/resource/link/link_test.go
new file mode 100644
index 0000000..8da674b
--- /dev/null
+++ b/internal/resource/link/link_test.go
@@ -0,0 +1,189 @@
+package link
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+)
+
+func TestHaveSymlinkCreateAndIdempotent(t *testing.T) {
+ dir := t.TempDir()
+ target := filepath.Join(dir, "target.txt")
+ link := filepath.Join(dir, "link.txt")
+ if err := os.WriteFile(target, []byte("t"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(link, IsSymlink(target))
+ got, err := os.Readlink(link)
+ if err != nil {
+ t.Fatalf("readlink: %v", err)
+ }
+ if got != target {
+ t.Errorf("expected link -> %s, got %s", target, got)
+ }
+
+ // Re-applying the same link should be a no-op (tested directly to avoid the
+ // one-per-process resource registry rejecting a duplicate registration).
+ l := &Link{path: link, kind: symlinkKind, target: target}
+ if err := ensureSymlink(l); err != nil {
+ t.Fatalf("idempotent apply: %v", err)
+ }
+}
+
+func TestHaveSymlinkRepoints(t *testing.T) {
+ dir := t.TempDir()
+ old := filepath.Join(dir, "old.txt")
+ newT := filepath.Join(dir, "new.txt")
+ link := filepath.Join(dir, "link")
+ for _, p := range []string{old, newT} {
+ if err := os.WriteFile(p, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ }
+ if err := os.Symlink(old, link); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(link, IsSymlink(newT))
+ got, err := os.Readlink(link)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got != newT {
+ t.Errorf("expected repoint to %s, got %s", newT, got)
+ }
+}
+
+func TestHaveSymlinkMovesRealFileAside(t *testing.T) {
+ dir := t.TempDir()
+ target := filepath.Join(dir, "target.txt")
+ link := filepath.Join(dir, "real")
+ if err := os.WriteFile(target, []byte("t"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(link, []byte("original"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(link, IsSymlink(target))
+
+ got, err := os.Readlink(link)
+ if err != nil {
+ t.Fatalf("expected %s to be a symlink: %v", link, err)
+ }
+ if got != target {
+ t.Errorf("expected link -> %s, got %s", target, got)
+ }
+ if data, err := os.ReadFile(link + ".old"); err != nil || string(data) != "original" {
+ t.Errorf("expected original content preserved in %s.old, got %q err %v", link, string(data), err)
+ }
+}
+
+func TestHaveHardlinkCreateAndIdempotent(t *testing.T) {
+ dir := t.TempDir()
+ target := filepath.Join(dir, "target.txt")
+ link := filepath.Join(dir, "link.txt")
+ if err := os.WriteFile(target, []byte("payload"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(link, IsHardlink(target))
+
+ ti, err := os.Stat(target)
+ if err != nil {
+ t.Fatal(err)
+ }
+ li, err := os.Stat(link)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !sameInode(ti, li) {
+ t.Errorf("expected %s and %s to share an inode", link, target)
+ }
+
+ // Re-applying the same link should be a no-op (direct call to avoid the
+ // one-per-process resource registry rejecting a duplicate registration).
+ l := &Link{path: link, kind: hardlinkKind, target: target}
+ if err := ensureHardlink(l); err != nil {
+ t.Fatalf("idempotent apply: %v", err)
+ }
+}
+
+func TestHaveHardlinkMovesRealFileAside(t *testing.T) {
+ dir := t.TempDir()
+ target := filepath.Join(dir, "target.txt")
+ link := filepath.Join(dir, "real")
+ if err := os.WriteFile(target, []byte("payload"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(link, []byte("original"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(link, IsHardlink(target))
+
+ ti, err := os.Stat(target)
+ if err != nil {
+ t.Fatal(err)
+ }
+ li, err := os.Stat(link)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !sameInode(ti, li) {
+ t.Errorf("expected %s to be hardlinked to %s", link, target)
+ }
+ if data, err := os.ReadFile(link + ".old"); err != nil || string(data) != "original" {
+ t.Errorf("expected original content preserved in %s.old, got %q err %v", link, string(data), err)
+ }
+}
+
+func TestHaveHardlinkMissingTarget(t *testing.T) {
+ dir := t.TempDir()
+ link := filepath.Join(dir, "link")
+ if err := Ensure(link, IsHardlink(filepath.Join(dir, "nope"))); err == nil {
+ t.Error("expected error when hardlink target does not exist")
+ }
+}
+
+func TestHaveAbsentSymlink(t *testing.T) {
+ dir := t.TempDir()
+ target := filepath.Join(dir, "target.txt")
+ link := filepath.Join(dir, "link.txt")
+ if err := os.WriteFile(target, []byte("t"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink(target, link); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(link, IsAbsent())
+ if _, err := os.Lstat(link); !os.IsNotExist(err) {
+ t.Errorf("expected %s to be removed", link)
+ }
+
+ // Idempotent: removing a missing link is not an error (direct call to
+ // avoid duplicate registration in the one-per-process registry).
+ if err := ensureAbsent(link); err != nil {
+ t.Fatalf("absent on missing link: %v", err)
+ }
+}
+
+func TestHaveAbsentWithoutKindRegistersGenericLink(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "whatever")
+
+ l := build(path, IsAbsent())
+ if got := l.resourceType(); got != "Link" {
+ t.Errorf("expected generic resource type %q, got %q", "Link", got)
+ }
+}
+
+func TestBuildRequiresKindOrAbsent(t *testing.T) {
+ dir := t.TempDir()
+ path := filepath.Join(dir, "nope")
+ if err := Ensure(path); err == nil {
+ t.Error("expected error when neither IsSymlink, IsHardlink, nor IsAbsent is set")
+ }
+}