mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
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:
@@ -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"
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user