-
Notifications
You must be signed in to change notification settings - Fork 97
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add UpdateSubscription endpoint #398
Changes from 8 commits
5244dd8
2cfe0cd
510b352
f36333b
468730c
0cbd9dc
059cc1a
1836901
43ffdbc
a678883
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -614,6 +614,43 @@ JSON object: | |
|
||
--- | ||
|
||
#### Update Subscription | ||
|
||
**Endpoint** | ||
|
||
`PUT <Configuration API URL>/v1/spaces/<space>/subscriptions/<subscription ID>` | ||
|
||
**Request** | ||
|
||
* `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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 404 if subscription not found |
||
* `400 Bad Request` on validation error | ||
|
||
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** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -185,6 +185,26 @@ 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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 404 missing |
||
$ref: '#/components/responses/ValidationError' | ||
500: | ||
$ref: '#/components/responses/Error' | ||
delete: | ||
summary: "Delete subscription" | ||
tags: | ||
|
@@ -468,6 +488,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" | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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.StatusBadRequest) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it should be 404 |
||
} 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) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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.StatusBadRequest, resp.Code) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 404 |
||
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, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add some info that those fields cannot be updated?