diff --git a/README.md b/README.md index 07cff2d..f80b169 100644 --- a/README.md +++ b/README.md @@ -614,6 +614,46 @@ JSON object: --- +#### Update Subscription + +**Endpoint** + +`PUT /v1/spaces//subscriptions/` + +**Request** + +_Note that `event`, `functionId`, `path`, and `method` may not be updated in an UpdateSubscription call._ + +* `event` - `string` - event name +* `functionId` - `string` - ID of function to receive events +* `path` - `string` - optional, URL path under which events (HTTP requests) are accepted, default: `/` +* `method` - `string` - required for `http` event, HTTP method that accepts requests +* `cors` - `object` - optional, in case of `http` event, By default CORS is disabled. When set to empty object CORS configuration will use default values for all fields below. Available fields: + * `origins` - `array` of `string` - list of allowed origins. An origin may contain a wildcard (\*) to replace 0 or more characters (i.e.: http://\*.domain.com), default: `*` + * `methods` - `array` of `string` - list of allowed methods, default: `HEAD`, `GET`, `POST` + * `headers` - `array` of `string` - list of allowed headers, default: `Origin`, `Accept`, `Content-Type` + * `allowCredentials` - `bool` - default: false + +**Response** + +Status code: + +* `200 Created` on success +* `400 Bad Request` on validation error +* `404 Not Found` if subscription doesn't exist + +JSON object: + +* `space` - `string` - space name +* `subscriptionId` - `string` - subscription ID +* `event` - `string` - event name +* `functionId` - function ID +* `method` - `string` - optional, in case of `http` event, HTTP method that accepts requests +* `path` - `string` - optional, in case of `http` event, path that accepts requests, starts with `/` +* `cors` - `object` - optional, in case of `http` event, CORS configuration + +--- + #### Delete Subscription **Endpoint** diff --git a/docs/openapi/openapi-config-api.yaml b/docs/openapi/openapi-config-api.yaml index d193f3d..01fdcde 100644 --- a/docs/openapi/openapi-config-api.yaml +++ b/docs/openapi/openapi-config-api.yaml @@ -185,6 +185,28 @@ paths: $ref: '#/components/responses/NotFoundError' 500: $ref: '#/components/responses/Error' + put: + summary: "Update subscription" + tags: + - "subscription" + operationId: "UpdateSubscription" + parameters: + - $ref: "#/components/parameters/Space" + requestBody: + $ref: "#/components/requestBodies/UpdateSubscription" + responses: + 200: + description: "subscription updated" + content: + application/json: + schema: + $ref: "#/components/schemas/Subscription" + 400: + $ref: '#/components/responses/ValidationError' + 404: + $ref: '#/components/responses/NotFoundError' + 500: + $ref: '#/components/responses/Error' delete: summary: "Delete subscription" tags: @@ -468,6 +490,23 @@ components: $ref: '#/components/schemas/Method' cors: $ref: '#/components/schemas/CORS' + UpdateSubscription: + description: "subscription update request body" + content: + application/json: + schema: + type: object + properties: + functionId: + $ref: '#/components/schemas/FunctionID' + event: + $ref: '#/components/schemas/Event' + path: + $ref: '#/components/schemas/Path' + method: + $ref: '#/components/schemas/Method' + cors: + $ref: '#/components/schemas/CORS' responses: Error: description: "internal server error" diff --git a/httpapi/httpapi.go b/httpapi/httpapi.go index 01f855e..154a6e6 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/subscriptions", h.listSubscriptions) router.GET("/v1/spaces/:space/subscriptions/*id", h.getSubscription) router.POST("/v1/spaces/:space/subscriptions", h.createSubscription) + router.PUT("/v1/spaces/:space/subscriptions/*id", h.updateSubscription) router.DELETE("/v1/spaces/:space/subscriptions/*id", h.deleteSubscription) } @@ -253,6 +254,45 @@ func (h HTTPAPI) createSubscription(w http.ResponseWriter, r *http.Request, para metricConfigRequests.WithLabelValues(s.Space, "subscription", "create").Inc() } +func (h HTTPAPI) updateSubscription(w http.ResponseWriter, r *http.Request, params httprouter.Params) { + w.Header().Set("Content-Type", "application/json") + encoder := json.NewEncoder(w) + + s := &subscription.Subscription{} + dec := json.NewDecoder(r.Body) + err := dec.Decode(s) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + validationErr := subscription.ErrSubscriptionValidation{Message: err.Error()} + encoder.Encode(&Response{Errors: []Error{{Message: validationErr.Error()}}}) + return + } + + s.Space = params.ByName("space") + s.ID = extractSubscriptionID(r.URL.RawPath) + output, err := h.Subscriptions.UpdateSubscription(s.ID, s) + if err != nil { + if _, ok := err.(*subscription.ErrInvalidSubscriptionUpdate); ok { + w.WriteHeader(http.StatusBadRequest) + } else if _, ok := err.(*subscription.ErrSubscriptionNotFound); ok { + w.WriteHeader(http.StatusNotFound) + } else if _, ok := err.(*function.ErrFunctionNotFound); ok { + w.WriteHeader(http.StatusBadRequest) + } else if _, ok := err.(*subscription.ErrSubscriptionValidation); 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(s.Space, "subscription", "update").Inc() +} + func (h HTTPAPI) deleteSubscription(w http.ResponseWriter, r *http.Request, params httprouter.Params) { w.Header().Set("Content-Type", "application/json") encoder := json.NewEncoder(w) diff --git a/httpapi/httpapi_test.go b/httpapi/httpapi_test.go index 3e0199c..1dcc211 100644 --- a/httpapi/httpapi_test.go +++ b/httpapi/httpapi_test.go @@ -11,6 +11,7 @@ import ( "github.com/golang/mock/gomock" "github.com/julienschmidt/httprouter" "github.com/serverless/event-gateway/function" + "github.com/serverless/event-gateway/subscription" "github.com/serverless/event-gateway/httpapi" "github.com/serverless/event-gateway/mock" "github.com/stretchr/testify/assert" @@ -305,6 +306,225 @@ func TestDeleteFunction_OK(t *testing.T) { assert.Equal(t, http.StatusNoContent, resp.Code) } +func TestUpdateSubscription_OK(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + router, _, subscriptions := setup(ctrl) + + returned := &subscription.Subscription{ + Space: "default", + ID: subscription.ID("http,GET,%2F"), + Event: "http", + FunctionID: "func", + Method: "GET", + Path: "/", + CORS: &subscription.CORS{ + Origins: []string{"*"}, + Methods: []string{"HEAD", "GET", "POST"}, + Headers: []string{"Origin", "Accept", "Content-Type"}, + AllowCredentials: false, + }, + } + subscriptions.EXPECT().UpdateSubscription(subscription.ID("http,GET,%2F"), returned).Return(returned, nil) + + resp := httptest.NewRecorder() + payload := bytes.NewReader([]byte(` + {"space":"default","subscriptionId":"http,GET,%2F","event":"http","functionId":"func","method":"GET","path":"/","cors":{"origins":["*"],"methods":["HEAD","GET","POST"],"headers":["Origin","Accept","Content-Type"],"allowCredentials":false}} + `)) + req, _ := http.NewRequest(http.MethodPut, "/v1/spaces/default/subscriptions/http,GET,%2F", payload) + router.ServeHTTP(resp, req) + + sub := &subscription.Subscription{} + json.Unmarshal(resp.Body.Bytes(), sub) + assert.Equal(t, http.StatusOK, resp.Code) + assert.Equal(t, "default", sub.Space) + assert.Equal(t, subscription.ID("http,GET,%2F"), sub.ID) +} + +func TestUpdateSubscription_InvalidJSON(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + router, _, _:= setup(ctrl) + + resp := httptest.NewRecorder() + payload := bytes.NewReader([]byte(`{"name":"te`)) + req, _ := http.NewRequest(http.MethodPut, "/v1/spaces/default/subscriptions/http,GET,%2F", payload) + router.ServeHTTP(resp, req) + + sub := &subscription.Subscription{} + json.Unmarshal(resp.Body.Bytes(), sub) + assert.Equal(t, http.StatusBadRequest, resp.Code) +} + +func TestUpdateSubscription_InvalidSubscriptionUpdate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + router, _, subscriptions := setup(ctrl) + + input := &subscription.Subscription{ + Space: "default", + ID: subscription.ID("http,GET,%2F"), + Event: "http", + FunctionID: "func2", + Method: "GET", + Path: "/", + CORS: &subscription.CORS{ + Origins: []string{"*"}, + Methods: []string{"HEAD", "GET", "POST"}, + Headers: []string{"Origin", "Accept", "Content-Type"}, + AllowCredentials: false, + }, + } + subscriptions.EXPECT().UpdateSubscription(subscription.ID("http,GET,%2F"), input).Return(nil, &subscription.ErrInvalidSubscriptionUpdate{Field: "FunctionID"}) + + resp := httptest.NewRecorder() + payload := bytes.NewReader([]byte(` + {"space":"default","subscriptionId":"http,GET,%2F","event":"http","functionId":"func2","method":"GET","path":"/","cors":{"origins":["*"],"methods":["HEAD","GET","POST"],"headers":["Origin","Accept","Content-Type"],"allowCredentials":false}} + `)) + req, _ := http.NewRequest(http.MethodPut, "/v1/spaces/default/subscriptions/http,GET,%2F", payload) + router.ServeHTTP(resp, req) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusBadRequest, resp.Code) + assert.Equal(t, `Invalid update. 'FunctionID' of existing subscription cannot be updated.`, httpresp.Errors[0].Message) +} + +func TestUpdateSubscription_SubscriptionNotFound(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + router, _, subscriptions := setup(ctrl) + + input := &subscription.Subscription{ + Space: "default", + ID: subscription.ID("http,GET,%2F"), + Event: "http", + FunctionID: "func", + Method: "GET", + Path: "/", + CORS: &subscription.CORS{ + Origins: []string{"*"}, + Methods: []string{"HEAD", "GET", "POST"}, + Headers: []string{"Origin", "Accept", "Content-Type"}, + AllowCredentials: false, + }, + } + subscriptions.EXPECT().UpdateSubscription(subscription.ID("http,GET,%2F"), input).Return(nil, &subscription.ErrSubscriptionNotFound{ID: subscription.ID("http,GET,%2F")}) + + resp := httptest.NewRecorder() + payload := bytes.NewReader([]byte(` + {"space":"default","subscriptionId":"http,GET,%2F","event":"http","functionId":"func","method":"GET","path":"/","cors":{"origins":["*"],"methods":["HEAD","GET","POST"],"headers":["Origin","Accept","Content-Type"],"allowCredentials":false}} + `)) + req, _ := http.NewRequest(http.MethodPut, "/v1/spaces/default/subscriptions/http,GET,%2F", payload) + router.ServeHTTP(resp, req) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusNotFound, resp.Code) + assert.Equal(t, `Subscription "http,GET,%2F" not found.`, httpresp.Errors[0].Message) +} + +func TestUpdateSubscription_FunctionNotFound(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + router, _, subscriptions := setup(ctrl) + + input := &subscription.Subscription{ + Space: "default", + ID: subscription.ID("http,GET,%2F"), + Event: "http", + FunctionID: "func", + Method: "GET", + Path: "/", + CORS: &subscription.CORS{ + Origins: []string{"*"}, + Methods: []string{"HEAD", "GET", "POST"}, + Headers: []string{"Origin", "Accept", "Content-Type"}, + AllowCredentials: false, + }, + } + subscriptions.EXPECT().UpdateSubscription(subscription.ID("http,GET,%2F"), input).Return(nil, &function.ErrFunctionNotFound{ID: function.ID("func")}) + + resp := httptest.NewRecorder() + payload := bytes.NewReader([]byte(` + {"space":"default","subscriptionId":"http,GET,%2F","event":"http","functionId":"func","method":"GET","path":"/","cors":{"origins":["*"],"methods":["HEAD","GET","POST"],"headers":["Origin","Accept","Content-Type"],"allowCredentials":false}} + `)) + req, _ := http.NewRequest(http.MethodPut, "/v1/spaces/default/subscriptions/http,GET,%2F", payload) + router.ServeHTTP(resp, req) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusBadRequest, resp.Code) + assert.Equal(t, `Function "func" not found.`, httpresp.Errors[0].Message) +} + +func TestUpdateSubscription_SubscriptionValidationError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + router, _, subscriptions := setup(ctrl) + + input := &subscription.Subscription{ + Space: "default", + ID: subscription.ID("http,GET,%2F"), + FunctionID: "func", + Method: "GET", + Path: "/", + CORS: &subscription.CORS{ + Origins: []string{"*"}, + Methods: []string{"HEAD", "GET", "POST"}, + Headers: []string{"Origin", "Accept", "Content-Type"}, + AllowCredentials: false, + }, + } + subscriptions.EXPECT().UpdateSubscription(subscription.ID("http,GET,%2F"), input).Return(nil, &subscription.ErrSubscriptionValidation{Message: "" }) + + resp := httptest.NewRecorder() + payload := bytes.NewReader([]byte(` + {"space":"default","subscriptionId":"http,GET,%2F","functionId":"func","method":"GET","path":"/","cors":{"origins":["*"],"methods":["HEAD","GET","POST"],"headers":["Origin","Accept","Content-Type"],"allowCredentials":false}} + `)) + req, _ := http.NewRequest(http.MethodPut, "/v1/spaces/default/subscriptions/http,GET,%2F", payload) + router.ServeHTTP(resp, req) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusBadRequest, resp.Code) + assert.Contains(t, httpresp.Errors[0].Message, "Subscription doesn't validate. Validation error") +} + +func TestUpdateSubscription_InternalError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + router, _, subscriptions := setup(ctrl) + + input := &subscription.Subscription{ + Space: "default", + ID: subscription.ID("http,GET,%2F"), + Event: "http", + FunctionID: "func", + Method: "GET", + Path: "/", + CORS: &subscription.CORS{ + Origins: []string{"*"}, + Methods: []string{"HEAD", "GET", "POST"}, + Headers: []string{"Origin", "Accept", "Content-Type"}, + AllowCredentials: false, + }, + } + subscriptions.EXPECT().UpdateSubscription(subscription.ID("http,GET,%2F"), input).Return(nil, errors.New("processing failed")) + + resp := httptest.NewRecorder() + payload := bytes.NewReader([]byte(` + {"space":"default","subscriptionId":"http,GET,%2F","event":"http","functionId":"func","method":"GET","path":"/","cors":{"origins":["*"],"methods":["HEAD","GET","POST"],"headers":["Origin","Accept","Content-Type"],"allowCredentials":false}} + `)) + req, _ := http.NewRequest(http.MethodPut, "/v1/spaces/default/subscriptions/http,GET,%2F", payload) + router.ServeHTTP(resp, req) + + httpresp := &httpapi.Response{} + json.Unmarshal(resp.Body.Bytes(), httpresp) + assert.Equal(t, http.StatusInternalServerError, resp.Code) + assert.Equal(t, "processing failed", httpresp.Errors[0].Message) +} + func setup(ctrl *gomock.Controller) ( *httprouter.Router, *mock.MockFunctionService, diff --git a/libkv/subscription.go b/libkv/subscription.go index e3df50b..f5941f7 100644 --- a/libkv/subscription.go +++ b/libkv/subscription.go @@ -20,7 +20,7 @@ import ( // CreateSubscription creates subscription. func (service Service) CreateSubscription(s *subscription.Subscription) (*subscription.Subscription, error) { - err := service.validateSubscription(s) + err := validateSubscription(s) if err != nil { return nil, err } @@ -65,6 +65,48 @@ func (service Service) CreateSubscription(s *subscription.Subscription) (*subscr return s, nil } +// UpdateSubscription updates subscription. +func (service Service) UpdateSubscription(id subscription.ID, s *subscription.Subscription) (*subscription.Subscription, error) { + err := validateSubscription(s) + if err != nil { + return nil, err + } + + sub, err := service.GetSubscription(s.Space, id) + if err != nil { + return nil, err + } + + err = validateSubscriptionUpdate(s, sub) + if err != nil { + return nil, err + } + + f, err := service.GetFunction(s.Space, s.FunctionID) + if err != nil { + return nil, err + } + if f == nil { + return nil, &function.ErrFunctionNotFound{ID: s.FunctionID} + } + + buf, err := json.Marshal(s) + if err != nil { + return nil, err + } + + err = service.SubscriptionStore.Put(subscriptionPath(s.Space, s.ID), buf, nil) + if err != nil { + return nil, err + } + + service.Log.Debug("Subscription updated.", + zap.String("event", string(s.Event)), + zap.String("space", s.Space), + zap.String("functionId", string(s.FunctionID))) + return s, nil +} + // DeleteSubscription deletes subscription. func (service Service) DeleteSubscription(space string, id subscription.ID) error { sub, err := service.GetSubscription(space, id) @@ -184,7 +226,7 @@ func (service Service) deleteEndpoint(space, method, path string) error { return nil } -func (service Service) validateSubscription(s *subscription.Subscription) error { +func validateSubscription(s *subscription.Subscription) error { if s.Space == "" { s.Space = defaultSpace } @@ -300,3 +342,20 @@ func urlPathValidator(fl validator.FieldLevel) bool { func eventTypeValidator(fl validator.FieldLevel) bool { return regexp.MustCompile(`^[a-zA-Z0-9\.\-_]+$`).MatchString(fl.Field().String()) } + +func validateSubscriptionUpdate(newSub *subscription.Subscription, oldSub *subscription.Subscription) error { + if newSub.Event != oldSub.Event { + return &subscription.ErrInvalidSubscriptionUpdate{Field: "Event"} + } + if newSub.FunctionID != oldSub.FunctionID { + return &subscription.ErrInvalidSubscriptionUpdate{Field: "FunctionID"} + } + if newSub.Path != oldSub.Path { + return &subscription.ErrInvalidSubscriptionUpdate{Field: "Path"} + } + if newSub.Method != oldSub.Method { + return &subscription.ErrInvalidSubscriptionUpdate{Field: "Method"} + } + + return nil +} diff --git a/libkv/subscription_test.go b/libkv/subscription_test.go index 62bb218..18d2448 100644 --- a/libkv/subscription_test.go +++ b/libkv/subscription_test.go @@ -169,6 +169,93 @@ func TestCreateSubscription_PutError(t *testing.T) { assert.EqualError(t, err, "KV Put err") } +func TestUpdateSubscription_OK(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionsDB := mock.NewMockStore(ctrl) + subscriptionsDB.EXPECT().Get("default/http,GET,%2F", &store.ReadOptions{Consistent: true}).Return(&store.KVPair{Value: []byte(`{"space":"default","subscriptionId":"http,GET,%2F","event":"http","functionId":"func","method":"GET","path":"/"}`)}, nil) + subscriptionsDB.EXPECT().Put("default/http,GET,%2F", []byte(`{"space":"default","subscriptionId":"http,GET,%2F","event":"http","functionId":"func","method":"GET","path":"/","cors":{"origins":["*"],"methods":["HEAD","GET","POST"],"headers":["Origin","Accept","Content-Type"],"allowCredentials":false}}`), nil).Return(nil) + endpointsDB := mock.NewMockStore(ctrl) + functionsDB := mock.NewMockStore(ctrl) + functionsDB.EXPECT().Get("default/func", &store.ReadOptions{Consistent: true}).Return(&store.KVPair{Value: []byte(`{"functionId":"func","type":"http","provider":{"url": "http://test.com"}}}`)}, nil) + subs := &Service{SubscriptionStore: subscriptionsDB, EndpointStore: endpointsDB, FunctionStore: functionsDB, Log: zap.NewNop()} + + _, err := subs.UpdateSubscription(subscription.ID("http,GET,%2F"), &subscription.Subscription{ID: "http,GET,%2F", Event: "http", FunctionID: "func", Method: "GET", Path: "/", CORS: &subscription.CORS{ Origins: []string{"*"} } }) + + assert.Nil(t, err) +} + +func TestUpdateSubscription_ValidationError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subs := &Service{Log: zap.NewNop()} + + _, err := subs.UpdateSubscription(subscription.ID("http,GET,%2F"), &subscription.Subscription{}) + + assert.Equal(t, err, &subscription.ErrSubscriptionValidation{Message: "Key: 'Subscription.Event' Error:Field validation for 'Event' failed on the 'required' tag\nKey: 'Subscription.FunctionID' Error:Field validation for 'FunctionID' failed on the 'required' tag"}) +} + +func TestUpdateSubscription_InvalidSubscriptionUpdate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionsDB := mock.NewMockStore(ctrl) + subscriptionsDB.EXPECT().Get("default/http,GET,%2F", &store.ReadOptions{Consistent: true}).Return(&store.KVPair{Value: []byte(`{"space":"default","subscriptionId":"http,GET,%2F","event":"http","functionId":"func","method":"GET","path":"/"}`)}, nil) + subs := &Service{SubscriptionStore: subscriptionsDB, Log: zap.NewNop()} + _, err := subs.UpdateSubscription(subscription.ID("http,GET,%2F"), &subscription.Subscription{ID: "http,GET,%2F", Event: "http", FunctionID: "func2", Method: "GET", Path: "/", CORS: &subscription.CORS{ Origins: []string{"*"} } }) + + assert.Equal(t, err, &subscription.ErrInvalidSubscriptionUpdate{Field: "FunctionID"}) +} + +func TestUpdateSubscription_SubscriptionNotFound(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionsDB := mock.NewMockStore(ctrl) + subscriptionsDB.EXPECT().Get("default/http,GET,%2F", &store.ReadOptions{Consistent: true}).Return(nil, errors.New("Key not found in store")) + endpointsDB := mock.NewMockStore(ctrl) + functionsDB := mock.NewMockStore(ctrl) + subs := &Service{SubscriptionStore: subscriptionsDB, EndpointStore: endpointsDB, FunctionStore: functionsDB, Log: zap.NewNop()} + _, err := subs.UpdateSubscription(subscription.ID("http,GET,%2F"), &subscription.Subscription{ID: "http,GET,%2F", Event: "http", FunctionID: "func", Method: "GET", Path: "/", CORS: &subscription.CORS{ Origins: []string{"*"} } }) + + assert.Equal(t, err, &subscription.ErrSubscriptionNotFound{ID: "http,GET,%2F"}) +} + +func TestUpdateSubscription_FunctionNotFound(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionsDB := mock.NewMockStore(ctrl) + subscriptionsDB.EXPECT().Get("default/http,GET,%2F", &store.ReadOptions{Consistent: true}).Return(&store.KVPair{Value: []byte(`{"space":"default","subscriptionId":"http,GET,%2F","event":"http","functionId":"func","method":"GET","path":"/"}`)}, nil) + endpointsDB := mock.NewMockStore(ctrl) + functionsDB := mock.NewMockStore(ctrl) + functionsDB.EXPECT().Get("default/func", &store.ReadOptions{Consistent: true}).Return(nil, errors.New("Key not found in store")) + subs := &Service{SubscriptionStore: subscriptionsDB, EndpointStore: endpointsDB, FunctionStore: functionsDB, Log: zap.NewNop()} + + _, err := subs.UpdateSubscription(subscription.ID("http,GET,%2F"), &subscription.Subscription{ID: "http,GET,%2F", Event: "http", FunctionID: "func", Method: "GET", Path: "/", CORS: &subscription.CORS{ Origins: []string{"*"} } }) + + assert.EqualError(t, err, "Function \"func\" not found.") +} + +func TestUpdateSubscription_PutError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + subscriptionsDB := mock.NewMockStore(ctrl) + subscriptionsDB.EXPECT().Get("default/http,GET,%2F", &store.ReadOptions{Consistent: true}).Return(&store.KVPair{Value: []byte(`{"space":"default","subscriptionId":"http,GET,%2F","event":"http","functionId":"func","method":"GET","path":"/"}`)}, nil) + subscriptionsDB.EXPECT().Put("default/http,GET,%2F", []byte(`{"space":"default","subscriptionId":"http,GET,%2F","event":"http","functionId":"func","method":"GET","path":"/","cors":{"origins":["*"],"methods":["HEAD","GET","POST"],"headers":["Origin","Accept","Content-Type"],"allowCredentials":false}}`), nil).Return(errors.New("KV Put err")) + endpointsDB := mock.NewMockStore(ctrl) + functionsDB := mock.NewMockStore(ctrl) + functionsDB.EXPECT().Get("default/func", &store.ReadOptions{Consistent: true}).Return(&store.KVPair{Value: []byte(`{"functionId":"func","type":"http","provider":{"url": "http://test.com"}}}`)}, nil) + subs := &Service{SubscriptionStore: subscriptionsDB, EndpointStore: endpointsDB, FunctionStore: functionsDB, Log: zap.NewNop()} + + _, err := subs.UpdateSubscription(subscription.ID("http,GET,%2F"), &subscription.Subscription{ID: "http,GET,%2F", Event: "http", FunctionID: "func", Method: "GET", Path: "/", CORS: &subscription.CORS{ Origins: []string{"*"} } }) + + assert.EqualError(t, err, "KV Put err") +} + func TestDeleteSubscription_OK(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() diff --git a/mock/subscription.go b/mock/subscription.go index 0bc5ffa..8f50ea2 100644 --- a/mock/subscription.go +++ b/mock/subscription.go @@ -83,3 +83,16 @@ func (m *MockSubscriptionService) GetSubscriptions(arg0 string) (subscription.Su func (mr *MockSubscriptionServiceMockRecorder) GetSubscriptions(arg0 interface{}) *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetSubscriptions", reflect.TypeOf((*MockSubscriptionService)(nil).GetSubscriptions), arg0) } + +// UpdateSubscription mocks base method +func (m *MockSubscriptionService) UpdateSubscription(arg0 subscription.ID, arg1 *subscription.Subscription) (*subscription.Subscription, error) { + ret := m.ctrl.Call(m, "UpdateSubscription", arg0, arg1) + ret0, _ := ret[0].(*subscription.Subscription) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateSubscription indicates an expected call of UpdateSubscription +func (mr *MockSubscriptionServiceMockRecorder) UpdateSubscription(arg0, arg1 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateSubscription", reflect.TypeOf((*MockSubscriptionService)(nil).UpdateSubscription), arg0, arg1) +} diff --git a/subscription/errors.go b/subscription/errors.go index 135e9e7..d490133 100644 --- a/subscription/errors.go +++ b/subscription/errors.go @@ -22,6 +22,15 @@ func (e ErrSubscriptionAlreadyExists) Error() string { return fmt.Sprintf("Subscription %q already exists.", e.ID) } +// ErrInvalidSubscriptionUpdate occurs when a client tries to update a subscription in a way that changes the subscription ID. +type ErrInvalidSubscriptionUpdate struct { + Field string +} + +func (e ErrInvalidSubscriptionUpdate) Error() string { + return fmt.Sprintf("Invalid update. '%v' of existing subscription cannot be updated.", e.Field) +} + // ErrSubscriptionValidation occurs when subscription payload doesn't validate. type ErrSubscriptionValidation struct { Message string diff --git a/subscription/service.go b/subscription/service.go index 0490b48..1a9e36b 100644 --- a/subscription/service.go +++ b/subscription/service.go @@ -3,6 +3,7 @@ package subscription // Service represents service for managing subscriptions. type Service interface { CreateSubscription(s *Subscription) (*Subscription, error) + UpdateSubscription(id ID, s *Subscription) (*Subscription, error) GetSubscription(space string, id ID) (*Subscription, error) GetSubscriptions(space string) (Subscriptions, error) DeleteSubscription(space string, id ID) error