From 887fc05f084f07de7d4641f569014233e4673b04 Mon Sep 17 00:00:00 2001 From: "Felipe C. Gehrke" Date: Thu, 7 Nov 2024 20:11:53 -0300 Subject: [PATCH 1/5] added delegate error / improve delegate coverage / add new mocks --- pkg/ext/apiserver.go | 28 +- pkg/ext/delegate.go | 7 +- pkg/ext/delegate_error.go | 110 ++++ pkg/ext/delegate_error_test.go | 60 ++ pkg/ext/delegate_test.go | 1012 ++++++++++++++++++++++++++++++++ pkg/ext/rest_mock.go | 66 +++ pkg/ext/store_mock.go | 131 +++++ 7 files changed, 1400 insertions(+), 14 deletions(-) create mode 100644 pkg/ext/delegate_error.go create mode 100644 pkg/ext/delegate_error_test.go create mode 100644 pkg/ext/delegate_test.go create mode 100644 pkg/ext/rest_mock.go create mode 100644 pkg/ext/store_mock.go diff --git a/pkg/ext/apiserver.go b/pkg/ext/apiserver.go index 3b7cd87f..86544864 100644 --- a/pkg/ext/apiserver.go +++ b/pkg/ext/apiserver.go @@ -239,20 +239,22 @@ func InstallStore[T runtime.Object, TList runtime.Object]( apiGroup.VersionedResourcesStorageMap[gvk.Version] = make(map[string]rest.Storage) } - delegate := &delegate[T, TList]{ - scheme: s.scheme, - - t: t, - tList: tList, - singularName: singularName, - gvk: gvk, - gvr: schema.GroupVersionResource{ - Group: gvk.Group, - Version: gvk.Version, - Resource: resourceName, + delegate := &delegateError[T, TList]{ + inner: &delegate[T, TList]{ + scheme: s.scheme, + + t: t, + tList: tList, + singularName: singularName, + gvk: gvk, + gvr: schema.GroupVersionResource{ + Group: gvk.Group, + Version: gvk.Version, + Resource: resourceName, + }, + authorizer: s.authorizer, + store: store, }, - authorizer: s.authorizer, - store: store, } apiGroup.VersionedResourcesStorageMap[gvk.Version][resourceName] = delegate diff --git a/pkg/ext/delegate.go b/pkg/ext/delegate.go index 4df1dfbf..56747775 100644 --- a/pkg/ext/delegate.go +++ b/pkg/ext/delegate.go @@ -2,6 +2,7 @@ package ext import ( "context" + "errors" "fmt" "sync" @@ -16,6 +17,10 @@ import ( "k8s.io/apiserver/pkg/registry/rest" ) +var ( + errMissingUserInfo error = errors.New("missing user info") +) + // delegate is the bridge between k8s.io/apiserver's [rest.Storage] interface and // our own Store interface we want developers to use // @@ -328,7 +333,7 @@ func (s *delegate[T, TList]) GetSingularName() string { func (s *delegate[T, TList]) makeContext(parentCtx context.Context) (Context, error) { userInfo, ok := request.UserFrom(parentCtx) if !ok { - return Context{}, fmt.Errorf("missing user info") + return Context{}, errMissingUserInfo } ctx := Context{ diff --git a/pkg/ext/delegate_error.go b/pkg/ext/delegate_error.go new file mode 100644 index 00000000..1fa6e6e6 --- /dev/null +++ b/pkg/ext/delegate_error.go @@ -0,0 +1,110 @@ +package ext + +import ( + "context" + + "k8s.io/apimachinery/pkg/api/errors" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/registry/rest" +) + +// delegateError wraps an inner delegate and converts unknown errors. +type delegateError[T runtime.Object, TList runtime.Object] struct { + inner *delegate[T, TList] +} + +func (d *delegateError[T, TList]) convertError(err error) error { + if _, ok := err.(errors.APIStatus); ok { + return err + } + + return errors.NewInternalError(err) +} + +func (d *delegateError[T, TList]) New() runtime.Object { + return d.inner.New() +} + +func (d *delegateError[T, TList]) Destroy() { + d.inner.Destroy() +} + +func (d *delegateError[T, TList]) NewList() runtime.Object { + return d.inner.NewList() +} + +func (d *delegateError[T, TList]) List(parentCtx context.Context, internaloptions *metainternalversion.ListOptions) (runtime.Object, error) { + result, err := d.inner.List(parentCtx, internaloptions) + if err != nil { + return nil, d.convertError(err) + } + return result, nil +} + +func (d *delegateError[T, TList]) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { + result, err := d.inner.ConvertToTable(ctx, object, tableOptions) + if err != nil { + return nil, d.convertError(err) + } + return result, nil +} + +func (d *delegateError[T, TList]) Get(parentCtx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { + result, err := d.inner.Get(parentCtx, name, options) + if err != nil { + return nil, d.convertError(err) + } + return result, nil +} + +func (d *delegateError[T, TList]) Delete(parentCtx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { + result, completed, err := d.inner.Delete(parentCtx, name, deleteValidation, options) + if err != nil { + return nil, false, d.convertError(err) + } + return result, completed, nil +} + +func (d *delegateError[T, TList]) Create(parentCtx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { + result, err := d.inner.Create(parentCtx, obj, createValidation, options) + if err != nil { + return nil, d.convertError(err) + } + return result, nil +} + +func (d *delegateError[T, TList]) Update(parentCtx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { + result, created, err := d.inner.Update(parentCtx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) + if err != nil { + return nil, false, d.convertError(err) + } + return result, created, nil +} + +func (d *delegateError[T, TList]) Watch(parentCtx context.Context, internaloptions *metainternalversion.ListOptions) (watch.Interface, error) { + result, err := d.inner.Watch(parentCtx, internaloptions) + if err != nil { + return nil, d.convertError(err) + } + return result, nil +} + +func (d *delegateError[T, TList]) GroupVersionKind(groupVersion schema.GroupVersion) schema.GroupVersionKind { + return d.inner.GroupVersionKind(groupVersion) +} + +func (d *delegateError[T, TList]) NamespaceScoped() bool { + return d.inner.NamespaceScoped() +} + +func (d *delegateError[T, TList]) Kind() string { + return d.inner.Kind() +} + +func (d *delegateError[T, TList]) GetSingularName() string { + return d.inner.GetSingularName() +} diff --git a/pkg/ext/delegate_error_test.go b/pkg/ext/delegate_error_test.go new file mode 100644 index 00000000..c61b24e2 --- /dev/null +++ b/pkg/ext/delegate_error_test.go @@ -0,0 +1,60 @@ +package ext + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestDelegateError_convertError(t *testing.T) { + tests := []struct { + name string + input error + output error + }{ + { + name: "api status error", + input: &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + output: &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + }, + { + name: "generic error", + input: assert.AnError, + output: &apierrors.StatusError{ErrStatus: metav1.Status{ + Status: metav1.StatusFailure, + Code: http.StatusInternalServerError, + Reason: metav1.StatusReasonInternalError, + Details: &metav1.StatusDetails{ + Causes: []metav1.StatusCause{{Message: assert.AnError.Error()}}, + }, + Message: fmt.Sprintf("Internal error occurred: %v", assert.AnError), + }}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + delegateError := delegateError[*TestType, *TestTypeList]{ + inner: &delegate[*TestType, *TestTypeList]{}, + } + + output := delegateError.convertError(tt.input) + assert.Equal(t, tt.output, output) + }) + } + +} diff --git a/pkg/ext/delegate_test.go b/pkg/ext/delegate_test.go new file mode 100644 index 00000000..ba340a56 --- /dev/null +++ b/pkg/ext/delegate_test.go @@ -0,0 +1,1012 @@ +package ext + +import ( + "context" + "errors" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + gomock "go.uber.org/mock/gomock" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" +) + +func TestDelegate_Watch(t *testing.T) { + type input struct { + ctx context.Context + internaloptions *metainternalversion.ListOptions + } + + type output struct { + watch watch.Interface + err error + } + + type testCase struct { + name string + input input + expected output + storeSetup func(*MockStore[*TestType, *TestTypeList]) + simulateConvertionError bool + wantedErr bool + } + + tests := []testCase{ + { + name: "missing user in context", + input: input{ + ctx: context.TODO(), + internaloptions: &metainternalversion.ListOptions{}, + }, + expected: output{ + err: errMissingUserInfo, + }, + wantedErr: true, + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + }, + { + name: "convert list error", + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + internaloptions: &metainternalversion.ListOptions{}, + }, + simulateConvertionError: true, + wantedErr: true, + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + }, + } + + for _, tt := range tests { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + if !tt.simulateConvertionError { + scheme.AddConversionFunc(&metainternalversion.ListOptions{}, &metav1.ListOptions{}, convert_internalversion_ListOptions_to_v1_ListOptions) + } + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + + deleg4te := &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + } + + watch, err := deleg4te.Watch(tt.input.ctx, tt.input.internaloptions) + if tt.wantedErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, watch, tt.expected.watch) + } + } +} + +func TestDelegate_Update(t *testing.T) { + type input struct { + parentCtx context.Context + name string + objInfo rest.UpdatedObjectInfo + createValidation rest.ValidateObjectFunc + updateValidation rest.ValidateObjectUpdateFunc + forceAllowCreate bool + options *metav1.UpdateOptions + } + + type output struct { + obj runtime.Object + created bool + err error + } + + type testCase struct { + name string + setup func(*MockUpdatedObjectInfo, *MockStore[*TestType, *TestTypeList]) + input input + expect output + wantErr bool + } + + tests := []testCase{ + { + name: "working case", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + // objInfo is created in the for loop + forceAllowCreate: false, + options: &metav1.UpdateOptions{}, + }, + expect: output{ + obj: &TestType{}, + created: false, + err: nil, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + store.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + wantErr: false, + }, + { + name: "missing user in context", + input: input{ + parentCtx: context.TODO(), + }, + setup: func(muoi *MockUpdatedObjectInfo, ms *MockStore[*TestType, *TestTypeList]) {}, + expect: output{ + obj: nil, + created: false, + err: errMissingUserInfo, + }, + wantErr: true, + }, + { + name: "get failed - other error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + wantErr: true, + }, + { + name: "get failed - not found - updated object error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, + &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + ) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + wantErr: true, + }, + { + name: "get failed - not found - create succeeded", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { return nil }, + }, + expect: output{ + obj: &TestType{}, + created: true, + err: nil, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, + &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + ) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + store.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + wantErr: false, + }, + { + name: "get failed - not found - create validation error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return assert.AnError + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, + &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + ) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + wantErr: true, + }, + { + name: "get failed - not found - type error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, + &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + ) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&runtime.Unknown{}, nil) + + }, + wantErr: true, + }, + { + name: "get failed - not found - store create error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, + &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + ) + store.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + + }, + wantErr: true, + }, + { + name: "get worked - updated object error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + + }, + wantErr: true, + }, + { + name: "get worked - type error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&runtime.Unknown{}, nil) + + }, + wantErr: true, + }, + { + name: "get worked - update validation error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + updateValidation: func(ctx context.Context, obj, old runtime.Object) error { + return assert.AnError + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + + }, + wantErr: true, + }, + { + name: "get worked - store update error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + store.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + mockObjInfo := NewMockUpdatedObjectInfo(ctrl) + tt.input.objInfo = mockObjInfo + tt.setup(mockObjInfo, mockStore) + + deleg4te := &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + } + + obj, created, err := deleg4te.Update(tt.input.parentCtx, tt.input.name, tt.input.objInfo, tt.input.createValidation, tt.input.updateValidation, tt.input.forceAllowCreate, tt.input.options) + + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expect.obj, obj) + assert.Equal(t, tt.expect.created, created) + } + }) + } + +} + +func TestDelegate_Create(t *testing.T) { + type input struct { + ctx context.Context + obj runtime.Object + createValidation rest.ValidateObjectFunc + options *metav1.CreateOptions + } + + type output struct { + createResult runtime.Object + err error + } + + type testCase struct { + name string + storeSetup func(*MockStore[*TestType, *TestTypeList]) + wantErr bool + input input + expect output + } + + tests := []testCase{ + { + name: "working case", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + obj: &TestType{}, + options: &metav1.CreateOptions{}, + }, + expect: output{ + createResult: &TestType{}, + err: nil, + }, + wantErr: false, + }, + { + name: "missing user in the context", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + input: input{ + ctx: context.Background(), + obj: &TestType{}, + options: &metav1.CreateOptions{}, + }, + expect: output{ + createResult: nil, + err: errMissingUserInfo, + }, + wantErr: true, + }, + { + name: "store create error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + obj: &TestType{}, + options: &metav1.CreateOptions{}, + }, + expect: output{ + createResult: nil, + err: assert.AnError, + }, + wantErr: true, + }, + { + name: "wrong type error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + obj: &runtime.Unknown{}, + options: &metav1.CreateOptions{}, + }, + expect: output{ + createResult: nil, + err: assert.AnError, + }, + wantErr: true, + }, + { + name: "create validation error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + obj: &TestType{}, + createValidation: func(ctx context.Context, obj runtime.Object) error { + return assert.AnError + }, + options: &metav1.CreateOptions{}, + }, + expect: output{ + createResult: &TestType{}, + err: assert.AnError, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + tt.storeSetup(mockStore) + + deleg4te := &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + } + + result, err := deleg4te.Create(tt.input.ctx, tt.input.obj, tt.input.createValidation, tt.input.options) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expect.createResult, result) + } + }) + } +} + +func TestDelegate_Delete(t *testing.T) { + type input struct { + ctx context.Context + name string + deleteValidation rest.ValidateObjectFunc + options *metav1.DeleteOptions + } + + type output struct { + deleteResult runtime.Object + completed bool + err error + } + + type testCase struct { + name string + storeSetup func(*MockStore[*TestType, *TestTypeList]) + wantErr bool + input input + expect output + } + + tests := []testCase{ + { + name: "working case", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + ms.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + options: &metav1.DeleteOptions{}, + }, + expect: output{ + deleteResult: &TestType{}, + completed: true, + err: nil, + }, + wantErr: false, + }, + { + name: "missing user in the context", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + input: input{ + ctx: context.Background(), + name: "test-object", + options: &metav1.DeleteOptions{}, + }, + expect: output{ + deleteResult: nil, + completed: false, + err: errMissingUserInfo, + }, + wantErr: true, + }, + { + name: "store get error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + options: &metav1.DeleteOptions{}, + }, + expect: output{ + deleteResult: nil, + completed: false, + err: assert.AnError, + }, + wantErr: true, + }, + { + name: "store delete error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + ms.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Return(assert.AnError) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + options: &metav1.DeleteOptions{}, + }, + expect: output{ + deleteResult: &TestType{}, + completed: true, + err: assert.AnError, + }, + wantErr: true, + }, + { + name: "delete validation error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + deleteValidation: func(ctx context.Context, obj runtime.Object) error { return assert.AnError }, + options: &metav1.DeleteOptions{}, + }, + expect: output{ + deleteResult: nil, + completed: false, + err: assert.AnError, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + tt.storeSetup(mockStore) + + deleg4te := &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + } + + result, completed, err := deleg4te.Delete(tt.input.ctx, tt.input.name, tt.input.deleteValidation, tt.input.options) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expect.deleteResult, result) + assert.Equal(t, tt.expect.completed, completed) + } + }) + } +} + +func TestDelegate_Get(t *testing.T) { + type input struct { + ctx context.Context + name string + options *metav1.GetOptions + } + + type output struct { + getResult runtime.Object + err error + } + + type testCase struct { + name string + storeSetup func(*MockStore[*TestType, *TestTypeList]) + wantErr bool + input input + expect output + } + + tests := []testCase{ + { + name: "working case", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + options: &metav1.GetOptions{}, + }, + expect: output{ + getResult: &TestType{}, + err: nil, + }, + wantErr: false, + }, + { + name: "missing user in the context", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + input: input{ + ctx: context.Background(), + name: "testing-obj", + options: &metav1.GetOptions{}, + }, + expect: output{ + getResult: nil, + err: errMissingUserInfo, + }, + wantErr: true, + }, + { + name: "store get error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + options: &metav1.GetOptions{}, + }, + expect: output{ + getResult: &TestType{}, + err: assert.AnError, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + tt.storeSetup(mockStore) + + deleg4te := &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + } + + result, err := deleg4te.Get(tt.input.ctx, tt.input.name, tt.input.options) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expect.getResult, result) + } + + }) + } +} + +func TestDelegate_List(t *testing.T) { + type input struct { + ctx context.Context + listOptions *metainternalversion.ListOptions + } + + type output struct { + listResult runtime.Object + err error + } + + type testCase struct { + name string + storeSetup func(*MockStore[*TestType, *TestTypeList]) + simulateConvertError bool + wantErr bool + input input + expect output + } + + tests := []testCase{ + { + name: "working case, for completion reasons", + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + listOptions: &metainternalversion.ListOptions{}, + }, + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().List(gomock.Any(), gomock.Any()).Return(&TestTypeList{}, nil) + }, + wantErr: false, + expect: output{ + listResult: &TestTypeList{}, + err: nil, + }, + }, + { + name: "missing user in the context", + input: input{ + ctx: context.Background(), + listOptions: &metainternalversion.ListOptions{}, + }, + storeSetup: func(mockStore *MockStore[*TestType, *TestTypeList]) {}, + wantErr: true, + expect: output{ + listResult: nil, + err: errMissingUserInfo, + }, + }, + { + name: "convertListOptions error", + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + listOptions: &metainternalversion.ListOptions{}, + }, + storeSetup: func(mockStore *MockStore[*TestType, *TestTypeList]) {}, + simulateConvertError: true, + wantErr: true, + expect: output{ + listResult: nil, + err: assert.AnError, + }, + }, + { + name: "error returned by store", + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + listOptions: &metainternalversion.ListOptions{}, + }, + storeSetup: func(mockStore *MockStore[*TestType, *TestTypeList]) { + mockStore.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + wantErr: true, + expect: output{ + listResult: nil, + err: assert.AnError, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + if !tt.simulateConvertError { + scheme.AddConversionFunc(&metainternalversion.ListOptions{}, &metav1.ListOptions{}, convert_internalversion_ListOptions_to_v1_ListOptions) + } + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + tt.storeSetup(mockStore) + + deleg4te := &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + } + + result, err := deleg4te.List(tt.input.ctx, tt.input.listOptions) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expect.listResult, result) + } + }) + } +} + +func convert_internalversion_ListOptions_to_v1_ListOptions(in, out interface{}, s conversion.Scope) error { + i, ok := in.(*metainternalversion.ListOptions) + if !ok { + return errors.New("cannot convert in param into internalversion.ListOptions") + } + o, ok := out.(*metav1.ListOptions) + if !ok { + return errors.New("cannot convert out param into metav1.ListOptions") + } + if i.LabelSelector != nil { + o.LabelSelector = i.LabelSelector.String() + } + if i.FieldSelector != nil { + o.FieldSelector = i.FieldSelector.String() + } + o.Watch = i.Watch + o.ResourceVersion = i.ResourceVersion + o.TimeoutSeconds = i.TimeoutSeconds + return nil +} diff --git a/pkg/ext/rest_mock.go b/pkg/ext/rest_mock.go new file mode 100644 index 00000000..40b13fce --- /dev/null +++ b/pkg/ext/rest_mock.go @@ -0,0 +1,66 @@ +// Code generated by MockGen. DO NOT EDIT. + +// Package ext is a generated GoMock package. +package ext + +import ( + context "context" + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// MockUpdatedObjectInfo is a mock of UpdatedObjectInfo interface. +type MockUpdatedObjectInfo struct { + ctrl *gomock.Controller + recorder *MockUpdatedObjectInfoMockRecorder + isgomock struct{} +} + +// MockUpdatedObjectInfoMockRecorder is the mock recorder for MockUpdatedObjectInfo. +type MockUpdatedObjectInfoMockRecorder struct { + mock *MockUpdatedObjectInfo +} + +// NewMockUpdatedObjectInfo creates a new mock instance. +func NewMockUpdatedObjectInfo(ctrl *gomock.Controller) *MockUpdatedObjectInfo { + mock := &MockUpdatedObjectInfo{ctrl: ctrl} + mock.recorder = &MockUpdatedObjectInfoMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockUpdatedObjectInfo) EXPECT() *MockUpdatedObjectInfoMockRecorder { + return m.recorder +} + +// Preconditions mocks base method. +func (m *MockUpdatedObjectInfo) Preconditions() *v1.Preconditions { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Preconditions") + ret0, _ := ret[0].(*v1.Preconditions) + return ret0 +} + +// Preconditions indicates an expected call of Preconditions. +func (mr *MockUpdatedObjectInfoMockRecorder) Preconditions() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Preconditions", reflect.TypeOf((*MockUpdatedObjectInfo)(nil).Preconditions)) +} + +// UpdatedObject mocks base method. +func (m *MockUpdatedObjectInfo) UpdatedObject(ctx context.Context, oldObj runtime.Object) (runtime.Object, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdatedObject", ctx, oldObj) + ret0, _ := ret[0].(runtime.Object) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdatedObject indicates an expected call of UpdatedObject. +func (mr *MockUpdatedObjectInfoMockRecorder) UpdatedObject(ctx, oldObj any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdatedObject", reflect.TypeOf((*MockUpdatedObjectInfo)(nil).UpdatedObject), ctx, oldObj) +} diff --git a/pkg/ext/store_mock.go b/pkg/ext/store_mock.go new file mode 100644 index 00000000..159d98ed --- /dev/null +++ b/pkg/ext/store_mock.go @@ -0,0 +1,131 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./pkg/ext/store.go +// +// Generated by this command: +// +// mockgen -source=./pkg/ext/store.go -destination=./pkg/ext/store_mock.go -package=ext +// + +// Package ext is a generated GoMock package. +package ext + +import ( + reflect "reflect" + + gomock "go.uber.org/mock/gomock" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// MockStore is a mock of Store interface. +type MockStore[T runtime.Object, TList runtime.Object] struct { + ctrl *gomock.Controller + recorder *MockStoreMockRecorder[T, TList] + isgomock struct{} +} + +// MockStoreMockRecorder is the mock recorder for MockStore. +type MockStoreMockRecorder[T runtime.Object, TList runtime.Object] struct { + mock *MockStore[T, TList] +} + +// NewMockStore creates a new mock instance. +func NewMockStore[T runtime.Object, TList runtime.Object](ctrl *gomock.Controller) *MockStore[T, TList] { + mock := &MockStore[T, TList]{ctrl: ctrl} + mock.recorder = &MockStoreMockRecorder[T, TList]{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockStore[T, TList]) EXPECT() *MockStoreMockRecorder[T, TList] { + return m.recorder +} + +// Create mocks base method. +func (m *MockStore[T, TList]) Create(ctx Context, obj T, opts *v1.CreateOptions) (T, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Create", ctx, obj, opts) + ret0, _ := ret[0].(T) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Create indicates an expected call of Create. +func (mr *MockStoreMockRecorder[T, TList]) Create(ctx, obj, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Create", reflect.TypeOf((*MockStore[T, TList])(nil).Create), ctx, obj, opts) +} + +// Delete mocks base method. +func (m *MockStore[T, TList]) Delete(ctx Context, name string, opts *v1.DeleteOptions) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", ctx, name, opts) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockStoreMockRecorder[T, TList]) Delete(ctx, name, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockStore[T, TList])(nil).Delete), ctx, name, opts) +} + +// Get mocks base method. +func (m *MockStore[T, TList]) Get(ctx Context, name string, opts *v1.GetOptions) (T, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Get", ctx, name, opts) + ret0, _ := ret[0].(T) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Get indicates an expected call of Get. +func (mr *MockStoreMockRecorder[T, TList]) Get(ctx, name, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockStore[T, TList])(nil).Get), ctx, name, opts) +} + +// List mocks base method. +func (m *MockStore[T, TList]) List(ctx Context, opts *v1.ListOptions) (TList, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List", ctx, opts) + ret0, _ := ret[0].(TList) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockStoreMockRecorder[T, TList]) List(ctx, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockStore[T, TList])(nil).List), ctx, opts) +} + +// Update mocks base method. +func (m *MockStore[T, TList]) Update(ctx Context, obj T, opts *v1.UpdateOptions) (T, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Update", ctx, obj, opts) + ret0, _ := ret[0].(T) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Update indicates an expected call of Update. +func (mr *MockStoreMockRecorder[T, TList]) Update(ctx, obj, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Update", reflect.TypeOf((*MockStore[T, TList])(nil).Update), ctx, obj, opts) +} + +// Watch mocks base method. +func (m *MockStore[T, TList]) Watch(ctx Context, opts *v1.ListOptions) (<-chan WatchEvent[T], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Watch", ctx, opts) + ret0, _ := ret[0].(<-chan WatchEvent[T]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Watch indicates an expected call of Watch. +func (mr *MockStoreMockRecorder[T, TList]) Watch(ctx, opts any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Watch", reflect.TypeOf((*MockStore[T, TList])(nil).Watch), ctx, opts) +} From 330f1b6d93568d533af0bc2f40031be0054a6bf9 Mon Sep 17 00:00:00 2001 From: "Felipe C. Gehrke" Date: Thu, 7 Nov 2024 20:19:11 -0300 Subject: [PATCH 2/5] fixing ci lint --- pkg/ext/delegate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/ext/delegate.go b/pkg/ext/delegate.go index 56747775..ff40bb1c 100644 --- a/pkg/ext/delegate.go +++ b/pkg/ext/delegate.go @@ -18,7 +18,7 @@ import ( ) var ( - errMissingUserInfo error = errors.New("missing user info") + errMissingUserInfo = errors.New("missing user info") ) // delegate is the bridge between k8s.io/apiserver's [rest.Storage] interface and From d4d2d2153ef78fb34f393a1b0a2cd21b54f3f71e Mon Sep 17 00:00:00 2001 From: "Felipe C. Gehrke" Date: Mon, 11 Nov 2024 10:34:28 -0300 Subject: [PATCH 3/5] addressing comments from @ericpromislow --- pkg/ext/apiserver.go | 4 ++-- pkg/ext/delegate_test.go | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pkg/ext/apiserver.go b/pkg/ext/apiserver.go index 86544864..695372b0 100644 --- a/pkg/ext/apiserver.go +++ b/pkg/ext/apiserver.go @@ -239,7 +239,7 @@ func InstallStore[T runtime.Object, TList runtime.Object]( apiGroup.VersionedResourcesStorageMap[gvk.Version] = make(map[string]rest.Storage) } - delegate := &delegateError[T, TList]{ + del := &delegateError[T, TList]{ inner: &delegate[T, TList]{ scheme: s.scheme, @@ -257,7 +257,7 @@ func InstallStore[T runtime.Object, TList runtime.Object]( }, } - apiGroup.VersionedResourcesStorageMap[gvk.Version][resourceName] = delegate + apiGroup.VersionedResourcesStorageMap[gvk.Version][resourceName] = del s.apiGroups[gvk.Group] = apiGroup return nil } diff --git a/pkg/ext/delegate_test.go b/pkg/ext/delegate_test.go index ba340a56..99830c33 100644 --- a/pkg/ext/delegate_test.go +++ b/pkg/ext/delegate_test.go @@ -36,7 +36,7 @@ func TestDelegate_Watch(t *testing.T) { input input expected output storeSetup func(*MockStore[*TestType, *TestTypeList]) - simulateConvertionError bool + simulateConversionError bool wantedErr bool } @@ -61,7 +61,7 @@ func TestDelegate_Watch(t *testing.T) { }), internaloptions: &metainternalversion.ListOptions{}, }, - simulateConvertionError: true, + simulateConversionError: true, wantedErr: true, storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, }, @@ -71,7 +71,7 @@ func TestDelegate_Watch(t *testing.T) { scheme := runtime.NewScheme() addToSchemeTest(scheme) - if !tt.simulateConvertionError { + if !tt.simulateConversionError { scheme.AddConversionFunc(&metainternalversion.ListOptions{}, &metav1.ListOptions{}, convert_internalversion_ListOptions_to_v1_ListOptions) } @@ -83,7 +83,7 @@ func TestDelegate_Watch(t *testing.T) { mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) - deleg4te := &delegate[*TestType, *TestTypeList]{ + testDelegate := &delegate[*TestType, *TestTypeList]{ scheme: scheme, t: &TestType{}, tList: &TestTypeList{}, @@ -92,7 +92,7 @@ func TestDelegate_Watch(t *testing.T) { store: mockStore, } - watch, err := deleg4te.Watch(tt.input.ctx, tt.input.internaloptions) + watch, err := testDelegate.Watch(tt.input.ctx, tt.input.internaloptions) if tt.wantedErr { assert.Error(t, err) } else { @@ -438,7 +438,7 @@ func TestDelegate_Update(t *testing.T) { tt.input.objInfo = mockObjInfo tt.setup(mockObjInfo, mockStore) - deleg4te := &delegate[*TestType, *TestTypeList]{ + testDelegate := &delegate[*TestType, *TestTypeList]{ scheme: scheme, t: &TestType{}, tList: &TestTypeList{}, @@ -447,7 +447,7 @@ func TestDelegate_Update(t *testing.T) { store: mockStore, } - obj, created, err := deleg4te.Update(tt.input.parentCtx, tt.input.name, tt.input.objInfo, tt.input.createValidation, tt.input.updateValidation, tt.input.forceAllowCreate, tt.input.options) + obj, created, err := testDelegate.Update(tt.input.parentCtx, tt.input.name, tt.input.objInfo, tt.input.createValidation, tt.input.updateValidation, tt.input.forceAllowCreate, tt.input.options) if tt.wantErr { assert.Error(t, err) @@ -584,7 +584,7 @@ func TestDelegate_Create(t *testing.T) { mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) tt.storeSetup(mockStore) - deleg4te := &delegate[*TestType, *TestTypeList]{ + testDelegate := &delegate[*TestType, *TestTypeList]{ scheme: scheme, t: &TestType{}, tList: &TestTypeList{}, @@ -593,7 +593,7 @@ func TestDelegate_Create(t *testing.T) { store: mockStore, } - result, err := deleg4te.Create(tt.input.ctx, tt.input.obj, tt.input.createValidation, tt.input.options) + result, err := testDelegate.Create(tt.input.ctx, tt.input.obj, tt.input.createValidation, tt.input.options) if tt.wantErr { assert.Error(t, err) } else { @@ -737,7 +737,7 @@ func TestDelegate_Delete(t *testing.T) { mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) tt.storeSetup(mockStore) - deleg4te := &delegate[*TestType, *TestTypeList]{ + testDelegate := &delegate[*TestType, *TestTypeList]{ scheme: scheme, t: &TestType{}, tList: &TestTypeList{}, @@ -746,7 +746,7 @@ func TestDelegate_Delete(t *testing.T) { store: mockStore, } - result, completed, err := deleg4te.Delete(tt.input.ctx, tt.input.name, tt.input.deleteValidation, tt.input.options) + result, completed, err := testDelegate.Delete(tt.input.ctx, tt.input.name, tt.input.deleteValidation, tt.input.options) if tt.wantErr { assert.Error(t, err) } else { @@ -845,7 +845,7 @@ func TestDelegate_Get(t *testing.T) { mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) tt.storeSetup(mockStore) - deleg4te := &delegate[*TestType, *TestTypeList]{ + testDelegate := &delegate[*TestType, *TestTypeList]{ scheme: scheme, t: &TestType{}, tList: &TestTypeList{}, @@ -854,7 +854,7 @@ func TestDelegate_Get(t *testing.T) { store: mockStore, } - result, err := deleg4te.Get(tt.input.ctx, tt.input.name, tt.input.options) + result, err := testDelegate.Get(tt.input.ctx, tt.input.name, tt.input.options) if tt.wantErr { assert.Error(t, err) } else { @@ -970,7 +970,7 @@ func TestDelegate_List(t *testing.T) { mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) tt.storeSetup(mockStore) - deleg4te := &delegate[*TestType, *TestTypeList]{ + testDelegate := &delegate[*TestType, *TestTypeList]{ scheme: scheme, t: &TestType{}, tList: &TestTypeList{}, @@ -979,7 +979,7 @@ func TestDelegate_List(t *testing.T) { store: mockStore, } - result, err := deleg4te.List(tt.input.ctx, tt.input.listOptions) + result, err := testDelegate.List(tt.input.ctx, tt.input.listOptions) if tt.wantErr { assert.Error(t, err) } else { From fc802338feb8a0333efbbdf3fe6c139dfb9d0345 Mon Sep 17 00:00:00 2001 From: "Felipe C. Gehrke" Date: Tue, 12 Nov 2024 11:57:39 -0300 Subject: [PATCH 4/5] addressing comments from @tomleb --- pkg/ext/delegate_error.go | 30 +- pkg/ext/delegate_error_test.go | 1059 +++++++++++++++++++++++++++++++- pkg/ext/delegate_test.go | 1012 ------------------------------ 3 files changed, 1069 insertions(+), 1032 deletions(-) delete mode 100644 pkg/ext/delegate_test.go diff --git a/pkg/ext/delegate_error.go b/pkg/ext/delegate_error.go index 1fa6e6e6..1c4b2a97 100644 --- a/pkg/ext/delegate_error.go +++ b/pkg/ext/delegate_error.go @@ -17,14 +17,6 @@ type delegateError[T runtime.Object, TList runtime.Object] struct { inner *delegate[T, TList] } -func (d *delegateError[T, TList]) convertError(err error) error { - if _, ok := err.(errors.APIStatus); ok { - return err - } - - return errors.NewInternalError(err) -} - func (d *delegateError[T, TList]) New() runtime.Object { return d.inner.New() } @@ -40,7 +32,7 @@ func (d *delegateError[T, TList]) NewList() runtime.Object { func (d *delegateError[T, TList]) List(parentCtx context.Context, internaloptions *metainternalversion.ListOptions) (runtime.Object, error) { result, err := d.inner.List(parentCtx, internaloptions) if err != nil { - return nil, d.convertError(err) + return nil, convertError(err) } return result, nil } @@ -48,7 +40,7 @@ func (d *delegateError[T, TList]) List(parentCtx context.Context, internaloption func (d *delegateError[T, TList]) ConvertToTable(ctx context.Context, object runtime.Object, tableOptions runtime.Object) (*metav1.Table, error) { result, err := d.inner.ConvertToTable(ctx, object, tableOptions) if err != nil { - return nil, d.convertError(err) + return nil, convertError(err) } return result, nil } @@ -56,7 +48,7 @@ func (d *delegateError[T, TList]) ConvertToTable(ctx context.Context, object run func (d *delegateError[T, TList]) Get(parentCtx context.Context, name string, options *metav1.GetOptions) (runtime.Object, error) { result, err := d.inner.Get(parentCtx, name, options) if err != nil { - return nil, d.convertError(err) + return nil, convertError(err) } return result, nil } @@ -64,7 +56,7 @@ func (d *delegateError[T, TList]) Get(parentCtx context.Context, name string, op func (d *delegateError[T, TList]) Delete(parentCtx context.Context, name string, deleteValidation rest.ValidateObjectFunc, options *metav1.DeleteOptions) (runtime.Object, bool, error) { result, completed, err := d.inner.Delete(parentCtx, name, deleteValidation, options) if err != nil { - return nil, false, d.convertError(err) + return nil, false, convertError(err) } return result, completed, nil } @@ -72,7 +64,7 @@ func (d *delegateError[T, TList]) Delete(parentCtx context.Context, name string, func (d *delegateError[T, TList]) Create(parentCtx context.Context, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) { result, err := d.inner.Create(parentCtx, obj, createValidation, options) if err != nil { - return nil, d.convertError(err) + return nil, convertError(err) } return result, nil } @@ -80,7 +72,7 @@ func (d *delegateError[T, TList]) Create(parentCtx context.Context, obj runtime. func (d *delegateError[T, TList]) Update(parentCtx context.Context, name string, objInfo rest.UpdatedObjectInfo, createValidation rest.ValidateObjectFunc, updateValidation rest.ValidateObjectUpdateFunc, forceAllowCreate bool, options *metav1.UpdateOptions) (runtime.Object, bool, error) { result, created, err := d.inner.Update(parentCtx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options) if err != nil { - return nil, false, d.convertError(err) + return nil, false, convertError(err) } return result, created, nil } @@ -88,7 +80,7 @@ func (d *delegateError[T, TList]) Update(parentCtx context.Context, name string, func (d *delegateError[T, TList]) Watch(parentCtx context.Context, internaloptions *metainternalversion.ListOptions) (watch.Interface, error) { result, err := d.inner.Watch(parentCtx, internaloptions) if err != nil { - return nil, d.convertError(err) + return nil, convertError(err) } return result, nil } @@ -108,3 +100,11 @@ func (d *delegateError[T, TList]) Kind() string { func (d *delegateError[T, TList]) GetSingularName() string { return d.inner.GetSingularName() } + +func convertError(err error) error { + if _, ok := err.(errors.APIStatus); ok { + return err + } + + return errors.NewInternalError(err) +} diff --git a/pkg/ext/delegate_error_test.go b/pkg/ext/delegate_error_test.go index c61b24e2..6c5c08d6 100644 --- a/pkg/ext/delegate_error_test.go +++ b/pkg/ext/delegate_error_test.go @@ -1,16 +1,27 @@ package ext import ( + "context" + "errors" "fmt" "net/http" "testing" "github.com/stretchr/testify/assert" + gomock "go.uber.org/mock/gomock" apierrors "k8s.io/apimachinery/pkg/api/errors" + metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/conversion" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/watch" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/registry/rest" ) -func TestDelegateError_convertError(t *testing.T) { +func TestConvertError(t *testing.T) { tests := []struct { name string input error @@ -48,13 +59,1051 @@ func TestDelegateError_convertError(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - delegateError := delegateError[*TestType, *TestTypeList]{ - inner: &delegate[*TestType, *TestTypeList]{}, + output := convertError(tt.input) + assert.Equal(t, tt.output, output) + }) + } + +} + +func TestDelegateError_Watch(t *testing.T) { + type input struct { + ctx context.Context + internaloptions *metainternalversion.ListOptions + } + + type output struct { + watch watch.Interface + err error + } + + type testCase struct { + name string + input input + expected output + storeSetup func(*MockStore[*TestType, *TestTypeList]) + simulateConversionError bool + wantedErr bool + } + + tests := []testCase{ + { + name: "missing user in context", + input: input{ + ctx: context.TODO(), + internaloptions: &metainternalversion.ListOptions{}, + }, + wantedErr: true, + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + }, + { + name: "convert list error", + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + internaloptions: &metainternalversion.ListOptions{}, + }, + simulateConversionError: true, + wantedErr: true, + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + }, + } + + for _, tt := range tests { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + if !tt.simulateConversionError { + scheme.AddConversionFunc(&metainternalversion.ListOptions{}, &metav1.ListOptions{}, convert_internalversion_ListOptions_to_v1_ListOptions) + } + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + + testDelegate := &delegateError[*TestType, *TestTypeList]{ + inner: &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + }, + } + + watch, err := testDelegate.Watch(tt.input.ctx, tt.input.internaloptions) + if tt.wantedErr { + // check if we have an error + assert.Error(t, err) + + // check if error is an api error + if _, ok := err.(apierrors.APIStatus); ok { + assert.True(t, ok) + return } + } else { + assert.NoError(t, err) + assert.Equal(t, watch, tt.expected.watch) + } + } +} - output := delegateError.convertError(tt.input) - assert.Equal(t, tt.output, output) +func TestDelegateError_Update(t *testing.T) { + type input struct { + parentCtx context.Context + name string + objInfo rest.UpdatedObjectInfo + createValidation rest.ValidateObjectFunc + updateValidation rest.ValidateObjectUpdateFunc + forceAllowCreate bool + options *metav1.UpdateOptions + } + + type output struct { + obj runtime.Object + created bool + err error + } + + type testCase struct { + name string + setup func(*MockUpdatedObjectInfo, *MockStore[*TestType, *TestTypeList]) + input input + expect output + wantErr bool + } + + tests := []testCase{ + { + name: "working case", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + // objInfo is created in the for loop + forceAllowCreate: false, + options: &metav1.UpdateOptions{}, + }, + expect: output{ + obj: &TestType{}, + created: false, + err: nil, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + store.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + wantErr: false, + }, + { + name: "missing user in context", + input: input{ + parentCtx: context.TODO(), + }, + setup: func(muoi *MockUpdatedObjectInfo, ms *MockStore[*TestType, *TestTypeList]) {}, + expect: output{ + obj: nil, + created: false, + err: errMissingUserInfo, + }, + wantErr: true, + }, + { + name: "get failed - other error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + wantErr: true, + }, + { + name: "get failed - not found - updated object error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, + &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + ) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + wantErr: true, + }, + { + name: "get failed - not found - create succeeded", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { return nil }, + }, + expect: output{ + obj: &TestType{}, + created: true, + err: nil, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, + &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + ) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + store.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + wantErr: false, + }, + { + name: "get failed - not found - create validation error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return assert.AnError + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, + &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + ) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + wantErr: true, + }, + { + name: "get failed - not found - type error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, + &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + ) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&runtime.Unknown{}, nil) + + }, + wantErr: true, + }, + { + name: "get failed - not found - store create error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, + &apierrors.StatusError{ + ErrStatus: metav1.Status{ + Code: http.StatusNotFound, + Reason: metav1.StatusReasonNotFound, + }, + }, + ) + store.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + + }, + wantErr: true, + }, + { + name: "get worked - updated object error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + + }, + wantErr: true, + }, + { + name: "get worked - type error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&runtime.Unknown{}, nil) + + }, + wantErr: true, + }, + { + name: "get worked - update validation error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + updateValidation: func(ctx context.Context, obj, old runtime.Object) error { + return assert.AnError + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + + }, + wantErr: true, + }, + { + name: "get worked - store update error", + input: input{ + parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-user", + createValidation: func(ctx context.Context, obj runtime.Object) error { + return nil + }, + }, + expect: output{ + obj: nil, + created: false, + err: assert.AnError, + }, + setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { + store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + store.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + mockObjInfo := NewMockUpdatedObjectInfo(ctrl) + tt.input.objInfo = mockObjInfo + tt.setup(mockObjInfo, mockStore) + + testDelegate := &delegateError[*TestType, *TestTypeList]{ + inner: &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + }, + } + + obj, created, err := testDelegate.Update(tt.input.parentCtx, tt.input.name, tt.input.objInfo, tt.input.createValidation, tt.input.updateValidation, tt.input.forceAllowCreate, tt.input.options) + + if tt.wantErr { + // check if we have an error + assert.Error(t, err) + + // check if error is an apierror + if _, ok := err.(apierrors.APIStatus); ok { + assert.True(t, ok) + return + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expect.obj, obj) + assert.Equal(t, tt.expect.created, created) + } }) } } + +func TestDelegateError_Create(t *testing.T) { + type input struct { + ctx context.Context + obj runtime.Object + createValidation rest.ValidateObjectFunc + options *metav1.CreateOptions + } + + type output struct { + createResult runtime.Object + err error + } + + type testCase struct { + name string + storeSetup func(*MockStore[*TestType, *TestTypeList]) + wantErr bool + input input + expect output + } + + tests := []testCase{ + { + name: "working case", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + obj: &TestType{}, + options: &metav1.CreateOptions{}, + }, + expect: output{ + createResult: &TestType{}, + err: nil, + }, + wantErr: false, + }, + { + name: "missing user in the context", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + input: input{ + ctx: context.Background(), + obj: &TestType{}, + options: &metav1.CreateOptions{}, + }, + expect: output{ + createResult: nil, + err: errMissingUserInfo, + }, + wantErr: true, + }, + { + name: "store create error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + obj: &TestType{}, + options: &metav1.CreateOptions{}, + }, + expect: output{ + createResult: nil, + err: assert.AnError, + }, + wantErr: true, + }, + { + name: "wrong type error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + obj: &runtime.Unknown{}, + options: &metav1.CreateOptions{}, + }, + expect: output{ + createResult: nil, + err: assert.AnError, + }, + wantErr: true, + }, + { + name: "create validation error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + obj: &TestType{}, + createValidation: func(ctx context.Context, obj runtime.Object) error { + return assert.AnError + }, + options: &metav1.CreateOptions{}, + }, + expect: output{ + createResult: &TestType{}, + err: assert.AnError, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + tt.storeSetup(mockStore) + + testDelegate := &delegateError[*TestType, *TestTypeList]{ + inner: &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + }, + } + + result, err := testDelegate.Create(tt.input.ctx, tt.input.obj, tt.input.createValidation, tt.input.options) + if tt.wantErr { + // check if we have an error + assert.Error(t, err) + + // check if error is an apierror + if _, ok := err.(apierrors.APIStatus); ok { + assert.True(t, ok) + return + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expect.createResult, result) + } + }) + } +} + +func TestDelegateError_Delete(t *testing.T) { + type input struct { + ctx context.Context + name string + deleteValidation rest.ValidateObjectFunc + options *metav1.DeleteOptions + } + + type output struct { + deleteResult runtime.Object + completed bool + err error + } + + type testCase struct { + name string + storeSetup func(*MockStore[*TestType, *TestTypeList]) + wantErr bool + input input + expect output + } + + tests := []testCase{ + { + name: "working case", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + ms.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + options: &metav1.DeleteOptions{}, + }, + expect: output{ + deleteResult: &TestType{}, + completed: true, + err: nil, + }, + wantErr: false, + }, + { + name: "missing user in the context", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + input: input{ + ctx: context.Background(), + name: "test-object", + options: &metav1.DeleteOptions{}, + }, + expect: output{ + deleteResult: nil, + completed: false, + err: errMissingUserInfo, + }, + wantErr: true, + }, + { + name: "store get error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + options: &metav1.DeleteOptions{}, + }, + expect: output{ + deleteResult: nil, + completed: false, + err: assert.AnError, + }, + wantErr: true, + }, + { + name: "store delete error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + ms.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Return(assert.AnError) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + options: &metav1.DeleteOptions{}, + }, + expect: output{ + deleteResult: &TestType{}, + completed: true, + err: assert.AnError, + }, + wantErr: true, + }, + { + name: "delete validation error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + deleteValidation: func(ctx context.Context, obj runtime.Object) error { return assert.AnError }, + options: &metav1.DeleteOptions{}, + }, + expect: output{ + deleteResult: nil, + completed: false, + err: assert.AnError, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + tt.storeSetup(mockStore) + + testDelegate := &delegateError[*TestType, *TestTypeList]{ + inner: &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + }, + } + + result, completed, err := testDelegate.Delete(tt.input.ctx, tt.input.name, tt.input.deleteValidation, tt.input.options) + if tt.wantErr { + // check if we have an error + assert.Error(t, err) + + // check if error is an apierror + if _, ok := err.(apierrors.APIStatus); ok { + assert.True(t, ok) + return + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expect.deleteResult, result) + assert.Equal(t, tt.expect.completed, completed) + } + }) + } +} + +func TestDelegateError_Get(t *testing.T) { + type input struct { + ctx context.Context + name string + options *metav1.GetOptions + } + + type output struct { + getResult runtime.Object + err error + } + + type testCase struct { + name string + storeSetup func(*MockStore[*TestType, *TestTypeList]) + wantErr bool + input input + expect output + } + + tests := []testCase{ + { + name: "working case", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + options: &metav1.GetOptions{}, + }, + expect: output{ + getResult: &TestType{}, + err: nil, + }, + wantErr: false, + }, + { + name: "missing user in the context", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, + input: input{ + ctx: context.Background(), + name: "testing-obj", + options: &metav1.GetOptions{}, + }, + expect: output{ + getResult: nil, + err: errMissingUserInfo, + }, + wantErr: true, + }, + { + name: "store get error", + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + name: "test-object", + options: &metav1.GetOptions{}, + }, + expect: output{ + getResult: &TestType{}, + err: assert.AnError, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + tt.storeSetup(mockStore) + + testDelegate := &delegateError[*TestType, *TestTypeList]{ + inner: &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + }, + } + + result, err := testDelegate.Get(tt.input.ctx, tt.input.name, tt.input.options) + if tt.wantErr { + // check if we have an error + assert.Error(t, err) + + // check if error is an apierror + if _, ok := err.(apierrors.APIStatus); ok { + assert.True(t, ok) + return + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expect.getResult, result) + } + + }) + } +} + +func TestDelegateError_List(t *testing.T) { + type input struct { + ctx context.Context + listOptions *metainternalversion.ListOptions + } + + type output struct { + listResult runtime.Object + err error + } + + type testCase struct { + name string + storeSetup func(*MockStore[*TestType, *TestTypeList]) + simulateConvertError bool + wantErr bool + input input + expect output + } + + tests := []testCase{ + { + name: "working case, for completion reasons", + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + listOptions: &metainternalversion.ListOptions{}, + }, + storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { + ms.EXPECT().List(gomock.Any(), gomock.Any()).Return(&TestTypeList{}, nil) + }, + wantErr: false, + expect: output{ + listResult: &TestTypeList{}, + err: nil, + }, + }, + { + name: "missing user in the context", + input: input{ + ctx: context.Background(), + listOptions: &metainternalversion.ListOptions{}, + }, + storeSetup: func(mockStore *MockStore[*TestType, *TestTypeList]) {}, + wantErr: true, + expect: output{ + listResult: nil, + err: errMissingUserInfo, + }, + }, + { + name: "convertListOptions error", + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + listOptions: &metainternalversion.ListOptions{}, + }, + storeSetup: func(mockStore *MockStore[*TestType, *TestTypeList]) {}, + simulateConvertError: true, + wantErr: true, + expect: output{ + listResult: nil, + err: assert.AnError, + }, + }, + { + name: "error returned by store", + input: input{ + ctx: request.WithUser(context.Background(), &user.DefaultInfo{ + Name: "test-user", + }), + listOptions: &metainternalversion.ListOptions{}, + }, + storeSetup: func(mockStore *MockStore[*TestType, *TestTypeList]) { + mockStore.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, assert.AnError) + }, + wantErr: true, + expect: output{ + listResult: nil, + err: assert.AnError, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + scheme := runtime.NewScheme() + addToSchemeTest(scheme) + + if !tt.simulateConvertError { + scheme.AddConversionFunc(&metainternalversion.ListOptions{}, &metav1.ListOptions{}, convert_internalversion_ListOptions_to_v1_ListOptions) + } + + gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} + gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) + tt.storeSetup(mockStore) + + testDelegate := &delegateError[*TestType, *TestTypeList]{ + inner: &delegate[*TestType, *TestTypeList]{ + scheme: scheme, + t: &TestType{}, + tList: &TestTypeList{}, + gvk: gvk, + gvr: gvr, + store: mockStore, + }, + } + + result, err := testDelegate.List(tt.input.ctx, tt.input.listOptions) + if tt.wantErr { + // check if we have an error + assert.Error(t, err) + + // check if error is an apierror + if _, ok := err.(apierrors.APIStatus); ok { + assert.True(t, ok) + return + } + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expect.listResult, result) + } + }) + } +} + +func convert_internalversion_ListOptions_to_v1_ListOptions(in, out interface{}, s conversion.Scope) error { + i, ok := in.(*metainternalversion.ListOptions) + if !ok { + return errors.New("cannot convert in param into internalversion.ListOptions") + } + o, ok := out.(*metav1.ListOptions) + if !ok { + return errors.New("cannot convert out param into metav1.ListOptions") + } + if i.LabelSelector != nil { + o.LabelSelector = i.LabelSelector.String() + } + if i.FieldSelector != nil { + o.FieldSelector = i.FieldSelector.String() + } + o.Watch = i.Watch + o.ResourceVersion = i.ResourceVersion + o.TimeoutSeconds = i.TimeoutSeconds + return nil +} diff --git a/pkg/ext/delegate_test.go b/pkg/ext/delegate_test.go deleted file mode 100644 index 99830c33..00000000 --- a/pkg/ext/delegate_test.go +++ /dev/null @@ -1,1012 +0,0 @@ -package ext - -import ( - "context" - "errors" - "net/http" - "testing" - - "github.com/stretchr/testify/assert" - gomock "go.uber.org/mock/gomock" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metainternalversion "k8s.io/apimachinery/pkg/apis/meta/internalversion" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/conversion" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/watch" - "k8s.io/apiserver/pkg/authentication/user" - "k8s.io/apiserver/pkg/endpoints/request" - "k8s.io/apiserver/pkg/registry/rest" -) - -func TestDelegate_Watch(t *testing.T) { - type input struct { - ctx context.Context - internaloptions *metainternalversion.ListOptions - } - - type output struct { - watch watch.Interface - err error - } - - type testCase struct { - name string - input input - expected output - storeSetup func(*MockStore[*TestType, *TestTypeList]) - simulateConversionError bool - wantedErr bool - } - - tests := []testCase{ - { - name: "missing user in context", - input: input{ - ctx: context.TODO(), - internaloptions: &metainternalversion.ListOptions{}, - }, - expected: output{ - err: errMissingUserInfo, - }, - wantedErr: true, - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, - }, - { - name: "convert list error", - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - internaloptions: &metainternalversion.ListOptions{}, - }, - simulateConversionError: true, - wantedErr: true, - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, - }, - } - - for _, tt := range tests { - scheme := runtime.NewScheme() - addToSchemeTest(scheme) - - if !tt.simulateConversionError { - scheme.AddConversionFunc(&metainternalversion.ListOptions{}, &metav1.ListOptions{}, convert_internalversion_ListOptions_to_v1_ListOptions) - } - - gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} - gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) - - testDelegate := &delegate[*TestType, *TestTypeList]{ - scheme: scheme, - t: &TestType{}, - tList: &TestTypeList{}, - gvk: gvk, - gvr: gvr, - store: mockStore, - } - - watch, err := testDelegate.Watch(tt.input.ctx, tt.input.internaloptions) - if tt.wantedErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, watch, tt.expected.watch) - } - } -} - -func TestDelegate_Update(t *testing.T) { - type input struct { - parentCtx context.Context - name string - objInfo rest.UpdatedObjectInfo - createValidation rest.ValidateObjectFunc - updateValidation rest.ValidateObjectUpdateFunc - forceAllowCreate bool - options *metav1.UpdateOptions - } - - type output struct { - obj runtime.Object - created bool - err error - } - - type testCase struct { - name string - setup func(*MockUpdatedObjectInfo, *MockStore[*TestType, *TestTypeList]) - input input - expect output - wantErr bool - } - - tests := []testCase{ - { - name: "working case", - input: input{ - parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-user", - // objInfo is created in the for loop - forceAllowCreate: false, - options: &metav1.UpdateOptions{}, - }, - expect: output{ - obj: &TestType{}, - created: false, - err: nil, - }, - setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { - objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - store.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - }, - wantErr: false, - }, - { - name: "missing user in context", - input: input{ - parentCtx: context.TODO(), - }, - setup: func(muoi *MockUpdatedObjectInfo, ms *MockStore[*TestType, *TestTypeList]) {}, - expect: output{ - obj: nil, - created: false, - err: errMissingUserInfo, - }, - wantErr: true, - }, - { - name: "get failed - other error", - input: input{ - parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-user", - }, - expect: output{ - obj: nil, - created: false, - err: assert.AnError, - }, - setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { - store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) - }, - wantErr: true, - }, - { - name: "get failed - not found - updated object error", - input: input{ - parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-user", - }, - expect: output{ - obj: nil, - created: false, - err: assert.AnError, - }, - setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { - store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, - &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusNotFound, - Reason: metav1.StatusReasonNotFound, - }, - }, - ) - objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(nil, assert.AnError) - }, - wantErr: true, - }, - { - name: "get failed - not found - create succeeded", - input: input{ - parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-user", - createValidation: func(ctx context.Context, obj runtime.Object) error { return nil }, - }, - expect: output{ - obj: &TestType{}, - created: true, - err: nil, - }, - setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { - store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, - &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusNotFound, - Reason: metav1.StatusReasonNotFound, - }, - }, - ) - objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - store.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - }, - wantErr: false, - }, - { - name: "get failed - not found - create validation error", - input: input{ - parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-user", - createValidation: func(ctx context.Context, obj runtime.Object) error { - return assert.AnError - }, - }, - expect: output{ - obj: nil, - created: false, - err: assert.AnError, - }, - setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { - store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, - &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusNotFound, - Reason: metav1.StatusReasonNotFound, - }, - }, - ) - objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - }, - wantErr: true, - }, - { - name: "get failed - not found - type error", - input: input{ - parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-user", - createValidation: func(ctx context.Context, obj runtime.Object) error { - return nil - }, - }, - expect: output{ - obj: nil, - created: false, - err: assert.AnError, - }, - setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { - store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, - &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusNotFound, - Reason: metav1.StatusReasonNotFound, - }, - }, - ) - objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&runtime.Unknown{}, nil) - - }, - wantErr: true, - }, - { - name: "get failed - not found - store create error", - input: input{ - parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-user", - createValidation: func(ctx context.Context, obj runtime.Object) error { - return nil - }, - }, - expect: output{ - obj: nil, - created: false, - err: assert.AnError, - }, - setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { - store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, - &apierrors.StatusError{ - ErrStatus: metav1.Status{ - Code: http.StatusNotFound, - Reason: metav1.StatusReasonNotFound, - }, - }, - ) - store.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) - objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - - }, - wantErr: true, - }, - { - name: "get worked - updated object error", - input: input{ - parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-user", - createValidation: func(ctx context.Context, obj runtime.Object) error { - return nil - }, - }, - expect: output{ - obj: nil, - created: false, - err: assert.AnError, - }, - setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { - store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(nil, assert.AnError) - - }, - wantErr: true, - }, - { - name: "get worked - type error", - input: input{ - parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-user", - createValidation: func(ctx context.Context, obj runtime.Object) error { - return nil - }, - }, - expect: output{ - obj: nil, - created: false, - err: assert.AnError, - }, - setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { - store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&runtime.Unknown{}, nil) - - }, - wantErr: true, - }, - { - name: "get worked - update validation error", - input: input{ - parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-user", - createValidation: func(ctx context.Context, obj runtime.Object) error { - return nil - }, - updateValidation: func(ctx context.Context, obj, old runtime.Object) error { - return assert.AnError - }, - }, - expect: output{ - obj: nil, - created: false, - err: assert.AnError, - }, - setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { - store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - - }, - wantErr: true, - }, - { - name: "get worked - store update error", - input: input{ - parentCtx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-user", - createValidation: func(ctx context.Context, obj runtime.Object) error { - return nil - }, - }, - expect: output{ - obj: nil, - created: false, - err: assert.AnError, - }, - setup: func(objInfo *MockUpdatedObjectInfo, store *MockStore[*TestType, *TestTypeList]) { - store.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - objInfo.EXPECT().UpdatedObject(gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - store.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) - }, - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - scheme := runtime.NewScheme() - addToSchemeTest(scheme) - - gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} - gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) - mockObjInfo := NewMockUpdatedObjectInfo(ctrl) - tt.input.objInfo = mockObjInfo - tt.setup(mockObjInfo, mockStore) - - testDelegate := &delegate[*TestType, *TestTypeList]{ - scheme: scheme, - t: &TestType{}, - tList: &TestTypeList{}, - gvk: gvk, - gvr: gvr, - store: mockStore, - } - - obj, created, err := testDelegate.Update(tt.input.parentCtx, tt.input.name, tt.input.objInfo, tt.input.createValidation, tt.input.updateValidation, tt.input.forceAllowCreate, tt.input.options) - - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expect.obj, obj) - assert.Equal(t, tt.expect.created, created) - } - }) - } - -} - -func TestDelegate_Create(t *testing.T) { - type input struct { - ctx context.Context - obj runtime.Object - createValidation rest.ValidateObjectFunc - options *metav1.CreateOptions - } - - type output struct { - createResult runtime.Object - err error - } - - type testCase struct { - name string - storeSetup func(*MockStore[*TestType, *TestTypeList]) - wantErr bool - input input - expect output - } - - tests := []testCase{ - { - name: "working case", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { - ms.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - }, - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - obj: &TestType{}, - options: &metav1.CreateOptions{}, - }, - expect: output{ - createResult: &TestType{}, - err: nil, - }, - wantErr: false, - }, - { - name: "missing user in the context", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, - input: input{ - ctx: context.Background(), - obj: &TestType{}, - options: &metav1.CreateOptions{}, - }, - expect: output{ - createResult: nil, - err: errMissingUserInfo, - }, - wantErr: true, - }, - { - name: "store create error", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { - ms.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) - }, - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - obj: &TestType{}, - options: &metav1.CreateOptions{}, - }, - expect: output{ - createResult: nil, - err: assert.AnError, - }, - wantErr: true, - }, - { - name: "wrong type error", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - obj: &runtime.Unknown{}, - options: &metav1.CreateOptions{}, - }, - expect: output{ - createResult: nil, - err: assert.AnError, - }, - wantErr: true, - }, - { - name: "create validation error", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - obj: &TestType{}, - createValidation: func(ctx context.Context, obj runtime.Object) error { - return assert.AnError - }, - options: &metav1.CreateOptions{}, - }, - expect: output{ - createResult: &TestType{}, - err: assert.AnError, - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - scheme := runtime.NewScheme() - addToSchemeTest(scheme) - - gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} - gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) - tt.storeSetup(mockStore) - - testDelegate := &delegate[*TestType, *TestTypeList]{ - scheme: scheme, - t: &TestType{}, - tList: &TestTypeList{}, - gvk: gvk, - gvr: gvr, - store: mockStore, - } - - result, err := testDelegate.Create(tt.input.ctx, tt.input.obj, tt.input.createValidation, tt.input.options) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expect.createResult, result) - } - }) - } -} - -func TestDelegate_Delete(t *testing.T) { - type input struct { - ctx context.Context - name string - deleteValidation rest.ValidateObjectFunc - options *metav1.DeleteOptions - } - - type output struct { - deleteResult runtime.Object - completed bool - err error - } - - type testCase struct { - name string - storeSetup func(*MockStore[*TestType, *TestTypeList]) - wantErr bool - input input - expect output - } - - tests := []testCase{ - { - name: "working case", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { - ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - ms.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil) - }, - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-object", - options: &metav1.DeleteOptions{}, - }, - expect: output{ - deleteResult: &TestType{}, - completed: true, - err: nil, - }, - wantErr: false, - }, - { - name: "missing user in the context", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, - input: input{ - ctx: context.Background(), - name: "test-object", - options: &metav1.DeleteOptions{}, - }, - expect: output{ - deleteResult: nil, - completed: false, - err: errMissingUserInfo, - }, - wantErr: true, - }, - { - name: "store get error", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { - ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) - }, - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-object", - options: &metav1.DeleteOptions{}, - }, - expect: output{ - deleteResult: nil, - completed: false, - err: assert.AnError, - }, - wantErr: true, - }, - { - name: "store delete error", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { - ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - ms.EXPECT().Delete(gomock.Any(), gomock.Any(), gomock.Any()).Return(assert.AnError) - }, - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-object", - options: &metav1.DeleteOptions{}, - }, - expect: output{ - deleteResult: &TestType{}, - completed: true, - err: assert.AnError, - }, - wantErr: true, - }, - { - name: "delete validation error", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { - ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - }, - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-object", - deleteValidation: func(ctx context.Context, obj runtime.Object) error { return assert.AnError }, - options: &metav1.DeleteOptions{}, - }, - expect: output{ - deleteResult: nil, - completed: false, - err: assert.AnError, - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - scheme := runtime.NewScheme() - addToSchemeTest(scheme) - - gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} - gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) - tt.storeSetup(mockStore) - - testDelegate := &delegate[*TestType, *TestTypeList]{ - scheme: scheme, - t: &TestType{}, - tList: &TestTypeList{}, - gvk: gvk, - gvr: gvr, - store: mockStore, - } - - result, completed, err := testDelegate.Delete(tt.input.ctx, tt.input.name, tt.input.deleteValidation, tt.input.options) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expect.deleteResult, result) - assert.Equal(t, tt.expect.completed, completed) - } - }) - } -} - -func TestDelegate_Get(t *testing.T) { - type input struct { - ctx context.Context - name string - options *metav1.GetOptions - } - - type output struct { - getResult runtime.Object - err error - } - - type testCase struct { - name string - storeSetup func(*MockStore[*TestType, *TestTypeList]) - wantErr bool - input input - expect output - } - - tests := []testCase{ - { - name: "working case", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { - ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(&TestType{}, nil) - }, - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-object", - options: &metav1.GetOptions{}, - }, - expect: output{ - getResult: &TestType{}, - err: nil, - }, - wantErr: false, - }, - { - name: "missing user in the context", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) {}, - input: input{ - ctx: context.Background(), - name: "testing-obj", - options: &metav1.GetOptions{}, - }, - expect: output{ - getResult: nil, - err: errMissingUserInfo, - }, - wantErr: true, - }, - { - name: "store get error", - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { - ms.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, assert.AnError) - }, - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - name: "test-object", - options: &metav1.GetOptions{}, - }, - expect: output{ - getResult: &TestType{}, - err: assert.AnError, - }, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - scheme := runtime.NewScheme() - addToSchemeTest(scheme) - - gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} - gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) - tt.storeSetup(mockStore) - - testDelegate := &delegate[*TestType, *TestTypeList]{ - scheme: scheme, - t: &TestType{}, - tList: &TestTypeList{}, - gvk: gvk, - gvr: gvr, - store: mockStore, - } - - result, err := testDelegate.Get(tt.input.ctx, tt.input.name, tt.input.options) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expect.getResult, result) - } - - }) - } -} - -func TestDelegate_List(t *testing.T) { - type input struct { - ctx context.Context - listOptions *metainternalversion.ListOptions - } - - type output struct { - listResult runtime.Object - err error - } - - type testCase struct { - name string - storeSetup func(*MockStore[*TestType, *TestTypeList]) - simulateConvertError bool - wantErr bool - input input - expect output - } - - tests := []testCase{ - { - name: "working case, for completion reasons", - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - listOptions: &metainternalversion.ListOptions{}, - }, - storeSetup: func(ms *MockStore[*TestType, *TestTypeList]) { - ms.EXPECT().List(gomock.Any(), gomock.Any()).Return(&TestTypeList{}, nil) - }, - wantErr: false, - expect: output{ - listResult: &TestTypeList{}, - err: nil, - }, - }, - { - name: "missing user in the context", - input: input{ - ctx: context.Background(), - listOptions: &metainternalversion.ListOptions{}, - }, - storeSetup: func(mockStore *MockStore[*TestType, *TestTypeList]) {}, - wantErr: true, - expect: output{ - listResult: nil, - err: errMissingUserInfo, - }, - }, - { - name: "convertListOptions error", - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - listOptions: &metainternalversion.ListOptions{}, - }, - storeSetup: func(mockStore *MockStore[*TestType, *TestTypeList]) {}, - simulateConvertError: true, - wantErr: true, - expect: output{ - listResult: nil, - err: assert.AnError, - }, - }, - { - name: "error returned by store", - input: input{ - ctx: request.WithUser(context.Background(), &user.DefaultInfo{ - Name: "test-user", - }), - listOptions: &metainternalversion.ListOptions{}, - }, - storeSetup: func(mockStore *MockStore[*TestType, *TestTypeList]) { - mockStore.EXPECT().List(gomock.Any(), gomock.Any()).Return(nil, assert.AnError) - }, - wantErr: true, - expect: output{ - listResult: nil, - err: assert.AnError, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - scheme := runtime.NewScheme() - addToSchemeTest(scheme) - - if !tt.simulateConvertError { - scheme.AddConversionFunc(&metainternalversion.ListOptions{}, &metav1.ListOptions{}, convert_internalversion_ListOptions_to_v1_ListOptions) - } - - gvk := schema.GroupVersionKind{Group: "ext.cattle.io", Version: "v1", Kind: "TestType"} - gvr := schema.GroupVersionResource{Group: "ext.cattle.io", Version: "v1", Resource: "testtypes"} - - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - mockStore := NewMockStore[*TestType, *TestTypeList](ctrl) - tt.storeSetup(mockStore) - - testDelegate := &delegate[*TestType, *TestTypeList]{ - scheme: scheme, - t: &TestType{}, - tList: &TestTypeList{}, - gvk: gvk, - gvr: gvr, - store: mockStore, - } - - result, err := testDelegate.List(tt.input.ctx, tt.input.listOptions) - if tt.wantErr { - assert.Error(t, err) - } else { - assert.NoError(t, err) - assert.Equal(t, tt.expect.listResult, result) - } - }) - } -} - -func convert_internalversion_ListOptions_to_v1_ListOptions(in, out interface{}, s conversion.Scope) error { - i, ok := in.(*metainternalversion.ListOptions) - if !ok { - return errors.New("cannot convert in param into internalversion.ListOptions") - } - o, ok := out.(*metav1.ListOptions) - if !ok { - return errors.New("cannot convert out param into metav1.ListOptions") - } - if i.LabelSelector != nil { - o.LabelSelector = i.LabelSelector.String() - } - if i.FieldSelector != nil { - o.FieldSelector = i.FieldSelector.String() - } - o.Watch = i.Watch - o.ResourceVersion = i.ResourceVersion - o.TimeoutSeconds = i.TimeoutSeconds - return nil -} From 971e2b7f3d90bbd90ecb25ebee1d90b73eb87fad Mon Sep 17 00:00:00 2001 From: "Felipe C. Gehrke" Date: Tue, 12 Nov 2024 12:40:57 -0300 Subject: [PATCH 5/5] fixing apistatus type casting assert --- pkg/ext/delegate_error_test.go | 36 ++++++++++++---------------------- 1 file changed, 12 insertions(+), 24 deletions(-) diff --git a/pkg/ext/delegate_error_test.go b/pkg/ext/delegate_error_test.go index 6c5c08d6..234b2d59 100644 --- a/pkg/ext/delegate_error_test.go +++ b/pkg/ext/delegate_error_test.go @@ -143,10 +143,8 @@ func TestDelegateError_Watch(t *testing.T) { assert.Error(t, err) // check if error is an api error - if _, ok := err.(apierrors.APIStatus); ok { - assert.True(t, ok) - return - } + _, ok := err.(apierrors.APIStatus) + assert.True(t, ok) } else { assert.NoError(t, err) assert.Equal(t, watch, tt.expected.watch) @@ -508,10 +506,8 @@ func TestDelegateError_Update(t *testing.T) { assert.Error(t, err) // check if error is an apierror - if _, ok := err.(apierrors.APIStatus); ok { - assert.True(t, ok) - return - } + _, ok := err.(apierrors.APIStatus) + assert.True(t, ok) } else { assert.NoError(t, err) assert.Equal(t, tt.expect.obj, obj) @@ -662,10 +658,8 @@ func TestDelegateError_Create(t *testing.T) { assert.Error(t, err) // check if error is an apierror - if _, ok := err.(apierrors.APIStatus); ok { - assert.True(t, ok) - return - } + _, ok := err.(apierrors.APIStatus) + assert.True(t, ok) } else { assert.NoError(t, err) assert.Equal(t, tt.expect.createResult, result) @@ -824,10 +818,8 @@ func TestDelegateError_Delete(t *testing.T) { assert.Error(t, err) // check if error is an apierror - if _, ok := err.(apierrors.APIStatus); ok { - assert.True(t, ok) - return - } + _, ok := err.(apierrors.APIStatus) + assert.True(t, ok) } else { assert.NoError(t, err) assert.Equal(t, tt.expect.deleteResult, result) @@ -941,10 +933,8 @@ func TestDelegateError_Get(t *testing.T) { assert.Error(t, err) // check if error is an apierror - if _, ok := err.(apierrors.APIStatus); ok { - assert.True(t, ok) - return - } + _, ok := err.(apierrors.APIStatus) + assert.True(t, ok) } else { assert.NoError(t, err) assert.Equal(t, tt.expect.getResult, result) @@ -1075,10 +1065,8 @@ func TestDelegateError_List(t *testing.T) { assert.Error(t, err) // check if error is an apierror - if _, ok := err.(apierrors.APIStatus); ok { - assert.True(t, ok) - return - } + _, ok := err.(apierrors.APIStatus) + assert.True(t, ok) } else { assert.NoError(t, err) assert.Equal(t, tt.expect.listResult, result)