summaryrefslogtreecommitdiff
path: root/internal/resource/dir/dir_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/resource/dir/dir_test.go')
-rw-r--r--internal/resource/dir/dir_test.go319
1 files changed, 319 insertions, 0 deletions
diff --git a/internal/resource/dir/dir_test.go b/internal/resource/dir/dir_test.go
new file mode 100644
index 0000000..b93564a
--- /dev/null
+++ b/internal/resource/dir/dir_test.go
@@ -0,0 +1,319 @@
+package dir
+
+import (
+ "os"
+ "path/filepath"
+ "testing"
+
+ "codeberg.org/snonux/gonf/internal/resource/file"
+)
+
+func TestHaveDirectoryCreate(t *testing.T) {
+ tmp := t.TempDir()
+ path := filepath.Join(tmp, "sub", "nested")
+
+ Have(path)
+
+ info, err := os.Stat(path)
+ if err != nil {
+ t.Fatalf("stat: %v", err)
+ }
+ if !info.IsDir() {
+ t.Errorf("expected a directory at %s", path)
+ }
+ if info.Mode().Perm() != 0o750 {
+ t.Errorf("expected default dir mode 0750, got %v", info.Mode().Perm())
+ }
+}
+
+func TestHaveDirectoryIdempotentWithMode(t *testing.T) {
+ tmp := t.TempDir()
+ path := filepath.Join(tmp, "d")
+
+ // Call ensureDirectorySelf directly to exercise idempotency without the
+ // one-per-process resource registry rejecting a duplicate registration.
+ d1 := &Dir{path: path, mode: 0o755}
+ if err := ensureDirectorySelf(d1); err != nil {
+ t.Fatalf("first apply: %v", err)
+ }
+ d2 := &Dir{path: path, mode: 0o700}
+ if err := ensureDirectorySelf(d2); err != nil {
+ t.Fatalf("second apply: %v", err)
+ }
+
+ info, err := os.Stat(path)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if info.Mode().Perm() != 0o700 {
+ t.Errorf("expected mode 0700 enforced, got %v", info.Mode().Perm())
+ }
+}
+
+func TestHaveDirectoryFailsWhenFileExists(t *testing.T) {
+ tmp := t.TempDir()
+ path := filepath.Join(tmp, "afile")
+ if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Ensure(path); err == nil {
+ t.Error("expected error when a regular file is in the way of a directory")
+ }
+}
+
+func TestHaveAbsentNonEmptyDirWithoutPruneFails(t *testing.T) {
+ tmp := t.TempDir()
+ target := filepath.Join(tmp, "d")
+ if err := os.MkdirAll(filepath.Join(target, "sub"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := Ensure(target, IsAbsent()); err == nil {
+ t.Error("expected error removing a non-empty directory without WithPrune()")
+ }
+ if _, err := os.Stat(target); err != nil {
+ t.Errorf("expected %s to still exist, got %v", target, err)
+ }
+}
+
+func TestHaveAbsentPruneDirectoryRecursive(t *testing.T) {
+ tmp := t.TempDir()
+ target := filepath.Join(tmp, "d")
+ if err := os.MkdirAll(filepath.Join(target, "sub", "deep"), 0o755); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.WriteFile(filepath.Join(target, "sub", "f.txt"), []byte("x"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(target, IsAbsent(), WithPrune())
+ if _, err := os.Stat(target); !os.IsNotExist(err) {
+ t.Errorf("expected %s to be removed recursively", target)
+ }
+
+ // Idempotent: removing a missing tree is not an error.
+ d := &Dir{path: target, absent: true, prune: true}
+ if err := ensureAbsent(d); err != nil {
+ t.Fatalf("prune on missing tree: %v", err)
+ }
+}
+
+func TestHaveDirectoryWithSource(t *testing.T) {
+ t.Run("recursive copy", func(t *testing.T) {
+ tmp := t.TempDir()
+ src := t.TempDir()
+ dst := filepath.Join(tmp, "dst")
+
+ srcFile := filepath.Join(src, "file.txt")
+ if err := os.WriteFile(srcFile, []byte("hello"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ srcSub := filepath.Join(src, "sub")
+ if err := os.MkdirAll(srcSub, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ srcSubFile := filepath.Join(srcSub, "subfile.txt")
+ if err := os.WriteFile(srcSubFile, []byte("sub hello"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(dst, WithSource(src))
+
+ if data, err := os.ReadFile(filepath.Join(dst, "file.txt")); err != nil || string(data) != "hello" {
+ t.Errorf("expected 'hello' at %s, got %q err %v", filepath.Join(dst, "file.txt"), string(data), err)
+ }
+ if data, err := os.ReadFile(filepath.Join(dst, "sub", "subfile.txt")); err != nil || string(data) != "sub hello" {
+ t.Errorf("expected 'sub hello' at %s, got %q err %v", filepath.Join(dst, "sub", "subfile.txt"), string(data), err)
+ }
+ })
+
+ t.Run("pruning", func(t *testing.T) {
+ tmp := t.TempDir()
+ src := t.TempDir()
+ dst := filepath.Join(tmp, "dst")
+
+ srcFile := filepath.Join(src, "file.txt")
+ if err := os.WriteFile(srcFile, []byte("hello"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := os.MkdirAll(dst, 0o755); err != nil {
+ t.Fatal(err)
+ }
+ extra := filepath.Join(dst, "extra.txt")
+ if err := os.WriteFile(extra, []byte("extra"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ extraSub := filepath.Join(dst, "extra-sub")
+ if err := os.MkdirAll(extraSub, 0o755); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(dst, WithSource(src), WithPrune())
+
+ if _, err := os.Stat(extra); !os.IsNotExist(err) {
+ t.Errorf("expected %s to be pruned", extra)
+ }
+ if _, err := os.Stat(extraSub); !os.IsNotExist(err) {
+ t.Errorf("expected %s to be pruned", extraSub)
+ }
+ if data, err := os.ReadFile(filepath.Join(dst, "file.txt")); err != nil || string(data) != "hello" {
+ t.Errorf("expected 'hello' at %s, got %q err %v", filepath.Join(dst, "file.txt"), string(data), err)
+ }
+ })
+}
+
+func TestSourceCopyUsesFileModeDefaultNotDirMode(t *testing.T) {
+ tmp := t.TempDir()
+ src := t.TempDir()
+ dst := filepath.Join(tmp, "dst")
+
+ if err := os.WriteFile(filepath.Join(src, "file.txt"), []byte("hello"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(dst, WithSource(src))
+
+ dirInfo, err := os.Stat(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if dirInfo.Mode().Perm() != 0o750 {
+ t.Errorf("expected dir mode 0750, got %v", dirInfo.Mode().Perm())
+ }
+
+ fileInfo, err := os.Stat(filepath.Join(dst, "file.txt"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fileInfo.Mode().Perm() != 0o640 {
+ t.Errorf("expected copied file mode 0640 (not the directory's 0750), got %v", fileInfo.Mode().Perm())
+ }
+}
+
+func TestSourceCopyRespectsExplicitWithFileMode(t *testing.T) {
+ tmp := t.TempDir()
+ src := t.TempDir()
+ dst := filepath.Join(tmp, "dst")
+
+ if err := os.WriteFile(filepath.Join(src, "file.txt"), []byte("hello"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(dst, WithSource(src), WithMode(0o755), WithFileMode(0o600))
+
+ dirInfo, err := os.Stat(dst)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if dirInfo.Mode().Perm() != 0o755 {
+ t.Errorf("expected dir mode 0755, got %v", dirInfo.Mode().Perm())
+ }
+
+ fileInfo, err := os.Stat(filepath.Join(dst, "file.txt"))
+ if err != nil {
+ t.Fatal(err)
+ }
+ if fileInfo.Mode().Perm() != 0o600 {
+ t.Errorf("expected copied file mode 0600, got %v", fileInfo.Mode().Perm())
+ }
+}
+
+func TestSourceCopyStripsTmplSuffixOnCopiedFile(t *testing.T) {
+ tmp := t.TempDir()
+ src := t.TempDir()
+ dst := filepath.Join(tmp, "dst")
+
+ if err := os.WriteFile(filepath.Join(src, "foo.conf.tmpl"), []byte("hello {{.Param}}"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(dst, WithSource(src))
+
+ if _, err := os.Stat(filepath.Join(dst, "foo.conf")); err != nil {
+ t.Errorf("expected de-suffixed foo.conf to exist: %v", err)
+ }
+ if _, err := os.Stat(filepath.Join(dst, "foo.conf.tmpl")); !os.IsNotExist(err) {
+ t.Errorf("expected foo.conf.tmpl to NOT exist on disk")
+ }
+}
+
+func TestSourceCopyWithPruneKeepsTemplatedFile(t *testing.T) {
+ tmp := t.TempDir()
+ src := t.TempDir()
+ dst := filepath.Join(tmp, "dst")
+
+ if err := os.WriteFile(filepath.Join(src, "foo.conf.tmpl"), []byte("hello {{.Param}}"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ // WithPrune reconciles the destination against the source on every
+ // apply; a de-suffixed templated file must not be pruned just because
+ // its own name has no direct match in the source tree.
+ Have(dst, WithSource(src), WithPrune())
+
+ if _, err := os.Stat(filepath.Join(dst, "foo.conf")); err != nil {
+ t.Errorf("expected foo.conf to survive pruning, got %v", err)
+ }
+}
+
+func TestSourceCopyParamMatchesSingleFilePath(t *testing.T) {
+ tmp := t.TempDir()
+ src := t.TempDir()
+ dst := filepath.Join(tmp, "dst")
+ sourcePath := filepath.Join(src, "foo.conf.tmpl")
+ if err := os.WriteFile(sourcePath, []byte("{{.Param}}"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(dst, WithSource(src))
+ viaDir, err := os.ReadFile(filepath.Join(dst, "foo.conf"))
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ singleTarget := filepath.Join(tmp, "single.conf")
+ if err := file.Ensure(singleTarget, file.WithSource(sourcePath)); err != nil {
+ t.Fatal(err)
+ }
+ viaFile, err := os.ReadFile(singleTarget)
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ if string(viaDir) != string(viaFile) {
+ t.Errorf("expected identical .Param rendering via both paths, dir-copy=%q file-direct=%q", viaDir, viaFile)
+ }
+}
+
+func TestSourceCopyRecreatesSymlinkNotContent(t *testing.T) {
+ tmp := t.TempDir()
+ src := t.TempDir()
+ dst := filepath.Join(tmp, "dst")
+
+ if err := os.WriteFile(filepath.Join(src, "target.txt"), []byte("hello"), 0o644); err != nil {
+ t.Fatal(err)
+ }
+ if err := os.Symlink("target.txt", filepath.Join(src, "link.txt")); err != nil {
+ t.Fatal(err)
+ }
+
+ Have(dst, WithSource(src))
+
+ linkPath := filepath.Join(dst, "link.txt")
+ info, err := os.Lstat(linkPath)
+ if err != nil {
+ t.Fatalf("lstat: %v", err)
+ }
+ if info.Mode()&os.ModeSymlink == 0 {
+ t.Fatalf("expected %s to be a symlink, got mode %v", linkPath, info.Mode())
+ }
+ got, err := os.Readlink(linkPath)
+ if err != nil {
+ t.Fatalf("readlink: %v", err)
+ }
+ if got != "target.txt" {
+ t.Errorf("expected symlink target %q, got %q", "target.txt", got)
+ }
+}