From 078c476ed21836c5a559d9cd243b6da10b2d6537 Mon Sep 17 00:00:00 2001 From: Maciej Winnicki Date: Thu, 7 Jun 2018 12:09:45 +0200 Subject: [PATCH] Add endpoint for updating event type --- event/errors.go | 6 +-- event/service.go | 1 + function/errors.go | 6 +-- httpapi/httpapi.go | 40 ++++++++++++++- httpapi/httpapi_test.go | 109 +++++++++++++++++++++++++++++++++++++++- libkv/eventtype.go | 39 ++++++++++++-- libkv/eventtype_test.go | 66 +++++++++++++++++++++++- libkv/function.go | 8 +-- libkv/function_test.go | 14 ++---- libkv/subscription.go | 5 +- mock/eventtype.go | 8 +-- 11 files changed, 267 insertions(+), 35 deletions(-) diff --git a/event/errors.go b/event/errors.go index c00112d..471065a 100644 --- a/event/errors.go +++ b/event/errors.go @@ -29,10 +29,10 @@ func (e ErrEventTypeValidation) Error() string { return fmt.Sprintf("Event Type doesn't validate. Validation error: %s", e.Message) } -// ErrEventTypeHasSubscriptionsError occurs when there are subscription for the event type. -type ErrEventTypeHasSubscriptionsError struct{} +// ErrEventTypeHasSubscriptions occurs when there are subscription for the event type. +type ErrEventTypeHasSubscriptions struct{} -func (e ErrEventTypeHasSubscriptionsError) Error() string { +func (e ErrEventTypeHasSubscriptions) Error() string { return fmt.Sprintf("Event type cannot be deleted because there are subscriptions using it.") } diff --git a/event/service.go b/event/service.go index f8754bb..888f50b 100644 --- a/event/service.go +++ b/event/service.go @@ -5,5 +5,6 @@ type Service interface { CreateEventType(eventType *Type) (*Type, error) GetEventType(space string, name TypeName) (*Type, error) GetEventTypes(space string) (Types, error) + UpdateEventType(newEventType *Type) (*Type, error) DeleteEventType(space string, name TypeName) error } diff --git a/function/errors.go b/function/errors.go index 71ccaa2..f523234 100644 --- a/function/errors.go +++ b/function/errors.go @@ -67,9 +67,9 @@ func (e ErrFunctionError) Error() string { return fmt.Sprintf("Function call failed because of runtime error. Error: %s", e.Original) } -// ErrFunctionHasSubscriptionsError occurs when function with subscription is being deleted. -type ErrFunctionHasSubscriptionsError struct{} +// ErrFunctionHasSubscriptions occurs when function with subscription is being deleted. +type ErrFunctionHasSubscriptions struct{} -func (e ErrFunctionHasSubscriptionsError) Error() string { +func (e ErrFunctionHasSubscriptions) Error() string { return fmt.Sprintf("Function cannot be deleted because it's subscribed to a least one event.") } diff --git a/httpapi/httpapi.go b/httpapi/httpapi.go index f002f69..4c5c822 100644 --- a/httpapi/httpapi.go +++ b/httpapi/httpapi.go @@ -41,6 +41,7 @@ func (h HTTPAPI) RegisterRoutes(router *httprouter.Router) { router.GET("/v1/spaces/:space/eventtypes", h.listEventTypes) router.GET("/v1/spaces/:space/eventtypes/:name", h.getEventType) router.POST("/v1/spaces/:space/eventtypes", h.createEventType) + router.PUT("/v1/spaces/:space/eventtypes/:name", h.updateEventType) router.DELETE("/v1/spaces/:space/eventtypes/:name", h.deleteEventType) router.GET("/v1/spaces/:space/functions", h.listFunctions) @@ -129,6 +130,41 @@ func (h HTTPAPI) createEventType(w http.ResponseWriter, r *http.Request, params metricConfigRequests.WithLabelValues(eventType.Space, "eventtype", "create").Inc() } +func (h HTTPAPI) updateEventType(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + w.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(w) + + eventType := &event.Type{} + dec := json.NewDecoder(r.Body) + err := dec.Decode(eventType) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + validationErr := event.ErrEventTypeValidation{Message: err.Error()} + encoder.Encode(&Response{Errors: []Error{{Message: validationErr.Error()}}}) + return + } + + eventType.Space = params.ByName("space") + eventType.Name = event.TypeName(params.ByName("name")) + output, err := h.EventTypes.UpdateEventType(eventType) + if err != nil { + if _, ok := err.(*event.ErrEventTypeNotFound); ok { + w.WriteHeader(http.StatusNotFound) + } else if _, ok := err.(*event.ErrEventTypeValidation); ok { + w.WriteHeader(http.StatusBadRequest) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + + encoder.Encode(&Response{Errors: []Error{{Message: err.Error()}}}) + } else { + w.WriteHeader(http.StatusOK) + encoder.Encode(output) + } + + metricConfigRequests.WithLabelValues(eventType.Space, "eventtype", "update").Inc() +} + func (h HTTPAPI) deleteEventType(w http.ResponseWriter, r *http.Request, params httprouter.Params) { w.Header().Set("Content-Type", "application/json") encoder := json.NewEncoder(w) @@ -138,7 +174,7 @@ func (h HTTPAPI) deleteEventType(w http.ResponseWriter, r *http.Request, params if err != nil { if _, ok := err.(*event.ErrEventTypeNotFound); ok { w.WriteHeader(http.StatusNotFound) - } else if _, ok := err.(*event.ErrEventTypeHasSubscriptionsError); ok { + } else if _, ok := err.(*event.ErrEventTypeHasSubscriptions); ok { w.WriteHeader(http.StatusBadRequest) } else { w.WriteHeader(http.StatusInternalServerError) @@ -270,7 +306,7 @@ func (h HTTPAPI) deleteFunction(w http.ResponseWriter, r *http.Request, params h if err != nil { if _, ok := err.(*function.ErrFunctionNotFound); ok { w.WriteHeader(http.StatusNotFound) - } else if _, ok := err.(*function.ErrFunctionHasSubscriptionsError); ok { + } else if _, ok := err.(*function.ErrFunctionHasSubscriptions); ok { w.WriteHeader(http.StatusBadRequest) } else { w.WriteHeader(http.StatusInternalServerError) diff --git a/httpapi/httpapi_test.go b/httpapi/httpapi_test.go index 2cd57ca..f66fd70 100644 --- a/httpapi/httpapi_test.go +++ b/httpapi/httpapi_test.go @@ -165,6 +165,73 @@ func TestCreateEventType(t *testing.T) { }) } +func TestUpdateEventType(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + router, eventTypes, _, _ := setup(ctrl) + + typePayload := []byte(`{"name":"test.event","space":"test1"}`) + + t.Run("event type updated", func(t *testing.T) { + eventType := &event.Type{Space: "default", Name: event.TypeName("test.event")} + eventTypes.EXPECT().UpdateEventType(eventType).Return(eventType, nil) + + resp := request(router, http.MethodPut, "/v1/spaces/default/eventtypes/test.event", typePayload) + + returnedType := &event.Type{} + json.Unmarshal(resp.Body.Bytes(), returnedType) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "application/json", resp.Header().Get("Content-Type")) + assert.Equal(t, event.TypeName("test.event"), returnedType.Name) + assert.Equal(t, "default", returnedType.Space) + }) + + t.Run("event type doesn't exists", func(t *testing.T) { + eventTypes.EXPECT().UpdateEventType(gomock.Any()). + Return(nil, &event.ErrEventTypeNotFound{Name: event.TypeName("test.event")}) + + resp := request(router, http.MethodPut, "/v1/spaces/default/eventtypes/test.event", typePayload) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusNotFound, resp.Code) + assert.Equal(t, `Event Type "test.event" not found.`, httpresp.Errors[0].Message) + }) + + t.Run("validation error", func(t *testing.T) { + eventTypes.EXPECT().UpdateEventType(gomock.Any()). + Return(nil, &event.ErrEventTypeValidation{Message: "some error"}) + + payload := []byte(`{"name":"test"}`) + resp := request(router, http.MethodPut, "/v1/spaces/default/eventtypes/test.event", payload) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusBadRequest, resp.Code) + assert.Equal(t, "Event Type doesn't validate. Validation error: some error", httpresp.Errors[0].Message) + }) + + t.Run("malformed JSON", func(t *testing.T) { + resp := request(router, http.MethodPut, "/v1/spaces/default/eventtypes/test.event", []byte("{")) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusBadRequest, resp.Code) + assert.Equal(t, "Event Type doesn't validate. Validation error: unexpected EOF", httpresp.Errors[0].Message) + }) + + t.Run("internal error", func(t *testing.T) { + eventTypes.EXPECT().UpdateEventType(gomock.Any()).Return(nil, errors.New("processing error")) + + resp := request(router, http.MethodPut, "/v1/spaces/default/eventtypes/test.event", typePayload) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusInternalServerError, resp.Code) + assert.Equal(t, `processing error`, httpresp.Errors[0].Message) + }) +} + func TestDeleteEventType(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -181,7 +248,7 @@ func TestDeleteEventType(t *testing.T) { }) t.Run("event type has subscriptions", func(t *testing.T) { - eventTypes.EXPECT().DeleteEventType(gomock.Any(), gomock.Any()).Return(&event.ErrEventTypeHasSubscriptionsError{}) + eventTypes.EXPECT().DeleteEventType(gomock.Any(), gomock.Any()).Return(&event.ErrEventTypeHasSubscriptions{}) resp := request(router, http.MethodDelete, "/v1/spaces/default/eventtypes/test.event", nil) @@ -388,7 +455,7 @@ func TestDeleteFunction(t *testing.T) { }) t.Run("function has subscriptions", func(t *testing.T) { - functions.EXPECT().DeleteFunction(gomock.Any(), gomock.Any()).Return(&function.ErrFunctionHasSubscriptionsError{}) + functions.EXPECT().DeleteFunction(gomock.Any(), gomock.Any()).Return(&function.ErrFunctionHasSubscriptions{}) resp := request(router, http.MethodDelete, "/v1/spaces/default/functions/func1", nil) @@ -520,6 +587,44 @@ func TestUpdateSubscription(t *testing.T) { }) } +func TestDeleteSubscription(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + router, _, _, subscriptions := setup(ctrl) + + t.Run("subscription deleted", func(t *testing.T) { + subscriptions.EXPECT().DeleteSubscription("default", subscription.ID("testid")).Return(nil) + + resp := request(router, http.MethodDelete, "/v1/spaces/default/subscriptions/testid", nil) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusNoContent, resp.Code) + }) + + t.Run("subscriptions not found", func(t *testing.T) { + subscriptions.EXPECT().DeleteSubscription(gomock.Any(), gomock.Any()).Return(&subscription.ErrSubscriptionNotFound{ID: subscription.ID("testid")}) + + resp := request(router, http.MethodDelete, "/v1/spaces/default/subscriptions/testid", nil) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusNotFound, resp.Code) + assert.Equal(t, `Subscription "testid" not found.`, httpresp.Errors[0].Message) + }) + + t.Run("internal error", func(t *testing.T) { + subscriptions.EXPECT().DeleteSubscription(gomock.Any(), gomock.Any()).Return(errors.New("internal error")) + + resp := request(router, http.MethodDelete, "/v1/spaces/default/subscriptions/testid", nil) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusInternalServerError, resp.Code) + assert.Equal(t, "internal error", httpresp.Errors[0].Message) + }) +} + func request(router *httprouter.Router, method string, url string, payload []byte) *httptest.ResponseRecorder { resp := httptest.NewRecorder() body := bytes.NewReader(payload) diff --git a/libkv/eventtype.go b/libkv/eventtype.go index dbf456b..fbe79d6 100644 --- a/libkv/eventtype.go +++ b/libkv/eventtype.go @@ -24,7 +24,7 @@ func (key EventTypeKey) String() string { // CreateEventType creates event type in configuration. func (service Service) CreateEventType(eventType *event.Type) (*event.Type, error) { - if err := service.validateEventType(eventType); err != nil { + if err := validateEventType(eventType); err != nil { return nil, err } @@ -97,6 +97,39 @@ func (service Service) GetEventTypes(space string) (event.Types, error) { return event.Types(types), nil } +// UpdateEventType updates subscription. +func (service Service) UpdateEventType(newEventType *event.Type) (*event.Type, error) { + if err := validateEventType(newEventType); err != nil { + return nil, err + } + + _, err := service.GetEventType(newEventType.Space, newEventType.Name) + if err != nil { + return nil, err + } + + if newEventType.AuthorizerID != nil { + function, _ := service.GetFunction(newEventType.Space, *newEventType.AuthorizerID) + if function == nil { + return nil, &event.ErrEventTypeValidation{Message: "Authorizer function doesn't exists."} + } + } + + buf, err := json.Marshal(newEventType) + if err != nil { + return nil, &event.ErrEventTypeValidation{Message: err.Error()} + } + + err = service.EventTypeStore.Put(EventTypeKey{newEventType.Space, newEventType.Name}.String(), buf, nil) + if err != nil { + return nil, err + } + + service.Log.Debug("Event Type updated.", zap.Object("eventType", newEventType)) + + return newEventType, nil +} + // DeleteEventType deletes event type from the configuration. func (service Service) DeleteEventType(space string, name event.TypeName) error { subs, err := service.GetSubscriptions(space) @@ -105,7 +138,7 @@ func (service Service) DeleteEventType(space string, name event.TypeName) error } for _, sub := range subs { if name == sub.EventType { - return &event.ErrEventTypeHasSubscriptionsError{} + return &event.ErrEventTypeHasSubscriptions{} } } @@ -119,7 +152,7 @@ func (service Service) DeleteEventType(space string, name event.TypeName) error return nil } -func (service Service) validateEventType(eventType *event.Type) error { +func validateEventType(eventType *event.Type) error { if eventType.Space == "" { eventType.Space = defaultSpace } diff --git a/libkv/eventtype_test.go b/libkv/eventtype_test.go index 27ba9c0..6d58de8 100644 --- a/libkv/eventtype_test.go +++ b/libkv/eventtype_test.go @@ -163,6 +163,70 @@ func TestGetEventTypes(t *testing.T) { }) } +func TestUpdateEventType(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + existingEventTypeKV := &store.KVPair{Value: []byte(`{"space":"default","name":"test.event"}`)} + authorizerID := function.ID("auth") + newEventType := &event.Type{Space: "default", Name: "test.event", AuthorizerID: &authorizerID} + newEventTypePayload := []byte(`{"space":"default","name":"test.event","authorizerId":"auth"}`) + functionKV := &store.KVPair{ + Value: []byte(`{"functionId":"f1","type":"http","provider":{"url": "http://test.com"}}}`)} + + t.Run("event type updated", func(t *testing.T) { + functionsDB := mock.NewMockStore(ctrl) + functionsDB.EXPECT().Get("default/auth", gomock.Any()).Return(functionKV, nil) + eventTypesDB := mock.NewMockStore(ctrl) + eventTypesDB.EXPECT(). + Get("default/test.event", &store.ReadOptions{Consistent: true}). + Return(existingEventTypeKV, nil) + eventTypesDB.EXPECT().Put("default/test.event", newEventTypePayload, nil).Return(nil) + service := &Service{FunctionStore: functionsDB, EventTypeStore: eventTypesDB, Log: zap.NewNop()} + + _, err := service.UpdateEventType(newEventType) + + assert.Nil(t, err) + }) + + t.Run("event type not found", func(t *testing.T) { + db := mock.NewMockStore(ctrl) + db.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, errors.New("Key not found in store")) + service := &Service{EventTypeStore: db, Log: zap.NewNop()} + + _, err := service.UpdateEventType(newEventType) + + assert.Equal(t, &event.ErrEventTypeNotFound{Name: "test.event"}, err) + }) + + t.Run("authorizer function doesn't exists error", func(t *testing.T) { + functionsDB := mock.NewMockStore(ctrl) + functionsDB.EXPECT().Get(gomock.Any(), gomock.Any()).Return(nil, errors.New("Key not found in store")) + eventTypesDB := mock.NewMockStore(ctrl) + eventTypesDB.EXPECT().Get(gomock.Any(), gomock.Any()).Return(existingEventTypeKV, nil) + service := &Service{EventTypeStore: eventTypesDB, FunctionStore: functionsDB, Log: zap.NewNop()} + + _, err := service.UpdateEventType(newEventType) + + assert.Equal(t, &event.ErrEventTypeValidation{ + Message: "Authorizer function doesn't exists.", + }, err) + }) + + t.Run("KV Put error", func(t *testing.T) { + functionsDB := mock.NewMockStore(ctrl) + functionsDB.EXPECT().Get(gomock.Any(), gomock.Any()).Return(functionKV, nil) + eventTypesDB := mock.NewMockStore(ctrl) + eventTypesDB.EXPECT().Get(gomock.Any(), gomock.Any()).Return(existingEventTypeKV, nil) + eventTypesDB.EXPECT().Put(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("KV put error")) + service := &Service{FunctionStore: functionsDB, EventTypeStore: eventTypesDB, Log: zap.NewNop()} + + _, err := service.UpdateEventType(newEventType) + + assert.EqualError(t, err, "KV put error") + }) +} + func TestDeleteEventType(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -201,6 +265,6 @@ func TestDeleteEventType(t *testing.T) { err := service.DeleteEventType("default", event.TypeName("test.event")) - assert.Equal(t, &event.ErrEventTypeHasSubscriptionsError{}, err) + assert.Equal(t, &event.ErrEventTypeHasSubscriptions{}, err) }) } diff --git a/libkv/function.go b/libkv/function.go index d06b2b6..69588ef 100644 --- a/libkv/function.go +++ b/libkv/function.go @@ -25,7 +25,7 @@ func (key FunctionKey) String() string { // RegisterFunction registers function in configuration. func (service Service) RegisterFunction(fn *function.Function) (*function.Function, error) { - if err := service.validateFunction(fn); err != nil { + if err := validateFunction(fn); err != nil { return nil, err } @@ -51,7 +51,7 @@ func (service Service) RegisterFunction(fn *function.Function) (*function.Functi // UpdateFunction updates function configuration. func (service Service) UpdateFunction(fn *function.Function) (*function.Function, error) { - if err := service.validateFunction(fn); err != nil { + if err := validateFunction(fn); err != nil { return nil, err } @@ -125,7 +125,7 @@ func (service Service) DeleteFunction(space string, id function.ID) error { } for _, sub := range subs { if id == sub.FunctionID { - return &function.ErrFunctionHasSubscriptionsError{} + return &function.ErrFunctionHasSubscriptions{} } } @@ -139,7 +139,7 @@ func (service Service) DeleteFunction(space string, id function.ID) error { return nil } -func (service Service) validateFunction(fn *function.Function) error { +func validateFunction(fn *function.Function) error { if fn.Space == "" { fn.Space = defaultSpace } diff --git a/libkv/function_test.go b/libkv/function_test.go index d57a314..759bea1 100644 --- a/libkv/function_test.go +++ b/libkv/function_test.go @@ -275,39 +275,33 @@ func TestDeleteFunction_SubscriptionExists(t *testing.T) { err := service.DeleteFunction("default", function.ID("testid")) - assert.Equal(t, err, &function.ErrFunctionHasSubscriptionsError{}) + assert.Equal(t, err, &function.ErrFunctionHasSubscriptions{}) } func TestValidateFunction_MissingID(t *testing.T) { - service := &Service{Log: zap.NewNop()} - - err := service.validateFunction(&function.Function{}) + err := validateFunction(&function.Function{}) assert.Equal(t, err, &function.ErrFunctionValidation{ Message: "Key: 'Function.ID' Error:Field validation for 'ID' failed on the 'required' tag"}) } func TestValidateFunction_SpaceInvalid(t *testing.T) { - service := &Service{Log: zap.NewNop()} - fn := &function.Function{ ID: "id", Space: "///"} - err := service.validateFunction(fn) + err := validateFunction(fn) assert.Equal(t, err, &function.ErrFunctionValidation{ Message: "Key: 'Function.Space' Error:Field validation for 'Space' failed on the 'space' tag"}) } func TestValidateFunction_SetDefaultSpace(t *testing.T) { - service := &Service{Log: zap.NewNop()} - fn := &function.Function{ ID: "id", ProviderType: http.Type, Provider: http.HTTP{URL: "http://example.com"}, } - service.validateFunction(fn) + validateFunction(fn) assert.Equal(t, "default", fn.Space) } diff --git a/libkv/subscription.go b/libkv/subscription.go index fb881f9..0cf75fa 100644 --- a/libkv/subscription.go +++ b/libkv/subscription.go @@ -66,8 +66,7 @@ func (service Service) CreateSubscription(sub *subscription.Subscription) (*subs // UpdateSubscription updates subscription. func (service Service) UpdateSubscription(id subscription.ID, newSub *subscription.Subscription) (*subscription.Subscription, error) { - err := validateSubscription(newSub) - if err != nil { + if err := validateSubscription(newSub); err != nil { return nil, err } @@ -88,7 +87,7 @@ func (service Service) UpdateSubscription(id subscription.ID, newSub *subscripti buf, err := json.Marshal(newSub) if err != nil { - return nil, err + return nil, &subscription.ErrSubscriptionValidation{Message: err.Error()} } err = service.SubscriptionStore.Put(subscriptionPath(newSub.Space, newSub.ID), buf, nil) diff --git a/mock/eventtype.go b/mock/eventtype.go index d6460cc..070e12f 100644 --- a/mock/eventtype.go +++ b/mock/eventtype.go @@ -85,14 +85,14 @@ func (mr *MockEventTypeServiceMockRecorder) GetEventTypes(arg0 interface{}) *gom } // UpdateEventType mocks base method -func (m *MockEventTypeService) UpdateEventType(arg0 event.TypeName, arg1 *event.Type) (*event.Type, error) { - ret := m.ctrl.Call(m, "UpdateEventType", arg0, arg1) +func (m *MockEventTypeService) UpdateEventType(arg0 *event.Type) (*event.Type, error) { + ret := m.ctrl.Call(m, "UpdateEventType", arg0) ret0, _ := ret[0].(*event.Type) ret1, _ := ret[1].(error) return ret0, ret1 } // UpdateEventType indicates an expected call of UpdateEventType -func (mr *MockEventTypeServiceMockRecorder) UpdateEventType(arg0, arg1 interface{}) *gomock.Call { - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEventType", reflect.TypeOf((*MockEventTypeService)(nil).UpdateEventType), arg0, arg1) +func (mr *MockEventTypeServiceMockRecorder) UpdateEventType(arg0 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateEventType", reflect.TypeOf((*MockEventTypeService)(nil).UpdateEventType), arg0) }