diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 55586fd9c..f81f9b982 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -52,6 +52,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: @@ -199,6 +200,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 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-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 818d613ca..81b270183 100644 --- a/Makefile +++ b/Makefile @@ -163,6 +163,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)) diff --git a/api/openapi/twins.yml b/api/openapi/twins.yml index 7398a2852..42204089f 100644 --- a/api/openapi/twins.yml +++ b/api/openapi/twins.yml @@ -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: http://docs.mainflux.io/ - 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" diff --git a/twins/api/http/transport.go b/twins/api/http/transport.go index ae79b7b12..88bc32195 100644 --- a/twins/api/http/transport.go +++ b/twins/api/http/transport.go @@ -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,67 +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) - } - } -}