mirror of
https://github.com/absmach/magistrala.git
synced 2026-06-23 04:10:28 +00:00
NOISSUE - Add property based testing to certs API (#2096)
Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com>
This commit is contained in:
@@ -33,6 +33,7 @@ env:
|
||||
INVITATIONS_URL: http://localhost:9020
|
||||
AUTH_URL: http://localhost:8189
|
||||
BOOTSTRAP_URL: http://localhost:9013
|
||||
CERTS_URL: http://localhost:9019
|
||||
|
||||
jobs:
|
||||
api-test:
|
||||
@@ -170,6 +171,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 Certs API tests
|
||||
if: steps.changes.outputs.certs == 'true'
|
||||
uses: schemathesis/action@v1
|
||||
with:
|
||||
schema: api/openapi/certs.yml
|
||||
base-url: ${{ env.CERTS_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"
|
||||
|
||||
@@ -155,6 +155,7 @@ test_api_things: TEST_API_URL := http://localhost:9000
|
||||
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):
|
||||
$(call test_api_service,$(@),$(TEST_API_URL))
|
||||
|
||||
+58
-27
@@ -29,6 +29,7 @@ tags:
|
||||
paths:
|
||||
/certs:
|
||||
post:
|
||||
operationId: createCert
|
||||
summary: Creates a certificate for thing
|
||||
description: Creates a certificate for thing
|
||||
tags:
|
||||
@@ -36,16 +37,23 @@ paths:
|
||||
requestBody:
|
||||
$ref: "#/components/requestBodies/CertReq"
|
||||
responses:
|
||||
'201':
|
||||
"201":
|
||||
description: Created
|
||||
'400':
|
||||
"400":
|
||||
description: Failed due to malformed JSON.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'500':
|
||||
description: Unexpected server-side error ocurred.
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"415":
|
||||
description: Missing or invalid content type.
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/certs/{certID}:
|
||||
get:
|
||||
operationId: getCert
|
||||
summary: Retrieves a certificate
|
||||
description: |
|
||||
Retrieves a certificate for a given cert ID.
|
||||
@@ -54,18 +62,23 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/CertID"
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
$ref: "#/components/responses/CertRes"
|
||||
'400':
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: |
|
||||
Failed to retrieve corresponding certificate.
|
||||
'500':
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
delete:
|
||||
operationId: revokeCert
|
||||
summary: Revokes a certificate
|
||||
description: |
|
||||
Revokes a certificate for a given cert ID.
|
||||
@@ -74,17 +87,22 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/CertID"
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
$ref: "#/components/responses/RevokeRes"
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: |
|
||||
Failed to revoke corresponding certificate.
|
||||
'500':
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/serials/{thingID}:
|
||||
get:
|
||||
operationId: getSerials
|
||||
summary: Retrieves certificates' serial IDs
|
||||
description: |
|
||||
Retrieves a list of certificates' serial IDs for a given thing ID.
|
||||
@@ -93,16 +111,20 @@ paths:
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/ThingID"
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
$ref: "#/components/responses/SerialsPageRes"
|
||||
'400':
|
||||
"400":
|
||||
description: Failed due to malformed query parameters.
|
||||
"401":
|
||||
description: Missing or invalid access token provided.
|
||||
'404':
|
||||
"403":
|
||||
description: Failed to perform authorization over the entity.
|
||||
"404":
|
||||
description: |
|
||||
Failed to retrieve corresponding certificates.
|
||||
'500':
|
||||
"422":
|
||||
description: Database can't process request.
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
/health:
|
||||
get:
|
||||
@@ -110,9 +132,9 @@ paths:
|
||||
tags:
|
||||
- health
|
||||
responses:
|
||||
'200':
|
||||
"200":
|
||||
$ref: "#/components/responses/HealthRes"
|
||||
'500':
|
||||
"500":
|
||||
$ref: "#/components/responses/ServiceError"
|
||||
|
||||
components:
|
||||
@@ -210,9 +232,9 @@ components:
|
||||
requestBodies:
|
||||
CertReq:
|
||||
description: |
|
||||
Issues a certificate that is required for mTLS. To create a certificate for a thing
|
||||
provide a thing id, data identifying particular thing will be embedded into the Certificate.
|
||||
x509 and ECC certificates are supported when using when Vault is used as PKI.
|
||||
Issues a certificate that is required for mTLS. To create a certificate for a thing
|
||||
provide a thing id, data identifying particular thing will be embedded into the Certificate.
|
||||
x509 and ECC certificates are supported when using when Vault is used as PKI.
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
@@ -221,12 +243,12 @@ components:
|
||||
- thing_id
|
||||
- ttl
|
||||
properties:
|
||||
thing_id:
|
||||
type: string
|
||||
format: uuid
|
||||
ttl:
|
||||
type: string
|
||||
example: "10h"
|
||||
thing_id:
|
||||
type: string
|
||||
format: uuid
|
||||
ttl:
|
||||
type: string
|
||||
example: "10h"
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
@@ -237,6 +259,15 @@ components:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Cert"
|
||||
links:
|
||||
serial:
|
||||
operationId: getSerials
|
||||
parameters:
|
||||
thingID: $response.body#/thing_id
|
||||
delete:
|
||||
operationId: revokeCert
|
||||
parameters:
|
||||
certID: $response.body#/serial
|
||||
CertsPageRes:
|
||||
description: Certificates page.
|
||||
content:
|
||||
@@ -258,7 +289,7 @@ components:
|
||||
HealthRes:
|
||||
description: Service Health Check.
|
||||
content:
|
||||
application/json:
|
||||
application/health+json:
|
||||
schema:
|
||||
$ref: "./schemas/HealthInfo.yml"
|
||||
|
||||
|
||||
+6
-65
@@ -11,9 +11,9 @@ import (
|
||||
|
||||
"github.com/absmach/magistrala"
|
||||
"github.com/absmach/magistrala/certs"
|
||||
"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"
|
||||
@@ -31,7 +31,7 @@ const (
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
func MakeHandler(svc certs.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()
|
||||
@@ -40,26 +40,26 @@ func MakeHandler(svc certs.Service, logger *slog.Logger, instanceID string) http
|
||||
r.Post("/", otelhttp.NewHandler(kithttp.NewServer(
|
||||
issueCert(svc),
|
||||
decodeCerts,
|
||||
encodeResponse,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "issue").ServeHTTP)
|
||||
r.Get("/{certID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
viewCert(svc),
|
||||
decodeViewCert,
|
||||
encodeResponse,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "view").ServeHTTP)
|
||||
r.Delete("/{certID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
revokeCert(svc),
|
||||
decodeRevokeCerts,
|
||||
encodeResponse,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "revoke").ServeHTTP)
|
||||
})
|
||||
r.Get("/serials/{thingID}", otelhttp.NewHandler(kithttp.NewServer(
|
||||
listSerials(svc),
|
||||
decodeListCerts,
|
||||
encodeResponse,
|
||||
api.EncodeResponse,
|
||||
opts...,
|
||||
), "list_serials").ServeHTTP)
|
||||
|
||||
@@ -69,24 +69,6 @@ func MakeHandler(svc certs.Service, logger *slog.Logger, instanceID string) http
|
||||
return r
|
||||
}
|
||||
|
||||
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 decodeListCerts(_ context.Context, r *http.Request) (interface{}, error) {
|
||||
l, err := apiutil.ReadNumQuery[uint64](r, limitKey, defLimit)
|
||||
if err != nil {
|
||||
@@ -136,44 +118,3 @@ func decodeRevokeCerts(_ context.Context, r *http.Request) (interface{}, error)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
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.ErrUnsupportedContentType):
|
||||
w.WriteHeader(http.StatusUnsupportedMediaType)
|
||||
case errors.Contains(err, svcerr.ErrMalformedEntity),
|
||||
errors.Contains(err, apiutil.ErrMissingID),
|
||||
errors.Contains(err, apiutil.ErrMissingCertData),
|
||||
errors.Contains(err, apiutil.ErrInvalidCertData),
|
||||
errors.Contains(err, apiutil.ErrLimitSize):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, svcerr.ErrConflict):
|
||||
w.WriteHeader(http.StatusConflict)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,6 +134,8 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
|
||||
errors.Contains(err, apiutil.ErrInvalidAPIKey),
|
||||
errors.Contains(err, apiutil.ErrMissingName),
|
||||
errors.Contains(err, apiutil.ErrBootstrapState),
|
||||
errors.Contains(err, apiutil.ErrMissingCertData),
|
||||
errors.Contains(err, apiutil.ErrInvalidCertData),
|
||||
errors.Contains(err, apiutil.ErrInvalidQueryParams):
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
case errors.Contains(err, svcerr.ErrAuthentication),
|
||||
|
||||
@@ -105,7 +105,7 @@ func TestIssueCert(t *testing.T) {
|
||||
thingID: "ah",
|
||||
duration: "10h",
|
||||
token: validToken,
|
||||
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, certs.ErrFailedCertCreation), http.StatusInternalServerError),
|
||||
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, certs.ErrFailedCertCreation), http.StatusBadRequest),
|
||||
},
|
||||
{
|
||||
desc: "create new cert with thing id and empty duration",
|
||||
@@ -199,7 +199,7 @@ func TestViewCert(t *testing.T) {
|
||||
desc: "get non-existent cert",
|
||||
certID: "43",
|
||||
token: token,
|
||||
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, svcerr.ErrNotFound), http.StatusInternalServerError),
|
||||
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, svcerr.ErrNotFound), http.StatusNotFound),
|
||||
response: sdk.Subscription{},
|
||||
},
|
||||
{
|
||||
@@ -262,7 +262,7 @@ func TestViewCertByThing(t *testing.T) {
|
||||
desc: "get non-existent cert",
|
||||
thingID: "43",
|
||||
token: token,
|
||||
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, repoerr.ErrNotFound), http.StatusInternalServerError),
|
||||
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, repoerr.ErrNotFound), http.StatusNotFound),
|
||||
response: sdk.Subscription{},
|
||||
},
|
||||
{
|
||||
@@ -323,7 +323,7 @@ func TestRevokeCert(t *testing.T) {
|
||||
desc: "revoke non-existing cert",
|
||||
thingID: "2",
|
||||
token: token,
|
||||
err: errors.NewSDKErrorWithStatus(errors.Wrap(certs.ErrFailedCertRevocation, svcerr.ErrNotFound), http.StatusInternalServerError),
|
||||
err: errors.NewSDKErrorWithStatus(errors.Wrap(certs.ErrFailedCertRevocation, svcerr.ErrNotFound), http.StatusNotFound),
|
||||
},
|
||||
{
|
||||
desc: "revoke cert with empty token",
|
||||
@@ -341,7 +341,7 @@ func TestRevokeCert(t *testing.T) {
|
||||
desc: "revoke deleted cert",
|
||||
thingID: thingID,
|
||||
token: token,
|
||||
err: errors.NewSDKErrorWithStatus(errors.Wrap(certs.ErrFailedToRemoveCertFromDB, svcerr.ErrNotFound), http.StatusInternalServerError),
|
||||
err: errors.NewSDKErrorWithStatus(errors.Wrap(certs.ErrFailedToRemoveCertFromDB, svcerr.ErrNotFound), http.StatusNotFound),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user