diff --git a/internal/repository/manifest/manifest_repo.go b/internal/repository/manifest/manifest_repo.go new file mode 100644 index 0000000000..e0148a83cc --- /dev/null +++ b/internal/repository/manifest/manifest_repo.go @@ -0,0 +1,46 @@ +package manifest + +import ( + "context" + "fmt" + + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" +) + +type Repository struct { + clnt client.Client + namespace string +} + +func NewRepository(clnt client.Client, namespace string) *Repository { + return &Repository{ + clnt: clnt, + namespace: namespace, + } +} + +func (r *Repository) DeleteAllForModule(ctx context.Context, moduleName string) error { + err := r.clnt.DeleteAllOf(ctx, &v1beta2.Manifest{}, client.InNamespace(r.namespace), + client.MatchingLabels{shared.ModuleName: moduleName}) + if err != nil { + return fmt.Errorf("failed to delete all manifests for module %s: %w", moduleName, err) + } + return nil +} + +func (r *Repository) ListAllForModule(ctx context.Context, moduleName string) ( + []apimetav1.PartialObjectMetadata, error, +) { + var manifestList apimetav1.PartialObjectMetadataList + manifestList.SetGroupVersionKind(v1beta2.GroupVersion.WithKind("ManifestList")) + + if err := r.clnt.List(ctx, &manifestList, client.InNamespace(r.namespace), + client.MatchingLabels{shared.ModuleName: moduleName}); err != nil { + return nil, fmt.Errorf("failed to list Manifests for module %s: %w", moduleName, err) + } + return manifestList.Items, nil +} diff --git a/internal/repository/manifest/manifest_repo_test.go b/internal/repository/manifest/manifest_repo_test.go new file mode 100644 index 0000000000..fded7d12f1 --- /dev/null +++ b/internal/repository/manifest/manifest_repo_test.go @@ -0,0 +1,170 @@ +package manifest_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/repository/manifest" + "github.com/kyma-project/lifecycle-manager/pkg/testutils/random" +) + +type clientStub struct { + client.Client + + deleteAllOfCalled bool + listCalled bool + deleteAllOfErr error + listErr error + + capturedNamespace string + capturedLabels map[string]string + capturedObjectType client.Object + + partialObjectMetadata []apimetav1.PartialObjectMetadata +} + +func (c *clientStub) DeleteAllOf(_ context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { + c.deleteAllOfCalled = true + c.capturedObjectType = obj + + for _, opt := range opts { + if nsOpt, ok := opt.(client.InNamespace); ok { + c.capturedNamespace = string(nsOpt) + } + if labelOpt, ok := opt.(client.MatchingLabels); ok { + c.capturedLabels = labelOpt + } + } + + return c.deleteAllOfErr +} + +func (c *clientStub) List(_ context.Context, list client.ObjectList, opts ...client.ListOption) error { + c.listCalled = true + + for _, opt := range opts { + if nsOpt, ok := opt.(client.InNamespace); ok { + c.capturedNamespace = string(nsOpt) + } + if labelOpt, ok := opt.(client.MatchingLabels); ok { + c.capturedLabels = labelOpt + } + } + + if c.listErr != nil { + return c.listErr + } + + if partialList, ok := list.(*apimetav1.PartialObjectMetadataList); ok { + partialList.Items = c.partialObjectMetadata + } + + return nil +} + +func TestRepository_DeleteAllForModule(t *testing.T) { + ctx := context.Background() + testNamespace := random.Name() + testModuleName := random.Name() + + t.Run("successfully deletes all manifests for module", func(t *testing.T) { + stub := &clientStub{} + repo := manifest.NewRepository(stub, testNamespace) + + err := repo.DeleteAllForModule(ctx, testModuleName) + + require.NoError(t, err) + require.True(t, stub.deleteAllOfCalled) + require.Equal(t, testNamespace, stub.capturedNamespace) + require.Equal(t, testModuleName, stub.capturedLabels[shared.ModuleName]) + require.IsType(t, &v1beta2.Manifest{}, stub.capturedObjectType) + }) + + t.Run("returns error when deleteAllOf fails", func(t *testing.T) { + expectedErr := errors.New("delete error") + stub := &clientStub{deleteAllOfErr: expectedErr} + repo := manifest.NewRepository(stub, testNamespace) + + err := repo.DeleteAllForModule(ctx, testModuleName) + + require.Error(t, err) + require.Contains(t, err.Error(), "failed to delete all manifests for module") + require.Contains(t, err.Error(), testModuleName) + require.True(t, stub.deleteAllOfCalled) + require.Equal(t, testNamespace, stub.capturedNamespace) + require.Equal(t, testModuleName, stub.capturedLabels[shared.ModuleName]) + }) +} + +func TestRepository_ListAllForModule(t *testing.T) { + ctx := context.Background() + testNamespace := random.Name() + testModuleName := random.Name() + + t.Run("successfully lists all manifests for module", func(t *testing.T) { + expectedMetadata := []apimetav1.PartialObjectMetadata{ + { + ObjectMeta: apimetav1.ObjectMeta{ + Name: "manifest1", + Namespace: testNamespace, + Labels: map[string]string{shared.ModuleName: testModuleName}, + }, + }, + { + ObjectMeta: apimetav1.ObjectMeta{ + Name: "manifest2", + Namespace: testNamespace, + Labels: map[string]string{shared.ModuleName: testModuleName}, + }, + }, + } + + stub := &clientStub{partialObjectMetadata: expectedMetadata} + repo := manifest.NewRepository(stub, testNamespace) + + result, err := repo.ListAllForModule(ctx, testModuleName) + + require.NoError(t, err) + require.Len(t, result, 2) + require.Equal(t, expectedMetadata, result) + require.True(t, stub.listCalled) + require.Equal(t, testNamespace, stub.capturedNamespace) + require.Equal(t, testModuleName, stub.capturedLabels[shared.ModuleName]) + }) + + t.Run("returns empty list when no manifests found", func(t *testing.T) { + stub := &clientStub{partialObjectMetadata: []apimetav1.PartialObjectMetadata{}} + repo := manifest.NewRepository(stub, testNamespace) + + result, err := repo.ListAllForModule(ctx, testModuleName) + + require.NoError(t, err) + require.Empty(t, result) + require.True(t, stub.listCalled) + require.Equal(t, testNamespace, stub.capturedNamespace) + require.Equal(t, testModuleName, stub.capturedLabels[shared.ModuleName]) + }) + + t.Run("returns error when list fails", func(t *testing.T) { + expectedErr := errors.New("list error") + stub := &clientStub{listErr: expectedErr} + repo := manifest.NewRepository(stub, testNamespace) + + result, err := repo.ListAllForModule(ctx, testModuleName) + + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "failed to list Manifests for module") + require.Contains(t, err.Error(), testModuleName) + require.True(t, stub.listCalled) + require.Equal(t, testNamespace, stub.capturedNamespace) + require.Equal(t, testModuleName, stub.capturedLabels[shared.ModuleName]) + }) +} diff --git a/internal/repository/modulereleasemeta/mrm_repo.go b/internal/repository/modulereleasemeta/mrm_repo.go new file mode 100644 index 0000000000..ea87205cd2 --- /dev/null +++ b/internal/repository/modulereleasemeta/mrm_repo.go @@ -0,0 +1,58 @@ +package modulereleasemeta + +import ( + "context" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" +) + +type Repository struct { + clnt client.Client + namespace string +} + +func NewRepository(clnt client.Client, namespace string) *Repository { + return &Repository{ + clnt: clnt, + namespace: namespace, + } +} + +func (r *Repository) EnsureFinalizer(ctx context.Context, mrmName string, finalizer string) error { + mrm, err := r.Get(ctx, mrmName) + if err != nil { + return err + } + if updated := controllerutil.AddFinalizer(mrm, finalizer); updated { + if err := r.clnt.Update(ctx, mrm); err != nil { + return fmt.Errorf("failed to add finalizer to ModuleReleaseMeta %s: %w", mrmName, err) + } + } + return nil +} + +func (r *Repository) RemoveFinalizer(ctx context.Context, mrmName string, finalizer string) error { + mrm, err := r.Get(ctx, mrmName) + if err != nil { + return err + } + if updated := controllerutil.RemoveFinalizer(mrm, finalizer); updated { + if err := r.clnt.Update(ctx, mrm); err != nil { + return fmt.Errorf("failed to remove finalizer from ModuleReleaseMeta %s: %w", mrmName, err) + } + } + return nil +} + +func (r *Repository) Get(ctx context.Context, mrmName string) (*v1beta2.ModuleReleaseMeta, error) { + mrm := &v1beta2.ModuleReleaseMeta{} + err := r.clnt.Get(ctx, client.ObjectKey{Name: mrmName, Namespace: r.namespace}, mrm) + if err != nil { + return nil, fmt.Errorf("failed to get ModuleReleaseMeta %s in namespace %s: %w", mrmName, r.namespace, err) + } + return mrm, nil +} diff --git a/internal/repository/modulereleasemeta/mrm_repo_test.go b/internal/repository/modulereleasemeta/mrm_repo_test.go new file mode 100644 index 0000000000..b6007f44df --- /dev/null +++ b/internal/repository/modulereleasemeta/mrm_repo_test.go @@ -0,0 +1,234 @@ +package modulereleasemeta_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/repository/modulereleasemeta" +) + +type clientStub struct { + client.Client + + getCalled bool + updateCalled bool + getErr error + updateErr error + mrm *v1beta2.ModuleReleaseMeta +} + +func (c *clientStub) Get(_ context.Context, _ client.ObjectKey, obj client.Object, _ ...client.GetOption) error { + c.getCalled = true + if c.mrm != nil { + c.mrm.DeepCopyInto(obj.(*v1beta2.ModuleReleaseMeta)) + } + return c.getErr +} + +func (c *clientStub) Update(_ context.Context, _ client.Object, _ ...client.UpdateOption) error { + c.updateCalled = true + return c.updateErr +} + +func TestRepository_EnsureFinalizer(t *testing.T) { + ctx := context.Background() + testNamespace := "test-namespace" + testMRMName := "test-mrm" + testFinalizer := "test-finalizer" + + t.Run("adds finalizer when not present", func(t *testing.T) { + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testMRMName, + Namespace: testNamespace, + Finalizers: []string{}, + }, + } + + stub := &clientStub{mrm: mrm} + repo := modulereleasemeta.NewRepository(stub, testNamespace) + + err := repo.EnsureFinalizer(ctx, testMRMName, testFinalizer) + + require.NoError(t, err) + require.True(t, stub.getCalled) + require.True(t, stub.updateCalled) + }) + + t.Run("does not update when finalizer already present", func(t *testing.T) { + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testMRMName, + Namespace: testNamespace, + Finalizers: []string{testFinalizer}, + }, + } + + stub := &clientStub{mrm: mrm} + repo := modulereleasemeta.NewRepository(stub, testNamespace) + + err := repo.EnsureFinalizer(ctx, testMRMName, testFinalizer) + + require.NoError(t, err) + require.True(t, stub.getCalled) + require.False(t, stub.updateCalled) + }) + + t.Run("returns error when get fails", func(t *testing.T) { + expectedErr := errors.New("get error") + stub := &clientStub{getErr: expectedErr} + repo := modulereleasemeta.NewRepository(stub, testNamespace) + + err := repo.EnsureFinalizer(ctx, testMRMName, testFinalizer) + + require.Error(t, err) + require.True(t, stub.getCalled) + require.False(t, stub.updateCalled) + }) + + t.Run("returns error when update fails", func(t *testing.T) { + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testMRMName, + Namespace: testNamespace, + Finalizers: []string{}, + }, + } + + expectedErr := errors.New("update error") + stub := &clientStub{mrm: mrm, updateErr: expectedErr} + repo := modulereleasemeta.NewRepository(stub, testNamespace) + + err := repo.EnsureFinalizer(ctx, testMRMName, testFinalizer) + + require.Error(t, err) + require.Contains(t, err.Error(), "failed to add finalizer to ModuleReleaseMeta") + require.True(t, stub.getCalled) + require.True(t, stub.updateCalled) + }) +} + +func TestRepository_RemoveFinalizer(t *testing.T) { + ctx := context.Background() + testNamespace := "test-namespace" + testMRMName := "test-mrm" + testFinalizer := "test-finalizer" + + t.Run("removes finalizer when present", func(t *testing.T) { + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testMRMName, + Namespace: testNamespace, + Finalizers: []string{testFinalizer}, + }, + } + + stub := &clientStub{mrm: mrm} + repo := modulereleasemeta.NewRepository(stub, testNamespace) + + err := repo.RemoveFinalizer(ctx, testMRMName, testFinalizer) + + require.NoError(t, err) + require.True(t, stub.getCalled) + require.True(t, stub.updateCalled) + }) + + t.Run("does not update when finalizer not present", func(t *testing.T) { + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testMRMName, + Namespace: testNamespace, + Finalizers: []string{}, + }, + } + + stub := &clientStub{mrm: mrm} + repo := modulereleasemeta.NewRepository(stub, testNamespace) + + err := repo.RemoveFinalizer(ctx, testMRMName, testFinalizer) + + require.NoError(t, err) + require.True(t, stub.getCalled) + require.False(t, stub.updateCalled) + }) + + t.Run("returns error when get fails", func(t *testing.T) { + expectedErr := errors.New("get error") + stub := &clientStub{getErr: expectedErr} + repo := modulereleasemeta.NewRepository(stub, testNamespace) + + err := repo.RemoveFinalizer(ctx, testMRMName, testFinalizer) + + require.Error(t, err) + require.True(t, stub.getCalled) + require.False(t, stub.updateCalled) + }) + + t.Run("returns error when update fails", func(t *testing.T) { + mrm := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testMRMName, + Namespace: testNamespace, + Finalizers: []string{testFinalizer}, + }, + } + + expectedErr := errors.New("update error") + stub := &clientStub{mrm: mrm, updateErr: expectedErr} + repo := modulereleasemeta.NewRepository(stub, testNamespace) + + err := repo.RemoveFinalizer(ctx, testMRMName, testFinalizer) + + require.Error(t, err) + require.Contains(t, err.Error(), "failed to remove finalizer from ModuleReleaseMeta") + require.True(t, stub.getCalled) + require.True(t, stub.updateCalled) + }) +} + +func TestRepository_Get(t *testing.T) { + ctx := context.Background() + testNamespace := "test-namespace" + testMRMName := "test-mrm" + + t.Run("returns MRM when successful", func(t *testing.T) { + expectedMRM := &v1beta2.ModuleReleaseMeta{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testMRMName, + Namespace: testNamespace, + }, + } + + stub := &clientStub{mrm: expectedMRM} + repo := modulereleasemeta.NewRepository(stub, testNamespace) + + result, err := repo.Get(ctx, testMRMName) + + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, testMRMName, result.Name) + require.Equal(t, testNamespace, result.Namespace) + require.True(t, stub.getCalled) + }) + + t.Run("returns error when client get fails", func(t *testing.T) { + expectedErr := errors.New("client get error") + stub := &clientStub{getErr: expectedErr} + repo := modulereleasemeta.NewRepository(stub, testNamespace) + + result, err := repo.Get(ctx, testMRMName) + + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "failed to get ModuleReleaseMeta") + require.Contains(t, err.Error(), testMRMName) + require.Contains(t, err.Error(), testNamespace) + require.True(t, stub.getCalled) + }) +} diff --git a/internal/repository/moduletemplate/moduletemplate_repo.go b/internal/repository/moduletemplate/moduletemplate_repo.go new file mode 100644 index 0000000000..b869f19c49 --- /dev/null +++ b/internal/repository/moduletemplate/moduletemplate_repo.go @@ -0,0 +1,69 @@ +package moduletemplate + +import ( + "context" + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" +) + +type Repository struct { + clnt client.Client + namespace string +} + +func NewRepository(clnt client.Client, namespace string) *Repository { + return &Repository{ + clnt: clnt, + namespace: namespace, + } +} + +func (r *Repository) ListAllForModule(ctx context.Context, moduleName string) ([]v1beta2.ModuleTemplate, error) { + var moduleTemplateList v1beta2.ModuleTemplateList + if err := r.clnt.List(ctx, &moduleTemplateList, client.InNamespace(r.namespace), + client.MatchingLabels{shared.ModuleName: moduleName}); err != nil { + return nil, fmt.Errorf("failed to list ModuleTemplates for module %s: %w", moduleName, err) + } + return moduleTemplateList.Items, nil +} + +func (r *Repository) EnsureFinalizer(ctx context.Context, moduleTemplateName string, finalizer string) error { + moduleTemplate, err := r.Get(ctx, moduleTemplateName) + if err != nil { + return err + } + if updated := controllerutil.AddFinalizer(moduleTemplate, finalizer); updated { + if err := r.clnt.Update(ctx, moduleTemplate); err != nil { + return fmt.Errorf("failed to add finalizer to ModuleTemplate %s: %w", moduleTemplateName, err) + } + } + return nil +} + +func (r *Repository) RemoveFinalizer(ctx context.Context, moduleTemplateName string, finalizer string) error { + moduleTemplate, err := r.Get(ctx, moduleTemplateName) + if err != nil { + return err + } + if updated := controllerutil.RemoveFinalizer(moduleTemplate, finalizer); updated { + if err := r.clnt.Update(ctx, moduleTemplate); err != nil { + return fmt.Errorf("failed to remove finalizer from ModuleTemplate %s: %w", moduleTemplateName, err) + } + } + return nil +} + +func (r *Repository) Get(ctx context.Context, moduleTemplateName string) (*v1beta2.ModuleTemplate, error) { + moduleTemplate := &v1beta2.ModuleTemplate{} + err := r.clnt.Get(ctx, client.ObjectKey{Name: moduleTemplateName, Namespace: r.namespace}, moduleTemplate) + if err != nil { + return nil, fmt.Errorf("failed to get ModuleTemplate %s in namespace %s: %w", moduleTemplateName, r.namespace, + err) + } + return moduleTemplate, nil +} diff --git a/internal/repository/moduletemplate/moduletemplate_repo_test.go b/internal/repository/moduletemplate/moduletemplate_repo_test.go new file mode 100644 index 0000000000..5cc2ff01b1 --- /dev/null +++ b/internal/repository/moduletemplate/moduletemplate_repo_test.go @@ -0,0 +1,343 @@ +package moduletemplate_test + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + apimetav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/kyma-project/lifecycle-manager/api/shared" + "github.com/kyma-project/lifecycle-manager/api/v1beta2" + "github.com/kyma-project/lifecycle-manager/internal/repository/moduletemplate" +) + +type clientStub struct { + client.Client + + getCalled bool + updateCalled bool + listCalled bool + getErr error + updateErr error + listErr error + + capturedNamespace string + capturedLabels map[string]string + + moduleTemplate *v1beta2.ModuleTemplate + moduleTemplates []v1beta2.ModuleTemplate +} + +func (c *clientStub) Get(_ context.Context, _ client.ObjectKey, obj client.Object, _ ...client.GetOption) error { + c.getCalled = true + if c.moduleTemplate != nil { + c.moduleTemplate.DeepCopyInto(obj.(*v1beta2.ModuleTemplate)) + } + return c.getErr +} + +func (c *clientStub) Update(_ context.Context, _ client.Object, _ ...client.UpdateOption) error { + c.updateCalled = true + return c.updateErr +} + +func (c *clientStub) List(_ context.Context, list client.ObjectList, opts ...client.ListOption) error { + c.listCalled = true + + for _, opt := range opts { + if nsOpt, ok := opt.(client.InNamespace); ok { + c.capturedNamespace = string(nsOpt) + } + if labelOpt, ok := opt.(client.MatchingLabels); ok { + c.capturedLabels = map[string]string(labelOpt) + } + } + + if c.listErr != nil { + return c.listErr + } + + if moduleTemplateList, ok := list.(*v1beta2.ModuleTemplateList); ok { + moduleTemplateList.Items = c.moduleTemplates + } + + return nil +} + +func TestRepository_EnsureFinalizer(t *testing.T) { + ctx := context.Background() + testNamespace := "test-namespace" + testModuleTemplateName := "test-moduletemplate" + testFinalizer := "test-finalizer" + + t.Run("adds finalizer when not present", func(t *testing.T) { + moduleTemplate := &v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testModuleTemplateName, + Namespace: testNamespace, + Finalizers: []string{}, + }, + } + + stub := &clientStub{moduleTemplate: moduleTemplate} + repo := moduletemplate.NewRepository(stub, testNamespace) + + err := repo.EnsureFinalizer(ctx, testModuleTemplateName, testFinalizer) + + require.NoError(t, err) + require.True(t, stub.getCalled) + require.True(t, stub.updateCalled) + }) + + t.Run("does not update when finalizer already present", func(t *testing.T) { + moduleTemplate := &v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testModuleTemplateName, + Namespace: testNamespace, + Finalizers: []string{testFinalizer}, + }, + } + + stub := &clientStub{moduleTemplate: moduleTemplate} + repo := moduletemplate.NewRepository(stub, testNamespace) + + err := repo.EnsureFinalizer(ctx, testModuleTemplateName, testFinalizer) + + require.NoError(t, err) + require.True(t, stub.getCalled) + require.False(t, stub.updateCalled) + }) + + t.Run("returns error when get fails", func(t *testing.T) { + expectedErr := errors.New("get error") + stub := &clientStub{getErr: expectedErr} + repo := moduletemplate.NewRepository(stub, testNamespace) + + err := repo.EnsureFinalizer(ctx, testModuleTemplateName, testFinalizer) + + require.Error(t, err) + require.True(t, stub.getCalled) + require.False(t, stub.updateCalled) + }) + + t.Run("returns error when update fails", func(t *testing.T) { + moduleTemplate := &v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testModuleTemplateName, + Namespace: testNamespace, + Finalizers: []string{}, + }, + } + + expectedErr := errors.New("update error") + stub := &clientStub{moduleTemplate: moduleTemplate, updateErr: expectedErr} + repo := moduletemplate.NewRepository(stub, testNamespace) + + err := repo.EnsureFinalizer(ctx, testModuleTemplateName, testFinalizer) + + require.Error(t, err) + require.Contains(t, err.Error(), "failed to add finalizer to ModuleTemplate") + require.True(t, stub.getCalled) + require.True(t, stub.updateCalled) + }) +} + +func TestRepository_RemoveFinalizer(t *testing.T) { + ctx := context.Background() + testNamespace := "test-namespace" + testModuleTemplateName := "test-moduletemplate" + testFinalizer := "test-finalizer" + + t.Run("removes finalizer when present", func(t *testing.T) { + moduleTemplate := &v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testModuleTemplateName, + Namespace: testNamespace, + Finalizers: []string{testFinalizer}, + }, + } + + stub := &clientStub{moduleTemplate: moduleTemplate} + repo := moduletemplate.NewRepository(stub, testNamespace) + + err := repo.RemoveFinalizer(ctx, testModuleTemplateName, testFinalizer) + + require.NoError(t, err) + require.True(t, stub.getCalled) + require.True(t, stub.updateCalled) + }) + + t.Run("does not update when finalizer not present", func(t *testing.T) { + moduleTemplate := &v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testModuleTemplateName, + Namespace: testNamespace, + Finalizers: []string{}, + }, + } + + stub := &clientStub{moduleTemplate: moduleTemplate} + repo := moduletemplate.NewRepository(stub, testNamespace) + + err := repo.RemoveFinalizer(ctx, testModuleTemplateName, testFinalizer) + + require.NoError(t, err) + require.True(t, stub.getCalled) + require.False(t, stub.updateCalled) + }) + + t.Run("returns error when get fails", func(t *testing.T) { + expectedErr := errors.New("get error") + stub := &clientStub{getErr: expectedErr} + repo := moduletemplate.NewRepository(stub, testNamespace) + + err := repo.RemoveFinalizer(ctx, testModuleTemplateName, testFinalizer) + + require.Error(t, err) + require.True(t, stub.getCalled) + require.False(t, stub.updateCalled) + }) + + t.Run("returns error when update fails", func(t *testing.T) { + moduleTemplate := &v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testModuleTemplateName, + Namespace: testNamespace, + Finalizers: []string{testFinalizer}, + }, + } + + expectedErr := errors.New("update error") + stub := &clientStub{moduleTemplate: moduleTemplate, updateErr: expectedErr} + repo := moduletemplate.NewRepository(stub, testNamespace) + + err := repo.RemoveFinalizer(ctx, testModuleTemplateName, testFinalizer) + + require.Error(t, err) + require.Contains(t, err.Error(), "failed to remove finalizer from ModuleTemplate") + require.True(t, stub.getCalled) + require.True(t, stub.updateCalled) + }) +} + +func TestRepository_Get(t *testing.T) { + ctx := context.Background() + testNamespace := "test-namespace" + testModuleTemplateName := "test-moduletemplate" + + t.Run("returns ModuleTemplate when successful", func(t *testing.T) { + expectedModuleTemplate := &v1beta2.ModuleTemplate{ + ObjectMeta: apimetav1.ObjectMeta{ + Name: testModuleTemplateName, + Namespace: testNamespace, + }, + } + + stub := &clientStub{moduleTemplate: expectedModuleTemplate} + repo := moduletemplate.NewRepository(stub, testNamespace) + + result, err := repo.Get(ctx, testModuleTemplateName) + + require.NoError(t, err) + require.NotNil(t, result) + require.Equal(t, testModuleTemplateName, result.Name) + require.Equal(t, testNamespace, result.Namespace) + require.True(t, stub.getCalled) + }) + + t.Run("returns error when client get fails", func(t *testing.T) { + expectedErr := errors.New("client get error") + stub := &clientStub{getErr: expectedErr} + repo := moduletemplate.NewRepository(stub, testNamespace) + + result, err := repo.Get(ctx, testModuleTemplateName) + + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "failed to get ModuleTemplate") + require.Contains(t, err.Error(), testModuleTemplateName) + require.Contains(t, err.Error(), testNamespace) + require.True(t, stub.getCalled) + }) +} + +func TestRepository_ListAllForModule(t *testing.T) { + ctx := context.Background() + testNamespace := "test-namespace" + testModuleName := "test-module" + + t.Run("successfully lists all ModuleTemplates for module", func(t *testing.T) { + expectedModuleTemplates := []v1beta2.ModuleTemplate{ + { + ObjectMeta: apimetav1.ObjectMeta{ + Name: "template1", + Namespace: testNamespace, + Labels: map[string]string{shared.ModuleName: testModuleName}, + }, + }, + { + ObjectMeta: apimetav1.ObjectMeta{ + Name: "template2", + Namespace: testNamespace, + Labels: map[string]string{shared.ModuleName: testModuleName}, + }, + }, + } + + stub := &clientStub{moduleTemplates: expectedModuleTemplates} + repo := moduletemplate.NewRepository(stub, testNamespace) + + result, err := repo.ListAllForModule(ctx, testModuleName) + + require.NoError(t, err) + require.Len(t, result, 2) + require.Equal(t, expectedModuleTemplates, result) + require.True(t, stub.listCalled) + require.Equal(t, testNamespace, stub.capturedNamespace) + require.Equal(t, testModuleName, stub.capturedLabels[shared.ModuleName]) + }) + + t.Run("returns empty list when no ModuleTemplates found", func(t *testing.T) { + stub := &clientStub{moduleTemplates: []v1beta2.ModuleTemplate{}} + repo := moduletemplate.NewRepository(stub, testNamespace) + + result, err := repo.ListAllForModule(ctx, testModuleName) + + require.NoError(t, err) + require.Empty(t, result) + require.True(t, stub.listCalled) + require.Equal(t, testNamespace, stub.capturedNamespace) + require.Equal(t, testModuleName, stub.capturedLabels[shared.ModuleName]) + }) + + t.Run("returns error when list fails", func(t *testing.T) { + expectedErr := errors.New("list error") + stub := &clientStub{listErr: expectedErr} + repo := moduletemplate.NewRepository(stub, testNamespace) + + result, err := repo.ListAllForModule(ctx, testModuleName) + + require.Error(t, err) + require.Nil(t, result) + require.Contains(t, err.Error(), "failed to list ModuleTemplates for module") + require.Contains(t, err.Error(), testModuleName) + require.True(t, stub.listCalled) + require.Equal(t, testNamespace, stub.capturedNamespace) + require.Equal(t, testModuleName, stub.capturedLabels[shared.ModuleName]) + }) + + t.Run("uses correct module name in label selector", func(t *testing.T) { + differentModuleName := "different-module-name" + stub := &clientStub{moduleTemplates: []v1beta2.ModuleTemplate{}} + repo := moduletemplate.NewRepository(stub, testNamespace) + + _, err := repo.ListAllForModule(ctx, differentModuleName) + + require.NoError(t, err) + require.True(t, stub.listCalled) + require.Equal(t, differentModuleName, stub.capturedLabels[shared.ModuleName]) + }) +} diff --git a/unit-test-coverage-lifecycle-manager.yaml b/unit-test-coverage-lifecycle-manager.yaml index aaf59e29c7..3fa5e36a79 100644 --- a/unit-test-coverage-lifecycle-manager.yaml +++ b/unit-test-coverage-lifecycle-manager.yaml @@ -28,6 +28,9 @@ packages: internal/repository/watcher/certificate/gcm/certificate: 98 internal/repository/watcher/certificate/gcm/renewal: 100 internal/repository/secret: 94 + internal/repository/manifest: 100 + internal/repository/moduletemplate: 100 + internal/repository/modulereleasemeta: 100 internal/service/kyma/status/modules/generator: 100 internal/service/kyma/status/modules/generator/fromerror: 100