Skip to content

Commit

Permalink
Add property based testing to notifiers API using schemathesis
Browse files Browse the repository at this point in the history
Signed-off-by: Rodney Osodo <[email protected]>
  • Loading branch information
rodneyosodo committed Dec 15, 2023
1 parent 759c9c6 commit cf5c64d
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 73 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/api-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
43 changes: 37 additions & 6 deletions api/openapi/notifiers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -102,9 +128,9 @@ paths:
tags:
- health
responses:
'200':
"200":
$ref: "#/components/responses/HealthRes"
'500':
"500":
$ref: "#/components/responses/ServiceError"

components:
Expand Down Expand Up @@ -140,7 +166,7 @@ components:
contact:
type: string
example: [email protected]
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:
Expand Down Expand Up @@ -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:
Expand All @@ -240,7 +271,7 @@ components:
HealthRes:
description: Service Health Check.
content:
application/json:
application/health+json:
schema:
$ref: "./schemas/HealthInfo.yml"

Expand Down
74 changes: 7 additions & 67 deletions consumers/notifiers/api/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
Expand All @@ -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)
})
Expand Down Expand Up @@ -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)
}
}
}
2 changes: 2 additions & 0 deletions internal/api/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit cf5c64d

Please sign in to comment.