From 19c1261e814425209553158e4d8cb2095075c032 Mon Sep 17 00:00:00 2001 From: b1ackd0t <28790446+rodneyosodo@users.noreply.github.com> Date: Mon, 29 Apr 2024 17:07:57 +0300 Subject: [PATCH] NOISSUE - Add property based testing to notifiers API (#2175) Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> --- .github/dependabot.yml | 2 +- .github/workflows/api-tests.yml | 26 ++++++- Makefile | 1 + api/openapi/http.yml | 10 +-- api/openapi/invitations.yml | 4 +- api/openapi/notifiers.yml | 43 +++++++++-- api/openapi/things.yml | 12 +-- api/openapi/twins.yml | 1 - api/openapi/users.yml | 6 +- cmd/smpp-notifier/main.go | 2 + consumers/notifiers/api/transport.go | 74 ++----------------- .../addons/smpp-notifier/docker-compose.yml | 2 +- docker/addons/vault/docker-compose.yml | 2 - internal/api/common.go | 2 + 14 files changed, 93 insertions(+), 94 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5f0bb38b56..ce6fd6f138 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ version: 2 updates: - package-ecosystem: "github-actions" - directory: "/" + directory: "./.github/workflows" schedule: interval: "weekly" diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index f321432098..1497654f58 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -41,6 +41,8 @@ env: MONGO_READER_URL: http://localhost:9007 POSTGRES_READER_URL: http://localhost:9009 TIMESCALE_READER_URL: http://localhost:9011 + SMPP_NOTIFIER_URL: http://localhost:9014 + SMTP_NOTIFIER_URL: http://localhost:9015 jobs: api-test: @@ -206,7 +208,7 @@ jobs: base-url: ${{ env.PROVISION_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' + args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links' - name: Seed Messages if: steps.changes.outputs.readers == 'true' @@ -264,6 +266,26 @@ jobs: report: false args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --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-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links' + + - name: Run SMTP Notifier API tests + if: steps.changes.outputs.notifiers == 'true' + uses: schemathesis/action@v1 + with: + schema: api/openapi/notifiers.yml + base-url: ${{ env.SMTP_NOTIFIER_URL }} + checks: all + report: false + args: '--header "Authorization: Bearer ${{ env.USER_TOKEN }}" --contrib-openapi-formats-uuid --hypothesis-suppress-health-check=filter_too_much --stateful=links' + - name: Stop containers if: always() - run: make run down args="-v" + run: make run down args="-v" && make run_addons down args="-v" diff --git a/Makefile b/Makefile index 30ddae8929..373392ecc4 100644 --- a/Makefile +++ b/Makefile @@ -159,6 +159,7 @@ 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_readers: TEST_API_URL := http://localhost:9009 # This can be the URL of any reader service. +test_api_notifiers: TEST_API_URL := http://localhost:9014 # This can be the URL of any notifier service. $(TEST_API): $(call test_api_service,$(@),$(TEST_API_URL)) diff --git a/api/openapi/http.yml b/api/openapi/http.yml index 7e8f6a2b7f..b755712263 100644 --- a/api/openapi/http.yml +++ b/api/openapi/http.yml @@ -25,7 +25,7 @@ tags: externalDocs: description: Find out more about messages url: https://docs.magistrala.abstractmachines.fr/ - + paths: /channels/{id}/messages: post: @@ -50,7 +50,7 @@ paths: description: Message discarded due to invalid channel id. "415": description: Message discarded due to invalid or missing content type. - '500': + "500": $ref: "#/components/responses/ServiceError" /health: get: @@ -58,9 +58,9 @@ paths: tags: - health responses: - '200': + "200": $ref: "#/components/responses/HealthRes" - '500': + "500": $ref: "#/components/responses/ServiceError" components: @@ -154,7 +154,7 @@ components: responses: ServiceError: description: Unexpected server-side error occurred. - + HealthRes: description: Service Health Check. content: diff --git a/api/openapi/invitations.yml b/api/openapi/invitations.yml index d31f1a9d59..8bc66efe06 100644 --- a/api/openapi/invitations.yml +++ b/api/openapi/invitations.yml @@ -47,7 +47,7 @@ paths: "401": description: Missing or invalid access token provided. "404": - description: A non-existent entity request. + description: A non-existent entity request. "409": description: Failed due to using an existing identity. "415": @@ -111,7 +111,7 @@ paths: "401": description: Missing or invalid access token provided. "404": - description: A non-existent entity request. + description: A non-existent entity request. "500": $ref: "#/components/responses/ServiceError" diff --git a/api/openapi/notifiers.yml b/api/openapi/notifiers.yml index d6e861f7e7..37fd400e47 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: https://docs.magistrala.abstractmachines.fr/ - + 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/api/openapi/things.yml b/api/openapi/things.yml index 33c71af38b..2f3de8bb9f 100644 --- a/api/openapi/things.yml +++ b/api/openapi/things.yml @@ -94,7 +94,7 @@ paths: description: | Missing or invalid access token provided. "403": - description: Failed to perform authorization over the entity. + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -182,7 +182,7 @@ paths: "404": description: Failed due to non existing thing. "409": - description: Failed due to using an existing identity. + description: Failed due to using an existing identity. "415": description: Missing or invalid content type. "422": @@ -299,7 +299,7 @@ paths: "404": description: A non-existent entity request. "409": - description: Failed due to already disabled thing. + description: Failed due to already disabled thing. "422": description: Database can't process request. "500": @@ -329,7 +329,7 @@ paths: "404": description: A non-existent entity request. "409": - description: Failed due to already enabled thing. + description: Failed due to already enabled thing. "422": description: Database can't process request. "500": @@ -595,7 +595,7 @@ paths: "404": description: A non-existent entity request. "409": - description: Failed due to already enabled channel. + description: Failed due to already enabled channel. "422": description: Database can't process request. "500": @@ -625,7 +625,7 @@ paths: "404": description: A non-existent entity request. "409": - description: Failed due to already disabled channel. + description: Failed due to already disabled channel. "422": description: Database can't process request. "500": diff --git a/api/openapi/twins.yml b/api/openapi/twins.yml index 78e9af3e18..04cfeccd50 100644 --- a/api/openapi/twins.yml +++ b/api/openapi/twins.yml @@ -312,7 +312,6 @@ components: twins: type: array minItems: 0 - uniqueItems: true items: $ref: "#/components/schemas/TwinResObj" total: diff --git a/api/openapi/users.yml b/api/openapi/users.yml index 1ec12a7e18..5c6643a561 100644 --- a/api/openapi/users.yml +++ b/api/openapi/users.yml @@ -57,7 +57,7 @@ paths: "415": description: Missing or invalid content type. "422": - description: Database can't process request. + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" @@ -572,6 +572,8 @@ paths: description: Missing or invalid access token provided. "403": description: Failed to perform authorization over the entity. + "404": + description: A non-existent entity request. "409": description: Failed due to using an existing identity. "415": @@ -695,6 +697,8 @@ paths: description: Missing or invalid access token provided. "403": description: Unauthorized access to group id. + "404": + description: A non-existent entity request. "500": $ref: "#/components/responses/ServiceError" diff --git a/cmd/smpp-notifier/main.go b/cmd/smpp-notifier/main.go index 6ae77b4d2f..e249738919 100644 --- a/cmd/smpp-notifier/main.go +++ b/cmd/smpp-notifier/main.go @@ -91,6 +91,8 @@ func main() { db, err := pgclient.Setup(dbConfig, *notifierpg.Migration()) if err != nil { logger.Error(err.Error()) + exitCode = 1 + return } defer db.Close() diff --git a/consumers/notifiers/api/transport.go b/consumers/notifiers/api/transport.go index a603d2dac0..e37a7788d6 100644 --- a/consumers/notifiers/api/transport.go +++ b/consumers/notifiers/api/transport.go @@ -12,9 +12,9 @@ import ( "github.com/absmach/magistrala" "github.com/absmach/magistrala/consumers/notifiers" + "github.com/absmach/magistrala/internal/api" "github.com/absmach/magistrala/internal/apiutil" "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 *slog.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 *slog.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/docker/addons/smpp-notifier/docker-compose.yml b/docker/addons/smpp-notifier/docker-compose.yml index 50a9f60b83..213eb1acf4 100644 --- a/docker/addons/smpp-notifier/docker-compose.yml +++ b/docker/addons/smpp-notifier/docker-compose.yml @@ -24,7 +24,7 @@ services: networks: - magistrala-base-net volumes: - - magistrala-smpp-notifier-volume:/var/lib/postgresql/datab + - magistrala-smpp-notifier-volume:/var/lib/postgresql/data smpp-notifier: image: magistrala/smpp-notifier:latest diff --git a/docker/addons/vault/docker-compose.yml b/docker/addons/vault/docker-compose.yml index 102ca168a3..8f380b4725 100644 --- a/docker/addons/vault/docker-compose.yml +++ b/docker/addons/vault/docker-compose.yml @@ -8,8 +8,6 @@ # from project root. Vault default port (8200) is exposed, so you can use Vault CLI tool for # vault inspection and administration, as well as access the UI. -version: '3.7' - networks: magistrala-base-net: diff --git a/internal/api/common.go b/internal/api/common.go index 23b92887cd..17703ab8f9 100644 --- a/internal/api/common.go +++ b/internal/api/common.go @@ -136,6 +136,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),