Skip to content
This repository has been archived by the owner on Oct 14, 2024. It is now read-only.

Commit

Permalink
Add property based testing to certs 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 18, 2023
1 parent b6742cf commit 57242a0
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 96 deletions.
11 changes: 11 additions & 0 deletions .github/workflows/api-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,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:
Expand Down Expand Up @@ -188,6 +189,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 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-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 @@ -160,6 +160,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))
Expand Down
85 changes: 58 additions & 27 deletions api/openapi/certs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,23 +29,31 @@ tags:
paths:
/certs:
post:
operationId: createCert
summary: Creates a certificate for thing
description: Creates a certificate for thing
tags:
- certs
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.
Expand All @@ -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.
Expand All @@ -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.
Expand All @@ -93,26 +111,30 @@ 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:
summary: Retrieves service health check info.
tags:
- health
responses:
'200':
"200":
$ref: "#/components/responses/HealthRes"
'500':
"500":
$ref: "#/components/responses/ServiceError"

components:
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -258,7 +289,7 @@ components:
HealthRes:
description: Service Health Check.
content:
application/json:
application/health+json:
schema:
$ref: "./schemas/HealthInfo.yml"

Expand Down
71 changes: 6 additions & 65 deletions certs/api/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import (

"github.com/absmach/magistrala"
"github.com/absmach/magistrala/certs"
"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 @@ -31,7 +31,7 @@ const (
// MakeHandler returns a HTTP handler for API endpoints.
func MakeHandler(svc certs.Service, logger mglog.Logger, instanceID string) http.Handler {
opts := []kithttp.ServerOption{
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, encodeError)),
kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)),
}

r := chi.NewRouter()
Expand All @@ -40,26 +40,26 @@ func MakeHandler(svc certs.Service, logger mglog.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)

Expand All @@ -69,24 +69,6 @@ func MakeHandler(svc certs.Service, logger mglog.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.ReadUintQuery(r, limitKey, defLimit)
if err != nil {
Expand Down Expand Up @@ -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)
}
}
}
2 changes: 2 additions & 0 deletions internal/api/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,8 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
errors.Contains(err, errors.ErrPasswordFormat),
errors.Contains(err, apiutil.ErrInvalidLevel),
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),
Expand Down
8 changes: 4 additions & 4 deletions pkg/sdk/go/certs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,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{},
},
{
Expand Down Expand Up @@ -266,7 +266,7 @@ func TestViewCertByThing(t *testing.T) {
desc: "get non-existent cert",
thingID: "43",
token: token,
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, errors.ErrNotFound), http.StatusInternalServerError),
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, errors.ErrNotFound), http.StatusNotFound),
response: sdk.Subscription{},
},
{
Expand Down Expand Up @@ -328,7 +328,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",
Expand All @@ -346,7 +346,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),
},
}

Expand Down

0 comments on commit 57242a0

Please sign in to comment.