summaryrefslogtreecommitdiff
path: root/internal/resource
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-07-05 11:25:57 +0300
committerPaul Buetow <paul@buetow.org>2026-07-05 11:25:57 +0300
commit4c5b95d5e3811025e2148a6f2a059738e79ca2c9 (patch)
treee0aaaef14e1ed6fc0d78c460b671289e39e94c73 /internal/resource
parent5256a4d1e5a56510758bfac710ba2ad2a45af66e (diff)
add tests for repository's Apply
Diffstat (limited to 'internal/resource')
-rw-r--r--internal/resource/dir/dir.go7
-rw-r--r--internal/resource/dir/dir_test.go49
-rw-r--r--internal/resource/file/file.go7
-rw-r--r--internal/resource/file/file_test.go32
-rw-r--r--internal/resource/link/link.go3
-rw-r--r--internal/resource/link/link_test.go32
-rw-r--r--internal/resource/repository.go2
-rw-r--r--internal/resource/repository_test.go126
-rw-r--r--internal/resource/resource_test.go12
9 files changed, 255 insertions, 15 deletions
diff --git a/internal/resource/dir/dir.go b/internal/resource/dir/dir.go
index 1934272..ba50caf 100644
--- a/internal/resource/dir/dir.go
+++ b/internal/resource/dir/dir.go
@@ -180,8 +180,11 @@ func applyAttributesTo(path string, mode os.FileMode, usr, group string) error {
// Ensure builds and applies the directory resource described by opts,
// without registering it.
func Ensure(path string, opts ...opt.Option) error {
- _, err := build(path, opts...)
- return err
+ d, err := build(path, opts...)
+ if err != nil {
+ return err
+ }
+ return d.apply()
}
func Have(path string, opts ...opt.Option) resource.Resource {
diff --git a/internal/resource/dir/dir_test.go b/internal/resource/dir/dir_test.go
index 5ffb5cf..8b90fac 100644
--- a/internal/resource/dir/dir_test.go
+++ b/internal/resource/dir/dir_test.go
@@ -5,15 +5,21 @@ import (
"path/filepath"
"testing"
+ resource "codeberg.org/snonux/gonf/internal/resource"
"codeberg.org/snonux/gonf/internal/resource/file"
+
. "codeberg.org/snonux/gonf/internal/resource/opt"
)
func TestHaveDirectoryCreate(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
path := filepath.Join(tmp, "sub", "nested")
Have(path)
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
info, err := os.Stat(path)
if err != nil {
@@ -28,6 +34,7 @@ func TestHaveDirectoryCreate(t *testing.T) {
}
func TestHaveDirectoryIdempotentWithMode(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
path := filepath.Join(tmp, "d")
@@ -52,6 +59,7 @@ func TestHaveDirectoryIdempotentWithMode(t *testing.T) {
}
func TestHaveDirectoryFailsWhenFileExists(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
path := filepath.Join(tmp, "afile")
if err := os.WriteFile(path, []byte("x"), 0o644); err != nil {
@@ -64,6 +72,7 @@ func TestHaveDirectoryFailsWhenFileExists(t *testing.T) {
}
func TestHaveAbsentNonEmptyDirWithoutPruneFails(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
target := filepath.Join(tmp, "d")
if err := os.MkdirAll(filepath.Join(target, "sub"), 0o755); err != nil {
@@ -79,6 +88,7 @@ func TestHaveAbsentNonEmptyDirWithoutPruneFails(t *testing.T) {
}
func TestHaveAbsentPruneDirectoryRecursive(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
target := filepath.Join(tmp, "d")
if err := os.MkdirAll(filepath.Join(target, "sub", "deep"), 0o755); err != nil {
@@ -89,6 +99,9 @@ func TestHaveAbsentPruneDirectoryRecursive(t *testing.T) {
}
Have(target, IsAbsent(), WithPrune())
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if _, err := os.Stat(target); !os.IsNotExist(err) {
t.Errorf("expected %s to be removed recursively", target)
}
@@ -101,6 +114,7 @@ func TestHaveAbsentPruneDirectoryRecursive(t *testing.T) {
}
func TestAbsentPruneDirectoryRecursive(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
target := filepath.Join(tmp, "d")
if err := os.MkdirAll(filepath.Join(target, "sub", "deep"), 0o755); err != nil {
@@ -111,6 +125,9 @@ func TestAbsentPruneDirectoryRecursive(t *testing.T) {
}
Absent(target, WithPrune())
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if _, err := os.Stat(target); !os.IsNotExist(err) {
t.Errorf("expected %s to be removed recursively", target)
}
@@ -124,6 +141,7 @@ func TestAbsentPruneDirectoryRecursive(t *testing.T) {
func TestHaveDirectoryWithSource(t *testing.T) {
t.Run("recursive copy", func(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
src := t.TempDir()
dst := filepath.Join(tmp, "dst")
@@ -142,6 +160,9 @@ func TestHaveDirectoryWithSource(t *testing.T) {
}
Have(dst, WithSource(src))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
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)
@@ -152,6 +173,7 @@ func TestHaveDirectoryWithSource(t *testing.T) {
})
t.Run("pruning", func(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
src := t.TempDir()
dst := filepath.Join(tmp, "dst")
@@ -174,6 +196,9 @@ func TestHaveDirectoryWithSource(t *testing.T) {
}
Have(dst, WithSource(src), WithPrune())
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if _, err := os.Stat(extra); !os.IsNotExist(err) {
t.Errorf("expected %s to be pruned", extra)
@@ -188,6 +213,7 @@ func TestHaveDirectoryWithSource(t *testing.T) {
}
func TestSourceCopyUsesFileModeDefaultNotDirMode(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
src := t.TempDir()
dst := filepath.Join(tmp, "dst")
@@ -197,6 +223,9 @@ func TestSourceCopyUsesFileModeDefaultNotDirMode(t *testing.T) {
}
Have(dst, WithSource(src))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
dirInfo, err := os.Stat(dst)
if err != nil {
@@ -216,6 +245,7 @@ func TestSourceCopyUsesFileModeDefaultNotDirMode(t *testing.T) {
}
func TestSourceCopyRespectsExplicitWithFileMode(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
src := t.TempDir()
dst := filepath.Join(tmp, "dst")
@@ -225,6 +255,9 @@ func TestSourceCopyRespectsExplicitWithFileMode(t *testing.T) {
}
Have(dst, WithSource(src), WithMode(0o755), WithFileMode(0o600))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
dirInfo, err := os.Stat(dst)
if err != nil {
@@ -244,6 +277,7 @@ func TestSourceCopyRespectsExplicitWithFileMode(t *testing.T) {
}
func TestSourceCopyStripsTmplSuffixOnCopiedFile(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
src := t.TempDir()
dst := filepath.Join(tmp, "dst")
@@ -253,6 +287,9 @@ func TestSourceCopyStripsTmplSuffixOnCopiedFile(t *testing.T) {
}
Have(dst, WithSource(src))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if _, err := os.Stat(filepath.Join(dst, "foo.conf")); err != nil {
t.Errorf("expected de-suffixed foo.conf to exist: %v", err)
@@ -263,6 +300,7 @@ func TestSourceCopyStripsTmplSuffixOnCopiedFile(t *testing.T) {
}
func TestSourceCopyWithPruneKeepsTemplatedFile(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
src := t.TempDir()
dst := filepath.Join(tmp, "dst")
@@ -275,6 +313,9 @@ func TestSourceCopyWithPruneKeepsTemplatedFile(t *testing.T) {
// 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 := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if _, err := os.Stat(filepath.Join(dst, "foo.conf")); err != nil {
t.Errorf("expected foo.conf to survive pruning, got %v", err)
@@ -282,6 +323,7 @@ func TestSourceCopyWithPruneKeepsTemplatedFile(t *testing.T) {
}
func TestSourceCopyParamMatchesSingleFilePath(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
src := t.TempDir()
dst := filepath.Join(tmp, "dst")
@@ -291,6 +333,9 @@ func TestSourceCopyParamMatchesSingleFilePath(t *testing.T) {
}
Have(dst, WithSource(src))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
viaDir, err := os.ReadFile(filepath.Join(dst, "foo.conf"))
if err != nil {
t.Fatal(err)
@@ -311,6 +356,7 @@ func TestSourceCopyParamMatchesSingleFilePath(t *testing.T) {
}
func TestSourceCopyRecreatesSymlinkNotContent(t *testing.T) {
+ resource.ResetRepository()
tmp := t.TempDir()
src := t.TempDir()
dst := filepath.Join(tmp, "dst")
@@ -323,6 +369,9 @@ func TestSourceCopyRecreatesSymlinkNotContent(t *testing.T) {
}
Have(dst, WithSource(src))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
linkPath := filepath.Join(dst, "link.txt")
info, err := os.Lstat(linkPath)
diff --git a/internal/resource/file/file.go b/internal/resource/file/file.go
index ea443a9..50ba6e3 100644
--- a/internal/resource/file/file.go
+++ b/internal/resource/file/file.go
@@ -211,8 +211,11 @@ func ensureAbsent(path string) error {
// registering it. Used by other resource packages (e.g. dir) to write an
// individual file without it becoming its own top-level resource.
func Ensure(path string, opts ...opt.Option) error {
- _, err := build(path, opts...)
- return err
+ f, err := build(path, opts...)
+ if err != nil {
+ return err
+ }
+ return f.apply()
}
func Have(path string, opts ...opt.Option) resource.Resource {
diff --git a/internal/resource/file/file_test.go b/internal/resource/file/file_test.go
index 49deb4c..0669b88 100644
--- a/internal/resource/file/file_test.go
+++ b/internal/resource/file/file_test.go
@@ -6,10 +6,12 @@ import (
"strings"
"testing"
+ "codeberg.org/snonux/gonf/internal/resource"
. "codeberg.org/snonux/gonf/internal/resource/opt"
)
func TestGetChecksum(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
path := filepath.Join(dir, "test.txt")
@@ -29,6 +31,7 @@ func TestGetChecksum(t *testing.T) {
}
func TestWriteTmpFile(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
tmpPath := filepath.Join(dir, "test.tmp")
content := []byte("temp content")
@@ -47,6 +50,7 @@ func TestWriteTmpFile(t *testing.T) {
}
func TestUpdateFromTmpChecksumChanged(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
tmpPath := filepath.Join(dir, "test.tmp")
path := filepath.Join(dir, "test.txt")
@@ -72,6 +76,7 @@ func TestUpdateFromTmpChecksumChanged(t *testing.T) {
}
func TestUpdateFromTmpChecksumUnchanged(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
tmpPath := filepath.Join(dir, "test.tmp")
path := filepath.Join(dir, "test.txt")
@@ -100,10 +105,14 @@ func TestUpdateFromTmpChecksumUnchanged(t *testing.T) {
}
func TestHaveStringCreateNewFile(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
path := filepath.Join(dir, "new.txt")
Have(path, WithContent("hello world"))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
got, err := os.ReadFile(path)
if err != nil {
@@ -115,11 +124,15 @@ func TestHaveStringCreateNewFile(t *testing.T) {
}
func TestHaveMode(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
path := filepath.Join(dir, "mode.txt")
mode := os.FileMode(0o600)
Have(path, WithContent("mode test"), WithMode(mode))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
info, err := os.Stat(path)
if err != nil {
@@ -131,11 +144,15 @@ func TestHaveMode(t *testing.T) {
}
func TestHaveSourceFile(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
sourcePath := filepath.Join("..", "..", "..", "assets", "testfiles", "test.txt")
targetPath := filepath.Join(dir, "target.txt")
Have(targetPath, WithSource(sourcePath))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
got, err := os.ReadFile(targetPath)
if err != nil {
@@ -148,11 +165,15 @@ func TestHaveSourceFile(t *testing.T) {
}
func TestHaveTemplateFile(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
sourcePath := filepath.Join("..", "..", "..", "assets", "testfiles", "test.tmpl")
targetPath := filepath.Join(dir, "target.conf")
Have(targetPath, WithSource(sourcePath))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
got, err := os.ReadFile(targetPath)
if err != nil {
@@ -166,6 +187,7 @@ func TestHaveTemplateFile(t *testing.T) {
}
func TestHaveAbsent(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
path := filepath.Join(dir, "gone.txt")
if err := os.WriteFile(path, []byte("bye"), 0o644); err != nil {
@@ -173,6 +195,9 @@ func TestHaveAbsent(t *testing.T) {
}
Have(path, IsAbsent())
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Errorf("expected %s to be removed", path)
}
@@ -185,6 +210,7 @@ func TestHaveAbsent(t *testing.T) {
}
func TestAbsent(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
path := filepath.Join(dir, "gone.txt")
if err := os.WriteFile(path, []byte("bye"), 0o644); err != nil {
@@ -192,6 +218,9 @@ func TestAbsent(t *testing.T) {
}
Absent(path)
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if _, err := os.Stat(path); !os.IsNotExist(err) {
t.Errorf("expected %s to be removed", path)
}
@@ -204,6 +233,7 @@ func TestAbsent(t *testing.T) {
}
func TestResolveStripsTmplSuffixWhenSourceHasTmplSuffix(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
sourcePath := filepath.Join(dir, "foo.conf.tmpl")
if err := os.WriteFile(sourcePath, []byte("hello {{.Param}}"), 0o644); err != nil {
@@ -231,6 +261,7 @@ func TestResolveStripsTmplSuffixWhenSourceHasTmplSuffix(t *testing.T) {
}
func TestResolveDoesNotStripTmplWhenOnlyContentTriggersTemplate(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
targetPath := filepath.Join(dir, "foo.conf.tmpl")
@@ -244,6 +275,7 @@ func TestResolveDoesNotStripTmplWhenOnlyContentTriggersTemplate(t *testing.T) {
}
func TestParamIsBareSourcePathNoPrefix(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
sourcePath := filepath.Join(dir, "src.tmpl")
if err := os.WriteFile(sourcePath, []byte("{{.Param}}"), 0o644); err != nil {
diff --git a/internal/resource/link/link.go b/internal/resource/link/link.go
index 8600166..0e5582c 100644
--- a/internal/resource/link/link.go
+++ b/internal/resource/link/link.go
@@ -83,8 +83,7 @@ func (l *Link) resourceType() string {
// registering it. Used by other resource packages (e.g. dir) to recreate an
// individual symlink without it becoming its own top-level resource.
func Ensure(path string, opts ...opt.Option) error {
- build(path, opts...)
- return nil
+ return build(path, opts...).apply()
}
func Have(path string, opts ...opt.Option) resource.Resource {
diff --git a/internal/resource/link/link_test.go b/internal/resource/link/link_test.go
index a892527..20542bd 100644
--- a/internal/resource/link/link_test.go
+++ b/internal/resource/link/link_test.go
@@ -5,10 +5,12 @@ import (
"path/filepath"
"testing"
+ "codeberg.org/snonux/gonf/internal/resource"
. "codeberg.org/snonux/gonf/internal/resource/opt"
)
func TestHaveSymlinkCreateAndIdempotent(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
target := filepath.Join(dir, "target.txt")
link := filepath.Join(dir, "link.txt")
@@ -17,6 +19,9 @@ func TestHaveSymlinkCreateAndIdempotent(t *testing.T) {
}
Have(link, WithSymlink(target))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
got, err := os.Readlink(link)
if err != nil {
t.Fatalf("readlink: %v", err)
@@ -34,6 +39,7 @@ func TestHaveSymlinkCreateAndIdempotent(t *testing.T) {
}
func TestHaveSymlinkRepoints(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
old := filepath.Join(dir, "old.txt")
newT := filepath.Join(dir, "new.txt")
@@ -48,6 +54,9 @@ func TestHaveSymlinkRepoints(t *testing.T) {
}
Have(link, WithSymlink(newT))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
got, err := os.Readlink(link)
if err != nil {
t.Fatal(err)
@@ -58,6 +67,7 @@ func TestHaveSymlinkRepoints(t *testing.T) {
}
func TestHaveSymlinkMovesRealFileAside(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
target := filepath.Join(dir, "target.txt")
link := filepath.Join(dir, "real")
@@ -69,6 +79,9 @@ func TestHaveSymlinkMovesRealFileAside(t *testing.T) {
}
Have(link, WithSymlink(target))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
got, err := os.Readlink(link)
if err != nil {
@@ -83,6 +96,7 @@ func TestHaveSymlinkMovesRealFileAside(t *testing.T) {
}
func TestHaveHardlinkCreateAndIdempotent(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
target := filepath.Join(dir, "target.txt")
link := filepath.Join(dir, "link.txt")
@@ -91,6 +105,9 @@ func TestHaveHardlinkCreateAndIdempotent(t *testing.T) {
}
Have(link, WithHardlink(target))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
ti, err := os.Stat(target)
if err != nil {
@@ -113,6 +130,7 @@ func TestHaveHardlinkCreateAndIdempotent(t *testing.T) {
}
func TestHaveHardlinkMovesRealFileAside(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
target := filepath.Join(dir, "target.txt")
link := filepath.Join(dir, "real")
@@ -124,6 +142,9 @@ func TestHaveHardlinkMovesRealFileAside(t *testing.T) {
}
Have(link, WithHardlink(target))
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
ti, err := os.Stat(target)
if err != nil {
@@ -142,6 +163,7 @@ func TestHaveHardlinkMovesRealFileAside(t *testing.T) {
}
func TestHaveHardlinkMissingTarget(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
link := filepath.Join(dir, "link")
if err := Ensure(link, WithHardlink(filepath.Join(dir, "nope"))); err == nil {
@@ -150,6 +172,7 @@ func TestHaveHardlinkMissingTarget(t *testing.T) {
}
func TestHaveAbsentSymlink(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
target := filepath.Join(dir, "target.txt")
link := filepath.Join(dir, "link.txt")
@@ -161,6 +184,9 @@ func TestHaveAbsentSymlink(t *testing.T) {
}
Have(link, IsAbsent())
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if _, err := os.Lstat(link); !os.IsNotExist(err) {
t.Errorf("expected %s to be removed", link)
}
@@ -173,6 +199,7 @@ func TestHaveAbsentSymlink(t *testing.T) {
}
func TestAbsentSymlink(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
target := filepath.Join(dir, "target.txt")
link := filepath.Join(dir, "link.txt")
@@ -184,6 +211,9 @@ func TestAbsentSymlink(t *testing.T) {
}
Absent(link)
+ if err := resource.Apply(); err != nil {
+ t.Fatalf("Apply failed: %v", err)
+ }
if _, err := os.Lstat(link); !os.IsNotExist(err) {
t.Errorf("expected %s to be removed", link)
}
@@ -196,6 +226,7 @@ func TestAbsentSymlink(t *testing.T) {
}
func TestHaveAbsentWithoutKindRegistersGenericLink(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
path := filepath.Join(dir, "whatever")
@@ -206,6 +237,7 @@ func TestHaveAbsentWithoutKindRegistersGenericLink(t *testing.T) {
}
func TestBuildRequiresKindOrAbsent(t *testing.T) {
+ resource.ResetRepository()
dir := t.TempDir()
path := filepath.Join(dir, "nope")
if err := Ensure(path); err == nil {
diff --git a/internal/resource/repository.go b/internal/resource/repository.go
index 69617d4..215f7e1 100644
--- a/internal/resource/repository.go
+++ b/internal/resource/repository.go
@@ -18,7 +18,7 @@ func getRepository() *repository {
return &repo
}
-func resetRepository() {
+func ResetRepository() {
repo = newRepository()
}
diff --git a/internal/resource/repository_test.go b/internal/resource/repository_test.go
new file mode 100644
index 0000000..8f1aef6
--- /dev/null
+++ b/internal/resource/repository_test.go
@@ -0,0 +1,126 @@
+package resource
+
+import (
+ "errors"
+ "strings"
+ "testing"
+)
+
+// mockApplier records the order in which Apply was called.
+type mockApplier struct {
+ name string
+ err error
+ logs *[]string
+}
+
+func (m *mockApplier) Apply() error {
+ *m.logs = append(*m.logs, m.name)
+ return m.err
+}
+
+func TestApply(t *testing.T) {
+ tests := []struct {
+ name string
+ setup func(r *repository, logs *[]string)
+ wantOrder []string
+ wantError bool
+ errorString string
+ }{
+ {
+ name: "dependency chain",
+ setup: func(r *repository, logs *[]string) {
+ // A depends on B, B depends on C
+ r.registered["A"] = Resource{
+ Type: "T", Name: "A",
+ Apply: &mockApplier{name: "A", logs: logs},
+ dependsOn: map[string]struct{}{"B": {}},
+ }
+ r.registered["B"] = Resource{
+ Type: "T", Name: "B",
+ Apply: &mockApplier{name: "B", logs: logs},
+ dependsOn: map[string]struct{}{"C": {}},
+ }
+ r.registered["C"] = Resource{
+ Type: "T", Name: "C",
+ Apply: &mockApplier{name: "C", logs: logs},
+ }
+ },
+ wantOrder: []string{"C", "B", "A"},
+ },
+ {
+ name: "circular dependency",
+ setup: func(r *repository, logs *[]string) {
+ r.registered["A"] = Resource{
+ Type: "T", Name: "A",
+ Apply: &mockApplier{name: "A", logs: logs},
+ dependsOn: map[string]struct{}{"B": {}},
+ }
+ r.registered["B"] = Resource{
+ Type: "T", Name: "B",
+ Apply: &mockApplier{name: "B", logs: logs},
+ dependsOn: map[string]struct{}{"A": {}},
+ }
+ },
+ wantError: true,
+ errorString: "circular dependency",
+ },
+ {
+ name: "missing dependency",
+ setup: func(r *repository, logs *[]string) {
+ r.registered["A"] = Resource{
+ Type: "T", Name: "A",
+ Apply: &mockApplier{name: "A", logs: logs},
+ dependsOn: map[string]struct{}{"Missing": {}},
+ }
+ },
+ wantError: true,
+ errorString: "not registered",
+ },
+ {
+ name: "execution failure",
+ setup: func(r *repository, logs *[]string) {
+ r.registered["A"] = Resource{
+ Type: "T", Name: "A",
+ Apply: &mockApplier{name: "A", err: errors.New("fail A"), logs: logs},
+ }
+ },
+ wantError: true,
+ errorString: "failed to apply",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ ResetRepository()
+ logs := make([]string, 0)
+ r := getRepository()
+ tt.setup(r, &logs)
+
+ err := Apply()
+
+ if (err != nil) != tt.wantError {
+ t.Errorf("Apply() error = %v, wantError %v", err, tt.wantError)
+ return
+ }
+
+ if tt.wantError && tt.errorString != "" {
+ if err == nil || !strings.Contains(err.Error(), tt.errorString) {
+ t.Errorf("Apply() error = %v, want error containing %q", err, tt.errorString)
+ }
+ }
+
+ if !tt.wantError && tt.wantOrder != nil {
+ if len(logs) != len(tt.wantOrder) {
+ t.Errorf("Apply() log length = %d, want %d", len(logs), len(tt.wantOrder))
+ return
+ }
+ for i := range logs {
+ if logs[i] != tt.wantOrder[i] {
+ t.Errorf("Apply() order = %v, want %v", logs, tt.wantOrder)
+ break
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/internal/resource/resource_test.go b/internal/resource/resource_test.go
index 8741f2a..23be8cc 100644
--- a/internal/resource/resource_test.go
+++ b/internal/resource/resource_test.go
@@ -5,7 +5,7 @@ import (
)
func TestResourceID(t *testing.T) {
- resetRepository()
+ ResetRepository()
res := Register("File", "/tmp/foo.txt", &mockApplier{})
expected := "File[/tmp/foo.txt]"
if res.ID() != expected {
@@ -14,7 +14,7 @@ func TestResourceID(t *testing.T) {
}
func TestResourceString(t *testing.T) {
- resetRepository()
+ ResetRepository()
res := Register("File", "/tmp/foo.txt", &mockApplier{})
expected := "File[/tmp/foo.txt]"
if res.String() != expected {
@@ -23,7 +23,7 @@ func TestResourceString(t *testing.T) {
}
func TestNew(t *testing.T) {
- resetRepository()
+ ResetRepository()
type_ := "File"
name := "/tmp/foo.txt"
res := Register(type_, name, &mockApplier{})
@@ -40,7 +40,7 @@ func TestNew(t *testing.T) {
}
func TestRepositoryRegister(t *testing.T) {
- resetRepository()
+ ResetRepository()
repo := getRepository()
res := Register("File", "/tmp/foo.txt", &mockApplier{})
@@ -56,7 +56,3 @@ func TestRepositoryRegister(t *testing.T) {
t.Error("expected error when registering the same resource twice, got nil")
}
}
-
-type mockApplier struct{}
-
-func (m *mockApplier) Apply() error { return nil }