From e97b1b4d702279aa859257de356d6043d7bfd445 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patryk=20Ma=C5=82ek?= Date: Fri, 23 Aug 2024 19:11:26 +0200 Subject: [PATCH] tests: add mocks for KongService SDK --- .mockery.yaml | 9 +- ...mock_test.go => controlplane_mock_test.go} | 0 .../konnect/ops/kongservice_mock_test.go | 264 +++++++++ controller/konnect/ops/ops.go | 2 +- .../konnect/ops/ops_controlplane_test.go | 8 +- controller/konnect/ops/ops_kongservice.go | 63 ++- .../konnect/ops/ops_kongservice_test.go | 501 ++++++++++++++++++ 7 files changed, 809 insertions(+), 38 deletions(-) rename controller/konnect/ops/{controlplanesdk_mock_test.go => controlplane_mock_test.go} (100%) create mode 100644 controller/konnect/ops/kongservice_mock_test.go create mode 100644 controller/konnect/ops/ops_kongservice_test.go diff --git a/.mockery.yaml b/.mockery.yaml index 8497c5f34..36d799f63 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -3,12 +3,13 @@ inpackage: True disable-version-string: True with-expecter: True -filename: "{{.InterfaceName | lower}}_mock_test.go" -dir: "{{.InterfaceDir}}" -mockname: "Mock{{.InterfaceName}}" -outpkg: "{{.PackageName}}" +filename: "{{ trimSuffix .InterfaceFile \".go\" | base | lower }}_mock_test.go" +dir: "{{ .InterfaceDir }}" +mockname: "Mock{{ .InterfaceName }}" +outpkg: "{{ .PackageName }}" packages: github.com/kong/gateway-operator/controller/konnect/ops: interfaces: ControlPlaneSDK: + ServicesSDK: diff --git a/controller/konnect/ops/controlplanesdk_mock_test.go b/controller/konnect/ops/controlplane_mock_test.go similarity index 100% rename from controller/konnect/ops/controlplanesdk_mock_test.go rename to controller/konnect/ops/controlplane_mock_test.go diff --git a/controller/konnect/ops/kongservice_mock_test.go b/controller/konnect/ops/kongservice_mock_test.go new file mode 100644 index 000000000..9340df0f0 --- /dev/null +++ b/controller/konnect/ops/kongservice_mock_test.go @@ -0,0 +1,264 @@ +// Code generated by mockery. DO NOT EDIT. + +package ops + +import ( + context "context" + + components "github.com/Kong/sdk-konnect-go/models/components" + + mock "github.com/stretchr/testify/mock" + + operations "github.com/Kong/sdk-konnect-go/models/operations" +) + +// MockServicesSDK is an autogenerated mock type for the ServicesSDK type +type MockServicesSDK struct { + mock.Mock +} + +type MockServicesSDK_Expecter struct { + mock *mock.Mock +} + +func (_m *MockServicesSDK) EXPECT() *MockServicesSDK_Expecter { + return &MockServicesSDK_Expecter{mock: &_m.Mock} +} + +// CreateService provides a mock function with given fields: ctx, controlPlaneID, service, opts +func (_m *MockServicesSDK) CreateService(ctx context.Context, controlPlaneID string, service components.ServiceInput, opts ...operations.Option) (*operations.CreateServiceResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, controlPlaneID, service) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CreateService") + } + + var r0 *operations.CreateServiceResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, components.ServiceInput, ...operations.Option) (*operations.CreateServiceResponse, error)); ok { + return rf(ctx, controlPlaneID, service, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, components.ServiceInput, ...operations.Option) *operations.CreateServiceResponse); ok { + r0 = rf(ctx, controlPlaneID, service, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.CreateServiceResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, components.ServiceInput, ...operations.Option) error); ok { + r1 = rf(ctx, controlPlaneID, service, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockServicesSDK_CreateService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CreateService' +type MockServicesSDK_CreateService_Call struct { + *mock.Call +} + +// CreateService is a helper method to define mock.On call +// - ctx context.Context +// - controlPlaneID string +// - service components.ServiceInput +// - opts ...operations.Option +func (_e *MockServicesSDK_Expecter) CreateService(ctx interface{}, controlPlaneID interface{}, service interface{}, opts ...interface{}) *MockServicesSDK_CreateService_Call { + return &MockServicesSDK_CreateService_Call{Call: _e.mock.On("CreateService", + append([]interface{}{ctx, controlPlaneID, service}, opts...)...)} +} + +func (_c *MockServicesSDK_CreateService_Call) Run(run func(ctx context.Context, controlPlaneID string, service components.ServiceInput, opts ...operations.Option)) *MockServicesSDK_CreateService_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(string), args[2].(components.ServiceInput), variadicArgs...) + }) + return _c +} + +func (_c *MockServicesSDK_CreateService_Call) Return(_a0 *operations.CreateServiceResponse, _a1 error) *MockServicesSDK_CreateService_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockServicesSDK_CreateService_Call) RunAndReturn(run func(context.Context, string, components.ServiceInput, ...operations.Option) (*operations.CreateServiceResponse, error)) *MockServicesSDK_CreateService_Call { + _c.Call.Return(run) + return _c +} + +// DeleteService provides a mock function with given fields: ctx, controlPlaneID, serviceID, opts +func (_m *MockServicesSDK) DeleteService(ctx context.Context, controlPlaneID string, serviceID string, opts ...operations.Option) (*operations.DeleteServiceResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, controlPlaneID, serviceID) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for DeleteService") + } + + var r0 *operations.DeleteServiceResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...operations.Option) (*operations.DeleteServiceResponse, error)); ok { + return rf(ctx, controlPlaneID, serviceID, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string, ...operations.Option) *operations.DeleteServiceResponse); ok { + r0 = rf(ctx, controlPlaneID, serviceID, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.DeleteServiceResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string, ...operations.Option) error); ok { + r1 = rf(ctx, controlPlaneID, serviceID, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockServicesSDK_DeleteService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DeleteService' +type MockServicesSDK_DeleteService_Call struct { + *mock.Call +} + +// DeleteService is a helper method to define mock.On call +// - ctx context.Context +// - controlPlaneID string +// - serviceID string +// - opts ...operations.Option +func (_e *MockServicesSDK_Expecter) DeleteService(ctx interface{}, controlPlaneID interface{}, serviceID interface{}, opts ...interface{}) *MockServicesSDK_DeleteService_Call { + return &MockServicesSDK_DeleteService_Call{Call: _e.mock.On("DeleteService", + append([]interface{}{ctx, controlPlaneID, serviceID}, opts...)...)} +} + +func (_c *MockServicesSDK_DeleteService_Call) Run(run func(ctx context.Context, controlPlaneID string, serviceID string, opts ...operations.Option)) *MockServicesSDK_DeleteService_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-3) + for i, a := range args[3:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(string), args[2].(string), variadicArgs...) + }) + return _c +} + +func (_c *MockServicesSDK_DeleteService_Call) Return(_a0 *operations.DeleteServiceResponse, _a1 error) *MockServicesSDK_DeleteService_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockServicesSDK_DeleteService_Call) RunAndReturn(run func(context.Context, string, string, ...operations.Option) (*operations.DeleteServiceResponse, error)) *MockServicesSDK_DeleteService_Call { + _c.Call.Return(run) + return _c +} + +// UpsertService provides a mock function with given fields: ctx, req, opts +func (_m *MockServicesSDK) UpsertService(ctx context.Context, req operations.UpsertServiceRequest, opts ...operations.Option) (*operations.UpsertServiceResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, req) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for UpsertService") + } + + var r0 *operations.UpsertServiceResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, operations.UpsertServiceRequest, ...operations.Option) (*operations.UpsertServiceResponse, error)); ok { + return rf(ctx, req, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, operations.UpsertServiceRequest, ...operations.Option) *operations.UpsertServiceResponse); ok { + r0 = rf(ctx, req, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*operations.UpsertServiceResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, operations.UpsertServiceRequest, ...operations.Option) error); ok { + r1 = rf(ctx, req, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockServicesSDK_UpsertService_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpsertService' +type MockServicesSDK_UpsertService_Call struct { + *mock.Call +} + +// UpsertService is a helper method to define mock.On call +// - ctx context.Context +// - req operations.UpsertServiceRequest +// - opts ...operations.Option +func (_e *MockServicesSDK_Expecter) UpsertService(ctx interface{}, req interface{}, opts ...interface{}) *MockServicesSDK_UpsertService_Call { + return &MockServicesSDK_UpsertService_Call{Call: _e.mock.On("UpsertService", + append([]interface{}{ctx, req}, opts...)...)} +} + +func (_c *MockServicesSDK_UpsertService_Call) Run(run func(ctx context.Context, req operations.UpsertServiceRequest, opts ...operations.Option)) *MockServicesSDK_UpsertService_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]operations.Option, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(operations.Option) + } + } + run(args[0].(context.Context), args[1].(operations.UpsertServiceRequest), variadicArgs...) + }) + return _c +} + +func (_c *MockServicesSDK_UpsertService_Call) Return(_a0 *operations.UpsertServiceResponse, _a1 error) *MockServicesSDK_UpsertService_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockServicesSDK_UpsertService_Call) RunAndReturn(run func(context.Context, operations.UpsertServiceRequest, ...operations.Option) (*operations.UpsertServiceResponse, error)) *MockServicesSDK_UpsertService_Call { + _c.Call.Return(run) + return _c +} + +// NewMockServicesSDK creates a new instance of MockServicesSDK. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockServicesSDK(t interface { + mock.TestingT + Cleanup(func()) +}) *MockServicesSDK { + mock := &MockServicesSDK{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/controller/konnect/ops/ops.go b/controller/konnect/ops/ops.go index 91bebe1dc..332e9c051 100644 --- a/controller/konnect/ops/ops.go +++ b/controller/konnect/ops/ops.go @@ -148,7 +148,7 @@ func Update[ case *konnectv1alpha1.KonnectControlPlane: return ctrl.Result{}, updateControlPlane(ctx, sdk.ControlPlanes, ent) case *configurationv1alpha1.KongService: - return ctrl.Result{}, updateService(ctx, sdk.Services, cl, ent) + return ctrl.Result{}, updateService(ctx, sdk.Services, ent) case *configurationv1alpha1.KongRoute: return ctrl.Result{}, updateRoute(ctx, sdk.Routes, cl, ent) case *configurationv1.KongConsumer: diff --git a/controller/konnect/ops/ops_controlplane_test.go b/controller/konnect/ops/ops_controlplane_test.go index dc40c3b87..73c39fb42 100644 --- a/controller/konnect/ops/ops_controlplane_test.go +++ b/controller/konnect/ops/ops_controlplane_test.go @@ -53,7 +53,7 @@ func TestCreateControlPlane(t *testing.T) { return sdk, cp }, assertions: func(t *testing.T, cp *konnectv1alpha1.KonnectControlPlane) { - assert.Equal(t, "12345", cp.Status.GetKonnectID()) + assert.Equal(t, "12345", cp.GetKonnectStatus().GetKonnectID()) cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, cp) require.True(t, ok, "Programmed condition not set on KonnectControlPlane") assert.Equal(t, metav1.ConditionTrue, cond.Status) @@ -97,7 +97,7 @@ func TestCreateControlPlane(t *testing.T) { assert.Equal(t, metav1.ConditionFalse, cond.Status) assert.Equal(t, "FailedToCreate", cond.Reason) assert.Equal(t, cp.GetGeneration(), cond.ObservedGeneration) - assert.Equal(t, "failed to create KonnectControlPlane default/cp-1: {\"status\":400,\"title\":\"\",\"instance\":\"\",\"detail\":\"bad request\",\"invalid_parameters\":null}", cond.Message) + assert.Equal(t, `failed to create KonnectControlPlane default/cp-1: {"status":400,"title":"","instance":"","detail":"bad request","invalid_parameters":null}`, cond.Message) }, expectedErr: true, }, @@ -197,7 +197,7 @@ func TestDeleteControlPlane(t *testing.T) { expectedErr: true, }, { - name: "not found error is ignore and considered a success when trying to delete", + name: "not found error is ignored and considered a success when trying to delete", mockCPPair: func() (*MockControlPlaneSDK, *konnectv1alpha1.KonnectControlPlane) { sdk := &MockControlPlaneSDK{} cp := &konnectv1alpha1.KonnectControlPlane{ @@ -358,7 +358,7 @@ func TestUpdateControlPlane(t *testing.T) { assert.Equal(t, metav1.ConditionFalse, cond.Status) assert.Equal(t, "FailedToUpdate", cond.Reason) assert.Equal(t, cp.GetGeneration(), cond.ObservedGeneration) - assert.Equal(t, "failed to update KonnectControlPlane default/cp-1: {\"status\":400,\"title\":\"\",\"instance\":\"\",\"detail\":\"bad request\",\"invalid_parameters\":null}", cond.Message) + assert.Equal(t, `failed to update KonnectControlPlane default/cp-1: {"status":400,"title":"","instance":"","detail":"bad request","invalid_parameters":null}`, cond.Message) }, expectedErr: true, }, diff --git a/controller/konnect/ops/ops_kongservice.go b/controller/konnect/ops/ops_kongservice.go index 9dce8957b..9a69a1d15 100644 --- a/controller/konnect/ops/ops_kongservice.go +++ b/controller/konnect/ops/ops_kongservice.go @@ -9,7 +9,6 @@ import ( sdkkonnectgoops "github.com/Kong/sdk-konnect-go/models/operations" sdkkonnectgoerrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" ctrllog "sigs.k8s.io/controller-runtime/pkg/log" @@ -17,7 +16,6 @@ import ( k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" - konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" ) func createService( @@ -26,7 +24,10 @@ func createService( svc *configurationv1alpha1.KongService, ) error { if svc.GetControlPlaneID() == "" { - return fmt.Errorf("can't create %T %s without a Konnect ControlPlane ID", svc, client.ObjectKeyFromObject(svc)) + return fmt.Errorf( + "can't create %T %s without a Konnect ControlPlane ID", + svc, client.ObjectKeyFromObject(svc), + ) } resp, err := sdk.CreateService(ctx, @@ -73,37 +74,19 @@ func createService( func updateService( ctx context.Context, sdk ServicesSDK, - cl client.Client, svc *configurationv1alpha1.KongService, ) error { - if svc.Spec.ControlPlaneRef == nil { - return fmt.Errorf("can't update %T without a ControlPlaneRef", svc) - } - - // TODO(pmalek) handle other types of CP ref - // TODO(pmalek) handle cross namespace refs - nnCP := types.NamespacedName{ - Namespace: svc.Namespace, - Name: svc.Spec.ControlPlaneRef.KonnectNamespacedRef.Name, - } - var cp konnectv1alpha1.KonnectControlPlane - if err := cl.Get(ctx, nnCP, &cp); err != nil { - return fmt.Errorf("failed to get KonnectControlPlane %s: for %T %s: %w", - nnCP, svc, client.ObjectKeyFromObject(svc), err, - ) - } - - if cp.Status.ID == "" { - return fmt.Errorf( - "can't update %T when referenced KonnectControlPlane %s does not have the Konnect ID", - svc, nnCP, + if svc.GetControlPlaneID() == "" { + return fmt.Errorf("can't update %T %s without a Konnect ControlPlane ID", + svc, client.ObjectKeyFromObject(svc), ) } + id := svc.GetKonnectStatus().GetKonnectID() resp, err := sdk.UpsertService(ctx, sdkkonnectgoops.UpsertServiceRequest{ - ControlPlaneID: cp.Status.ID, - ServiceID: svc.GetKonnectStatus().GetKonnectID(), + ControlPlaneID: svc.GetControlPlaneID(), + ServiceID: id, Service: kongServiceToSDKServiceInput(svc), }, ) @@ -112,11 +95,33 @@ func updateService( // Can't adopt it as it will cause conflicts between the controller // that created that entity and already manages it, hm if errWrapped := wrapErrIfKonnectOpFailed(err, UpdateOp, svc); errWrapped != nil { + // Service update operation returns an SDKError instead of a NotFoundError. + var sdkError *sdkkonnectgoerrs.SDKError + if errors.As(errWrapped, &sdkError) { + switch sdkError.StatusCode { + case 404: + if err := createService(ctx, sdk, svc); err != nil { + return FailedKonnectOpError[configurationv1alpha1.KongService]{ + Op: UpdateOp, + Err: err, + } + } + // Create succeeded, createService sets the status so no need to do this here. + + return nil + default: + return FailedKonnectOpError[configurationv1alpha1.KongService]{ + Op: UpdateOp, + Err: sdkError, + } + } + } + k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( conditions.KonnectEntityProgrammedConditionType, metav1.ConditionFalse, - "FailedToCreate", + "FailedToUpdate", errWrapped.Error(), svc.GetGeneration(), ), @@ -126,7 +131,7 @@ func updateService( } svc.Status.Konnect.SetKonnectID(*resp.Service.ID) - svc.Status.Konnect.SetControlPlaneID(cp.Status.ID) + svc.Status.Konnect.SetControlPlaneID(svc.GetControlPlaneID()) k8sutils.SetCondition( k8sutils.NewConditionWithGeneration( conditions.KonnectEntityProgrammedConditionType, diff --git a/controller/konnect/ops/ops_kongservice_test.go b/controller/konnect/ops/ops_kongservice_test.go new file mode 100644 index 000000000..2903ef7f4 --- /dev/null +++ b/controller/konnect/ops/ops_kongservice_test.go @@ -0,0 +1,501 @@ +package ops + +import ( + "context" + "testing" + + sdkkonnectgocomp "github.com/Kong/sdk-konnect-go/models/components" + sdkkonnectgoops "github.com/Kong/sdk-konnect-go/models/operations" + sdkkonnectgoerrs "github.com/Kong/sdk-konnect-go/models/sdkerrors" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/kong/gateway-operator/controller/konnect/conditions" + k8sutils "github.com/kong/gateway-operator/pkg/utils/kubernetes" + + configurationv1alpha1 "github.com/kong/kubernetes-configuration/api/configuration/v1alpha1" + konnectv1alpha1 "github.com/kong/kubernetes-configuration/api/konnect/v1alpha1" +) + +func TestCreateKongService(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + mockServicePair func() (*MockServicesSDK, *configurationv1alpha1.KongService) + expectedErr bool + assertions func(*testing.T, *configurationv1alpha1.KongService) + }{ + { + name: "success", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: "default", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + Host: "example.com", + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "123456789", + }, + }, + } + + sdk. + EXPECT(). + CreateService(ctx, "123456789", kongServiceToSDKServiceInput(svc)). + Return( + &sdkkonnectgoops.CreateServiceResponse{ + Service: &sdkkonnectgocomp.Service{ + ID: lo.ToPtr("12345"), + Host: "example.com", + Name: lo.ToPtr("svc-1"), + }, + }, + nil, + ) + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + assert.Equal(t, "12345", svc.GetKonnectStatus().GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, svc) + require.True(t, ok, "Programmed condition not set on KongService") + assert.Equal(t, metav1.ConditionTrue, cond.Status) + assert.Equal(t, conditions.KonnectEntityProgrammedReasonProgrammed, cond.Reason) + assert.Equal(t, svc.GetGeneration(), cond.ObservedGeneration) + }, + }, + { + name: "fail - no control plane ID in status returns an error and does not create the Service in Konnect", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: "default", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + Host: "example.com", + }, + }, + } + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + assert.Equal(t, "", svc.GetKonnectStatus().GetKonnectID()) + // TODO: we should probably set a condition when the control plane ID is missing in the status. + }, + expectedErr: true, + }, + { + name: "fail", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: "default", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + Host: "example.com", + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "123456789", + }, + }, + } + + sdk. + EXPECT(). + CreateService(ctx, "123456789", kongServiceToSDKServiceInput(svc)). + Return( + nil, + &sdkkonnectgoerrs.BadRequestError{ + Status: 400, + Detail: "bad request", + }, + ) + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + assert.Equal(t, "", svc.GetKonnectStatus().GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, svc) + require.True(t, ok, "Programmed condition not set on KonnectControlPlane") + assert.Equal(t, metav1.ConditionFalse, cond.Status) + assert.Equal(t, "FailedToCreate", cond.Reason) + assert.Equal(t, svc.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, `failed to create KongService default/svc-1: {"status":400,"title":"","instance":"","detail":"bad request","invalid_parameters":null}`, cond.Message) + }, + expectedErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sdk, svc := tc.mockServicePair() + + err := createService(ctx, sdk, svc) + t.Cleanup(func() { + assert.True(t, sdk.AssertExpectations(t)) + }) + + tc.assertions(t, svc) + + if tc.expectedErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestDeleteKongService(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + mockServicePair func() (*MockServicesSDK, *configurationv1alpha1.KongService) + expectedErr bool + assertions func(*testing.T, *configurationv1alpha1.KongService) + }{ + { + name: "success", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + DeleteService(ctx, "12345", "123456789"). + Return( + &sdkkonnectgoops.DeleteServiceResponse{ + StatusCode: 200, + }, + nil, + ) + + return sdk, svc + }, + }, + { + name: "fail", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + DeleteService(ctx, "12345", "123456789"). + Return( + nil, + &sdkkonnectgoerrs.BadRequestError{ + Status: 400, + Detail: "bad request", + }, + ) + + return sdk, svc + }, + expectedErr: true, + }, + { + name: "not found error is ignored and considered a success when trying to delete", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + DeleteService(ctx, "12345", "123456789"). + Return( + nil, + &sdkkonnectgoerrs.SDKError{ + Message: "not found", + StatusCode: 404, + }, + ) + + return sdk, svc + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sdk, svc := tc.mockServicePair() + + err := deleteService(ctx, sdk, svc) + + if tc.assertions != nil { + tc.assertions(t, svc) + } + + if tc.expectedErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.True(t, sdk.AssertExpectations(t)) + }) + } +} + +func TestUpdateKongService(t *testing.T) { + ctx := context.Background() + testCases := []struct { + name string + mockServicePair func() (*MockServicesSDK, *configurationv1alpha1.KongService) + expectedErr bool + assertions func(*testing.T, *configurationv1alpha1.KongService) + }{ + { + name: "success", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + UpsertService(ctx, + sdkkonnectgoops.UpsertServiceRequest{ + ControlPlaneID: "12345", + ServiceID: "123456789", + Service: kongServiceToSDKServiceInput(svc), + }, + ). + Return( + &sdkkonnectgoops.UpsertServiceResponse{ + StatusCode: 200, + Service: &sdkkonnectgocomp.Service{ + ID: lo.ToPtr("123456789"), + Name: lo.ToPtr("svc-1"), + }, + }, + nil, + ) + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + assert.Equal(t, "123456789", svc.GetKonnectStatus().GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, svc) + require.True(t, ok, "Programmed condition not set on KonnectControlPlane") + assert.Equal(t, metav1.ConditionTrue, cond.Status) + assert.Equal(t, conditions.KonnectEntityProgrammedReasonProgrammed, cond.Reason) + assert.Equal(t, svc.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, "", cond.Message) + }, + }, + { + name: "fail", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: "default", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + UpsertService(ctx, + sdkkonnectgoops.UpsertServiceRequest{ + ControlPlaneID: "12345", + ServiceID: "123456789", + Service: kongServiceToSDKServiceInput(svc), + }, + ). + Return( + nil, + &sdkkonnectgoerrs.BadRequestError{ + Status: 400, + Title: "bad request", + }, + ) + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + // TODO: When we fail to update a KongService, do we want to clear + // the Konnect ID from the status? Probably not. + // assert.Equal(t, "", svc.GetKonnectStatus().GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, svc) + require.True(t, ok, "Programmed condition not set on KonnectControlPlane") + assert.Equal(t, metav1.ConditionFalse, cond.Status) + assert.Equal(t, "FailedToUpdate", cond.Reason) + assert.Equal(t, svc.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, `failed to update KongService default/svc-1: {"status":400,"title":"bad request","instance":"","detail":"","invalid_parameters":null}`, cond.Message) + }, + expectedErr: true, + }, + { + name: "when not found then try to create", + mockServicePair: func() (*MockServicesSDK, *configurationv1alpha1.KongService) { + sdk := &MockServicesSDK{} + svc := &configurationv1alpha1.KongService{ + ObjectMeta: metav1.ObjectMeta{ + Name: "svc-1", + Namespace: "default", + }, + Spec: configurationv1alpha1.KongServiceSpec{ + KongServiceAPISpec: configurationv1alpha1.KongServiceAPISpec{ + Name: lo.ToPtr("svc-1"), + }, + }, + Status: configurationv1alpha1.KongServiceStatus{ + Konnect: &konnectv1alpha1.KonnectEntityStatusWithControlPlaneRef{ + ControlPlaneID: "12345", + KonnectEntityStatus: konnectv1alpha1.KonnectEntityStatus{ + ID: "123456789", + }, + }, + }, + } + sdk. + EXPECT(). + UpsertService(ctx, + sdkkonnectgoops.UpsertServiceRequest{ + ControlPlaneID: "12345", + ServiceID: "123456789", + Service: kongServiceToSDKServiceInput(svc), + }, + ). + Return( + nil, + &sdkkonnectgoerrs.SDKError{ + StatusCode: 404, + Message: "not found", + }, + ) + + sdk. + EXPECT(). + CreateService(ctx, "12345", kongServiceToSDKServiceInput(svc)). + Return( + &sdkkonnectgoops.CreateServiceResponse{ + Service: &sdkkonnectgocomp.Service{ + ID: lo.ToPtr("123456789"), + Name: lo.ToPtr("svc-1"), + }, + }, + nil, + ) + + return sdk, svc + }, + assertions: func(t *testing.T, svc *configurationv1alpha1.KongService) { + assert.Equal(t, "123456789", svc.GetKonnectStatus().GetKonnectID()) + cond, ok := k8sutils.GetCondition(conditions.KonnectEntityProgrammedConditionType, svc) + require.True(t, ok, "Programmed condition not set on KonnectControlPlane") + assert.Equal(t, metav1.ConditionTrue, cond.Status) + assert.Equal(t, conditions.KonnectEntityProgrammedReasonProgrammed, cond.Reason) + assert.Equal(t, svc.GetGeneration(), cond.ObservedGeneration) + assert.Equal(t, "", cond.Message) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + sdk, svc := tc.mockServicePair() + + err := updateService(ctx, sdk, svc) + + if tc.assertions != nil { + tc.assertions(t, svc) + } + + if tc.expectedErr { + assert.Error(t, err) + return + } + + assert.NoError(t, err) + assert.True(t, sdk.AssertExpectations(t)) + }) + } +}