diff --git a/pkg/cli/cmd_helpers_test.go b/pkg/cli/cmd_helpers_test.go new file mode 100644 index 00000000000..d669a64aac7 --- /dev/null +++ b/pkg/cli/cmd_helpers_test.go @@ -0,0 +1,73 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cli + +import ( + "errors" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" +) + +var _ = Describe("cmd_helpers", func() { + Context("error types", func() { + It("noResolvedPluginError should return correct message", func() { + err := noResolvedPluginError{} + Expect(err.Error()).To(ContainSubstring("no resolved plugin")) + Expect(err.Error()).To(ContainSubstring("verify the project version and plugins")) + }) + + It("noAvailablePluginError should return correct message with subcommand", func() { + err := noAvailablePluginError{subcommand: "init"} + Expect(err.Error()).To(ContainSubstring("init")) + Expect(err.Error()).To(ContainSubstring("do not provide any")) + }) + }) + + Context("cmdErr", func() { + It("should update command with error information", func() { + cmd := &cobra.Command{ + Long: "Original description", + RunE: func(*cobra.Command, []string) error { + return nil + }, + } + testError := errors.New("test error") + + cmdErr(cmd, testError) + + Expect(cmd.Long).To(ContainSubstring("Original description")) + Expect(cmd.Long).To(ContainSubstring("test error")) + Expect(cmd.RunE).NotTo(BeNil()) + + // Execute the modified RunE to verify it returns the error + err := cmd.RunE(cmd, []string{}) + Expect(err).To(Equal(testError)) + }) + }) + + Context("errCmdFunc", func() { + It("should return a function that returns the provided error", func() { + testError := errors.New("test error") + runE := errCmdFunc(testError) + + err := runE(nil, nil) + Expect(err).To(Equal(testError)) + }) + }) +}) diff --git a/pkg/machinery/mixins_delim_test.go b/pkg/machinery/mixins_delim_test.go new file mode 100644 index 00000000000..7762617b4ee --- /dev/null +++ b/pkg/machinery/mixins_delim_test.go @@ -0,0 +1,147 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package machinery + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "sigs.k8s.io/kubebuilder/v4/pkg/model/resource" +) + +var _ = Describe("TemplateMixin Delimiters", func() { + var tmp TemplateMixin + + BeforeEach(func() { + tmp = TemplateMixin{} + }) + + Context("SetDelim and GetDelim", func() { + It("should set and get custom delimiters", func() { + tmp.SetDelim("[[", "]]") + left, right := tmp.GetDelim() + Expect(left).To(Equal("[[")) + Expect(right).To(Equal("]]")) + }) + + It("should return empty strings when delimiters are not set", func() { + left, right := tmp.GetDelim() + Expect(left).To(Equal("")) + Expect(right).To(Equal("")) + }) + + It("should allow setting delimiters multiple times", func() { + tmp.SetDelim("[[", "]]") + left, right := tmp.GetDelim() + Expect(left).To(Equal("[[")) + Expect(right).To(Equal("]]")) + + tmp.SetDelim("<%", "%>") + left, right = tmp.GetDelim() + Expect(left).To(Equal("<%")) + Expect(right).To(Equal("%>")) + }) + }) +}) + +var _ = Describe("Mixins injection behaviors", func() { + Context("DomainMixin", func() { + It("should not overwrite existing domain", func() { + tmp := DomainMixin{Domain: "existing.domain"} + tmp.InjectDomain("new.domain") + Expect(tmp.Domain).To(Equal("existing.domain")) + }) + + It("should inject domain when empty", func() { + tmp := DomainMixin{} + tmp.InjectDomain("new.domain") + Expect(tmp.Domain).To(Equal("new.domain")) + }) + }) + + Context("RepositoryMixin", func() { + It("should not overwrite existing repository", func() { + tmp := RepositoryMixin{Repo: "existing.repo"} + tmp.InjectRepository("new.repo") + Expect(tmp.Repo).To(Equal("existing.repo")) + }) + + It("should inject repository when empty", func() { + tmp := RepositoryMixin{} + tmp.InjectRepository("new.repo") + Expect(tmp.Repo).To(Equal("new.repo")) + }) + }) + + Context("ProjectNameMixin", func() { + It("should not overwrite existing project name", func() { + tmp := ProjectNameMixin{ProjectName: "existing"} + tmp.InjectProjectName("new") + Expect(tmp.ProjectName).To(Equal("existing")) + }) + + It("should inject project name when empty", func() { + tmp := ProjectNameMixin{} + tmp.InjectProjectName("new") + Expect(tmp.ProjectName).To(Equal("new")) + }) + }) + + Context("BoilerplateMixin", func() { + It("should not overwrite existing boilerplate", func() { + tmp := BoilerplateMixin{Boilerplate: "existing"} + tmp.InjectBoilerplate("new") + Expect(tmp.Boilerplate).To(Equal("existing")) + }) + + It("should inject boilerplate when empty", func() { + tmp := BoilerplateMixin{} + tmp.InjectBoilerplate("new") + Expect(tmp.Boilerplate).To(Equal("new")) + }) + }) + + Context("ResourceMixin", func() { + It("should not overwrite existing resource", func() { + existing := &resource.Resource{GVK: resource.GVK{Group: "existing"}} + tmp := ResourceMixin{Resource: existing} + tmp.InjectResource(&resource.Resource{GVK: resource.GVK{Group: "new"}}) + Expect(tmp.Resource.Group).To(Equal("existing")) + }) + + It("should inject resource when nil", func() { + tmp := ResourceMixin{} + res := &resource.Resource{GVK: resource.GVK{Group: "new"}} + tmp.InjectResource(res) + Expect(tmp.Resource.Group).To(Equal("new")) + }) + }) +}) + +var _ = Describe("IfNotExistsActionMixin", func() { + Context("GetIfNotExistsAction", func() { + It("should return the configured action", func() { + tmp := IfNotExistsActionMixin{IfNotExistsAction: IgnoreFile} + Expect(tmp.GetIfNotExistsAction()).To(Equal(IgnoreFile)) + }) + + It("should return zero value when not set", func() { + tmp := IfNotExistsActionMixin{} + Expect(tmp.GetIfNotExistsAction()).To(Equal(IfNotExistsAction(0))) + }) + }) +}) diff --git a/pkg/model/resource/webhooks_copy_test.go b/pkg/model/resource/webhooks_copy_test.go new file mode 100644 index 00000000000..94d48b46539 --- /dev/null +++ b/pkg/model/resource/webhooks_copy_test.go @@ -0,0 +1,171 @@ +/* +Copyright 2025 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package resource + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Webhooks Copy and AddSpoke", func() { + Context("Copy", func() { + It("should create a deep copy of Webhooks", func() { + original := Webhooks{ + WebhookVersion: v1, + Defaulting: true, + Validation: true, + Conversion: false, + Spoke: []string{"v1", "v2"}, + } + + copied := original.Copy() + + Expect(copied.WebhookVersion).To(Equal(original.WebhookVersion)) + Expect(copied.Defaulting).To(Equal(original.Defaulting)) + Expect(copied.Validation).To(Equal(original.Validation)) + Expect(copied.Conversion).To(Equal(original.Conversion)) + Expect(copied.Spoke).To(Equal(original.Spoke)) + }) + + It("should not affect original when modifying the copy", func() { + original := Webhooks{ + WebhookVersion: v1, + Defaulting: true, + Spoke: []string{"v1"}, + } + + copied := original.Copy() + copied.Defaulting = false + copied.Spoke = append(copied.Spoke, "v2") + + Expect(original.Defaulting).To(BeTrue()) + Expect(original.Spoke).To(Equal([]string{"v1"})) + Expect(copied.Defaulting).To(BeFalse()) + Expect(copied.Spoke).To(Equal([]string{"v1", "v2"})) + }) + + It("should handle empty Spoke slice", func() { + original := Webhooks{ + WebhookVersion: v1, + Spoke: []string{}, + } + + copied := original.Copy() + Expect(copied.Spoke).To(BeNil()) + }) + + It("should handle nil Spoke slice", func() { + original := Webhooks{ + WebhookVersion: v1, + Spoke: nil, + } + + copied := original.Copy() + Expect(copied.Spoke).To(BeNil()) + }) + + It("should create independent Spoke slices", func() { + original := Webhooks{ + Spoke: []string{"v1"}, + } + + copied := original.Copy() + copied.Spoke[0] = "v2" + + Expect(original.Spoke[0]).To(Equal("v1")) + Expect(copied.Spoke[0]).To(Equal("v2")) + }) + }) + + Context("AddSpoke", func() { + It("should add a new spoke version", func() { + webhook := &Webhooks{} + webhook.AddSpoke("v1") + + Expect(webhook.Spoke).To(HaveLen(1)) + Expect(webhook.Spoke).To(ContainElement("v1")) + }) + + It("should not add duplicate spoke versions", func() { + webhook := &Webhooks{ + Spoke: []string{"v1"}, + } + webhook.AddSpoke("v1") + + Expect(webhook.Spoke).To(HaveLen(1)) + Expect(webhook.Spoke).To(Equal([]string{"v1"})) + }) + + It("should add multiple different spoke versions", func() { + webhook := &Webhooks{} + webhook.AddSpoke("v1") + webhook.AddSpoke("v2") + webhook.AddSpoke("v3") + + Expect(webhook.Spoke).To(HaveLen(3)) + Expect(webhook.Spoke).To(ContainElements("v1", "v2", "v3")) + }) + + It("should handle adding existing version in the middle", func() { + webhook := &Webhooks{ + Spoke: []string{"v1", "v2", "v3"}, + } + webhook.AddSpoke("v2") + + Expect(webhook.Spoke).To(HaveLen(3)) + Expect(webhook.Spoke).To(Equal([]string{"v1", "v2", "v3"})) + }) + }) + + Context("Validate with duplicate Spoke versions", func() { + It("should fail validation with duplicate spoke versions", func() { + webhook := Webhooks{ + WebhookVersion: v1, + Spoke: []string{"v1", "v1"}, + } + + Expect(webhook.Validate()).NotTo(Succeed()) + }) + + It("should succeed validation with unique spoke versions", func() { + webhook := Webhooks{ + WebhookVersion: v1, + Spoke: []string{"v1", "v2", "v3"}, + } + + Expect(webhook.Validate()).To(Succeed()) + }) + }) + + Context("IsEmpty with Spoke", func() { + It("should return false when only Spoke is set", func() { + webhook := Webhooks{ + Spoke: []string{"v1"}, + } + + Expect(webhook.IsEmpty()).To(BeFalse()) + }) + + It("should return true when Spoke is empty array", func() { + webhook := Webhooks{ + Spoke: []string{}, + } + + Expect(webhook.IsEmpty()).To(BeTrue()) + }) + }) +})