NOISSUE - Add property based testing to twins API (#2098)

Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
This commit is contained in:
b1ackd0t
2024-04-16 11:39:32 +03:00
committed by GitHub
parent f6477ed868
commit f334ee2606
4 changed files with 118 additions and 137 deletions
+11
View File
@@ -34,6 +34,7 @@ env:
AUTH_URL: http://localhost:8189
BOOTSTRAP_URL: http://localhost:9013
CERTS_URL: http://localhost:9019
TWINS_URL: http://localhost:9018
jobs:
api-test:
@@ -181,6 +182,16 @@ 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 Twins API tests
if: steps.changes.outputs.twins == 'true'
uses: schemathesis/action@v1
with:
schema: api/openapi/twins.yml
base-url: ${{ env.TWINS_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"
+1
View File
@@ -156,6 +156,7 @@ test_api_invitations: TEST_API_URL := http://localhost:9020
test_api_auth: TEST_API_URL := http://localhost:8189
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):
$(call test_api_service,$(@),$(TEST_API_URL))
+98 -66
View File
@@ -18,7 +18,7 @@ info:
servers:
- url: http://localhost:9018
- url: https://localhost:9018
tags:
- name: twins
description: Everything about your Twins
@@ -26,10 +26,10 @@ tags:
description: Find out more about twins
url: https://docs.magistrala.abstractmachines.fr/
paths:
/twins:
post:
operationId: createTwin
summary: Adds new twin
description: |
Adds new twin to the list of twins owned by user identified using
@@ -39,18 +39,21 @@ paths:
requestBody:
$ref: "#/components/requestBodies/TwinReq"
responses:
'201':
"201":
$ref: "#/components/responses/TwinCreateRes"
'400':
"400":
description: Failed due to malformed JSON.
'401':
"401":
description: Missing or invalid access token provided.
'415':
"415":
description: Missing or invalid content type.
'500':
$ref: '#/components/responses/ServiceError'
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
get:
operationId: getTwins
summary: Retrieves twins
description: |
Retrieves a list of twins. Due to performance concerns, data
@@ -58,39 +61,45 @@ paths:
tags:
- twins
parameters:
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
- $ref: '#/components/parameters/Name'
- $ref: '#/components/parameters/Metadata'
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/Name"
- $ref: "#/components/parameters/Metadata"
responses:
'200':
$ref: '#/components/responses/TwinsPageRes'
'400':
"200":
$ref: "#/components/responses/TwinsPageRes"
"400":
description: Failed due to malformed query parameters.
'401':
"401":
description: Missing or invalid access token provided.
'500':
$ref: '#/components/responses/ServiceError'
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/twins/{twinID}:
get:
operationId: getTwin
summary: Retrieves twin info
tags:
- twins
parameters:
- $ref: '#/components/parameters/TwinID'
- $ref: "#/components/parameters/TwinID"
responses:
'200':
$ref: '#/components/responses/TwinRes'
'400':
"200":
$ref: "#/components/responses/TwinRes"
"400":
description: Failed due to malformed twin's ID.
'401':
"401":
description: Missing or invalid access token provided.
'404':
"404":
description: Twin does not exist.
'500':
$ref: '#/components/responses/ServiceError'
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
put:
operationId: updateTwin
summary: Updates twin info
description: |
Update is performed by replacing the current resource data with values
@@ -98,43 +107,51 @@ paths:
tags:
- twins
parameters:
- $ref: '#/components/parameters/TwinID'
- $ref: "#/components/parameters/TwinID"
requestBody:
$ref: '#/components/requestBodies/TwinReq'
$ref: "#/components/requestBodies/TwinReq"
responses:
'200':
"200":
description: Twin updated.
'400':
"400":
description: Failed due to malformed twin's ID or malformed JSON.
'401':
"401":
description: Missing or invalid access token provided.
'404':
"404":
description: Twin does not exist.
'415':
"415":
description: Missing or invalid content type.
'500':
$ref: '#/components/responses/ServiceError'
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
delete:
operationId: removeTwin
summary: Removes a twin
description: Removes a twin.
tags:
- twins
parameters:
- $ref: '#/components/parameters/TwinID'
- $ref: "#/components/parameters/TwinID"
responses:
'204':
"204":
description: Twin removed.
'400':
"400":
description: Failed due to malformed twin's ID.
'401':
"401":
description: Missing or invalid access token provided
'404':
"404":
description: Twin does not exist.
'500':
$ref: '#/components/responses/ServiceError'
"415":
description: Missing or invalid content type.
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/states/{twinID}:
get:
operationId: getStates
summary: Retrieves states of twin with id twinID
description: |
Retrieves a list of states. Due to performance concerns, data
@@ -142,29 +159,31 @@ paths:
tags:
- states
parameters:
- $ref: '#/components/parameters/TwinID'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Offset'
- $ref: "#/components/parameters/TwinID"
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
responses:
'200':
$ref: '#/components/responses/StatesPageRes'
'400':
"200":
$ref: "#/components/responses/StatesPageRes"
"400":
description: Failed due to malformed query parameters.
'401':
"401":
description: Missing or invalid access token provided.
'404':
"404":
description: Twin does not exist.
'500':
$ref: '#/components/responses/ServiceError'
"422":
description: Database can't process request.
"500":
$ref: "#/components/responses/ServiceError"
/health:
get:
summary: Retrieves service health check info.
tags:
- health
responses:
'200':
"200":
$ref: "#/components/responses/HealthRes"
'500':
"500":
$ref: "#/components/responses/ServiceError"
components:
@@ -242,7 +261,7 @@ components:
minItems: 0
uniqueItems: true
items:
$ref: '#/components/schemas/Attribute'
$ref: "#/components/schemas/Attribute"
TwinReqObj:
type: object
properties:
@@ -253,7 +272,7 @@ components:
type: object
description: Arbitrary, object-encoded twin's data.
definition:
$ref: '#/components/schemas/Definition'
$ref: "#/components/schemas/Definition"
TwinResObj:
type: object
properties:
@@ -283,7 +302,7 @@ components:
minItems: 0
uniqueItems: true
items:
$ref: '#/components/schemas/Definition'
$ref: "#/components/schemas/Definition"
metadata:
type: object
description: Arbitrary, object-encoded twin's data.
@@ -295,7 +314,7 @@ components:
minItems: 0
uniqueItems: true
items:
$ref: '#/components/schemas/TwinResObj'
$ref: "#/components/schemas/TwinResObj"
total:
type: integer
description: Total number of items.
@@ -327,12 +346,12 @@ components:
StatesPage:
type: object
properties:
twins:
states:
type: array
minItems: 0
uniqueItems: true
items:
$ref: '#/components/schemas/State'
$ref: "#/components/schemas/State"
total:
type: integer
description: Total number of items.
@@ -343,7 +362,7 @@ components:
type: integer
description: Maximum number of items to return in one page.
required:
- twins
- states
requestBodies:
TwinReq:
@@ -351,7 +370,7 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/TwinReqObj'
$ref: "#/components/schemas/TwinReqObj"
required: true
responses:
@@ -368,25 +387,38 @@ components:
content:
application/json:
schema:
$ref: '#/components/schemas/TwinResObj'
$ref: "#/components/schemas/TwinResObj"
links:
update:
operationId: updateTwin
parameters:
twinID: $response.body#/id
delete:
operationId: removeTwin
parameters:
twinID: $response.body#/id
states:
operationId: getStates
parameters:
twinID: $response.body#/id
TwinsPageRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: '#/components/schemas/TwinsPage'
$ref: "#/components/schemas/TwinsPage"
StatesPageRes:
description: Data retrieved.
content:
application/json:
schema:
$ref: '#/components/schemas/StatesPage'
$ref: "#/components/schemas/StatesPage"
ServiceError:
description: Unexpected server-side error occurred.
HealthRes:
description: Service Health Check.
content:
application/json:
application/health+json:
schema:
$ref: "./schemas/HealthInfo.yml"
+8 -71
View File
@@ -11,9 +11,9 @@ import (
"strings"
"github.com/absmach/magistrala"
"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/absmach/magistrala/twins"
"github.com/go-chi/chi/v5"
kithttp "github.com/go-kit/kit/transport/http"
@@ -34,7 +34,7 @@ const (
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc twins.Service, logger *slog.Logger, instanceID string) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
}
r := chi.NewRouter()
@@ -43,38 +43,38 @@ func MakeHandler(svc twins.Service, logger *slog.Logger, instanceID string) http
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
addTwinEndpoint(svc),
decodeTwinCreation,
encodeResponse,
api.EncodeResponse,
opts...,
), "add_twin").ServeHTTP)
r.Get("/", otelhttp.NewHandler(kithttp.NewServer(
listTwinsEndpoint(svc),
decodeList,
encodeResponse,
api.EncodeResponse,
opts...,
), "list_twins").ServeHTTP)
r.Put("/{twinID}", otelhttp.NewHandler(kithttp.NewServer(
updateTwinEndpoint(svc),
decodeTwinUpdate,
encodeResponse,
api.EncodeResponse,
opts...,
), "update_twin").ServeHTTP)
r.Get("/{twinID}", otelhttp.NewHandler(kithttp.NewServer(
viewTwinEndpoint(svc),
decodeView,
encodeResponse,
api.EncodeResponse,
opts...,
), "view_twin").ServeHTTP)
r.Delete("/{twinID}", otelhttp.NewHandler(kithttp.NewServer(
removeTwinEndpoint(svc),
decodeView,
encodeResponse,
api.EncodeResponse,
opts...,
), "remove_twin").ServeHTTP)
})
r.Get("/states/{twinID}", otelhttp.NewHandler(kithttp.NewServer(
listStatesEndpoint(svc),
decodeListStates,
encodeResponse,
api.EncodeResponse,
opts...,
), "list_states").ServeHTTP)
@@ -174,66 +174,3 @@ func decodeListStates(_ context.Context, r *http.Request) (interface{}, error) {
return req, nil
}
func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
w.Header().Set("Content-Type", contentType)
if ar, ok := response.(magistrala.Response); ok {
for k, v := range ar.Headers() {
w.Header().Set(k, v)
}
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.ErrAuthentication),
errors.Contains(err, apiutil.ErrBearerToken):
w.WriteHeader(http.StatusUnauthorized)
case errors.Contains(err, apiutil.ErrInvalidQueryParams):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(err, apiutil.ErrUnsupportedContentType):
w.WriteHeader(http.StatusUnsupportedMediaType)
case errors.Contains(err, svcerr.ErrMalformedEntity),
errors.Contains(err, apiutil.ErrMissingID),
errors.Contains(err, apiutil.ErrNameSize),
errors.Contains(err, apiutil.ErrLimitSize):
w.WriteHeader(http.StatusBadRequest)
case errors.Contains(err, svcerr.ErrNotFound):
w.WriteHeader(http.StatusNotFound)
case errors.Contains(err, svcerr.ErrConflict):
w.WriteHeader(http.StatusConflict)
case errors.Contains(err, svcerr.ErrCreateEntity),
errors.Contains(err, svcerr.ErrUpdateEntity),
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)
}
}
}