From 74b57ba60d3bf6aeb7477d926d87cc831b5f9a91 Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Fri, 3 Apr 2026 11:13:25 -0400 Subject: [PATCH 1/3] use openshift eso operator on init --- src/cmd/init_test.go | 130 +++++++++++++++++++++-------- src/internal/types/clustergroup.go | 47 +++++++---- src/main_test.go | 13 ++- 3 files changed, 135 insertions(+), 55 deletions(-) diff --git a/src/cmd/init_test.go b/src/cmd/init_test.go index a11893c..1ad6137 100644 --- a/src/cmd/init_test.go +++ b/src/cmd/init_test.go @@ -322,9 +322,21 @@ var _ = Describe("patternizer init --with-secrets", func() { Namespaces: []types.NamespaceEntry{ types.NewNamespaceEntry(filepath.Base(tempDir)), types.NewNamespaceEntry("vault"), - types.NewNamespaceEntry("golang-external-secrets"), + types.NewMapNamespaceEntry(map[string]interface{}{ + "external-secrets-operator": map[string]interface{}{ + "operatorGroup": true, + "targetNamespaces": []interface{}{}, + }, + }), + types.NewNamespaceEntry("external-secrets"), + }, + Subscriptions: map[string]types.Subscription{ + "eso": { + Name: "openshift-external-secrets-operator", + Namespace: "external-secrets-operator", + Channel: "stable-v1", + }, }, - Subscriptions: map[string]types.Subscription{}, Applications: map[string]types.Application{ "vault": { Name: "vault", @@ -332,11 +344,11 @@ var _ = Describe("patternizer init --with-secrets", func() { Chart: "hashicorp-vault", ChartVersion: "0.1.*", }, - "golang-external-secrets": { - Name: "golang-external-secrets", - Namespace: "golang-external-secrets", - Chart: "golang-external-secrets", - ChartVersion: "0.1.*", + "openshift-external-secrets": { + Name: "openshift-external-secrets", + Namespace: "external-secrets", + Chart: "openshift-external-secrets", + ChartVersion: "0.0.*", }, }, }, @@ -396,9 +408,21 @@ var _ = Describe("patternizer init --with-secrets", func() { Namespaces: []types.NamespaceEntry{ types.NewNamespaceEntry(expectedNamespace), types.NewNamespaceEntry("vault"), - types.NewNamespaceEntry("golang-external-secrets"), + types.NewMapNamespaceEntry(map[string]interface{}{ + "external-secrets-operator": map[string]interface{}{ + "operatorGroup": true, + "targetNamespaces": []interface{}{}, + }, + }), + types.NewNamespaceEntry("external-secrets"), + }, + Subscriptions: map[string]types.Subscription{ + "eso": { + Name: "openshift-external-secrets-operator", + Namespace: "external-secrets-operator", + Channel: "stable-v1", + }, }, - Subscriptions: map[string]types.Subscription{}, Applications: map[string]types.Application{ "test-app1": { Name: "test-app1", @@ -416,11 +440,11 @@ var _ = Describe("patternizer init --with-secrets", func() { Chart: "hashicorp-vault", ChartVersion: "0.1.*", }, - "golang-external-secrets": { - Name: "golang-external-secrets", - Namespace: "golang-external-secrets", - Chart: "golang-external-secrets", - ChartVersion: "0.1.*", + "openshift-external-secrets": { + Name: "openshift-external-secrets", + Namespace: "external-secrets", + Chart: "openshift-external-secrets", + ChartVersion: "0.0.*", }, }, }, @@ -478,9 +502,21 @@ var _ = Describe("patternizer init --with-secrets", func() { Namespaces: []types.NamespaceEntry{ types.NewNamespaceEntry("test-pattern"), types.NewNamespaceEntry("vault"), - types.NewNamespaceEntry("golang-external-secrets"), + types.NewMapNamespaceEntry(map[string]interface{}{ + "external-secrets-operator": map[string]interface{}{ + "operatorGroup": true, + "targetNamespaces": []interface{}{}, + }, + }), + types.NewNamespaceEntry("external-secrets"), + }, + Subscriptions: map[string]types.Subscription{ + "eso": { + Name: "openshift-external-secrets-operator", + Namespace: "external-secrets-operator", + Channel: "stable-v1", + }, }, - Subscriptions: map[string]types.Subscription{}, Applications: map[string]types.Application{ "vault": { Name: "vault", @@ -488,11 +524,11 @@ var _ = Describe("patternizer init --with-secrets", func() { Chart: "hashicorp-vault", ChartVersion: "0.1.*", }, - "golang-external-secrets": { - Name: "golang-external-secrets", - Namespace: "golang-external-secrets", - Chart: "golang-external-secrets", - ChartVersion: "0.1.*", + "openshift-external-secrets": { + Name: "openshift-external-secrets", + Namespace: "external-secrets", + Chart: "openshift-external-secrets", + ChartVersion: "0.0.*", }, }, }, @@ -549,9 +585,21 @@ var _ = Describe("patternizer init --with-secrets", func() { Namespaces: []types.NamespaceEntry{ types.NewNamespaceEntry(expectedNamespace), types.NewNamespaceEntry("vault"), - types.NewNamespaceEntry("golang-external-secrets"), + types.NewMapNamespaceEntry(map[string]interface{}{ + "external-secrets-operator": map[string]interface{}{ + "operatorGroup": true, + "targetNamespaces": []interface{}{}, + }, + }), + types.NewNamespaceEntry("external-secrets"), + }, + Subscriptions: map[string]types.Subscription{ + "eso": { + Name: "openshift-external-secrets-operator", + Namespace: "external-secrets-operator", + Channel: "stable-v1", + }, }, - Subscriptions: map[string]types.Subscription{}, Applications: map[string]types.Application{ "test-app1": { Name: "test-app1", @@ -569,11 +617,11 @@ var _ = Describe("patternizer init --with-secrets", func() { Chart: "hashicorp-vault", ChartVersion: "0.1.*", }, - "golang-external-secrets": { - Name: "golang-external-secrets", - Namespace: "golang-external-secrets", - Chart: "golang-external-secrets", - ChartVersion: "0.1.*", + "openshift-external-secrets": { + Name: "openshift-external-secrets", + Namespace: "external-secrets", + Chart: "openshift-external-secrets", + ChartVersion: "0.0.*", }, }, }, @@ -632,9 +680,21 @@ var _ = Describe("patternizer init --with-secrets", func() { Namespaces: []types.NamespaceEntry{ types.NewNamespaceEntry("test-pattern"), types.NewNamespaceEntry("vault"), - types.NewNamespaceEntry("golang-external-secrets"), + types.NewMapNamespaceEntry(map[string]interface{}{ + "external-secrets-operator": map[string]interface{}{ + "operatorGroup": true, + "targetNamespaces": []interface{}{}, + }, + }), + types.NewNamespaceEntry("external-secrets"), + }, + Subscriptions: map[string]types.Subscription{ + "eso": { + Name: "openshift-external-secrets-operator", + Namespace: "external-secrets-operator", + Channel: "stable-v1", + }, }, - Subscriptions: map[string]types.Subscription{}, Applications: map[string]types.Application{ "custom-user-app": { Name: "custom-user-app", @@ -649,11 +709,11 @@ var _ = Describe("patternizer init --with-secrets", func() { Chart: "hashicorp-vault", ChartVersion: "0.1.*", }, - "golang-external-secrets": { - Name: "golang-external-secrets", - Namespace: "golang-external-secrets", - Chart: "golang-external-secrets", - ChartVersion: "0.1.*", + "openshift-external-secrets": { + Name: "openshift-external-secrets", + Namespace: "external-secrets", + Chart: "openshift-external-secrets", + ChartVersion: "0.0.*", }, }, OtherFields: map[string]interface{}{"customClusterField": "user-cluster-config"}, diff --git a/src/internal/types/clustergroup.go b/src/internal/types/clustergroup.go index 031f27f..7c8d73f 100644 --- a/src/internal/types/clustergroup.go +++ b/src/internal/types/clustergroup.go @@ -3,6 +3,7 @@ package types import ( "fmt" "path/filepath" + "reflect" "gopkg.in/yaml.v3" ) @@ -46,15 +47,7 @@ func (ne NamespaceEntry) GetString() (string, bool) { // Equal compares two NamespaceEntry values for equality func (ne NamespaceEntry) Equal(other NamespaceEntry) bool { - // Simple comparison - could be enhanced for deep map comparison if needed - if str1, ok1 := ne.GetString(); ok1 { - if str2, ok2 := other.GetString(); ok2 { - return str1 == str2 - } - } - // For maps or other complex types, we'd need deeper comparison - // For now, assume they're different if not both strings - return false + return reflect.DeepEqual(ne.value, other.value) } // NewNamespaceEntry creates a new NamespaceEntry from a string @@ -62,6 +55,11 @@ func NewNamespaceEntry(namespace string) NamespaceEntry { return NamespaceEntry{value: namespace} } +// NewMapNamespaceEntry creates a new NamespaceEntry from a map +func NewMapNamespaceEntry(m map[string]interface{}) NamespaceEntry { + return NamespaceEntry{value: m} +} + // Application defines the structure for an ArgoCD application entry. type Application struct { Name string `yaml:"name"` @@ -103,20 +101,37 @@ type ValuesClusterGroup struct { func NewDefaultValuesClusterGroup(patternName, clusterGroupName string, chartPaths []string, useSecrets bool) *ValuesClusterGroup { namespaces := []NamespaceEntry{NewNamespaceEntry(patternName)} applications := make(map[string]Application) + subscriptions := make(map[string]Subscription) if useSecrets { - namespaces = append(namespaces, NewNamespaceEntry("vault"), NewNamespaceEntry("golang-external-secrets")) + namespaces = append( + namespaces, + NewNamespaceEntry("vault"), + NamespaceEntry{map[string]interface{}{ + "external-secrets-operator": map[string]interface{}{ + "operatorGroup": true, + "targetNamespaces": []string{}, + }, + }, + }, + NewNamespaceEntry("external-secrets"), + ) + subscriptions["eso"] = Subscription{ + Name: "openshift-external-secrets-operator", + Namespace: "external-secrets-operator", + Channel: "stable-v1", + } applications["vault"] = Application{ Name: "vault", Namespace: "vault", Chart: "hashicorp-vault", ChartVersion: "0.1.*", } - applications["golang-external-secrets"] = Application{ - Name: "golang-external-secrets", - Namespace: "golang-external-secrets", - Chart: "golang-external-secrets", - ChartVersion: "0.1.*", + applications["openshift-external-secrets"] = Application{ + Name: "openshift-external-secrets", + Namespace: "external-secrets", + Chart: "openshift-external-secrets", + ChartVersion: "0.0.*", } } @@ -134,7 +149,7 @@ func NewDefaultValuesClusterGroup(patternName, clusterGroupName string, chartPat ClusterGroup: ClusterGroup{ Name: clusterGroupName, Namespaces: namespaces, - Subscriptions: make(map[string]Subscription), + Subscriptions: subscriptions, Applications: applications, OtherFields: make(map[string]interface{}), }, diff --git a/src/main_test.go b/src/main_test.go index f29af14..ee93bec 100644 --- a/src/main_test.go +++ b/src/main_test.go @@ -62,17 +62,22 @@ func TestNewDefaultValuesClusterGroup(t *testing.T) { // Test with secrets valuesWithSecrets := types.NewDefaultValuesClusterGroup("test-pattern", "test-group", []string{"charts/app1"}, true) - expectedNamespacesWithSecrets := []string{"test-pattern", "vault", "golang-external-secrets"} + expectedNamespacesWithSecrets := []string{"test-pattern", "vault", "external-secrets-operator", "external-secrets"} if len(valuesWithSecrets.ClusterGroup.Namespaces) != len(expectedNamespacesWithSecrets) { t.Errorf("Expected %d namespaces with secrets, got %d", len(expectedNamespacesWithSecrets), len(valuesWithSecrets.ClusterGroup.Namespaces)) } - // Check that vault and golang-external-secrets applications are added + // Check that vault and openshift-external-secrets applications are added if _, exists := valuesWithSecrets.ClusterGroup.Applications["vault"]; !exists { t.Error("Expected vault application to be present with secrets") } - if _, exists := valuesWithSecrets.ClusterGroup.Applications["golang-external-secrets"]; !exists { - t.Error("Expected golang-external-secrets application to be present with secrets") + if _, exists := valuesWithSecrets.ClusterGroup.Applications["openshift-external-secrets"]; !exists { + t.Error("Expected openshift-external-secrets application to be present with secrets") + } + + // Check that eso subscription is added + if _, exists := valuesWithSecrets.ClusterGroup.Subscriptions["eso"]; !exists { + t.Error("Expected eso subscription to be present with secrets") } } From 44bc02c40768c256d680e398673a6f7e252a4459 Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Fri, 3 Apr 2026 11:36:09 -0400 Subject: [PATCH 2/3] convert tests to ginkgo --- .../fileutils/fileutils_suite_test.go | 13 + src/internal/fileutils/fileutils_test.go | 481 ++++++-------- src/internal/helm/helm_suite_test.go | 13 + src/internal/helm/helm_test.go | 322 +++------- src/internal/pattern/pattern_suite_test.go | 13 + src/internal/pattern/pattern_test.go | 605 ++++++++---------- src/main_suite_test.go | 13 + src/main_test.go | 141 ++-- 8 files changed, 683 insertions(+), 918 deletions(-) create mode 100644 src/internal/fileutils/fileutils_suite_test.go create mode 100644 src/internal/helm/helm_suite_test.go create mode 100644 src/internal/pattern/pattern_suite_test.go create mode 100644 src/main_suite_test.go diff --git a/src/internal/fileutils/fileutils_suite_test.go b/src/internal/fileutils/fileutils_suite_test.go new file mode 100644 index 0000000..ad5c0de --- /dev/null +++ b/src/internal/fileutils/fileutils_suite_test.go @@ -0,0 +1,13 @@ +package fileutils + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestFileutils(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Fileutils Suite") +} diff --git a/src/internal/fileutils/fileutils_test.go b/src/internal/fileutils/fileutils_test.go index 08da417..15f38eb 100644 --- a/src/internal/fileutils/fileutils_test.go +++ b/src/internal/fileutils/fileutils_test.go @@ -5,296 +5,219 @@ import ( "path/filepath" "runtime" "strings" - "testing" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "gopkg.in/yaml.v3" ) -func writeFileWithMode(t *testing.T, path, content string, mode os.FileMode) { - t.Helper() - if err := os.WriteFile(path, []byte(content), 0o600); err != nil { // initial perms don't matter - t.Fatalf("write file failed: %v", err) - } - if err := os.Chmod(path, mode); err != nil { - t.Fatalf("chmod failed: %v", err) - } +func writeFileWithMode(path, content string, mode os.FileMode) { + Expect(os.WriteFile(path, []byte(content), 0o600)).To(Succeed()) + Expect(os.Chmod(path, mode)).To(Succeed()) } -func TestCopyFile_CopiesContentsAndMode(t *testing.T) { - dir := t.TempDir() - src := filepath.Join(dir, "src.txt") - dst := filepath.Join(dir, "dst.txt") - - content := "hello world" - srcMode := os.FileMode(0o640) - writeFileWithMode(t, src, content, srcMode) - - if err := CopyFile(src, dst); err != nil { - t.Fatalf("CopyFile failed: %v", err) - } - - got, err := os.ReadFile(dst) - if err != nil { - t.Fatalf("read dst failed: %v", err) - } - if string(got) != content { - t.Fatalf("unexpected content: %q", string(got)) - } - - info, err := os.Stat(dst) - if err != nil { - t.Fatalf("stat dst failed: %v", err) - } - // Compare permissions only (mask out non-permission bits) - if info.Mode().Perm() != srcMode.Perm() { - t.Fatalf("mode mismatch: got %v want %v", info.Mode().Perm(), srcMode.Perm()) - } -} +var _ = Describe("CopyFile", func() { + It("should copy contents and preserve file mode", func() { + dir := GinkgoT().TempDir() + src := filepath.Join(dir, "src.txt") + dst := filepath.Join(dir, "dst.txt") + + content := "hello world" + srcMode := os.FileMode(0o640) + writeFileWithMode(src, content, srcMode) + + Expect(CopyFile(src, dst)).To(Succeed()) + + got, err := os.ReadFile(dst) + Expect(err).NotTo(HaveOccurred()) + Expect(string(got)).To(Equal(content)) + + info, err := os.Stat(dst) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode().Perm()).To(Equal(srcMode.Perm())) + }) +}) + +var _ = Describe("HandleSecretsSetup", func() { + It("should copy template when missing and not overwrite when present", func() { + resources := GinkgoT().TempDir() + repoRoot := GinkgoT().TempDir() + + templatePath := filepath.Join(resources, "values-secret.yaml.template") + originalContent := "foo: bar\n" + Expect(os.WriteFile(templatePath, []byte(originalContent), 0o644)).To(Succeed()) + + // First call should copy + Expect(HandleSecretsSetup(resources, repoRoot)).To(Succeed()) + copied := filepath.Join(repoRoot, "values-secret.yaml.template") + data, err := os.ReadFile(copied) + Expect(err).NotTo(HaveOccurred()) + Expect(string(data)).To(Equal(originalContent)) + + // Change the source and call again; destination should remain unchanged + Expect(os.WriteFile(templatePath, []byte("baz: qux\n"), 0o644)).To(Succeed()) + Expect(HandleSecretsSetup(resources, repoRoot)).To(Succeed()) + data2, err := os.ReadFile(copied) + Expect(err).NotTo(HaveOccurred()) + Expect(string(data2)).To(Equal(originalContent)) + }) +}) + +var _ = Describe("GetResourcesPath", func() { + It("should return the path when the environment variable is set", func() { + old := os.Getenv("PATTERNIZER_RESOURCES_DIR") + DeferCleanup(func() { os.Setenv("PATTERNIZER_RESOURCES_DIR", old) }) + + tmp := GinkgoT().TempDir() + Expect(os.Setenv("PATTERNIZER_RESOURCES_DIR", tmp)).To(Succeed()) + got, err := GetResourcesPath() + Expect(err).NotTo(HaveOccurred()) + Expect(got).To(Equal(tmp)) + }) + + It("should return an error when the environment variable is unset", func() { + old := os.Getenv("PATTERNIZER_RESOURCES_DIR") + DeferCleanup(func() { os.Setenv("PATTERNIZER_RESOURCES_DIR", old) }) + + Expect(os.Unsetenv("PATTERNIZER_RESOURCES_DIR")).To(Succeed()) + _, err := GetResourcesPath() + Expect(err).To(HaveOccurred()) + }) +}) + +var _ = Describe("RemovePathIfExists", func() { + It("should remove a file", func() { + base := GinkgoT().TempDir() + f := filepath.Join(base, "file.txt") + Expect(os.WriteFile(f, []byte("x"), 0o644)).To(Succeed()) + + Expect(RemovePathIfExists(f)).To(Succeed()) + _, err := os.Stat(f) + Expect(os.IsNotExist(err)).To(BeTrue()) + }) + + It("should remove a directory", func() { + base := GinkgoT().TempDir() + d := filepath.Join(base, "dir") + Expect(os.MkdirAll(filepath.Join(d, "nested"), 0o755)).To(Succeed()) + + Expect(RemovePathIfExists(d)).To(Succeed()) + _, err := os.Stat(d) + Expect(os.IsNotExist(err)).To(BeTrue()) + }) + + It("should remove a symlink without removing the target", func() { + if runtime.GOOS == "windows" { + Skip("symlink tests require admin/dev mode on Windows") + } -func TestHandleSecretsSetup_CopiesWhenMissing_DoesNotOverwriteWhenPresent(t *testing.T) { - resources := t.TempDir() - repoRoot := t.TempDir() - - templatePath := filepath.Join(resources, "values-secret.yaml.template") - originalContent := "foo: bar\n" - if err := os.WriteFile(templatePath, []byte(originalContent), 0o644); err != nil { - t.Fatalf("write template failed: %v", err) - } - - // First call should copy - if err := HandleSecretsSetup(resources, repoRoot); err != nil { - t.Fatalf("HandleSecretsSetup failed: %v", err) - } - copied := filepath.Join(repoRoot, "values-secret.yaml.template") - data, err := os.ReadFile(copied) - if err != nil { - t.Fatalf("read copied failed: %v", err) - } - if string(data) != originalContent { - t.Fatalf("unexpected copied content: %q", string(data)) - } - - // Change the source and call again; destination should remain unchanged - if err := os.WriteFile(templatePath, []byte("baz: qux\n"), 0o644); err != nil { - t.Fatalf("rewrite template failed: %v", err) - } - if err := HandleSecretsSetup(resources, repoRoot); err != nil { - t.Fatalf("HandleSecretsSetup second call failed: %v", err) - } - data2, err := os.ReadFile(copied) - if err != nil { - t.Fatalf("read copied again failed: %v", err) - } - if string(data2) != originalContent { - t.Fatalf("destination was overwritten unexpectedly: %q", string(data2)) - } -} + base := GinkgoT().TempDir() + targetDir := GinkgoT().TempDir() + link := filepath.Join(base, "link") + Expect(os.Symlink(targetDir, link)).To(Succeed()) -func TestGetResourcesPath_EnvSetAndUnset(t *testing.T) { - old := os.Getenv("PATTERNIZER_RESOURCES_DIR") - t.Cleanup(func() { _ = os.Setenv("PATTERNIZER_RESOURCES_DIR", old) }) - - tmp := t.TempDir() - if err := os.Setenv("PATTERNIZER_RESOURCES_DIR", tmp); err != nil { - t.Fatalf("setenv failed: %v", err) - } - got, err := GetResourcesPath() - if err != nil || got != tmp { - t.Fatalf("GetResourcesPath with env set failed: got %q err %v", got, err) - } - - if err := os.Unsetenv("PATTERNIZER_RESOURCES_DIR"); err != nil { - t.Fatalf("unsetenv failed: %v", err) - } - if _, err := GetResourcesPath(); err == nil { - t.Fatalf("expected error when env is unset") - } -} + Expect(RemovePathIfExists(link)).To(Succeed()) + _, err := os.Lstat(link) + Expect(os.IsNotExist(err)).To(BeTrue()) -func TestRemovePathIfExists_FileDirSymlink(t *testing.T) { - base := t.TempDir() - - // File removal - f := filepath.Join(base, "file.txt") - if err := os.WriteFile(f, []byte("x"), 0o644); err != nil { - t.Fatalf("write file failed: %v", err) - } - if err := RemovePathIfExists(f); err != nil { - t.Fatalf("RemovePathIfExists(file) failed: %v", err) - } - if _, err := os.Stat(f); !os.IsNotExist(err) { - t.Fatalf("file not removed") - } - - // Directory removal - d := filepath.Join(base, "dir") - if err := os.MkdirAll(filepath.Join(d, "nested"), 0o755); err != nil { - t.Fatalf("mkdir failed: %v", err) - } - if err := RemovePathIfExists(d); err != nil { - t.Fatalf("RemovePathIfExists(dir) failed: %v", err) - } - if _, err := os.Stat(d); !os.IsNotExist(err) { - t.Fatalf("dir not removed") - } - - // Symlink removal (link to a directory) - targetDir := t.TempDir() - link := filepath.Join(base, "link") - // Windows symlinks require admin/dev mode; skip on windows - if runtime.GOOS != "windows" { - if err := os.Symlink(targetDir, link); err != nil { - t.Fatalf("symlink failed: %v", err) - } - if err := RemovePathIfExists(link); err != nil { - t.Fatalf("RemovePathIfExists(symlink) failed: %v", err) - } - if _, err := os.Lstat(link); !os.IsNotExist(err) { - t.Fatalf("symlink not removed") - } // Ensure target still exists - if _, err := os.Stat(targetDir); err != nil { - t.Fatalf("target dir should still exist: %v", err) - } - } - - // Non-existent path should be no-op - if err := RemovePathIfExists(filepath.Join(base, "does-not-exist")); err != nil { - t.Fatalf("RemovePathIfExists(nonexistent) failed: %v", err) - } -} - -func TestFileContainsIncludeMakefileCommon_Detection(t *testing.T) { - dir := t.TempDir() - p := filepath.Join(dir, "Makefile") - - cases := []struct { - content string - want bool - }{ - {content: "all:\n\t@echo hi\n", want: false}, - {content: "include Makefile-common\nall:\n\t@echo hi\n", want: true}, - {content: " include Makefile-common\nall:\n\t@echo hi\n", want: true}, - {content: "# include Makefile-common\nall:\n\t@echo hi\n", want: false}, - {content: "foo:\n\t@echo foo\n# comment\nbar:\n\t@echo bar\n", want: false}, - {content: strings.Join([]string{"foo:", "\t@echo foo", "include Makefile-common", "bar:", "\t@echo bar", ""}, "\n"), want: true}, - } - - for i, tc := range cases { - if err := os.WriteFile(p, []byte(tc.content), 0o644); err != nil { - t.Fatalf("write case %d failed: %v", i, err) - } - got, err := FileContainsIncludeMakefileCommon(p) - if err != nil { - t.Fatalf("case %d: err: %v", i, err) + _, err = os.Stat(targetDir) + Expect(err).NotTo(HaveOccurred()) + }) + + It("should be a no-op for a non-existent path", func() { + base := GinkgoT().TempDir() + Expect(RemovePathIfExists(filepath.Join(base, "does-not-exist"))).To(Succeed()) + }) +}) + +var _ = Describe("FileContainsIncludeMakefileCommon", func() { + DescribeTable("should detect include Makefile-common correctly", + func(content string, expected bool) { + dir := GinkgoT().TempDir() + p := filepath.Join(dir, "Makefile") + Expect(os.WriteFile(p, []byte(content), 0o644)).To(Succeed()) + + got, err := FileContainsIncludeMakefileCommon(p) + Expect(err).NotTo(HaveOccurred()) + Expect(got).To(Equal(expected)) + }, + Entry("no include directive", "all:\n\t@echo hi\n", false), + Entry("include at start of file", "include Makefile-common\nall:\n\t@echo hi\n", true), + Entry("include with leading whitespace", " include Makefile-common\nall:\n\t@echo hi\n", true), + Entry("commented out include", "# include Makefile-common\nall:\n\t@echo hi\n", false), + Entry("no include in multi-target file", "foo:\n\t@echo foo\n# comment\nbar:\n\t@echo bar\n", false), + Entry("include in the middle of the file", + strings.Join([]string{"foo:", "\t@echo foo", "include Makefile-common", "bar:", "\t@echo bar", ""}, "\n"), true), + ) +}) + +var _ = Describe("PrependLineToFile", func() { + It("should prepend a line and preserve file mode", func() { + dir := GinkgoT().TempDir() + p := filepath.Join(dir, "Makefile") + original := "all:\n\t@echo hi\n" + mode := os.FileMode(0o600) + writeFileWithMode(p, original, mode) + + line := "include Makefile-common" + Expect(PrependLineToFile(p, line)).To(Succeed()) + + data, err := os.ReadFile(p) + Expect(err).NotTo(HaveOccurred()) + Expect(string(data)).To(Equal(line + "\n" + original)) + + info, err := os.Stat(p) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode().Perm()).To(Equal(mode.Perm())) + }) +}) + +var _ = Describe("WriteYAMLWithIndent", func() { + It("should use 2-space indentation", func() { + dir := GinkgoT().TempDir() + p := filepath.Join(dir, "test.yaml") + + type NestedStruct struct { + Field1 string `yaml:"field1"` + Field2 int `yaml:"field2"` } - if got != tc.want { - t.Fatalf("case %d: got %v want %v", i, got, tc.want) + type TestStruct struct { + Name string `yaml:"name"` + Nested NestedStruct `yaml:"nested"` + Items []string `yaml:"items"` } - } -} -func TestPrependLineToFile_PrependsAndPreservesMode(t *testing.T) { - dir := t.TempDir() - p := filepath.Join(dir, "Makefile") - original := "all:\n\t@echo hi\n" - mode := os.FileMode(0o600) - writeFileWithMode(t, p, original, mode) - - line := "include Makefile-common" - if err := PrependLineToFile(p, line); err != nil { - t.Fatalf("PrependLineToFile failed: %v", err) - } - - data, err := os.ReadFile(p) - if err != nil { - t.Fatalf("read failed: %v", err) - } - expected := line + "\n" + original - if string(data) != expected { - t.Fatalf("unexpected content after prepend: %q", string(data)) - } - - info, err := os.Stat(p) - if err != nil { - t.Fatalf("stat failed: %v", err) - } - if info.Mode().Perm() != mode.Perm() { - t.Fatalf("mode not preserved: got %v want %v", info.Mode().Perm(), mode.Perm()) - } -} + data := TestStruct{ + Name: "test", + Nested: NestedStruct{ + Field1: "value1", + Field2: 42, + }, + Items: []string{"item1", "item2", "item3"}, + } -func TestWriteYAMLWithIndent_Uses2SpaceIndentation(t *testing.T) { - dir := t.TempDir() - p := filepath.Join(dir, "test.yaml") - - // Create a nested structure to verify indentation - type NestedStruct struct { - Field1 string `yaml:"field1"` - Field2 int `yaml:"field2"` - } - type TestStruct struct { - Name string `yaml:"name"` - Nested NestedStruct `yaml:"nested"` - Items []string `yaml:"items"` - } - - data := TestStruct{ - Name: "test", - Nested: NestedStruct{ - Field1: "value1", - Field2: 42, - }, - Items: []string{"item1", "item2", "item3"}, - } - - // Write the YAML with our function - if err := WriteYAMLWithIndent(data, p); err != nil { - t.Fatalf("WriteYAMLWithIndent failed: %v", err) - } - - // Read the file and verify indentation - content, err := os.ReadFile(p) - if err != nil { - t.Fatalf("read failed: %v", err) - } - - // Verify 2-space indentation by checking the content - // The nested fields should be indented with 2 spaces, not 4 - contentStr := string(content) - if !strings.Contains(contentStr, " field1: value1") { - t.Fatalf("expected 2-space indentation for nested.field1, got:\n%s", contentStr) - } - if !strings.Contains(contentStr, " field2: 42") { - t.Fatalf("expected 2-space indentation for nested.field2, got:\n%s", contentStr) - } - // Verify it's not 4-space indentation - if strings.Contains(contentStr, " field1") || strings.Contains(contentStr, " field2") { - t.Fatalf("unexpected 4-space indentation found:\n%s", contentStr) - } - - // Verify the file can be unmarshaled back correctly - var decoded TestStruct - if err := yaml.Unmarshal(content, &decoded); err != nil { - t.Fatalf("failed to unmarshal written YAML: %v", err) - } - if decoded.Name != data.Name { - t.Fatalf("name mismatch: got %q want %q", decoded.Name, data.Name) - } - if decoded.Nested.Field1 != data.Nested.Field1 { - t.Fatalf("nested.field1 mismatch: got %q want %q", decoded.Nested.Field1, data.Nested.Field1) - } - if decoded.Nested.Field2 != data.Nested.Field2 { - t.Fatalf("nested.field2 mismatch: got %d want %d", decoded.Nested.Field2, data.Nested.Field2) - } - - // Verify file permissions - info, err := os.Stat(p) - if err != nil { - t.Fatalf("stat failed: %v", err) - } - expectedMode := os.FileMode(0o644) - if info.Mode().Perm() != expectedMode.Perm() { - t.Fatalf("mode mismatch: got %v want %v", info.Mode().Perm(), expectedMode.Perm()) - } -} + Expect(WriteYAMLWithIndent(data, p)).To(Succeed()) + + content, err := os.ReadFile(p) + Expect(err).NotTo(HaveOccurred()) + contentStr := string(content) + + Expect(contentStr).To(ContainSubstring(" field1: value1")) + Expect(contentStr).To(ContainSubstring(" field2: 42")) + Expect(contentStr).NotTo(ContainSubstring(" field1")) + Expect(contentStr).NotTo(ContainSubstring(" field2")) + + // Verify round-trip + var decoded TestStruct + Expect(yaml.Unmarshal(content, &decoded)).To(Succeed()) + Expect(decoded.Name).To(Equal(data.Name)) + Expect(decoded.Nested.Field1).To(Equal(data.Nested.Field1)) + Expect(decoded.Nested.Field2).To(Equal(data.Nested.Field2)) + + // Verify file permissions + info, err := os.Stat(p) + Expect(err).NotTo(HaveOccurred()) + Expect(info.Mode().Perm()).To(Equal(os.FileMode(0o644).Perm())) + }) +}) diff --git a/src/internal/helm/helm_suite_test.go b/src/internal/helm/helm_suite_test.go new file mode 100644 index 0000000..9dcff8f --- /dev/null +++ b/src/internal/helm/helm_suite_test.go @@ -0,0 +1,13 @@ +package helm + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestHelm(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Helm Suite") +} diff --git a/src/internal/helm/helm_test.go b/src/internal/helm/helm_test.go index 631ca70..eefb552 100644 --- a/src/internal/helm/helm_test.go +++ b/src/internal/helm/helm_test.go @@ -3,266 +3,112 @@ package helm import ( "os" "path/filepath" - "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" ) // createTestChartStructure creates a comprehensive test directory structure for helm chart testing. -// It returns the temp directory path that should be cleaned up by the caller. -func createTestChartStructure(t *testing.T) string { +func createTestChartStructure() string { tempDir, err := os.MkdirTemp("", "helm-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - - // Create test directory structure: - // tempDir/ - // ├── chart1/ (valid top-level chart) - // │ ├── Chart.yaml - // │ ├── values.yaml - // │ └── templates/ - // ├── chart2/ (valid top-level chart) - // │ ├── Chart.yaml - // │ ├── values.yaml - // │ ├── templates/ - // │ └── charts/ (sub-chart directory) - // │ └── subchart/ (sub-chart - should be ignored) - // │ ├── Chart.yaml - // │ ├── values.yaml - // │ └── templates/ - // ├── incomplete-chart/ (invalid chart - missing templates) - // │ ├── Chart.yaml - // │ └── values.yaml - // ├── missing-chart-yaml/ (invalid chart - missing Chart.yaml) - // │ ├── values.yaml - // │ └── templates/ - // ├── missing-values-yaml/ (invalid chart - missing values.yaml) - // │ ├── Chart.yaml - // │ └── templates/ - // ├── templates-is-file/ (invalid chart - templates is a file) - // │ ├── Chart.yaml - // │ ├── values.yaml - // │ └── templates (file, not directory) - // ├── .hidden-chart/ (hidden directory - should be ignored) - // │ ├── Chart.yaml - // │ ├── values.yaml - // │ └── templates/ - // └── not-a-chart/ (not a chart directory) - // └── some-file.txt + Expect(err).NotTo(HaveOccurred()) - // Create chart1 (valid top-level chart) + // chart1 (valid top-level chart) chart1Dir := filepath.Join(tempDir, "chart1") - if err := os.MkdirAll(filepath.Join(chart1Dir, "templates"), 0o755); err != nil { - t.Fatalf("Failed to create chart1 structure: %v", err) - } - if err := os.WriteFile(filepath.Join(chart1Dir, "Chart.yaml"), []byte("name: chart1\nversion: 1.0.0\n"), 0o644); err != nil { - t.Fatalf("Failed to create Chart.yaml: %v", err) - } - if err := os.WriteFile(filepath.Join(chart1Dir, "values.yaml"), []byte("# values\n"), 0o644); err != nil { - t.Fatalf("Failed to create values.yaml: %v", err) - } + Expect(os.MkdirAll(filepath.Join(chart1Dir, "templates"), 0o755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(chart1Dir, "Chart.yaml"), []byte("name: chart1\nversion: 1.0.0\n"), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(chart1Dir, "values.yaml"), []byte("# values\n"), 0o644)).To(Succeed()) - // Create chart2 (valid top-level chart with sub-chart) + // chart2 (valid top-level chart with sub-chart) chart2Dir := filepath.Join(tempDir, "chart2") - chart2TemplatesDir := filepath.Join(chart2Dir, "templates") chart2SubchartDir := filepath.Join(chart2Dir, "charts", "subchart") - subchartTemplatesDir := filepath.Join(chart2SubchartDir, "templates") - - if err := os.MkdirAll(chart2TemplatesDir, 0o755); err != nil { - t.Fatalf("Failed to create chart2 structure: %v", err) - } - if err := os.MkdirAll(subchartTemplatesDir, 0o755); err != nil { - t.Fatalf("Failed to create subchart structure: %v", err) - } - - // Chart2 files - if err := os.WriteFile(filepath.Join(chart2Dir, "Chart.yaml"), []byte("name: chart2\nversion: 1.0.0\n"), 0o644); err != nil { - t.Fatalf("Failed to create chart2 Chart.yaml: %v", err) - } - if err := os.WriteFile(filepath.Join(chart2Dir, "values.yaml"), []byte("# values\n"), 0o644); err != nil { - t.Fatalf("Failed to create chart2 values.yaml: %v", err) - } - - // Sub-chart files (should be ignored by FindTopLevelCharts) - if err := os.WriteFile(filepath.Join(chart2SubchartDir, "Chart.yaml"), []byte("name: subchart\nversion: 1.0.0\n"), 0o644); err != nil { - t.Fatalf("Failed to create subchart Chart.yaml: %v", err) - } - if err := os.WriteFile(filepath.Join(chart2SubchartDir, "values.yaml"), []byte("# subchart values\n"), 0o644); err != nil { - t.Fatalf("Failed to create subchart values.yaml: %v", err) - } - - // Create incomplete-chart (missing templates directory) + Expect(os.MkdirAll(filepath.Join(chart2Dir, "templates"), 0o755)).To(Succeed()) + Expect(os.MkdirAll(filepath.Join(chart2SubchartDir, "templates"), 0o755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(chart2Dir, "Chart.yaml"), []byte("name: chart2\nversion: 1.0.0\n"), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(chart2Dir, "values.yaml"), []byte("# values\n"), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(chart2SubchartDir, "Chart.yaml"), []byte("name: subchart\nversion: 1.0.0\n"), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(chart2SubchartDir, "values.yaml"), []byte("# subchart values\n"), 0o644)).To(Succeed()) + + // incomplete-chart (missing templates directory) incompleteChartDir := filepath.Join(tempDir, "incomplete-chart") - if err := os.MkdirAll(incompleteChartDir, 0o755); err != nil { - t.Fatalf("Failed to create incomplete-chart structure: %v", err) - } - if err := os.WriteFile(filepath.Join(incompleteChartDir, "Chart.yaml"), []byte("name: incomplete\nversion: 1.0.0\n"), 0o644); err != nil { - t.Fatalf("Failed to create incomplete Chart.yaml: %v", err) - } - if err := os.WriteFile(filepath.Join(incompleteChartDir, "values.yaml"), []byte("# values\n"), 0o644); err != nil { - t.Fatalf("Failed to create incomplete values.yaml: %v", err) - } + Expect(os.MkdirAll(incompleteChartDir, 0o755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(incompleteChartDir, "Chart.yaml"), []byte("name: incomplete\nversion: 1.0.0\n"), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(incompleteChartDir, "values.yaml"), []byte("# values\n"), 0o644)).To(Succeed()) - // Create missing-chart-yaml (missing Chart.yaml) + // missing-chart-yaml missingChartYamlDir := filepath.Join(tempDir, "missing-chart-yaml") - if err := os.MkdirAll(filepath.Join(missingChartYamlDir, "templates"), 0o755); err != nil { - t.Fatalf("Failed to create missing-chart-yaml structure: %v", err) - } - if err := os.WriteFile(filepath.Join(missingChartYamlDir, "values.yaml"), []byte("# values\n"), 0o644); err != nil { - t.Fatalf("Failed to create missing-chart-yaml values.yaml: %v", err) - } + Expect(os.MkdirAll(filepath.Join(missingChartYamlDir, "templates"), 0o755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(missingChartYamlDir, "values.yaml"), []byte("# values\n"), 0o644)).To(Succeed()) - // Create missing-values-yaml (missing values.yaml) + // missing-values-yaml missingValuesYamlDir := filepath.Join(tempDir, "missing-values-yaml") - if err := os.MkdirAll(filepath.Join(missingValuesYamlDir, "templates"), 0o755); err != nil { - t.Fatalf("Failed to create missing-values-yaml structure: %v", err) - } - if err := os.WriteFile(filepath.Join(missingValuesYamlDir, "Chart.yaml"), []byte("name: missing-values\nversion: 1.0.0\n"), 0o644); err != nil { - t.Fatalf("Failed to create missing-values-yaml Chart.yaml: %v", err) - } + Expect(os.MkdirAll(filepath.Join(missingValuesYamlDir, "templates"), 0o755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(missingValuesYamlDir, "Chart.yaml"), []byte("name: missing-values\nversion: 1.0.0\n"), 0o644)).To(Succeed()) - // Create templates-is-file (templates is a file, not directory) + // templates-is-file (templates is a file, not directory) templatesIsFileDir := filepath.Join(tempDir, "templates-is-file") - if err := os.MkdirAll(templatesIsFileDir, 0o755); err != nil { - t.Fatalf("Failed to create templates-is-file structure: %v", err) - } - if err := os.WriteFile(filepath.Join(templatesIsFileDir, "Chart.yaml"), []byte("name: templates-file\nversion: 1.0.0\n"), 0o644); err != nil { - t.Fatalf("Failed to create templates-is-file Chart.yaml: %v", err) - } - if err := os.WriteFile(filepath.Join(templatesIsFileDir, "values.yaml"), []byte("# values\n"), 0o644); err != nil { - t.Fatalf("Failed to create templates-is-file values.yaml: %v", err) - } - if err := os.WriteFile(filepath.Join(templatesIsFileDir, "templates"), []byte("not a directory\n"), 0o644); err != nil { - t.Fatalf("Failed to create templates-is-file templates file: %v", err) - } + Expect(os.MkdirAll(templatesIsFileDir, 0o755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(templatesIsFileDir, "Chart.yaml"), []byte("name: templates-file\nversion: 1.0.0\n"), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(templatesIsFileDir, "values.yaml"), []byte("# values\n"), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(templatesIsFileDir, "templates"), []byte("not a directory\n"), 0o644)).To(Succeed()) - // Create hidden chart (should be ignored by FindTopLevelCharts) + // hidden chart hiddenChartDir := filepath.Join(tempDir, ".hidden-chart") - if err := os.MkdirAll(filepath.Join(hiddenChartDir, "templates"), 0o755); err != nil { - t.Fatalf("Failed to create hidden chart structure: %v", err) - } - if err := os.WriteFile(filepath.Join(hiddenChartDir, "Chart.yaml"), []byte("name: hidden\nversion: 1.0.0\n"), 0o644); err != nil { - t.Fatalf("Failed to create hidden Chart.yaml: %v", err) - } - if err := os.WriteFile(filepath.Join(hiddenChartDir, "values.yaml"), []byte("# values\n"), 0o644); err != nil { - t.Fatalf("Failed to create hidden values.yaml: %v", err) - } + Expect(os.MkdirAll(filepath.Join(hiddenChartDir, "templates"), 0o755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(hiddenChartDir, "Chart.yaml"), []byte("name: hidden\nversion: 1.0.0\n"), 0o644)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(hiddenChartDir, "values.yaml"), []byte("# values\n"), 0o644)).To(Succeed()) - // Create not-a-chart directory + // not-a-chart directory notChartDir := filepath.Join(tempDir, "not-a-chart") - if err := os.MkdirAll(notChartDir, 0o755); err != nil { - t.Fatalf("Failed to create not-a-chart structure: %v", err) - } - if err := os.WriteFile(filepath.Join(notChartDir, "some-file.txt"), []byte("not a chart\n"), 0o644); err != nil { - t.Fatalf("Failed to create some-file.txt: %v", err) - } + Expect(os.MkdirAll(notChartDir, 0o755)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(notChartDir, "some-file.txt"), []byte("not a chart\n"), 0o644)).To(Succeed()) return tempDir } -// TestFindTopLevelCharts tests that FindTopLevelCharts only returns top-level charts -// and properly skips sub-charts and non-chart directories. -func TestFindTopLevelCharts(t *testing.T) { - tempDir := createTestChartStructure(t) - defer os.RemoveAll(tempDir) - - // Run FindTopLevelCharts - charts, err := FindTopLevelCharts(tempDir) - if err != nil { - t.Fatalf("FindTopLevelCharts failed: %v", err) - } - - // Verify results - expectedCharts := []string{"chart1", "chart2"} - if len(charts) != len(expectedCharts) { - t.Fatalf("Expected %d charts, got %d: %v", len(expectedCharts), len(charts), charts) - } - - // Convert to map for easier checking - foundCharts := make(map[string]bool) - for _, chart := range charts { - foundCharts[chart] = true - } - - // Verify each expected chart was found - for _, expected := range expectedCharts { - if !foundCharts[expected] { - t.Errorf("Expected chart '%s' not found in results: %v", expected, charts) - } - } - - // Verify that sub-charts, incomplete charts, and hidden charts were NOT found - unexpectedCharts := []string{"charts/subchart", "subchart", "incomplete-chart", ".hidden-chart", "not-a-chart"} - for _, unexpected := range unexpectedCharts { - if foundCharts[unexpected] { - t.Errorf("Unexpected chart '%s' found in results: %v", unexpected, charts) - } - } -} - -// TestIsHelmChart tests the IsHelmChart function with various directory structures. -func TestIsHelmChart(t *testing.T) { - tempDir := createTestChartStructure(t) - defer os.RemoveAll(tempDir) - - tests := []struct { - name string - chartDir string - expectedResult bool - }{ - { - name: "valid helm chart 1", - chartDir: "chart1", - expectedResult: true, - }, - { - name: "valid helm chart 2", - chartDir: "chart2", - expectedResult: true, - }, - { - name: "valid subchart (still valid helm chart)", - chartDir: "chart2/charts/subchart", - expectedResult: true, - }, - { - name: "hidden chart (still valid helm chart)", - chartDir: ".hidden-chart", - expectedResult: true, - }, - { - name: "incomplete chart - missing templates", - chartDir: "incomplete-chart", - expectedResult: false, +var _ = Describe("FindTopLevelCharts", func() { + It("should only return top-level charts and skip sub-charts and non-chart directories", func() { + tempDir := createTestChartStructure() + defer os.RemoveAll(tempDir) + + charts, err := FindTopLevelCharts(tempDir) + Expect(err).NotTo(HaveOccurred()) + Expect(charts).To(HaveLen(2)) + Expect(charts).To(ContainElements("chart1", "chart2")) + + // Verify unexpected charts are not present + Expect(charts).NotTo(ContainElement("subchart")) + Expect(charts).NotTo(ContainElement("charts/subchart")) + Expect(charts).NotTo(ContainElement("incomplete-chart")) + Expect(charts).NotTo(ContainElement(".hidden-chart")) + Expect(charts).NotTo(ContainElement("not-a-chart")) + }) +}) + +var _ = Describe("IsHelmChart", func() { + var tempDir string + + BeforeEach(func() { + tempDir = createTestChartStructure() + }) + + AfterEach(func() { + os.RemoveAll(tempDir) + }) + + DescribeTable("should correctly identify helm charts", + func(chartDir string, expected bool) { + testDir := filepath.Join(tempDir, chartDir) + Expect(IsHelmChart(testDir)).To(Equal(expected)) }, - { - name: "missing Chart.yaml", - chartDir: "missing-chart-yaml", - expectedResult: false, - }, - { - name: "missing values.yaml", - chartDir: "missing-values-yaml", - expectedResult: false, - }, - { - name: "templates is a file not directory", - chartDir: "templates-is-file", - expectedResult: false, - }, - { - name: "not a chart directory", - chartDir: "not-a-chart", - expectedResult: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - testDir := filepath.Join(tempDir, tt.chartDir) - result := IsHelmChart(testDir) - if result != tt.expectedResult { - t.Errorf("IsHelmChart('%s') = %v, expected %v", tt.chartDir, result, tt.expectedResult) - } - }) - } -} + Entry("valid helm chart 1", "chart1", true), + Entry("valid helm chart 2", "chart2", true), + Entry("valid subchart (still valid helm chart)", "chart2/charts/subchart", true), + Entry("hidden chart (still valid helm chart)", ".hidden-chart", true), + Entry("incomplete chart - missing templates", "incomplete-chart", false), + Entry("missing Chart.yaml", "missing-chart-yaml", false), + Entry("missing values.yaml", "missing-values-yaml", false), + Entry("templates is a file not directory", "templates-is-file", false), + Entry("not a chart directory", "not-a-chart", false), + ) +}) diff --git a/src/internal/pattern/pattern_suite_test.go b/src/internal/pattern/pattern_suite_test.go new file mode 100644 index 0000000..f4e7271 --- /dev/null +++ b/src/internal/pattern/pattern_suite_test.go @@ -0,0 +1,13 @@ +package pattern + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestPattern(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Pattern Suite") +} diff --git a/src/internal/pattern/pattern_test.go b/src/internal/pattern/pattern_test.go index 7a57e8a..dc5ab61 100644 --- a/src/internal/pattern/pattern_test.go +++ b/src/internal/pattern/pattern_test.go @@ -3,365 +3,316 @@ package pattern import ( "os" "path/filepath" - "testing" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "gopkg.in/yaml.v3" "github.com/validatedpatterns/patternizer/internal/types" ) -// TestProcessGlobalValuesPreservesFields tests that ProcessGlobalValues preserves existing user fields. -func TestProcessGlobalValuesPreservesFields(t *testing.T) { - tempDir, err := os.MkdirTemp("", "pattern-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tempDir) - - // Create initial values-global.yaml with custom fields - initialValues := map[string]interface{}{ - "global": map[string]interface{}{ - "pattern": "existing-pattern", - "customField": "customValue", - "nestedCustom": map[string]interface{}{ - "key1": "value1", - "key2": 42, - }, - }, - "main": map[string]interface{}{ - "clusterGroupName": "custom-cluster-group", - "multiSourceConfig": map[string]interface{}{ - "enabled": false, // Different from default - "clusterGroupChartVersion": "1.0.*", // Different from default - "customMultiSource": "customValue", - }, - "customMainField": "mainCustomValue", - }, - "customTopLevel": map[string]interface{}{ - "someKey": "someValue", - "anotherKey": []string{"item1", "item2"}, - }, - } - - valuesPath := filepath.Join(tempDir, "values-global.yaml") - initialYaml, err := yaml.Marshal(initialValues) - if err != nil { - t.Fatalf("Failed to marshal initial values: %v", err) - } - if err := os.WriteFile(valuesPath, initialYaml, 0o644); err != nil { - t.Fatalf("Failed to write initial values file: %v", err) - } - - // Process the values (test without secrets) - actualPatternName, clusterGroupName, err := ProcessGlobalValues("new-pattern", tempDir, false) - if err != nil { - t.Fatalf("ProcessGlobalValues failed: %v", err) - } - - // Verify return values - if actualPatternName != "existing-pattern" { - t.Errorf("Expected pattern name 'existing-pattern', got '%s'", actualPatternName) - } - if clusterGroupName != "custom-cluster-group" { - t.Errorf("Expected cluster group name 'custom-cluster-group', got '%s'", clusterGroupName) - } - - // Read the processed file - processedData, err := os.ReadFile(valuesPath) - if err != nil { - t.Fatalf("Failed to read processed file: %v", err) - } - - var processedValues map[string]interface{} - if err := yaml.Unmarshal(processedData, &processedValues); err != nil { - t.Fatalf("Failed to unmarshal processed values: %v", err) - } - - // Verify all custom fields are preserved - tests := []struct { - path []string - expected interface{} - }{ - {[]string{"global", "pattern"}, "existing-pattern"}, - {[]string{"global", "customField"}, "customValue"}, - {[]string{"global", "nestedCustom", "key1"}, "value1"}, - {[]string{"global", "nestedCustom", "key2"}, 42}, - {[]string{"main", "clusterGroupName"}, "custom-cluster-group"}, - {[]string{"main", "multiSourceConfig", "enabled"}, false}, - {[]string{"main", "multiSourceConfig", "clusterGroupChartVersion"}, "1.0.*"}, - {[]string{"main", "multiSourceConfig", "customMultiSource"}, "customValue"}, - {[]string{"main", "customMainField"}, "mainCustomValue"}, - {[]string{"customTopLevel", "someKey"}, "someValue"}, - } - - for _, tt := range tests { - value := getNestedValue(processedValues, tt.path) - if value != tt.expected { - t.Errorf("Field %v = %v, expected %v", tt.path, value, tt.expected) +// getNestedValue is a helper function to get nested values from a map using a path. +func getNestedValue(m map[string]interface{}, path []string) interface{} { + current := m + for i, key := range path { + if i == len(path)-1 { + return current[key] } - } - - // Verify array field is preserved - customTopLevel, ok := processedValues["customTopLevel"].(map[string]interface{}) - if !ok { - t.Fatalf("customTopLevel is not a map") - } - anotherKey, ok := customTopLevel["anotherKey"].([]interface{}) - if !ok { - t.Fatalf("anotherKey is not an array") - } - expectedArray := []interface{}{"item1", "item2"} - if len(anotherKey) != len(expectedArray) { - t.Errorf("anotherKey length = %d, expected %d", len(anotherKey), len(expectedArray)) - } - for i, expected := range expectedArray { - if i < len(anotherKey) && anotherKey[i] != expected { - t.Errorf("anotherKey[%d] = %v, expected %v", i, anotherKey[i], expected) + next, ok := current[key].(map[string]interface{}) + if !ok { + return nil } + current = next } + return nil } -// TestProcessClusterGroupValuesPreservesFields tests that ProcessClusterGroupValues preserves existing user fields. -func TestProcessClusterGroupValuesPreservesFields(t *testing.T) { - tempDir, err := os.MkdirTemp("", "pattern-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tempDir) - - // Create initial values-prod.yaml with custom fields - initialValues := map[string]interface{}{ - "clusterGroup": map[string]interface{}{ - "name": "prod", - "isHubCluster": true, - "namespaces": []interface{}{"custom-ns1", "custom-ns2"}, - "projects": []interface{}{"custom-proj1", "custom-proj2"}, - "subscriptions": map[string]interface{}{ - "custom-operator": map[string]interface{}{ - "name": "custom-operator", - "namespace": "custom-namespace", - "channel": "stable", - "source": "community-operators", +var _ = Describe("ProcessGlobalValues", func() { + Context("with an existing values file containing custom fields", func() { + var ( + tempDir string + valuesPath string + ) + + BeforeEach(func() { + var err error + tempDir, err = os.MkdirTemp("", "pattern-test-*") + Expect(err).NotTo(HaveOccurred()) + + initialValues := map[string]interface{}{ + "global": map[string]interface{}{ + "pattern": "existing-pattern", + "customField": "customValue", + "nestedCustom": map[string]interface{}{ + "key1": "value1", + "key2": 42, + }, }, - }, - "applications": map[string]interface{}{ - "custom-app": map[string]interface{}{ - "name": "custom-app", - "namespace": "custom-namespace", - "project": "custom-project", - "path": "custom/path", - "customAppField": "customAppValue", + "main": map[string]interface{}{ + "clusterGroupName": "custom-cluster-group", + "multiSourceConfig": map[string]interface{}{ + "enabled": false, + "clusterGroupChartVersion": "1.0.*", + "customMultiSource": "customValue", + }, + "customMainField": "mainCustomValue", }, - }, - "customClusterField": "customClusterValue", - }, - "customTopLevel": map[string]interface{}{ - "customKey": "customValue", - }, - } + "customTopLevel": map[string]interface{}{ + "someKey": "someValue", + "anotherKey": []string{"item1", "item2"}, + }, + } + + valuesPath = filepath.Join(tempDir, "values-global.yaml") + initialYaml, err := yaml.Marshal(initialValues) + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(valuesPath, initialYaml, 0o644)).To(Succeed()) + }) + + AfterEach(func() { + os.RemoveAll(tempDir) + }) + + It("should preserve all custom fields", func() { + actualPatternName, clusterGroupName, err := ProcessGlobalValues("new-pattern", tempDir, false) + Expect(err).NotTo(HaveOccurred()) + Expect(actualPatternName).To(Equal("existing-pattern")) + Expect(clusterGroupName).To(Equal("custom-cluster-group")) + + processedData, err := os.ReadFile(valuesPath) + Expect(err).NotTo(HaveOccurred()) + + var processedValues map[string]interface{} + Expect(yaml.Unmarshal(processedData, &processedValues)).To(Succeed()) + + tests := []struct { + path []string + expected interface{} + }{ + {[]string{"global", "pattern"}, "existing-pattern"}, + {[]string{"global", "customField"}, "customValue"}, + {[]string{"global", "nestedCustom", "key1"}, "value1"}, + {[]string{"global", "nestedCustom", "key2"}, 42}, + {[]string{"main", "clusterGroupName"}, "custom-cluster-group"}, + {[]string{"main", "multiSourceConfig", "enabled"}, false}, + {[]string{"main", "multiSourceConfig", "clusterGroupChartVersion"}, "1.0.*"}, + {[]string{"main", "multiSourceConfig", "customMultiSource"}, "customValue"}, + {[]string{"main", "customMainField"}, "mainCustomValue"}, + {[]string{"customTopLevel", "someKey"}, "someValue"}, + } + + for _, tt := range tests { + Expect(getNestedValue(processedValues, tt.path)).To(Equal(tt.expected), + "Field %v should be %v", tt.path, tt.expected) + } + + // Verify array field is preserved + customTopLevel, ok := processedValues["customTopLevel"].(map[string]interface{}) + Expect(ok).To(BeTrue(), "customTopLevel should be a map") + anotherKey, ok := customTopLevel["anotherKey"].([]interface{}) + Expect(ok).To(BeTrue(), "anotherKey should be an array") + Expect(anotherKey).To(Equal([]interface{}{"item1", "item2"})) + }) + }) + + Context("when no existing file exists", func() { + var tempDir string + + BeforeEach(func() { + var err error + tempDir, err = os.MkdirTemp("", "pattern-test-*") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + os.RemoveAll(tempDir) + }) + + It("should create the file with defaults", func() { + actualPatternName, clusterGroupName, err := ProcessGlobalValues("test-pattern", tempDir, false) + Expect(err).NotTo(HaveOccurred()) + Expect(actualPatternName).To(Equal("test-pattern")) + Expect(clusterGroupName).To(Equal("prod")) + + valuesPath := filepath.Join(tempDir, "values-global.yaml") + Expect(valuesPath).To(BeAnExistingFile()) + + data, err := os.ReadFile(valuesPath) + Expect(err).NotTo(HaveOccurred()) + + var values types.ValuesGlobal + Expect(yaml.Unmarshal(data, &values)).To(Succeed()) + + Expect(values.Global.Pattern).To(Equal("test-pattern")) + Expect(values.Main.ClusterGroupName).To(Equal("prod")) + Expect(values.Main.MultiSourceConfig.Enabled).To(BeTrue()) + Expect(values.Main.MultiSourceConfig.ClusterGroupChartVersion).To(Equal("0.9.*")) + Expect(values.Global.SecretLoader.Disabled).To(BeTrue()) + }) + }) + + Context("with secrets enabled", func() { + var tempDir string + + BeforeEach(func() { + var err error + tempDir, err = os.MkdirTemp("", "pattern-test-*") + Expect(err).NotTo(HaveOccurred()) + }) + + AfterEach(func() { + os.RemoveAll(tempDir) + }) + + It("should set SecretLoader.Disabled to false", func() { + actualPatternName, clusterGroupName, err := ProcessGlobalValues("test-pattern", tempDir, true) + Expect(err).NotTo(HaveOccurred()) + Expect(actualPatternName).To(Equal("test-pattern")) + Expect(clusterGroupName).To(Equal("prod")) + + valuesPath := filepath.Join(tempDir, "values-global.yaml") + Expect(valuesPath).To(BeAnExistingFile()) + + data, err := os.ReadFile(valuesPath) + Expect(err).NotTo(HaveOccurred()) + + var values types.ValuesGlobal + Expect(yaml.Unmarshal(data, &values)).To(Succeed()) + + Expect(values.Global.Pattern).To(Equal("test-pattern")) + Expect(values.Global.SecretLoader.Disabled).To(BeFalse()) + }) + }) +}) + +var _ = Describe("ProcessClusterGroupValues", func() { + Context("with an existing values file containing custom fields", func() { + var ( + tempDir string + valuesPath string + ) + + BeforeEach(func() { + var err error + tempDir, err = os.MkdirTemp("", "pattern-test-*") + Expect(err).NotTo(HaveOccurred()) + + initialValues := map[string]interface{}{ + "clusterGroup": map[string]interface{}{ + "name": "prod", + "isHubCluster": true, + "namespaces": []interface{}{"custom-ns1", "custom-ns2"}, + "projects": []interface{}{"custom-proj1", "custom-proj2"}, + "subscriptions": map[string]interface{}{ + "custom-operator": map[string]interface{}{ + "name": "custom-operator", + "namespace": "custom-namespace", + "channel": "stable", + "source": "community-operators", + }, + }, + "applications": map[string]interface{}{ + "custom-app": map[string]interface{}{ + "name": "custom-app", + "namespace": "custom-namespace", + "project": "custom-project", + "path": "custom/path", + "customAppField": "customAppValue", + }, + }, + "customClusterField": "customClusterValue", + }, + "customTopLevel": map[string]interface{}{ + "customKey": "customValue", + }, + } - valuesPath := filepath.Join(tempDir, "values-prod.yaml") - initialYaml, err := yaml.Marshal(initialValues) - if err != nil { - t.Fatalf("Failed to marshal initial values: %v", err) - } - if err := os.WriteFile(valuesPath, initialYaml, 0o644); err != nil { - t.Fatalf("Failed to write initial values file: %v", err) - } + valuesPath = filepath.Join(tempDir, "values-prod.yaml") + initialYaml, err := yaml.Marshal(initialValues) + Expect(err).NotTo(HaveOccurred()) + Expect(os.WriteFile(valuesPath, initialYaml, 0o644)).To(Succeed()) + }) - // Process the values - chartPaths := []string{"charts/app1", "charts/app2"} - err = ProcessClusterGroupValues("test-pattern", "prod", tempDir, chartPaths, false) - if err != nil { - t.Fatalf("ProcessClusterGroupValues failed: %v", err) - } + AfterEach(func() { + os.RemoveAll(tempDir) + }) - // Read the processed file - processedData, err := os.ReadFile(valuesPath) - if err != nil { - t.Fatalf("Failed to read processed file: %v", err) - } + It("should preserve custom fields", func() { + chartPaths := []string{"charts/app1", "charts/app2"} + Expect(ProcessClusterGroupValues("test-pattern", "prod", tempDir, chartPaths, false)).To(Succeed()) - var processedValues map[string]interface{} - if err := yaml.Unmarshal(processedData, &processedValues); err != nil { - t.Fatalf("Failed to unmarshal processed values: %v", err) - } + processedData, err := os.ReadFile(valuesPath) + Expect(err).NotTo(HaveOccurred()) - // Verify custom fields are preserved - tests := []struct { - path []string - expected interface{} - }{ - {[]string{"clusterGroup", "name"}, "prod"}, - {[]string{"clusterGroup", "isHubCluster"}, true}, - {[]string{"clusterGroup", "customClusterField"}, "customClusterValue"}, - {[]string{"customTopLevel", "customKey"}, "customValue"}, - } + var processedValues map[string]interface{} + Expect(yaml.Unmarshal(processedData, &processedValues)).To(Succeed()) - for _, tt := range tests { - value := getNestedValue(processedValues, tt.path) - if value != tt.expected { - t.Errorf("Field %v = %v, expected %v", tt.path, value, tt.expected) - } - } + tests := []struct { + path []string + expected interface{} + }{ + {[]string{"clusterGroup", "name"}, "prod"}, + {[]string{"clusterGroup", "isHubCluster"}, true}, + {[]string{"clusterGroup", "customClusterField"}, "customClusterValue"}, + {[]string{"customTopLevel", "customKey"}, "customValue"}, + } - // Verify custom application fields are preserved - clusterGroup, ok := processedValues["clusterGroup"].(map[string]interface{}) - if !ok { - t.Fatalf("clusterGroup is not a map") - } - applications, ok := clusterGroup["applications"].(map[string]interface{}) - if !ok { - t.Fatalf("applications is not a map") - } - customApp, ok := applications["custom-app"].(map[string]interface{}) - if !ok { - t.Fatalf("custom-app is not a map") - } - if customApp["customAppField"] != "customAppValue" { - t.Errorf("custom-app customAppField = %v, expected 'customAppValue'", customApp["customAppField"]) - } - - // Verify custom subscription is preserved - subscriptions, ok := clusterGroup["subscriptions"].(map[string]interface{}) - if !ok { - t.Fatalf("subscriptions is not a map") - } - customSub, ok := subscriptions["custom-operator"].(map[string]interface{}) - if !ok { - t.Fatalf("custom-operator subscription is not a map") - } - if customSub["channel"] != "stable" { - t.Errorf("custom-operator channel = %v, expected 'stable'", customSub["channel"]) - } - - // Verify new applications were added while preserving existing ones - if _, exists := applications["app1"]; !exists { - t.Error("Expected new application 'app1' to be added") - } - if _, exists := applications["app2"]; !exists { - t.Error("Expected new application 'app2' to be added") - } - if _, exists := applications["custom-app"]; !exists { - t.Error("Expected existing application 'custom-app' to be preserved") - } -} + for _, tt := range tests { + Expect(getNestedValue(processedValues, tt.path)).To(Equal(tt.expected), + "Field %v should be %v", tt.path, tt.expected) + } + }) -// TestProcessGlobalValuesWithNewFile tests ProcessGlobalValues when no existing file exists. -func TestProcessGlobalValuesWithNewFile(t *testing.T) { - tempDir, err := os.MkdirTemp("", "pattern-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tempDir) + It("should preserve custom application fields", func() { + chartPaths := []string{"charts/app1", "charts/app2"} + Expect(ProcessClusterGroupValues("test-pattern", "prod", tempDir, chartPaths, false)).To(Succeed()) - // Process values without existing file (test without secrets) - actualPatternName, clusterGroupName, err := ProcessGlobalValues("test-pattern", tempDir, false) - if err != nil { - t.Fatalf("ProcessGlobalValues failed: %v", err) - } + processedData, err := os.ReadFile(valuesPath) + Expect(err).NotTo(HaveOccurred()) - // Verify return values - if actualPatternName != "test-pattern" { - t.Errorf("Expected pattern name 'test-pattern', got '%s'", actualPatternName) - } - if clusterGroupName != "prod" { // Default cluster group name - t.Errorf("Expected cluster group name 'prod', got '%s'", clusterGroupName) - } + var processedValues map[string]interface{} + Expect(yaml.Unmarshal(processedData, &processedValues)).To(Succeed()) - // Verify file was created with defaults - valuesPath := filepath.Join(tempDir, "values-global.yaml") - if _, err := os.Stat(valuesPath); os.IsNotExist(err) { - t.Fatal("values-global.yaml was not created") - } + clusterGroup := processedValues["clusterGroup"].(map[string]interface{}) + applications := clusterGroup["applications"].(map[string]interface{}) - // Read and verify content - data, err := os.ReadFile(valuesPath) - if err != nil { - t.Fatalf("Failed to read created file: %v", err) - } + customApp := applications["custom-app"].(map[string]interface{}) + Expect(customApp["customAppField"]).To(Equal("customAppValue")) + }) - var values types.ValuesGlobal - if err := yaml.Unmarshal(data, &values); err != nil { - t.Fatalf("Failed to unmarshal created file: %v", err) - } + It("should preserve custom subscriptions", func() { + chartPaths := []string{"charts/app1", "charts/app2"} + Expect(ProcessClusterGroupValues("test-pattern", "prod", tempDir, chartPaths, false)).To(Succeed()) - if values.Global.Pattern != "test-pattern" { - t.Errorf("Global pattern = %s, expected 'test-pattern'", values.Global.Pattern) - } - if values.Main.ClusterGroupName != "prod" { - t.Errorf("Main clusterGroupName = %s, expected 'prod'", values.Main.ClusterGroupName) - } - if !values.Main.MultiSourceConfig.Enabled { - t.Error("MultiSourceConfig.Enabled should be true by default") - } - if values.Main.MultiSourceConfig.ClusterGroupChartVersion != "0.9.*" { - t.Errorf("ClusterGroupChartVersion = %s, expected '0.9.*'", values.Main.MultiSourceConfig.ClusterGroupChartVersion) - } - if !values.Global.SecretLoader.Disabled { - t.Error("SecretLoader.Disabled should be true when withSecrets=false") - } -} + processedData, err := os.ReadFile(valuesPath) + Expect(err).NotTo(HaveOccurred()) -// TestProcessGlobalValuesWithSecrets tests ProcessGlobalValues with withSecrets=true. -func TestProcessGlobalValuesWithSecrets(t *testing.T) { - tempDir, err := os.MkdirTemp("", "pattern-test-*") - if err != nil { - t.Fatalf("Failed to create temp dir: %v", err) - } - defer os.RemoveAll(tempDir) + var processedValues map[string]interface{} + Expect(yaml.Unmarshal(processedData, &processedValues)).To(Succeed()) - // Process values without existing file (test with secrets) - actualPatternName, clusterGroupName, err := ProcessGlobalValues("test-pattern", tempDir, true) - if err != nil { - t.Fatalf("ProcessGlobalValues failed: %v", err) - } + clusterGroup := processedValues["clusterGroup"].(map[string]interface{}) + subscriptions := clusterGroup["subscriptions"].(map[string]interface{}) - // Verify return values - if actualPatternName != "test-pattern" { - t.Errorf("Expected pattern name 'test-pattern', got '%s'", actualPatternName) - } - if clusterGroupName != "prod" { // Default cluster group name - t.Errorf("Expected cluster group name 'prod', got '%s'", clusterGroupName) - } + customSub := subscriptions["custom-operator"].(map[string]interface{}) + Expect(customSub["channel"]).To(Equal("stable")) + }) - // Verify file was created with defaults - valuesPath := filepath.Join(tempDir, "values-global.yaml") - if _, err := os.Stat(valuesPath); os.IsNotExist(err) { - t.Fatal("values-global.yaml was not created") - } + It("should add new applications while preserving existing ones", func() { + chartPaths := []string{"charts/app1", "charts/app2"} + Expect(ProcessClusterGroupValues("test-pattern", "prod", tempDir, chartPaths, false)).To(Succeed()) - // Read and verify content - data, err := os.ReadFile(valuesPath) - if err != nil { - t.Fatalf("Failed to read created file: %v", err) - } + processedData, err := os.ReadFile(valuesPath) + Expect(err).NotTo(HaveOccurred()) - var values types.ValuesGlobal - if err := yaml.Unmarshal(data, &values); err != nil { - t.Fatalf("Failed to unmarshal created file: %v", err) - } + var processedValues map[string]interface{} + Expect(yaml.Unmarshal(processedData, &processedValues)).To(Succeed()) - if values.Global.Pattern != "test-pattern" { - t.Errorf("Global pattern = %s, expected 'test-pattern'", values.Global.Pattern) - } - if values.Global.SecretLoader.Disabled { - t.Error("SecretLoader.Disabled should be false when withSecrets=true") - } -} + clusterGroup := processedValues["clusterGroup"].(map[string]interface{}) + applications := clusterGroup["applications"].(map[string]interface{}) -// getNestedValue is a helper function to get nested values from a map using a path. -func getNestedValue(m map[string]interface{}, path []string) interface{} { - current := m - for i, key := range path { - if i == len(path)-1 { - return current[key] - } - next, ok := current[key].(map[string]interface{}) - if !ok { - return nil - } - current = next - } - return nil -} + Expect(applications).To(HaveKey("app1")) + Expect(applications).To(HaveKey("app2")) + Expect(applications).To(HaveKey("custom-app")) + }) + }) +}) diff --git a/src/main_suite_test.go b/src/main_suite_test.go new file mode 100644 index 0000000..91fdd97 --- /dev/null +++ b/src/main_suite_test.go @@ -0,0 +1,13 @@ +package main + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMain(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Main Suite") +} diff --git a/src/main_test.go b/src/main_test.go index ee93bec..823a49f 100644 --- a/src/main_test.go +++ b/src/main_test.go @@ -2,82 +2,75 @@ package main import ( "os" - "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" "github.com/validatedpatterns/patternizer/internal/fileutils" "github.com/validatedpatterns/patternizer/internal/types" ) -func TestGetResourcePath(t *testing.T) { - // Test with environment variable set - os.Setenv("PATTERNIZER_RESOURCES_DIR", "/tmp/test") - path, err := fileutils.GetResourcesPath() - if err != nil { - t.Fatalf("Expected no error, got %v", err) - } - if path != "/tmp/test" { - t.Fatalf("Expected /tmp/test, got %s", path) - } - - // Test with environment variable unset - os.Unsetenv("PATTERNIZER_RESOURCES_DIR") - path, err = fileutils.GetResourcesPath() - if err == nil { - t.Fatal("Expected error, got nil") - } - if path != "" { - t.Fatalf("Expected empty path, got %s", path) - } -} - -func TestNewDefaultValuesGlobal(t *testing.T) { - values := types.NewDefaultValuesGlobal() - - if values.Main.ClusterGroupName != "prod" { - t.Errorf("Expected clusterGroupName to be 'prod', got '%s'", values.Main.ClusterGroupName) - } - - if !values.Main.MultiSourceConfig.Enabled { - t.Error("Expected multiSourceConfig.enabled to be true") - } - - if values.Main.MultiSourceConfig.ClusterGroupChartVersion != "0.9.*" { - t.Errorf("Expected clusterGroupChartVersion to be '0.9.*', got '%s'", values.Main.MultiSourceConfig.ClusterGroupChartVersion) - } -} - -func TestNewDefaultValuesClusterGroup(t *testing.T) { - // Test without secrets - values := types.NewDefaultValuesClusterGroup("test-pattern", "test-group", []string{"charts/app1", "charts/app2"}, false) - - if values.ClusterGroup.Name != "test-group" { - t.Errorf("Expected name to be 'test-group', got '%s'", values.ClusterGroup.Name) - } - - expectedNamespaces := []string{"test-pattern"} - if len(values.ClusterGroup.Namespaces) != len(expectedNamespaces) { - t.Errorf("Expected %d namespaces, got %d", len(expectedNamespaces), len(values.ClusterGroup.Namespaces)) - } - - // Test with secrets - valuesWithSecrets := types.NewDefaultValuesClusterGroup("test-pattern", "test-group", []string{"charts/app1"}, true) - - expectedNamespacesWithSecrets := []string{"test-pattern", "vault", "external-secrets-operator", "external-secrets"} - if len(valuesWithSecrets.ClusterGroup.Namespaces) != len(expectedNamespacesWithSecrets) { - t.Errorf("Expected %d namespaces with secrets, got %d", len(expectedNamespacesWithSecrets), len(valuesWithSecrets.ClusterGroup.Namespaces)) - } - - // Check that vault and openshift-external-secrets applications are added - if _, exists := valuesWithSecrets.ClusterGroup.Applications["vault"]; !exists { - t.Error("Expected vault application to be present with secrets") - } - - if _, exists := valuesWithSecrets.ClusterGroup.Applications["openshift-external-secrets"]; !exists { - t.Error("Expected openshift-external-secrets application to be present with secrets") - } - - // Check that eso subscription is added - if _, exists := valuesWithSecrets.ClusterGroup.Subscriptions["eso"]; !exists { - t.Error("Expected eso subscription to be present with secrets") - } -} +var _ = Describe("GetResourcePath", func() { + AfterEach(func() { + os.Unsetenv("PATTERNIZER_RESOURCES_DIR") + }) + + It("should return the path when the environment variable is set", func() { + os.Setenv("PATTERNIZER_RESOURCES_DIR", "/tmp/test") + path, err := fileutils.GetResourcesPath() + Expect(err).NotTo(HaveOccurred()) + Expect(path).To(Equal("/tmp/test")) + }) + + It("should return an error when the environment variable is unset", func() { + os.Unsetenv("PATTERNIZER_RESOURCES_DIR") + path, err := fileutils.GetResourcesPath() + Expect(err).To(HaveOccurred()) + Expect(path).To(BeEmpty()) + }) +}) + +var _ = Describe("NewDefaultValuesGlobal", func() { + It("should create default global values with expected defaults", func() { + values := types.NewDefaultValuesGlobal() + + Expect(values.Main.ClusterGroupName).To(Equal("prod")) + Expect(values.Main.MultiSourceConfig.Enabled).To(BeTrue()) + Expect(values.Main.MultiSourceConfig.ClusterGroupChartVersion).To(Equal("0.9.*")) + }) +}) + +var _ = Describe("NewDefaultValuesClusterGroup", func() { + Context("without secrets", func() { + It("should create a cluster group with the correct name and namespaces", func() { + values := types.NewDefaultValuesClusterGroup("test-pattern", "test-group", []string{"charts/app1", "charts/app2"}, false) + + Expect(values.ClusterGroup.Name).To(Equal("test-group")) + Expect(values.ClusterGroup.Namespaces).To(HaveLen(1)) + }) + }) + + Context("with secrets", func() { + var valuesWithSecrets *types.ValuesClusterGroup + + BeforeEach(func() { + valuesWithSecrets = types.NewDefaultValuesClusterGroup("test-pattern", "test-group", []string{"charts/app1"}, true) + }) + + It("should include all expected namespaces", func() { + Expect(valuesWithSecrets.ClusterGroup.Namespaces).To(HaveLen(4)) + }) + + It("should include the vault application", func() { + Expect(valuesWithSecrets.ClusterGroup.Applications).To(HaveKey("vault")) + }) + + It("should include the openshift-external-secrets application", func() { + Expect(valuesWithSecrets.ClusterGroup.Applications).To(HaveKey("openshift-external-secrets")) + }) + + It("should include the eso subscription", func() { + Expect(valuesWithSecrets.ClusterGroup.Subscriptions).To(HaveKey("eso")) + }) + }) +}) From f4cede86ff8b3b1f0a9f34a59314f50bd8ec3f20 Mon Sep 17 00:00:00 2001 From: Drew Minnear Date: Fri, 3 Apr 2026 11:36:58 -0400 Subject: [PATCH 3/3] bump version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ff65272..f78f8d4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Patternizer -![Version: 1.0.1](https://img.shields.io/badge/Version-1.0.1-informational?style=flat-square) +![Version: 1.1.0](https://img.shields.io/badge/Version-1.1.0-informational?style=flat-square) [![Quay Repository](https://img.shields.io/badge/Quay.io-patternizer-blue?logo=quay)](https://quay.io/repository/validatedpatterns/patternizer) [![CI Pipeline](https://github.com/validatedpatterns/patternizer/actions/workflows/build-push.yaml/badge.svg?branch=main)](https://github.com/validatedpatterns/patternizer/actions/workflows/build-push.yaml)