diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index bcb98ca7e..03ec48976 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -54,6 +54,7 @@ env: CERTS_URL: http://localhost:9019 TWINS_URL: http://localhost:9018 PROVISION_URL: http://localhost:9016 + SMPP_NOTIFIER_URL: http://localhost:9014 jobs: api-test: @@ -221,6 +222,16 @@ jobs: report: false args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-unique-data --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links' + - name: Run SMPP Notifier API tests + if: steps.changes.outputs.notifiers == 'true' + uses: schemathesis/action@v1 + with: + schema: api/openapi/notifiers.yml + base-url: ${{ env.SMPP_NOTIFIER_URL }} + checks: all + report: false + args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-unique-data --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links' + - name: Stop containers if: always() run: make run down args="-v" diff --git a/Makefile b/Makefile index c465fecbe..479b73352 100644 --- a/Makefile +++ b/Makefile @@ -163,6 +163,7 @@ test_api_bootstrap: TEST_API_URL := http://localhost:9013 test_api_certs: TEST_API_URL := http://localhost:9019 test_api_twins: TEST_API_URL := http://localhost:9018 test_api_provision: TEST_API_URL := http://localhost:9016 +test_api_notifiers: TEST_API_URL := http://localhost:9014 # Either smtp (http://localhost:9015) or smpp (http://localhost:9014) $(TEST_API): $(call test_api_service,$(@),$(TEST_API_URL)) diff --git a/api/openapi/notifiers.yml b/api/openapi/notifiers.yml index be9c9ffcf..462190a97 100644 --- a/api/openapi/notifiers.yml +++ b/api/openapi/notifiers.yml @@ -20,17 +20,18 @@ servers: - url: https://localhost:9014 - url: http://localhost:9015 - url: https://localhost:9015 - + tags: - name: notifiers description: Everything about your Notifiers externalDocs: description: Find out more about notifiers url: http://docs.mainflux.io/ - + paths: /subscriptions: post: + operationId: createSubscription summary: Create subscription description: Creates a new subscription give a topic and contact. tags: @@ -42,13 +43,18 @@ paths: $ref: "#/components/responses/Create" "400": description: Failed due to malformed JSON. + "403": + description: Failed to perform authorization over the entity. "409": description: Failed due to using an existing topic and contact. "415": description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" get: + operationId: listSubscriptions summary: List subscriptions description: List subscriptions given list parameters. tags: @@ -65,10 +71,17 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. + "404": + description: A non-existent entity request. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /subscriptions/{id}: get: + operationId: viewSubscription summary: Get subscription with the provided id description: Retrieves a subscription with the provided id. tags: @@ -80,9 +93,16 @@ paths: $ref: "#/components/responses/View" "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. + "404": + description: A non-existent entity request. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" delete: + operationId: removeSubscription summary: Delete subscription with the provided id description: Removes a subscription with the provided id. tags: @@ -94,6 +114,12 @@ paths: description: Subscription removed "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. + "404": + description: A non-existent entity request. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /health: @@ -102,9 +128,9 @@ paths: tags: - health responses: - '200': + "200": $ref: "#/components/responses/HealthRes" - '500': + "500": $ref: "#/components/responses/ServiceError" components: @@ -140,7 +166,7 @@ components: contact: type: string example: user@example.com - description: The contact of the user to which the notification will be sent. + description: The contact of the user to which the notification will be sent. Page: type: object properties: @@ -229,6 +255,11 @@ components: application/json: schema: $ref: "#/components/schemas/Subscription" + links: + delete: + operationId: removeSubscription + parameters: + id: $response.body#/id Page: description: Data retrieved. content: @@ -240,7 +271,7 @@ components: HealthRes: description: Service Health Check. content: - application/json: + application/health+json: schema: $ref: "./schemas/HealthInfo.yml" diff --git a/consumers/notifiers/api/transport.go b/consumers/notifiers/api/transport.go index e6138693e..842f674c2 100644 --- a/consumers/notifiers/api/transport.go +++ b/consumers/notifiers/api/transport.go @@ -11,10 +11,10 @@ import ( "github.com/absmach/magistrala" "github.com/absmach/magistrala/consumers/notifiers" + "github.com/absmach/magistrala/internal/api" "github.com/absmach/magistrala/internal/apiutil" mglog "github.com/absmach/magistrala/logger" "github.com/absmach/magistrala/pkg/errors" - svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/go-chi/chi/v5" kithttp "github.com/go-kit/kit/transport/http" "github.com/prometheus/client_golang/prometheus/promhttp" @@ -34,7 +34,7 @@ const ( // MakeHandler returns a HTTP handler for API endpoints. func MakeHandler(svc notifiers.Service, logger mglog.Logger, instanceID string) http.Handler { opts := []kithttp.ServerOption{ - kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)), + kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), } mux := chi.NewRouter() @@ -43,35 +43,35 @@ func MakeHandler(svc notifiers.Service, logger mglog.Logger, instanceID string) r.Post("/", otelhttp.NewHandler(kithttp.NewServer( createSubscriptionEndpoint(svc), decodeCreate, - encodeResponse, + api.EncodeResponse, opts..., ), "create").ServeHTTP) r.Get("/", otelhttp.NewHandler(kithttp.NewServer( listSubscriptionsEndpoint(svc), decodeList, - encodeResponse, + api.EncodeResponse, opts..., ), "list").ServeHTTP) r.Delete("/", otelhttp.NewHandler(kithttp.NewServer( deleteSubscriptionEndpint(svc), decodeSubscription, - encodeResponse, + api.EncodeResponse, opts..., ), "delete").ServeHTTP) r.Get("/{subID}", otelhttp.NewHandler(kithttp.NewServer( viewSubscriptionEndpint(svc), decodeSubscription, - encodeResponse, + api.EncodeResponse, opts..., ), "view").ServeHTTP) r.Delete("/{subID}", otelhttp.NewHandler(kithttp.NewServer( deleteSubscriptionEndpint(svc), decodeSubscription, - encodeResponse, + api.EncodeResponse, opts..., ), "delete").ServeHTTP) }) @@ -130,63 +130,3 @@ func decodeList(_ context.Context, r *http.Request) (interface{}, error) { return req, nil } - -func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { - if ar, ok := response.(magistrala.Response); ok { - for k, v := range ar.Headers() { - w.Header().Set(k, v) - } - w.Header().Set("Content-Type", contentType) - w.WriteHeader(ar.Code()) - - if ar.Empty() { - return nil - } - } - - return json.NewEncoder(w).Encode(response) -} - -func encodeError(_ context.Context, err error, w http.ResponseWriter) { - var wrapper error - if errors.Contains(err, apiutil.ErrValidation) { - wrapper, err = errors.Unwrap(err) - } - - switch { - case errors.Contains(err, svcerr.ErrMalformedEntity), - errors.Contains(err, apiutil.ErrInvalidContact), - errors.Contains(err, apiutil.ErrInvalidTopic), - errors.Contains(err, apiutil.ErrMissingID), - errors.Contains(err, apiutil.ErrInvalidQueryParams): - w.WriteHeader(http.StatusBadRequest) - case errors.Contains(err, svcerr.ErrNotFound): - w.WriteHeader(http.StatusNotFound) - case errors.Contains(err, svcerr.ErrAuthentication), - errors.Contains(err, apiutil.ErrBearerToken): - w.WriteHeader(http.StatusUnauthorized) - case errors.Contains(err, svcerr.ErrConflict): - w.WriteHeader(http.StatusConflict) - case errors.Contains(err, apiutil.ErrUnsupportedContentType): - w.WriteHeader(http.StatusUnsupportedMediaType) - - case errors.Contains(err, svcerr.ErrCreateEntity), - errors.Contains(err, svcerr.ErrViewEntity), - errors.Contains(err, svcerr.ErrRemoveEntity): - w.WriteHeader(http.StatusInternalServerError) - - default: - w.WriteHeader(http.StatusInternalServerError) - } - - if wrapper != nil { - err = errors.Wrap(wrapper, err) - } - - if errorVal, ok := err.(errors.Error); ok { - w.Header().Set("Content-Type", contentType) - if err := json.NewEncoder(w).Encode(errorVal); err != nil { - w.WriteHeader(http.StatusInternalServerError) - } - } -} diff --git a/internal/api/common.go b/internal/api/common.go index c0cb57eae..557ee0ead 100644 --- a/internal/api/common.go +++ b/internal/api/common.go @@ -132,6 +132,8 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) { errors.Contains(err, apiutil.ErrBootstrapState), errors.Contains(err, apiutil.ErrMissingCertData), errors.Contains(err, apiutil.ErrInvalidCertData), + errors.Contains(err, apiutil.ErrInvalidContact), + errors.Contains(err, apiutil.ErrInvalidTopic), errors.Contains(err, apiutil.ErrInvalidQueryParams): w.WriteHeader(http.StatusBadRequest) case errors.Contains(err, svcerr.ErrAuthentication),