From 83f43b2cd02cc596518ab3ab45739712088c9093 Mon Sep 17 00:00:00 2001 From: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> Date: Wed, 13 Dec 2023 15:39:30 +0300 Subject: [PATCH 1/8] Add property based testing to users API using schemathesis Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> --- .github/workflows/api-tests.yml | 54 ++++++ .gitignore | 3 + Makefile | 12 +- api/openapi/users.yml | 287 ++++++++++++++++++++++++++++- auth/postgres/domains.go | 20 +- auth/service.go | 24 +-- internal/api/common.go | 26 ++- internal/apiutil/errors.go | 6 +- internal/groups/api/responses.go | 4 +- internal/groups/postgres/groups.go | 16 +- internal/groups/service.go | 22 +-- internal/groups/service_test.go | 5 +- internal/postgres/common.go | 9 - invitations/api/endpoint_test.go | 2 +- invitations/api/requests_test.go | 11 +- invitations/invitations.go | 11 +- invitations/invitations_test.go | 11 +- invitations/service_test.go | 4 +- pkg/clients/postgres/clients.go | 12 +- pkg/clients/status.go | 4 - pkg/errors/repository/types.go | 9 + pkg/errors/service/types.go | 12 ++ pkg/errors/types.go | 6 + pkg/groups/errors.go | 3 - pkg/sdk/go/channels_test.go | 8 +- pkg/sdk/go/groups_test.go | 8 +- pkg/sdk/go/things_test.go | 8 +- pkg/sdk/go/tokens_test.go | 2 +- pkg/sdk/go/users_test.go | 13 +- things/service.go | 2 +- things/service_test.go | 8 +- users/api/clients.go | 32 ++-- users/api/responses.go | 4 +- users/postgres/clients.go | 4 +- users/service.go | 26 +-- users/service_test.go | 10 +- 36 files changed, 514 insertions(+), 184 deletions(-) create mode 100644 .github/workflows/api-tests.yml diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml new file mode 100644 index 000000000..98a313d6c --- /dev/null +++ b/.github/workflows/api-tests.yml @@ -0,0 +1,54 @@ +# Copyright (c) Abstract Machines +# SPDX-License-Identifier: Apache-2.0 + +name: Property Based Tests + +on: + push: + branches: + - main + pull_request: + branches: + - main + +env: + TOKEN_URL: http://localhost:9002/users/tokens/issue + USER_IDENTITY: admin@example.com + USER_SECRET: 12345678 + +jobs: + api-test: + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v4 + with: + go-version: 1.21.x + cache-dependency-path: "go.sum" + + - name: Build images + run: make all -j $(nproc) && make dockers_dev -j $(nproc) + + - name: Start containers + run: make run up args="-d" && sleep 10 + + - name: Set access token + run: | + export USER_TOKEN=$(curl -sSX POST $TOKEN_URL -H "Content-Type: application/json" -d "{\"identity\": \"$USER_IDENTITY\",\"secret\": \"$USER_SECRET\"}" | jq -r .access_token) + echo "USER_TOKEN=$USER_TOKEN" >> $GITHUB_ENV + + - name: Run Users API tests + uses: schemathesis/action@v1 + with: + schema: api/openapi/users.yml + base-url: http://localhost:9002 + 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/.gitignore b/.gitignore index 8d9ae5778..60286b531 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ tools/provision/mgconn.toml # coverage files coverage + +# Schemathesis +.hypothesis diff --git a/Makefile b/Makefile index d1de47292..dc6a953da 100644 --- a/Makefile +++ b/Makefile @@ -97,7 +97,7 @@ FILTERED_SERVICES = $(filter-out $(RUN_ADDON_ARGS), $(SERVICES)) all: $(SERVICES) -.PHONY: all $(SERVICES) dockers dockers_dev latest release run run_addons grpc_mtls_certs check_mtls check_certs +.PHONY: all $(SERVICES) dockers dockers_dev latest release run run_addons grpc_mtls_certs check_mtls check_certs test_api clean: rm -rf ${BUILD_DIR} @@ -129,6 +129,16 @@ test: mocks done go test -v --race -count 1 -tags test -coverprofile=coverage/coverage.out $$(go list ./... | grep -v 'consumers\|readers\|postgres\|internal\|opcua\|cmd') +test_api: + @which st > /dev/null || (echo "schemathesis not found, please install it from https://github.com/schemathesis/schemathesis#getting-started" && exit 1) + st run api/openapi/users.yml \ + --checks all \ + --base-url http://localhost:9002 \ + --header "Authorization: Bearer $(USER_TOKEN)" \ + --contrib-unique-data --contrib-openapi-formats-uuid \ + --hypothesis-suppress-health-check=filter_too_much \ + --stateful=links + proto: protoc -I. --go_out=. --go_opt=paths=source_relative pkg/messaging/*.proto protoc -I. --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative ./*.proto diff --git a/api/openapi/users.yml b/api/openapi/users.yml index e4dd97b3b..3c2bfb5dc 100644 --- a/api/openapi/users.yml +++ b/api/openapi/users.yml @@ -34,6 +34,7 @@ tags: paths: /users: post: + operationId: createUser tags: - Users summary: Registers user account @@ -49,14 +50,19 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "409": description: Failed due to using an existing identity. "415": description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" get: + operationId: listUsers tags: - Users summary: List users @@ -93,6 +99,7 @@ paths: /users/profile: get: + operationId: getProfile summary: Gets info on currently logged in user. description: | Gets info on currently logged in user. Info is obtained using @@ -113,6 +120,7 @@ paths: /users/{userID}: get: + operationId: getUser summary: Retrieves a user description: | Retrieves a specific user that is identifier by the user ID. @@ -129,6 +137,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -137,6 +147,7 @@ paths: $ref: "#/components/responses/ServiceError" patch: + operationId: updateUser summary: Updates name and metadata of the user. description: | Updates name and metadata of the user with provided ID. Name and metadata @@ -154,15 +165,24 @@ paths: $ref: "#/components/responses/UserRes" "400": description: Failed due to malformed JSON. + "403": + description: Failed to perform authorization over the entity. "404": description: Failed due to non existing user. "401": description: Missing or invalid access token provided. + "409": + description: Failed due to using an existing identity. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /users/{userID}/tags: patch: + operationId: updateUserTags summary: Updates tags the user. description: | Updates tags of the user with provided ID. Tags is updated using @@ -180,15 +200,22 @@ paths: $ref: "#/components/responses/UserRes" "400": description: Failed due to malformed JSON. + "403": + description: Failed to perform authorization over the entity. "404": description: Failed due to non existing user. "401": description: Missing or invalid access token provided. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /users/{userID}/identity: patch: + operationId: updateUserIdentity summary: Updates Identity of the user. description: | Updates identity of the user with provided ID. Identity is @@ -206,15 +233,24 @@ paths: $ref: "#/components/responses/UserRes" "400": description: Failed due to malformed JSON. + "403": + description: Failed to perform authorization over the entity. "404": description: Failed due to non existing user. "401": description: Missing or invalid access token provided. + "409": + description: Failed due to using an existing identity. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /users/{userID}/role: patch: + operationId: updateUserRole summary: Updates the user role. description: | Updates role for the user with provided ID. @@ -231,15 +267,22 @@ paths: $ref: "#/components/responses/UserRes" "400": description: Failed due to malformed JSON. + "403": + description: Failed to perform authorization over the entity. "404": description: Failed due to non existing user. "401": description: Missing or invalid access token provided. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /users/{userID}/disable: post: + operationId: disableUser summary: Disables a user description: | Disables a specific user that is identifier by the user ID. @@ -256,8 +299,14 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "409": + description: Failed due to already disabled user. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -265,6 +314,7 @@ paths: /users/{userID}/enable: post: + operationId: enableUser summary: Enables a user description: | Enables a specific user that is identifier by the user ID. @@ -281,8 +331,14 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "409": + description: Failed due to already enabled user. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -290,6 +346,7 @@ paths: /users/secret: patch: + operationId: updateUserSecret summary: Updates Secret of currently logged in user. description: | Updates secret of currently logged in user. Secret is updated using @@ -305,15 +362,20 @@ paths: $ref: "#/components/responses/UserRes" "400": description: Failed due to malformed JSON. - "404": - description: Failed due to non existing user. "401": description: Missing or invalid access token provided. + "404": + description: Failed due to non existing user. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /password/reset-request: post: + operationId: requestPasswordReset summary: User password reset request description: | Generates a reset token and sends and @@ -331,11 +393,14 @@ paths: description: Failed due to malformed JSON. "415": description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /password/reset: put: + operationId: resetPassword summary: User password reset endpoint description: | When user gets reset token, after he submitted @@ -350,13 +415,18 @@ paths: description: User link . "400": description: Failed due to malformed JSON. + "401": + description: Missing or invalid access token provided. "415": description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /groups/{groupID}/users: get: + operationId: listUsersInGroup tags: - Users summary: List users in a group @@ -383,6 +453,8 @@ paths: description: | Missing or invalid access token provided. This endpoint is available only for administrators. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -392,6 +464,7 @@ paths: /channels/{channelID}/users: get: + operationId: listUsersInChannel tags: - Users summary: List users in a channel @@ -418,6 +491,8 @@ paths: description: | Missing or invalid access token provided. This endpoint is available only for administrators. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -427,6 +502,7 @@ paths: /users/tokens/issue: post: + operationId: issueToken summary: Issue Token description: | Issue Access and Refresh Token used for authenticating into the system. @@ -437,8 +513,12 @@ paths: responses: "200": $ref: "#/components/responses/TokenRes" + "400": + description: Failed due to malformed JSON. "404": description: A non-existent entity request. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -446,6 +526,7 @@ paths: /users/tokens/refresh: post: + operationId: refreshToken summary: Refresh Token description: | Refreshes Access and Refresh Token used for authenticating into the system. @@ -456,8 +537,12 @@ paths: responses: "200": $ref: "#/components/responses/TokenRes" + "400": + description: Failed due to malformed JSON. "404": description: A non-existent entity request. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -465,6 +550,7 @@ paths: /groups: post: + operationId: createGroup tags: - Groups summary: Creates new group @@ -482,14 +568,19 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "409": description: Failed due to using an existing identity. "415": description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" get: + operationId: listGroups summary: Lists groups. description: | Lists groups up to a max level of hierarchy that can be fetched in one @@ -515,13 +606,18 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /groups/{groupID}: get: + operationId: getGroup summary: Gets group info. description: | Gets info on a group specified by id. @@ -538,12 +634,17 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" put: + operationId: updateGroup summary: Updates group data. description: | Updates Name, Description or Metadata of a group. @@ -562,8 +663,16 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. + "409": + description: Failed due to using an existing identity. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" delete: @@ -589,6 +698,7 @@ paths: /groups/{groupID}/children: get: + operationId: listChildren summary: List children of a certain group description: | Lists groups up to a max level of hierarchy that can be fetched in one @@ -615,13 +725,18 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /groups/{groupID}/parents: get: + operationId: listParents summary: List parents of a certain group description: | Lists groups up to a max level of hierarchy that can be fetched in one @@ -648,13 +763,18 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /groups/{groupID}/enable: post: + operationId: enableGroup summary: Enables a group description: | Enables a specific group that is identifier by the group ID. @@ -671,8 +791,14 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "409": + description: Failed due to already enabled group. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -680,6 +806,7 @@ paths: /groups/{groupID}/disable: post: + operationId: disableGroup summary: Disables a group description: | Disables a specific group that is identifier by the group ID. @@ -696,8 +823,14 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "409": + description: Failed due to already disabled group. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -705,6 +838,7 @@ paths: /groups/{groupID}/users/assign: post: + operationId: assignUser summary: Assigns a user to a group description: | Assigns a specific user to a group that is identifier by the group ID. @@ -723,8 +857,12 @@ paths: description: Failed due to malformed group's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -732,6 +870,7 @@ paths: /groups/{groupID}/users/unassign: post: + operationId: unassignUser summary: Unassigns a user to a group description: | Unassigns a specific user to a group that is identifier by the group ID. @@ -750,8 +889,12 @@ paths: description: Failed due to malformed group's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. + "415": + description: Missing or invalid content type. "422": description: Database can't process request. "500": @@ -759,6 +902,7 @@ paths: /channels/{memberID}/groups: get: + operationId: listGroupsInChannel summary: Get group associated with the member description: | Gets groups associated with the channel member specified by id. @@ -780,13 +924,18 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /users/{memberID}/groups: get: + operationId: listGroupsByUser summary: Get group associated with the member description: | Gets groups associated with the user member specified by id. @@ -808,8 +957,12 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Group does not exist. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /domains/{domainID}/users: @@ -845,6 +998,7 @@ paths: $ref: "#/components/responses/ServiceError" /health: get: + operationId: health summary: Retrieves service health check info. tags: - health @@ -1128,7 +1282,7 @@ components: required: - groups - total - - level + - offset MembersPage: type: object @@ -1212,7 +1366,7 @@ components: properties: role: type: string - enum: ["admin","user"] + enum: ["admin", "user"] example: user description: User role example. required: @@ -1351,6 +1505,9 @@ components: schema: type: string format: uuid + minLength: 36 + maxLength: 36 + pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" required: true example: bb7edb32-2eac-4aad-aebe-ed96fe073879 @@ -1369,6 +1526,7 @@ components: in: query schema: type: string + pattern: "^[^\u0000-\u001F]*$" required: false example: "admin@example.com" @@ -1429,6 +1587,9 @@ components: schema: type: string format: uuid + minLength: 36 + maxLength: 36 + pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" required: true example: bb7edb32-2eac-4aad-aebe-ed96fe073879 @@ -1439,6 +1600,9 @@ components: schema: type: string format: uuid + minLength: 36 + maxLength: 36 + pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" required: true example: bb7edb32-2eac-4aad-aebe-ed96fe073879 @@ -1449,6 +1613,9 @@ components: schema: type: string format: uuid + minLength: 36 + maxLength: 36 + pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" required: true example: bb7edb32-2eac-4aad-aebe-ed96fe073879 @@ -1667,6 +1834,43 @@ components: application/json: schema: $ref: "#/components/schemas/User" + links: + get: + operationId: getUser + parameters: + userID: $response.body#/id + get_groups: + operationId: listUsersInGroup + parameters: + groupID: $response.body#/id + get_channels: + operationId: listUsersInChannel + parameters: + channelID: $response.body#/id + update: + operationId: updateUser + parameters: + userID: $response.body#/id + update_tags: + operationId: updateUserTags + parameters: + userID: $response.body#/id + update_identity: + operationId: updateUserIdentity + parameters: + userID: $response.body#/id + update_role: + operationId: updateUserRole + parameters: + userID: $response.body#/id + disable: + operationId: disableUser + parameters: + userID: $response.body#/id + enable: + operationId: enableUser + parameters: + userID: $response.body#/id UserRes: description: Data retrieved. @@ -1674,6 +1878,15 @@ components: application/json: schema: $ref: "#/components/schemas/User" + links: + get_groups: + operationId: listUsersInGroup + parameters: + groupID: $response.body#/id + get_channels: + operationId: listUsersInChannel + parameters: + channelID: $response.body#/id UserPageRes: description: Data retrieved. @@ -1694,6 +1907,47 @@ components: application/json: schema: $ref: "#/components/schemas/Group" + links: + get: + operationId: getGroup + parameters: + groupID: $response.body#/id + get_children: + operationId: listChildren + parameters: + groupID: $response.body#/id + get_parent: + operationId: listParents + parameters: + groupID: $response.body#/id + get_channels: + operationId: listGroupsInChannel + parameters: + memberID: $response.body#/id + get_users: + operationId: listGroupsByUser + parameters: + memberID: $response.body#/id + update: + operationId: updateGroup + parameters: + groupID: $response.body#/id + disable: + operationId: disableGroup + parameters: + groupID: $response.body#/id + enable: + operationId: enableGroup + parameters: + groupID: $response.body#/id + assign: + operationId: assignUser + parameters: + groupID: $response.body#/id + unassign: + operationId: unassignUser + parameters: + groupID: $response.body#/id GroupRes: description: Data retrieved. @@ -1701,6 +1955,31 @@ components: application/json: schema: $ref: "#/components/schemas/Group" + links: + get_children: + operationId: listChildren + parameters: + groupID: $response.body#/id + get_parent: + operationId: listParents + parameters: + groupID: $response.body#/id + get_channels: + operationId: listGroupsInChannel + parameters: + memberID: $response.body#/id + get_users: + operationId: listGroupsByUser + parameters: + memberID: $response.body#/id + assign: + operationId: assignUser + parameters: + groupID: $response.body#/id + unassign: + operationId: unassignUser + parameters: + groupID: $response.body#/id GroupPageRes: description: Data retrieved. diff --git a/auth/postgres/domains.go b/auth/postgres/domains.go index b3fe87566..de4624c20 100644 --- a/auth/postgres/domains.go +++ b/auth/postgres/domains.go @@ -107,13 +107,13 @@ func (repo domainRepo) RetrievePermissions(ctx context.Context, subject, id stri rows, err := repo.db.QueryxContext(ctx, q, id, subject) if err != nil { - return []string{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return []string{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } defer rows.Close() domains, err := repo.processRows(rows) if err != nil { - return []string{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return []string{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } permissions := []string{} @@ -142,18 +142,18 @@ func (repo domainRepo) RetrieveAllByIDs(ctx context.Context, pm auth.Page) (auth dbPage, err := toDBClientsPage(pm) if err != nil { - return auth.DomainsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) if err != nil { - return auth.DomainsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } defer rows.Close() domains, err := repo.processRows(rows) if err != nil { - return auth.DomainsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } cq := "SELECT COUNT(*) FROM domains d" @@ -163,7 +163,7 @@ func (repo domainRepo) RetrieveAllByIDs(ctx context.Context, pm auth.Page) (auth total, err := postgres.Total(ctx, repo.db, cq, dbPage) if err != nil { - return auth.DomainsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } pm.Total = total @@ -199,18 +199,18 @@ func (repo domainRepo) ListDomains(ctx context.Context, pm auth.Page) (auth.Doma dbPage, err := toDBClientsPage(pm) if err != nil { - return auth.DomainsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) if err != nil { - return auth.DomainsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } defer rows.Close() domains, err := repo.processRows(rows) if err != nil { - return auth.DomainsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } cq := "SELECT COUNT(*) FROM domains d JOIN policies pc ON pc.object_id = d.id" @@ -220,7 +220,7 @@ func (repo domainRepo) ListDomains(ctx context.Context, pm auth.Page) (auth.Doma total, err := postgres.Total(ctx, repo.db, cq, dbPage) if err != nil { - return auth.DomainsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return auth.DomainsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } pm.Total = total diff --git a/auth/service.go b/auth/service.go index 8a1700a13..c3e29b1b8 100644 --- a/auth/service.go +++ b/auth/service.go @@ -18,27 +18,6 @@ import ( const recoveryDuration = 5 * time.Minute var ( - errRollbackPolicy = errors.New("failed to rollback policy") - errRemoveLocalPolicy = errors.New("failed to remove from local policy copy") - errRemovePolicyEngine = errors.New("failed to remove from policy engine") -) - -var ( - // ErrFailedToRetrieveMembers failed to retrieve group members. - ErrFailedToRetrieveMembers = errors.New("failed to retrieve group members") - - // ErrFailedToRetrieveMembership failed to retrieve memberships. - ErrFailedToRetrieveMembership = errors.New("failed to retrieve memberships") - - // ErrFailedToRetrieveAll failed to retrieve groups. - ErrFailedToRetrieveAll = errors.New("failed to retrieve all groups") - - // ErrFailedToRetrieveParents failed to retrieve groups. - ErrFailedToRetrieveParents = errors.New("failed to retrieve all groups") - - // ErrFailedToRetrieveChildren failed to retrieve groups. - ErrFailedToRetrieveChildren = errors.New("failed to retrieve all groups") - // ErrExpiry indicates that the token is expired. ErrExpiry = errors.New("token is expired") @@ -51,6 +30,9 @@ var ( errCreateDomainPolicy = errors.New("failed to create domain policy") errAddPolicies = errors.New("failed to add policies") errRemovePolicies = errors.New("failed to remove the policies") + errRollbackPolicy = errors.New("failed to rollback policy") + errRemoveLocalPolicy = errors.New("failed to remove from local policy copy") + errRemovePolicyEngine = errors.New("failed to remove from policy engine") ) // Authn specifies an API that must be fullfiled by the domain service diff --git a/internal/api/common.go b/internal/api/common.go index 0f5193f16..88c3b69bd 100644 --- a/internal/api/common.go +++ b/internal/api/common.go @@ -13,6 +13,7 @@ import ( "github.com/absmach/magistrala/internal/postgres" mgclients "github.com/absmach/magistrala/pkg/clients" "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/gofrs/uuid" ) @@ -118,20 +119,33 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) { errors.Contains(err, apiutil.ErrNameSize), errors.Contains(err, svcerr.ErrInvalidStatus), errors.Contains(err, apiutil.ErrInvalidIDFormat), - errors.Contains(err, apiutil.ErrInvalidQueryParams), + errors.Contains(err, apiutil.ErrValidation), errors.Contains(err, apiutil.ErrInvalidStatus), + errors.Contains(err, apiutil.ErrInvitationState), + errors.Contains(err, apiutil.ErrInvalidRole), + errors.Contains(err, apiutil.ErrMissingEmail), + errors.Contains(err, apiutil.ErrMissingHost), + errors.Contains(err, apiutil.ErrMissingIdentity), + errors.Contains(err, apiutil.ErrMissingSecret), + errors.Contains(err, apiutil.ErrMissingPass), + errors.Contains(err, apiutil.ErrMissingConfPass), + errors.Contains(err, apiutil.ErrInvalidResetPass), errors.Contains(err, apiutil.ErrMissingRelation), - errors.Contains(err, apiutil.ErrValidation): + errors.Contains(err, errors.ErrPasswordFormat), + errors.Contains(err, apiutil.ErrInvalidLevel), + errors.Contains(err, apiutil.ErrInvalidQueryParams): w.WriteHeader(http.StatusBadRequest) case errors.Contains(err, svcerr.ErrAuthentication), errors.Contains(err, errors.ErrAuthentication), + errors.Contains(err, errors.ErrLogin), errors.Contains(err, apiutil.ErrBearerToken): w.WriteHeader(http.StatusUnauthorized) case errors.Contains(err, svcerr.ErrNotFound): w.WriteHeader(http.StatusNotFound) case errors.Contains(err, svcerr.ErrConflict), errors.Contains(err, postgres.ErrMemberAlreadyAssigned), - errors.Contains(err, errors.ErrConflict): + errors.Contains(err, errors.ErrConflict), + errors.Contains(err, errors.ErrStatusAlreadyAssigned): w.WriteHeader(http.StatusConflict) case errors.Contains(err, svcerr.ErrAuthorization), errors.Contains(err, errors.ErrAuthorization), @@ -141,9 +155,13 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) { w.WriteHeader(http.StatusUnsupportedMediaType) case errors.Contains(err, svcerr.ErrCreateEntity), errors.Contains(err, svcerr.ErrUpdateEntity), + errors.Contains(err, svcerr.ErrFailedUpdateRole), errors.Contains(err, svcerr.ErrViewEntity), + errors.Contains(err, repoerr.ErrFailedToRetrieveAllGroups), + errors.Contains(err, repoerr.ErrAddPolicies), + errors.Contains(err, repoerr.ErrDeletePolicies), errors.Contains(err, svcerr.ErrRemoveEntity): - w.WriteHeader(http.StatusInternalServerError) + w.WriteHeader(http.StatusUnprocessableEntity) default: w.WriteHeader(http.StatusInternalServerError) } diff --git a/internal/apiutil/errors.go b/internal/apiutil/errors.go index bca1c710e..e2346edd2 100644 --- a/internal/apiutil/errors.go +++ b/internal/apiutil/errors.go @@ -114,12 +114,12 @@ var ( // ErrMissingRelation indicates missing relation. ErrMissingRelation = errors.New("missing relation") + // ErrInvalidRelation indicates an invalid relation. + ErrInvalidRelation = errors.New("invalid relation") + // ErrInvalidAPIKey indicates an invalid API key type. ErrInvalidAPIKey = errors.New("invalid api key type") - // ErrMaxLevelExceeded indicates an invalid group level. - ErrMaxLevelExceeded = errors.New("invalid group level (should be lower than 5)") - // ErrBootstrapState indicates an invalid bootstrap state. ErrBootstrapState = errors.New("invalid bootstrap state") diff --git a/internal/groups/api/responses.go b/internal/groups/api/responses.go index 08b4b314c..a9fbcc5ef 100644 --- a/internal/groups/api/responses.go +++ b/internal/groups/api/responses.go @@ -86,9 +86,9 @@ type groupPageRes struct { } type pageRes struct { - Limit uint64 `json:"limit"` + Limit uint64 `json:"limit,omitempty"` Offset uint64 `json:"offset"` - Total uint64 `json:"total,omitempty"` + Total uint64 `json:"total"` Level uint64 `json:"level,omitempty"` } diff --git a/internal/groups/postgres/groups.go b/internal/groups/postgres/groups.go index b653fb701..756d73164 100644 --- a/internal/groups/postgres/groups.go +++ b/internal/groups/postgres/groups.go @@ -160,17 +160,17 @@ func (repo groupRepository) RetrieveAll(ctx context.Context, gm mggroups.Page) ( dbPage, err := toDBGroupPage(gm) if err != nil { - return mggroups.Page{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) if err != nil { - return mggroups.Page{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } defer rows.Close() items, err := repo.processRows(rows) if err != nil { - return mggroups.Page{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } cq := "SELECT COUNT(*) FROM groups g" @@ -180,7 +180,7 @@ func (repo groupRepository) RetrieveAll(ctx context.Context, gm mggroups.Page) ( total, err := postgres.Total(ctx, repo.db, cq, dbPage) if err != nil { - return mggroups.Page{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } page := gm @@ -208,17 +208,17 @@ func (repo groupRepository) RetrieveByIDs(ctx context.Context, gm mggroups.Page, dbPage, err := toDBGroupPage(gm) if err != nil { - return mggroups.Page{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } rows, err := repo.db.NamedQueryContext(ctx, q, dbPage) if err != nil { - return mggroups.Page{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } defer rows.Close() items, err := repo.processRows(rows) if err != nil { - return mggroups.Page{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } cq := "SELECT COUNT(*) FROM groups g" @@ -228,7 +228,7 @@ func (repo groupRepository) RetrieveByIDs(ctx context.Context, gm mggroups.Page, total, err := postgres.Total(ctx, repo.db, cq, dbPage) if err != nil { - return mggroups.Page{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return mggroups.Page{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } page := gm diff --git a/internal/groups/service.go b/internal/groups/service.go index 8dd6d08f0..cf0c94ed2 100644 --- a/internal/groups/service.go +++ b/internal/groups/service.go @@ -13,6 +13,8 @@ import ( "github.com/absmach/magistrala/internal/apiutil" mgclients "github.com/absmach/magistrala/pkg/clients" "github.com/absmach/magistrala/pkg/errors" + repoerr "github.com/absmach/magistrala/pkg/errors/repository" + svcerr "github.com/absmach/magistrala/pkg/errors/service" "github.com/absmach/magistrala/pkg/groups" "golang.org/x/sync/errgroup" ) @@ -20,8 +22,6 @@ import ( var ( errParentUnAuthz = errors.New("failed to authorize parent group") errMemberKind = errors.New("invalid member kind") - errAddPolicies = errors.New("failed to add policies") - errDeletePolicies = errors.New("failed to delete policies") errRetrieveGroups = errors.New("failed to retrieve groups") errGroupIDs = errors.New("invalid group ids") ) @@ -284,10 +284,10 @@ func (svc service) checkSuperAdmin(ctx context.Context, userID string) error { Object: auth.MagistralaObject, }) if err != nil { - return err + return errors.Wrap(svcerr.ErrAuthorization, err) } if !res.Authorized { - return errors.ErrAuthorization + return svcerr.ErrAuthorization } return nil } @@ -445,7 +445,7 @@ func (svc service) Assign(ctx context.Context, token, groupID, relation, memberK } if _, err := svc.auth.AddPolicies(ctx, &policies); err != nil { - return errors.Wrap(errAddPolicies, err) + return errors.Wrap(repoerr.ErrAddPolicies, err) } return nil @@ -596,7 +596,7 @@ func (svc service) Unassign(ctx context.Context, token, groupID, relation, membe } if _, err := svc.auth.DeletePolicies(ctx, &policies); err != nil { - return errors.Wrap(errDeletePolicies, err) + return errors.Wrap(repoerr.ErrDeletePolicies, err) } return nil } @@ -694,7 +694,7 @@ func (svc service) changeGroupStatus(ctx context.Context, token string, group gr return groups.Group{}, err } if dbGroup.Status == group.Status { - return groups.Group{}, mgclients.ErrStatusAlreadyAssigned + return groups.Group{}, errors.ErrStatusAlreadyAssigned } group.UpdatedBy = id @@ -723,10 +723,10 @@ func (svc service) authorizeToken(ctx context.Context, subjectType, subject, per } res, err := svc.auth.Authorize(ctx, req) if err != nil { - return "", err + return "", errors.Wrap(svcerr.ErrAuthorization, err) } if !res.GetAuthorized() { - return "", errors.ErrAuthorization + return "", svcerr.ErrAuthorization } return res.GetId(), nil } @@ -743,10 +743,10 @@ func (svc service) authorizeKind(ctx context.Context, domainID, subjectType, sub } res, err := svc.auth.Authorize(ctx, req) if err != nil { - return "", err + return "", errors.Wrap(svcerr.ErrAuthorization, err) } if !res.GetAuthorized() { - return "", errors.ErrAuthorization + return "", svcerr.ErrAuthorization } return res.GetId(), nil } diff --git a/internal/groups/service_test.go b/internal/groups/service_test.go index d28b92ef1..f158bc54a 100644 --- a/internal/groups/service_test.go +++ b/internal/groups/service_test.go @@ -17,7 +17,6 @@ import ( "github.com/absmach/magistrala/internal/groups" "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/pkg/clients" - mgclients "github.com/absmach/magistrala/pkg/clients" "github.com/absmach/magistrala/pkg/errors" mggroups "github.com/absmach/magistrala/pkg/groups" "github.com/absmach/magistrala/pkg/groups/mocks" @@ -585,7 +584,7 @@ func TestEnableGroup(t *testing.T) { retrieveResp: mggroups.Group{ Status: clients.Status(groups.EnabledStatus), }, - err: mgclients.ErrStatusAlreadyAssigned, + err: errors.ErrStatusAlreadyAssigned, }, { desc: "with retrieve error", @@ -685,7 +684,7 @@ func TestDisableGroup(t *testing.T) { retrieveResp: mggroups.Group{ Status: clients.Status(groups.DisabledStatus), }, - err: mgclients.ErrStatusAlreadyAssigned, + err: errors.ErrStatusAlreadyAssigned, }, { desc: "with retrieve error", diff --git a/internal/postgres/common.go b/internal/postgres/common.go index 1c5b18371..e924b8897 100644 --- a/internal/postgres/common.go +++ b/internal/postgres/common.go @@ -31,15 +31,6 @@ var ( // ErrFailedToRetrieveMembership failed to retrieve memberships. ErrFailedToRetrieveMembership = errors.New("failed to retrieve memberships") - - // ErrFailedToRetrieveAll failed to retrieve groups. - ErrFailedToRetrieveAll = errors.New("failed to retrieve all groups") - - // ErrFailedToRetrieveParents failed to retrieve groups. - ErrFailedToRetrieveParents = errors.New("failed to retrieve all groups") - - // ErrFailedToRetrieveChildren failed to retrieve groups. - ErrFailedToRetrieveChildren = errors.New("failed to retrieve all groups") ) func CreateMetadataQuery(entity string, um map[string]interface{}) (string, []byte, error) { diff --git a/invitations/api/endpoint_test.go b/invitations/api/endpoint_test.go index fbf0cd4d0..bf48b965e 100644 --- a/invitations/api/endpoint_test.go +++ b/invitations/api/endpoint_test.go @@ -266,7 +266,7 @@ func TestListInvitation(t *testing.T) { desc: "with invalid state", token: validToken, query: "state=invalid", - status: http.StatusInternalServerError, + status: http.StatusBadRequest, contentType: validContenType, svcErr: nil, }, diff --git a/invitations/api/requests_test.go b/invitations/api/requests_test.go index 1f1192a92..a2e22660a 100644 --- a/invitations/api/requests_test.go +++ b/invitations/api/requests_test.go @@ -4,7 +4,6 @@ package api import ( - "errors" "fmt" "testing" @@ -14,11 +13,7 @@ import ( "github.com/stretchr/testify/assert" ) -var ( - errMissingRelation = errors.New("missing relation") - errInvalidRelation = errors.New("invalid relation") - valid = "valid" -) +var valid = "valid" func TestSendInvitationReqValidation(t *testing.T) { cases := []struct { @@ -79,7 +74,7 @@ func TestSendInvitationReqValidation(t *testing.T) { Relation: "", Resend: true, }, - err: errMissingRelation, + err: apiutil.ErrMissingRelation, }, { desc: "invalid relation", @@ -90,7 +85,7 @@ func TestSendInvitationReqValidation(t *testing.T) { Relation: "invalid", Resend: true, }, - err: errInvalidRelation, + err: apiutil.ErrInvalidRelation, }, } diff --git a/invitations/invitations.go b/invitations/invitations.go index 354dd3108..0cf6c5a2a 100644 --- a/invitations/invitations.go +++ b/invitations/invitations.go @@ -6,15 +6,10 @@ package invitations import ( "context" "encoding/json" - "errors" "time" "github.com/absmach/magistrala/auth" -) - -var ( - errMissingRelation = errors.New("missing relation") - errInvalidRelation = errors.New("invalid relation") + "github.com/absmach/magistrala/internal/apiutil" ) // Invitation is an invitation to join a domain. @@ -122,7 +117,7 @@ type Repository interface { // It returns an error if the relation is empty or invalid. func CheckRelation(relation string) error { if relation == "" { - return errMissingRelation + return apiutil.ErrMissingRelation } if relation != auth.AdministratorRelation && relation != auth.EditorRelation && @@ -133,7 +128,7 @@ func CheckRelation(relation string) error { relation != auth.RoleGroupRelation && relation != auth.GroupRelation && relation != auth.PlatformRelation { - return errInvalidRelation + return apiutil.ErrInvalidRelation } return nil diff --git a/invitations/invitations_test.go b/invitations/invitations_test.go index 942eaec76..2c367d7a7 100644 --- a/invitations/invitations_test.go +++ b/invitations/invitations_test.go @@ -4,19 +4,14 @@ package invitations_test import ( - "errors" "fmt" "testing" + "github.com/absmach/magistrala/internal/apiutil" "github.com/absmach/magistrala/invitations" "github.com/stretchr/testify/assert" ) -var ( - errMissingRelation = errors.New("missing relation") - errInvalidRelation = errors.New("invalid relation") -) - func TestInvitation_MarshalJSON(t *testing.T) { cases := []struct { desc string @@ -60,8 +55,8 @@ func TestCheckRelation(t *testing.T) { relation string err error }{ - {"", errMissingRelation}, - {"admin", errInvalidRelation}, + {"", apiutil.ErrMissingRelation}, + {"admin", apiutil.ErrInvalidRelation}, {"editor", nil}, {"viewer", nil}, {"member", nil}, diff --git a/invitations/service_test.go b/invitations/service_test.go index 87fda2ef7..58b7593e7 100644 --- a/invitations/service_test.go +++ b/invitations/service_test.go @@ -5,7 +5,6 @@ package invitations_test import ( "context" - "errors" "math/rand" "testing" "time" @@ -13,6 +12,7 @@ import ( "github.com/absmach/magistrala" "github.com/absmach/magistrala/auth" authmocks "github.com/absmach/magistrala/auth/mocks" + "github.com/absmach/magistrala/internal/apiutil" "github.com/absmach/magistrala/internal/testsutil" "github.com/absmach/magistrala/invitations" "github.com/absmach/magistrala/invitations/mocks" @@ -79,7 +79,7 @@ func TestSendInvitation(t *testing.T) { token: validToken, tokenUserID: testsutil.GenerateUUID(t), req: invitations.Invitation{Relation: "invalid"}, - err: errors.New("invalid relation"), + err: apiutil.ErrInvalidRelation, authNErr: nil, domainErr: nil, adminErr: nil, diff --git a/pkg/clients/postgres/clients.go b/pkg/clients/postgres/clients.go index 6ca5255d1..1ada83f48 100644 --- a/pkg/clients/postgres/clients.go +++ b/pkg/clients/postgres/clients.go @@ -149,11 +149,11 @@ func (repo *Repository) RetrieveAll(ctx context.Context, pm clients.Page) (clien dbPage, err := ToDBClientsPage(pm) if err != nil { - return clients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return clients.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } rows, err := repo.DB.NamedQueryContext(ctx, q, dbPage) if err != nil { - return clients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return clients.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } defer rows.Close() @@ -197,12 +197,12 @@ func (repo *Repository) RetrieveAllBasicInfo(ctx context.Context, pm clients.Pag dbPage, err := ToDBClientsPage(pm) if err != nil { - return clients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return clients.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } rows, err := repo.DB.NamedQueryContext(ctx, q, dbPage) if err != nil { - return clients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return clients.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } defer rows.Close() @@ -255,11 +255,11 @@ func (repo *Repository) RetrieveAllByIDs(ctx context.Context, pm clients.Page) ( dbPage, err := ToDBClientsPage(pm) if err != nil { - return clients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return clients.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } rows, err := repo.DB.NamedQueryContext(ctx, q, dbPage) if err != nil { - return clients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return clients.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } defer rows.Close() diff --git a/pkg/clients/status.go b/pkg/clients/status.go index 9d4c59e2f..82a716d51 100644 --- a/pkg/clients/status.go +++ b/pkg/clients/status.go @@ -5,7 +5,6 @@ package clients import ( "encoding/json" - "errors" "strings" "github.com/absmach/magistrala/internal/apiutil" @@ -36,9 +35,6 @@ const ( Unknown = "unknown" ) -// ErrStatusAlreadyAssigned indicated that the client or group has already been assigned the status. -var ErrStatusAlreadyAssigned = errors.New("status already assigned") - // String converts client/group status to string literal. func (s Status) String() string { switch s { diff --git a/pkg/errors/repository/types.go b/pkg/errors/repository/types.go index 2637d79b3..a9bb4fb70 100644 --- a/pkg/errors/repository/types.go +++ b/pkg/errors/repository/types.go @@ -48,4 +48,13 @@ var ( // ErrInvalidSecret indicates invalid secret. ErrInvalidSecret = errors.New("missing secret") + + // ErrAddPolicies indicates failed to add policies. + ErrAddPolicies = errors.New("failed to add policies") + + // ErrDeletePolicies indicates failed to delete policies. + ErrDeletePolicies = errors.New("failed to delete policies") + + // ErrFailedToRetrieveAllGroups failed to retrieve groups. + ErrFailedToRetrieveAllGroups = errors.New("failed to retrieve all groups") ) diff --git a/pkg/errors/service/types.go b/pkg/errors/service/types.go index a0a3049f2..b0ec97709 100644 --- a/pkg/errors/service/types.go +++ b/pkg/errors/service/types.go @@ -45,4 +45,16 @@ var ( // ErrInvalidPolicy indicates that an invalid policy. ErrInvalidPolicy = errors.New("invalid policy") + + // ErrRecoveryToken indicates error in generating password recovery token. + ErrRecoveryToken = errors.New("failed to generate password recovery token") + + // ErrFailedPolicyUpdate indicates a failure to update user policy. + ErrFailedPolicyUpdate = errors.New("failed to update user policy") + + // ErrPasswordFormat indicates weak password. + ErrPasswordFormat = errors.New("password does not meet the requirements") + + // ErrFailedUpdateRole indicates a failure to update user role. + ErrFailedUpdateRole = errors.New("failed to update user role") ) diff --git a/pkg/errors/types.go b/pkg/errors/types.go index 684c4dec6..f49e71a79 100644 --- a/pkg/errors/types.go +++ b/pkg/errors/types.go @@ -45,6 +45,9 @@ var ( // ErrLogin indicates wrong login credentials. ErrLogin = New("invalid user id or secret") + // ErrPasswordFormat indicates weak password. + ErrPasswordFormat = errors.New("password does not meet the requirements") + // ErrUnsupportedContentType indicates invalid content type. ErrUnsupportedContentType = errors.New("invalid content type") @@ -53,4 +56,7 @@ var ( // ErrEmptyPath indicates empty file path. ErrEmptyPath = errors.New("empty file path") + + // ErrStatusAlreadyAssigned indicated that the client or group has already been assigned the status. + ErrStatusAlreadyAssigned = errors.New("status already assigned") ) diff --git a/pkg/groups/errors.go b/pkg/groups/errors.go index 24b1a29d9..b6665fa0b 100644 --- a/pkg/groups/errors.go +++ b/pkg/groups/errors.go @@ -14,7 +14,4 @@ var ( // ErrDisableGroup indicates error in disabling group. ErrDisableGroup = errors.New("failed to disable group") - - // ErrStatusAlreadyAssigned indicated that the group has already been assigned the status. - ErrStatusAlreadyAssigned = errors.New("status already assigned") ) diff --git a/pkg/sdk/go/channels_test.go b/pkg/sdk/go/channels_test.go index a64749028..441a82804 100644 --- a/pkg/sdk/go/channels_test.go +++ b/pkg/sdk/go/channels_test.go @@ -109,7 +109,7 @@ func TestCreateChannel(t *testing.T) { Status: mgclients.EnabledStatus.String(), }, token: token, - err: errors.NewSDKErrorWithStatus(errors.ErrCreateEntity, http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.ErrCreateEntity, http.StatusUnprocessableEntity), }, { desc: "create channel with missing name", @@ -463,7 +463,7 @@ func TestUpdateChannel(t *testing.T) { }, response: sdk.Channel{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update channel description with invalid token", @@ -473,7 +473,7 @@ func TestUpdateChannel(t *testing.T) { }, response: sdk.Channel{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update channel metadata with invalid token", @@ -485,7 +485,7 @@ func TestUpdateChannel(t *testing.T) { }, response: sdk.Channel{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update channel that can't be marshalled", diff --git a/pkg/sdk/go/groups_test.go b/pkg/sdk/go/groups_test.go index ca6f49a25..e196fb88d 100644 --- a/pkg/sdk/go/groups_test.go +++ b/pkg/sdk/go/groups_test.go @@ -94,7 +94,7 @@ func TestCreateGroup(t *testing.T) { ParentID: wrongID, Status: clients.EnabledStatus.String(), }, - err: errors.NewSDKErrorWithStatus(svcerr.ErrCreateEntity, http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(svcerr.ErrCreateEntity, http.StatusUnprocessableEntity), }, { desc: "create group with missing name", @@ -717,7 +717,7 @@ func TestUpdateGroup(t *testing.T) { }, response: sdk.Group{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update group description with invalid token", @@ -727,7 +727,7 @@ func TestUpdateGroup(t *testing.T) { }, response: sdk.Group{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update group metadata with invalid token", @@ -739,7 +739,7 @@ func TestUpdateGroup(t *testing.T) { }, response: sdk.Group{}, token: invalidToken, - err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthorization, http.StatusForbidden), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrAuthorization, svcerr.ErrAuthorization), http.StatusForbidden), }, { desc: "update a group that can't be marshalled", diff --git a/pkg/sdk/go/things_test.go b/pkg/sdk/go/things_test.go index 98defd07e..8e3fca196 100644 --- a/pkg/sdk/go/things_test.go +++ b/pkg/sdk/go/things_test.go @@ -97,7 +97,7 @@ func TestCreateThing(t *testing.T) { response: sdk.Thing{}, token: token, repoErr: sdk.ErrFailedCreation, - err: errors.NewSDKErrorWithStatus(errors.Wrap(sdk.ErrFailedCreation, repoerr.ErrCreateEntity), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(sdk.ErrFailedCreation, repoerr.ErrCreateEntity), http.StatusUnprocessableEntity), }, { desc: "register empty thing", @@ -246,7 +246,7 @@ func TestCreateThings(t *testing.T) { things: thingsList, response: []sdk.Thing{}, token: token, - err: errors.NewSDKErrorWithStatus(errors.Wrap(sdk.ErrFailedCreation, sdk.ErrFailedCreation), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(sdk.ErrFailedCreation, sdk.ErrFailedCreation), http.StatusUnprocessableEntity), }, { desc: "register empty things", @@ -725,7 +725,7 @@ func TestUpdateThing(t *testing.T) { thing: thing2, response: sdk.Thing{}, token: validToken, - err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusUnprocessableEntity), }, { desc: "update thing that can't be marshalled", @@ -810,7 +810,7 @@ func TestUpdateThingTags(t *testing.T) { thing: thing2, response: sdk.Thing{}, token: validToken, - err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusUnprocessableEntity), }, { desc: "update thing that can't be marshalled", diff --git a/pkg/sdk/go/tokens_test.go b/pkg/sdk/go/tokens_test.go index ebf6cfbd5..ad5f07a95 100644 --- a/pkg/sdk/go/tokens_test.go +++ b/pkg/sdk/go/tokens_test.go @@ -62,7 +62,7 @@ func TestIssueToken(t *testing.T) { desc: "issue token for an empty user", login: sdk.Login{}, token: &magistrala.Token{}, - err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingIdentity), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, apiutil.ErrMissingIdentity), http.StatusBadRequest), }, { desc: "issue token for invalid identity", diff --git a/pkg/sdk/go/users_test.go b/pkg/sdk/go/users_test.go index 39d6aeacf..a3fa90cb4 100644 --- a/pkg/sdk/go/users_test.go +++ b/pkg/sdk/go/users_test.go @@ -86,7 +86,7 @@ func TestCreateClient(t *testing.T) { client: user, response: sdk.User{}, token: token, - err: errors.NewSDKErrorWithStatus(errors.Wrap(sdk.ErrFailedCreation, sdk.ErrFailedCreation), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(sdk.ErrFailedCreation, sdk.ErrFailedCreation), http.StatusUnprocessableEntity), }, { desc: "register empty user", @@ -354,7 +354,6 @@ func TestListClients(t *testing.T) { for _, tc := range cases { pm := sdk.PageMetadata{ Status: tc.status, - Total: total, Offset: tc.offset, Limit: tc.limit, Name: tc.name, @@ -556,7 +555,7 @@ func TestUpdateClient(t *testing.T) { client: client2, response: sdk.User{}, token: validToken, - err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusUnprocessableEntity), }, { desc: "update a user that can't be marshalled", @@ -647,7 +646,7 @@ func TestUpdateClientTags(t *testing.T) { client: client2, response: sdk.User{}, token: validToken, - err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusUnprocessableEntity), }, { desc: "update a user that can't be marshalled", @@ -737,7 +736,7 @@ func TestUpdateClientIdentity(t *testing.T) { client: client2, response: sdk.User{}, token: validToken, - err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusUnprocessableEntity), }, { desc: "update a user that can't be marshalled", @@ -753,7 +752,7 @@ func TestUpdateClientIdentity(t *testing.T) { }, response: sdk.User{}, token: validToken, - err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrUpdateEntity, svcerr.ErrUpdateEntity), http.StatusUnprocessableEntity), }, } @@ -905,7 +904,7 @@ func TestUpdateClientRole(t *testing.T) { client: client2, response: sdk.User{}, token: validToken, - err: errors.NewSDKErrorWithStatus(errors.Wrap(users.ErrFailedUpdateRole, users.ErrFailedUpdateRole), http.StatusInternalServerError), + err: errors.NewSDKErrorWithStatus(errors.Wrap(svcerr.ErrFailedUpdateRole, svcerr.ErrFailedUpdateRole), http.StatusUnprocessableEntity), }, { desc: "update a user that can't be marshalled", diff --git a/things/service.go b/things/service.go index 8130609c6..78d331d4d 100644 --- a/things/service.go +++ b/things/service.go @@ -507,7 +507,7 @@ func (svc service) changeClientStatus(ctx context.Context, token string, client return mgclients.Client{}, errors.Wrap(repoerr.ErrNotFound, err) } if dbClient.Status == client.Status { - return mgclients.Client{}, mgclients.ErrStatusAlreadyAssigned + return mgclients.Client{}, errors.ErrStatusAlreadyAssigned } client.UpdatedBy = userID diff --git a/things/service_test.go b/things/service_test.go index 644bd6c0b..097a4d425 100644 --- a/things/service_test.go +++ b/things/service_test.go @@ -1079,8 +1079,8 @@ func TestEnableClient(t *testing.T) { changeStatusResponse: enabledClient1, retrieveByIDResponse: enabledClient1, authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, - changeStatusErr: mgclients.ErrStatusAlreadyAssigned, - err: mgclients.ErrStatusAlreadyAssigned, + changeStatusErr: errors.ErrStatusAlreadyAssigned, + err: errors.ErrStatusAlreadyAssigned, }, { desc: "enable non-existing client", @@ -1236,8 +1236,8 @@ func TestDisableClient(t *testing.T) { changeStatusResponse: mgclients.Client{}, retrieveByIDResponse: disabledClient1, authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, - changeStatusErr: mgclients.ErrStatusAlreadyAssigned, - err: mgclients.ErrStatusAlreadyAssigned, + changeStatusErr: errors.ErrStatusAlreadyAssigned, + err: errors.ErrStatusAlreadyAssigned, }, { desc: "disable non-existing client", diff --git a/users/api/clients.go b/users/api/clients.go index 84283d4e7..a0da7c589 100644 --- a/users/api/clients.go +++ b/users/api/clients.go @@ -26,6 +26,7 @@ func clientsHandler(svc users.Service, r *chi.Mux, logger *slog.Logger) http.Han opts := []kithttp.ServerOption{ kithttp.ServerErrorEncoder(apiutil.LoggingErrorEncoder(logger, api.EncodeError)), } + r.Route("/users", func(r chi.Router) { r.Post("/", otelhttp.NewHandler(kithttp.NewServer( registrationEndpoint(svc), @@ -83,20 +84,6 @@ func clientsHandler(svc users.Service, r *chi.Mux, logger *slog.Logger) http.Han opts..., ), "update_client_identity").ServeHTTP) - r.Post("/password/reset-request", otelhttp.NewHandler(kithttp.NewServer( - passwordResetRequestEndpoint(svc), - decodePasswordResetRequest, - api.EncodeResponse, - opts..., - ), "password_reset_req").ServeHTTP) - - r.Put("/password/reset", otelhttp.NewHandler(kithttp.NewServer( - passwordResetEndpoint(svc), - decodePasswordReset, - api.EncodeResponse, - opts..., - ), "password_reset").ServeHTTP) - r.Patch("/{id}/role", otelhttp.NewHandler(kithttp.NewServer( updateClientRoleEndpoint(svc), decodeUpdateClientRole, @@ -133,6 +120,22 @@ func clientsHandler(svc users.Service, r *chi.Mux, logger *slog.Logger) http.Han ), "disable_client").ServeHTTP) }) + r.Route("/password", func(r chi.Router) { + r.Post("/reset-request", otelhttp.NewHandler(kithttp.NewServer( + passwordResetRequestEndpoint(svc), + decodePasswordResetRequest, + api.EncodeResponse, + opts..., + ), "password_reset_req").ServeHTTP) + + r.Put("/reset", otelhttp.NewHandler(kithttp.NewServer( + passwordResetEndpoint(svc), + decodePasswordReset, + api.EncodeResponse, + opts..., + ), "password_reset").ServeHTTP) + }) + // Ideal location: users service, groups endpoint. // Reason for placing here : // SpiceDB provides list of user ids in given user_group_id @@ -217,7 +220,6 @@ func decodeListClients(_ context.Context, r *http.Request) (interface{}, error) if err != nil { return nil, errors.Wrap(apiutil.ErrValidation, err) } - order, err := apiutil.ReadStringQuery(r, api.OrderKey, api.DefOrder) if err != nil { return nil, errors.Wrap(apiutil.ErrValidation, err) diff --git a/users/api/responses.go b/users/api/responses.go index 716f5a912..fb05de493 100644 --- a/users/api/responses.go +++ b/users/api/responses.go @@ -25,8 +25,8 @@ var ( type pageRes struct { Limit uint64 `json:"limit,omitempty"` - Offset uint64 `json:"offset,omitempty"` - Total uint64 `json:"total,omitempty"` + Offset uint64 `json:"offset"` + Total uint64 `json:"total"` } type createClientRes struct { diff --git a/users/postgres/clients.go b/users/postgres/clients.go index 5a65de691..b504f2c8a 100644 --- a/users/postgres/clients.go +++ b/users/postgres/clients.go @@ -133,11 +133,11 @@ func (repo clientRepo) RetrieveAll(ctx context.Context, pm mgclients.Page) (mgcl dbPage, err := pgclients.ToDBClientsPage(pm) if err != nil { - return mgclients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return mgclients.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } rows, err := repo.DB.NamedQueryContext(ctx, q, dbPage) if err != nil { - return mgclients.ClientsPage{}, errors.Wrap(postgres.ErrFailedToRetrieveAll, err) + return mgclients.ClientsPage{}, errors.Wrap(repoerr.ErrFailedToRetrieveAllGroups, err) } defer rows.Close() diff --git a/users/service.go b/users/service.go index ae057b03f..fefb6340b 100644 --- a/users/service.go +++ b/users/service.go @@ -19,18 +19,6 @@ import ( ) var ( - // ErrRecoveryToken indicates error in generating password recovery token. - ErrRecoveryToken = errors.New("failed to generate password recovery token") - - // ErrPasswordFormat indicates weak password. - ErrPasswordFormat = errors.New("password does not meet the requirements") - - // ErrFailedPolicyUpdate indicates a failure to update user policy. - ErrFailedPolicyUpdate = errors.New("failed to update user policy") - - // ErrFailedUpdateRole indicates a failure to update user role. - ErrFailedUpdateRole = errors.New("failed to update user role") - // ErrAddPolicies indictaed a failre to add policies. errAddPolicies = errors.New("failed to add policies") @@ -292,7 +280,7 @@ func (svc service) GenerateResetToken(ctx context.Context, email, host string) e } token, err := svc.auth.Issue(ctx, issueReq) if err != nil { - return errors.Wrap(ErrRecoveryToken, err) + return errors.Wrap(svcerr.ErrRecoveryToken, err) } return svc.SendPasswordReset(ctx, host, email, client.Name, token.AccessToken) @@ -311,7 +299,7 @@ func (svc service) ResetSecret(ctx context.Context, resetToken, secret string) e return errors.ErrNotFound } if !svc.passRegex.MatchString(secret) { - return ErrPasswordFormat + return errors.ErrPasswordFormat } secret, err = svc.hasher.Hash(secret) if err != nil { @@ -337,7 +325,7 @@ func (svc service) UpdateClientSecret(ctx context.Context, token, oldSecret, new return mgclients.Client{}, err } if !svc.passRegex.MatchString(newSecret) { - return mgclients.Client{}, ErrPasswordFormat + return mgclients.Client{}, errors.ErrPasswordFormat } dbClient, err := svc.clients.RetrieveByID(ctx, id) if err != nil { @@ -384,7 +372,7 @@ func (svc service) UpdateClientRole(ctx context.Context, token string, cli mgcli } if err := svc.updateClientPolicy(ctx, cli.ID, cli.Role); err != nil { - return mgclients.Client{}, errors.Wrap(ErrFailedPolicyUpdate, err) + return mgclients.Client{}, errors.Wrap(svcerr.ErrFailedPolicyUpdate, err) } client, err = svc.clients.UpdateRole(ctx, client) if err != nil { @@ -392,7 +380,7 @@ func (svc service) UpdateClientRole(ctx context.Context, token string, cli mgcli if errRollback := svc.updateClientPolicy(ctx, cli.ID, mgclients.UserRole); errRollback != nil { return mgclients.Client{}, errors.Wrap(err, errors.Wrap(repoerr.ErrRollbackTx, errRollback)) } - return mgclients.Client{}, errors.Wrap(ErrFailedUpdateRole, err) + return mgclients.Client{}, errors.Wrap(svcerr.ErrFailedUpdateRole, err) } return client, nil } @@ -438,7 +426,7 @@ func (svc service) changeClientStatus(ctx context.Context, token string, client return mgclients.Client{}, errors.Wrap(repoerr.ErrNotFound, err) } if dbClient.Status == client.Status { - return mgclients.Client{}, mgclients.ErrStatusAlreadyAssigned + return mgclients.Client{}, errors.ErrStatusAlreadyAssigned } client.UpdatedBy = tokenUserID @@ -580,7 +568,7 @@ func (svc *service) authorize(ctx context.Context, subjType, subjKind, subj, per } if !res.GetAuthorized() { - return "", errors.Wrap(svcerr.ErrAuthorization, err) + return "", svcerr.ErrAuthorization } return res.GetId(), nil } diff --git a/users/service_test.go b/users/service_test.go index ba54c53d8..f355d5fc0 100644 --- a/users/service_test.go +++ b/users/service_test.go @@ -1183,7 +1183,7 @@ func TestUpdateClientSecret(t *testing.T) { newSecret: "weak", token: validToken, identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, - err: users.ErrPasswordFormat, + err: errors.ErrPasswordFormat, }, { desc: "update client secret with failed to retrieve client by ID", @@ -1349,7 +1349,7 @@ func TestEnableClient(t *testing.T) { identifyResponse: &magistrala.IdentityRes{UserId: enabledClient1.ID}, authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, retrieveByIDResponse: enabledClient1, - err: mgclients.ErrStatusAlreadyAssigned, + err: errors.ErrStatusAlreadyAssigned, }, { desc: "enable disabled client with failed to change status", @@ -1536,7 +1536,7 @@ func TestDisableClient(t *testing.T) { identifyResponse: &magistrala.IdentityRes{UserId: disabledClient1.ID}, authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, retrieveByIDResponse: disabledClient1, - err: mgclients.ErrStatusAlreadyAssigned, + err: errors.ErrStatusAlreadyAssigned, }, { desc: "disable enabled client with failed to change status", @@ -2278,7 +2278,7 @@ func TestGenerateResetToken(t *testing.T) { retrieveByIdentityResponse: client, issueResponse: &magistrala.Token{}, issueErr: svcerr.ErrAuthorization, - err: users.ErrRecoveryToken, + err: svcerr.ErrRecoveryToken, }, } @@ -2367,7 +2367,7 @@ func TestResetSecret(t *testing.T) { newSecret: "weak", identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, retrieveByIDResponse: client, - err: users.ErrPasswordFormat, + err: errors.ErrPasswordFormat, }, { desc: "reset secret with failed to update secret", From b752cbe07b70fd49da989940da5ac7bea065768f Mon Sep 17 00:00:00 2001 From: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> Date: Wed, 13 Dec 2023 22:47:00 +0300 Subject: [PATCH 2/8] Refactor API Things endpoints - Update API endpoints, error codes, schemas, and validation patterns - Improve descriptions and documentation for various components - Add operation IDs and links for operations Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> --- .github/workflows/api-tests.yml | 17 ++- api/openapi/auth.yml | 4 +- api/openapi/things.yml | 231 ++++++++++++++++++++++++++++++++ cmd/auth/main.go | 2 +- things/api/http/requests.go | 2 +- 5 files changed, 250 insertions(+), 6 deletions(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 98a313d6c..cefe1c4d1 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -12,9 +12,11 @@ on: - main env: - TOKEN_URL: http://localhost:9002/users/tokens/issue + TOKENS_URL: http://localhost:9002/users/tokens/issue + DOMAINS_URL: http://localhost:8189/domains USER_IDENTITY: admin@example.com USER_SECRET: 12345678 + DOMAIN_NAME: demo-test jobs: api-test: @@ -37,7 +39,9 @@ jobs: - name: Set access token run: | - export USER_TOKEN=$(curl -sSX POST $TOKEN_URL -H "Content-Type: application/json" -d "{\"identity\": \"$USER_IDENTITY\",\"secret\": \"$USER_SECRET\"}" | jq -r .access_token) + export USER_TOKEN=$(curl -sSX POST $TOKENS_URL -H "Content-Type: application/json" -d "{\"identity\": \"$USER_IDENTITY\",\"secret\": \"$USER_SECRET\"}" | jq -r .access_token) + export DOMAIN_ID=$(curl -sSX POST $DOMAINS_URL -H "Content-Type: application/json" -H "Authorization: Bearer $USER_TOKEN" -d "{\"name\":\"$DOMAIN_NAME\",\"alias\":\"$DOMAIN_NAME\"}" | jq -r .id) + export USER_TOKEN=$(curl -sSX POST $TOKENS_URL -H "Content-Type: application/json" -d "{\"identity\": \"$USER_IDENTITY\",\"secret\": \"$USER_SECRET\",\"domain_id\": \"$DOMAIN_ID\"}" | jq -r .access_token) echo "USER_TOKEN=$USER_TOKEN" >> $GITHUB_ENV - name: Run Users API tests @@ -49,6 +53,15 @@ 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 Things API tests + uses: schemathesis/action@v1 + with: + schema: api/openapi/things.yml + base-url: http://localhost:9000 + 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/api/openapi/auth.yml b/api/openapi/auth.yml index 5d798276f..520407882 100644 --- a/api/openapi/auth.yml +++ b/api/openapi/auth.yml @@ -16,8 +16,8 @@ info: version: 0.14.0 servers: - - url: http://localhost:8180 - - url: https://localhost:8180 + - url: http://localhost:8189 + - url: https://localhost:8189 tags: - name: Auth diff --git a/api/openapi/things.yml b/api/openapi/things.yml index 4ed91b8cd..1896d0fae 100644 --- a/api/openapi/things.yml +++ b/api/openapi/things.yml @@ -39,6 +39,7 @@ tags: paths: /things: post: + operationId: createThing tags: - Things summary: Adds new thing @@ -54,6 +55,8 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "409": description: Failed due to using an existing identity. "415": @@ -64,6 +67,7 @@ paths: $ref: "#/components/responses/ServiceError" get: + operationId: listThings tags: - Things summary: Retrieves things @@ -98,6 +102,7 @@ paths: /things/bulk: post: + operationId: bulkCreateThings summary: Bulk provisions new things description: | Adds new things to the list of things owned by user identified using @@ -113,13 +118,18 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "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" /things/{thingID}: get: + operationId: getThing summary: Retrieves thing info description: | Retrieves a specific thing that is identifier by the thing ID. @@ -134,6 +144,8 @@ paths: $ref: "#/components/responses/ThingRes" "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -142,6 +154,7 @@ paths: $ref: "#/components/responses/ServiceError" patch: + operationId: updateThing summary: Updates name and metadata of the thing. description: | Update is performed by replacing the current resource data with values @@ -162,10 +175,14 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Failed due to non existing thing. "415": description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" delete: @@ -190,6 +207,7 @@ paths: $ref: "#/components/responses/ServiceError" /things/{thingID}/tags: patch: + operationId: updateThingTags summary: Updates tags the thing. description: | Updates tags of the thing with provided ID. Tags is updated using @@ -207,15 +225,20 @@ paths: $ref: "#/components/responses/ThingRes" "400": description: Failed due to malformed JSON. + "403": + description: Failed to perform authorization over the entity. "404": description: Failed due to non existing thing. "401": description: Missing or invalid access token provided. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /things/{thingID}/secret: patch: + operationId: updateThingSecret summary: Updates Secret of the identified thing. description: | Updates secret of the identified in thing. Secret is updated using @@ -235,17 +258,22 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Failed due to non existing thing. "409": description: Specified key already exists. "415": description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /things/{thingID}/disable: post: + operationId: disableThing summary: Disables a thing description: | Disables a specific thing that is identifier by the thing ID. @@ -262,6 +290,8 @@ paths: description: Failed due to malformed thing's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -271,6 +301,7 @@ paths: /things/{thingID}/enable: post: + operationId: enableThing summary: Enables a thing description: | Enables a specific thing that is identifier by the thing ID. @@ -287,6 +318,8 @@ paths: description: Failed due to malformed thing's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -296,6 +329,7 @@ paths: /things/{thingID}/share: post: + operationId: shareThing summary: Shares a thing description: | Shares a specific thing that is identifier by the thing ID. @@ -314,6 +348,8 @@ paths: description: Failed due to malformed thing's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -323,6 +359,7 @@ paths: /things/{thingID}/unshare: post: + operationId: unshareThing summary: Unshares a thing description: | Unshares a specific thing that is identifier by the thing ID. @@ -341,6 +378,8 @@ paths: description: Failed due to malformed thing's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -350,6 +389,7 @@ paths: /channels/{chanID}/things: get: + operationId: listThingsInaChannel summary: List of things connected to specified channel description: | Retrieves list of things connected to specified channel with pagination @@ -368,6 +408,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -377,6 +419,7 @@ paths: /channels: post: + operationId: createChannel tags: - Channels summary: Creates new channel @@ -393,14 +436,19 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "409": description: Failed due to using an existing identity. "415": description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" get: + operationId: listChannels summary: Lists channels. description: | Retrieves a list of channels. Due to performance concerns, data @@ -423,6 +471,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Channel does not exist. "422": @@ -432,6 +482,7 @@ paths: /channels/{chanID}: get: + operationId: getChannel summary: Retrieves channel info. description: | Gets info on a channel specified by id. @@ -448,6 +499,8 @@ paths: description: Failed due to malformed channel's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Channel does not exist. "422": @@ -456,6 +509,7 @@ paths: $ref: "#/components/responses/ServiceError" put: + operationId: updateChannel summary: Updates channel data. description: | Update is performed by replacing the current resource data with values @@ -476,10 +530,14 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Channel does not exist. "415": description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" delete: @@ -505,6 +563,7 @@ paths: /channels/{chanID}/enable: post: + operationId: enableChannel summary: Enables a channel description: | Enables a specific channel that is identifier by the channel ID. @@ -521,6 +580,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -530,6 +591,7 @@ paths: /channels/{chanID}/disable: post: + operationId: disableChannel summary: Disables a channel description: | Disables a specific channel that is identifier by the channel ID. @@ -546,6 +608,8 @@ paths: description: Failed due to malformed channel's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -555,6 +619,7 @@ paths: /channels/{chanID}/users/assign: post: + operationId: assignUsersToChannel summary: Assigns a member to a channel description: | Assigns a specific member to a channel that is identifier by the channel ID. @@ -573,6 +638,8 @@ paths: description: Failed due to malformed thing's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -582,6 +649,7 @@ paths: /channels/{chanID}/users/unassign: post: + operationId: unassignUsersFromChannel summary: Unassigns a member from a channel description: | Unassigns a specific member from a channel that is identifier by the channel ID. @@ -600,6 +668,8 @@ paths: description: Failed due to malformed thing's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -609,6 +679,7 @@ paths: /channels/{chanID}/groups/assign: post: + operationId: assignGroupsToChannel summary: Assigns a member to a channel description: | Assigns a specific member to a channel that is identifier by the channel ID. @@ -627,6 +698,8 @@ paths: description: Failed due to malformed thing's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -636,6 +709,7 @@ paths: /channels/{chanID}/groups/unassign: post: + operationId: unassignGroupsFromChannel summary: Unassigns a member from a channel description: | Unassigns a specific member from a channel that is identifier by the channel ID. @@ -654,6 +728,8 @@ paths: description: Failed due to malformed thing's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -663,6 +739,7 @@ paths: /things/{thingID}/channels: get: + operationId: listChannelsConnectedToThing summary: List of channels connected to specified thing description: | Retrieves list of channels connected to specified thing with pagination @@ -680,6 +757,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Thing does not exist. "422": @@ -689,6 +768,7 @@ paths: /users/{memberID}/channels: get: + operationId: listChannelsConnectedToUser summary: List of channels connected to specified user description: | Retrieves list of channels connected to specified user with pagination @@ -706,6 +786,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Thing does not exist. "422": @@ -715,6 +797,7 @@ paths: /groups/{memberID}/channels: get: + operationId: listChannelsConnectedToGroup summary: List of channels connected to specified group description: | Retrieves list of channels connected to specified group with pagination @@ -732,6 +815,8 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: Thing does not exist. "422": @@ -741,6 +826,7 @@ paths: /connect: post: + operationId: connectThingsAndChannels summary: Connects thing and channel. description: | Connect things specified by IDs to channels specified by IDs. @@ -756,17 +842,22 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "409": description: Entity already exist. "415": description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /disconnect: post: + operationId: disconnectThingsAndChannels summary: Disconnect things and channels using lists of IDs. description: | Disconnect things from channels specified by lists of IDs. @@ -782,15 +873,20 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "415": description: Missing or invalid content type. + "422": + description: Database can't process request. "500": $ref: "#/components/responses/ServiceError" /channels/{chanID}/things/{thingID}/connect: post: + operationId: connectThingToChannel summary: Connects a thing to a channel description: | Connects a specific thing to a channel that is identifier by the channel ID. @@ -806,6 +902,8 @@ paths: description: Failed due to malformed thing's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -815,6 +913,7 @@ paths: /channels/{chanID}/things/{thingID}/disconnect: post: + operationId: disconnectThingFromChannel summary: Disconnects a thing to a channel description: | Disconnects a specific thing to a channel that is identifier by the channel ID. @@ -830,6 +929,8 @@ paths: description: Failed due to malformed thing's ID. "401": description: Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -1423,6 +1524,9 @@ components: schema: type: string format: uuid + minLength: 36 + maxLength: 36 + pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" required: true example: bb7edb32-2eac-4aad-aebe-ed96fe073879 @@ -1433,6 +1537,9 @@ components: schema: type: string format: uuid + minLength: 36 + maxLength: 36 + pattern: "^[a-f0-9]{8}-[a-f0-9]{4}-[1-5][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$" required: true example: bb7edb32-2eac-4aad-aebe-ed96fe073879 @@ -1686,6 +1793,43 @@ components: application/json: schema: $ref: "#/components/schemas/Thing" + links: + get: + operationId: getThing + parameters: + thingID: $response.body#/id + get_channels: + operationId: listChannelsConnectedToThing + parameters: + thingID: $response.body#/id + update: + operationId: updateThing + parameters: + thingID: $response.body#/id + update_tags: + operationId: updateThingTags + parameters: + thingID: $response.body#/id + update_secret: + operationId: updateThingSecret + parameters: + thingID: $response.body#/id + share: + operationId: shareThing + parameters: + thingID: $response.body#/id + unsahre: + operationId: unshareThing + parameters: + thingID: $response.body#/id + disable: + operationId: disableThing + parameters: + thingID: $response.body#/id + enable: + operationId: enableThing + parameters: + thingID: $response.body#/id ThingRes: description: Data retrieved. @@ -1693,6 +1837,19 @@ components: application/json: schema: $ref: "#/components/schemas/Thing" + links: + get_channels: + operationId: listChannelsConnectedToThing + parameters: + thingID: $response.body#/id + share: + operationId: shareThing + parameters: + thingID: $response.body#/id + unsahre: + operationId: unshareThing + parameters: + thingID: $response.body#/id ThingPageRes: description: Data retrieved. @@ -1720,6 +1877,51 @@ components: application/json: schema: $ref: "#/components/schemas/Channel" + links: + get: + operationId: getChannel + parameters: + chanID: $response.body#/id + get_things: + operationId: listThingsInaChannel + parameters: + chanID: $response.body#/id + get_users: + operationId: listChannelsConnectedToUser + parameters: + memberID: $response.body#/id + get_groups: + operationId: listChannelsConnectedToGroup + parameters: + memberID: $response.body#/id + update: + operationId: updateChannel + parameters: + chanID: $response.body#/id + disable: + operationId: disableChannel + parameters: + chanID: $response.body#/id + enable: + operationId: enableChannel + parameters: + chanID: $response.body#/id + assign_users: + operationId: assignUsersToChannel + parameters: + chanID: $response.body#/id + unassign_users: + operationId: unassignUsersFromChannel + parameters: + chanID: $response.body#/id + assign_groups: + operationId: assignGroupsToChannel + parameters: + chanID: $response.body#/id + unassign_groups: + operationId: unassignGroupsFromChannel + parameters: + chanID: $response.body#/id ChannelRes: description: Data retrieved. @@ -1727,6 +1929,35 @@ components: application/json: schema: $ref: "#/components/schemas/Channel" + links: + get_things: + operationId: listThingsInaChannel + parameters: + chanID: $response.body#/id + get_users: + operationId: listChannelsConnectedToUser + parameters: + memberID: $response.body#/id + get_groups: + operationId: listChannelsConnectedToGroup + parameters: + memberID: $response.body#/id + assign_users: + operationId: assignUsersToChannel + parameters: + chanID: $response.body#/id + unassign_users: + operationId: unassignUsersFromChannel + parameters: + chanID: $response.body#/id + assign_groups: + operationId: assignGroupsToChannel + parameters: + chanID: $response.body#/id + unassign_groups: + operationId: unassignGroupsFromChannel + parameters: + chanID: $response.body#/id ChannelPageRes: description: Data retrieved. diff --git a/cmd/auth/main.go b/cmd/auth/main.go index bacbeccd9..f94dc8057 100644 --- a/cmd/auth/main.go +++ b/cmd/auth/main.go @@ -49,7 +49,7 @@ const ( envPrefixGrpc = "MG_AUTH_GRPC_" envPrefixDB = "MG_AUTH_DB_" defDB = "auth" - defSvcHTTPPort = "8180" + defSvcHTTPPort = "8189" defSvcGRPCPort = "8181" ) diff --git a/things/api/http/requests.go b/things/api/http/requests.go index 687127757..807dc628e 100644 --- a/things/api/http/requests.go +++ b/things/api/http/requests.go @@ -186,7 +186,7 @@ func (req updateClientCredentialsReq) validate() error { return apiutil.ErrMissingID } if req.Secret == "" { - return apiutil.ErrBearerKey + return apiutil.ErrMissingSecret } return nil From 9aaadcfaeea750bd7b5028dfb63a904c391cc8f5 Mon Sep 17 00:00:00 2001 From: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> Date: Fri, 15 Dec 2023 13:06:18 +0300 Subject: [PATCH 3/8] Rename to notifiers for ease of testing purposes Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> --- api/openapi/{consumers-notifiers.yml => notifiers.yml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename api/openapi/{consumers-notifiers.yml => notifiers.yml} (100%) diff --git a/api/openapi/consumers-notifiers.yml b/api/openapi/notifiers.yml similarity index 100% rename from api/openapi/consumers-notifiers.yml rename to api/openapi/notifiers.yml From bb6e1f30939dd5d8e574954a80e709868c111bc2 Mon Sep 17 00:00:00 2001 From: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> Date: Fri, 15 Dec 2023 13:10:58 +0300 Subject: [PATCH 4/8] Update API test workflow and schemas Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> --- .github/workflows/api-tests.yml | 97 ++++++++++++++++++++++++++++++++- Makefile | 23 +++++++- api/openapi/things.yml | 18 ++++-- things/api/http/responses.go | 4 +- things/service.go | 4 +- 5 files changed, 131 insertions(+), 15 deletions(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index cefe1c4d1..e7bf15846 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -7,9 +7,38 @@ on: push: branches: - main + paths: + - ".github/workflows/api-tests.yml" + - "api/**" + - "auth/api/http/**" + - "bootstrap/api**" + - "certs/api/**" + - "consumers/notifiers/api/**" + - "http/api/**" + - "invitations/api/**" + - "provision/api/**" + - "readers/api/**" + - "things/api/**" + - "twins/api/**" + - "users/api/**" + pull_request: branches: - main + paths: + - ".github/workflows/api-tests.yml" + - "api/**" + - "auth/api/http/**" + - "bootstrap/api**" + - "certs/api/**" + - "consumers/notifiers/api/**" + - "http/api/**" + - "invitations/api/**" + - "provision/api/**" + - "readers/api/**" + - "things/api/**" + - "twins/api/**" + - "users/api/**" env: TOKENS_URL: http://localhost:9002/users/tokens/issue @@ -17,6 +46,8 @@ env: USER_IDENTITY: admin@example.com USER_SECRET: 12345678 DOMAIN_NAME: demo-test + USERS_URL: http://localhost:9002 + THINGS_URL: http://localhost:9000 jobs: api-test: @@ -44,20 +75,82 @@ jobs: export USER_TOKEN=$(curl -sSX POST $TOKENS_URL -H "Content-Type: application/json" -d "{\"identity\": \"$USER_IDENTITY\",\"secret\": \"$USER_SECRET\",\"domain_id\": \"$DOMAIN_ID\"}" | jq -r .access_token) echo "USER_TOKEN=$USER_TOKEN" >> $GITHUB_ENV + - name: Check for changes in specific paths + uses: dorny/paths-filter@v2 + id: changes + with: + filters: | + auth: + - ".github/workflows/api-tests.yml" + - "api/openapi/auth.yml" + - "auth/api/http/**" + + bootstrap: + - ".github/workflows/api-tests.yml" + - "api/openapi/bootstrap.yml" + - "bootstrap/api/**" + + certs: + - ".github/workflows/api-tests.yml" + - "api/openapi/certs.yml" + - "certs/api/**" + + notifiers: + - ".github/workflows/api-tests.yml" + - "api/openapi/notifiers.yml" + - "consumers/notifiers/api/**" + + http: + - ".github/workflows/api-tests.yml" + - "api/openapi/http.yml" + - "http/api/**" + + invitations: + - ".github/workflows/api-tests.yml" + - "api/openapi/invitations.yml" + - "invitations/api/**" + + provision: + - ".github/workflows/api-tests.yml" + - "api/openapi/provision.yml" + - "provision/api/**" + + readers: + - ".github/workflows/api-tests.yml" + - "api/openapi/readers.yml" + - "readers/api/**" + + things: + - ".github/workflows/api-tests.yml" + - "api/openapi/things.yml" + - "things/api/**" + + twins: + - ".github/workflows/api-tests.yml" + - "api/openapi/twins.yml" + - "twins/api/**" + + users: + - ".github/workflows/api-tests.yml" + - "api/openapi/users.yml" + - "users/api/**" + - name: Run Users API tests + if: steps.changes.outputs.users == 'true' uses: schemathesis/action@v1 with: schema: api/openapi/users.yml - base-url: http://localhost:9002 + base-url: ${{ env.USERS_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: Run Things API tests + if: steps.changes.outputs.things == 'true' uses: schemathesis/action@v1 with: schema: api/openapi/things.yml - base-url: http://localhost:9000 + base-url: ${{ env.THINGS_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' diff --git a/Makefile b/Makefile index dc6a953da..d225443d3 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,8 @@ BUILD_DIR = build SERVICES = auth users things http coap ws lora influxdb-writer influxdb-reader mongodb-writer \ mongodb-reader cassandra-writer cassandra-reader postgres-writer postgres-reader timescale-writer timescale-reader cli \ bootstrap opcua twins mqtt provision certs smtp-notifier smpp-notifier invitations +TEST_API_SERVICES = auth bootstrap certs http invitations notifiers provision readers things twins users +TEST_API = $(addprefix test_api_,$(TEST_API_SERVICES)) DOCKERS = $(addprefix docker_,$(SERVICES)) DOCKERS_DEV = $(addprefix docker_dev_,$(SERVICES)) CGO_ENABLED ?= 0 @@ -129,15 +131,30 @@ test: mocks done go test -v --race -count 1 -tags test -coverprofile=coverage/coverage.out $$(go list ./... | grep -v 'consumers\|readers\|postgres\|internal\|opcua\|cmd') -test_api: +define test_api_service + $(eval svc=$(subst test_api_,,$(1))) @which st > /dev/null || (echo "schemathesis not found, please install it from https://github.com/schemathesis/schemathesis#getting-started" && exit 1) - st run api/openapi/users.yml \ + + @if [ -z "$(USER_TOKEN)" ]; then \ + echo "USER_TOKEN is not set"; \ + echo "Please set it to a valid token"; \ + exit 1; \ + fi + + st run api/openapi/$(svc).yml \ --checks all \ - --base-url http://localhost:9002 \ + --base-url $(2) \ --header "Authorization: Bearer $(USER_TOKEN)" \ --contrib-unique-data --contrib-openapi-formats-uuid \ --hypothesis-suppress-health-check=filter_too_much \ --stateful=links +endef + +test_api_users: TEST_API_URL := http://localhost:9002 +test_api_things: TEST_API_URL := http://localhost:9000 + +$(TEST_API): + $(call test_api_service,$(@),$(TEST_API_URL)) proto: protoc -I. --go_out=. --go_opt=paths=source_relative pkg/messaging/*.proto diff --git a/api/openapi/things.yml b/api/openapi/things.yml index 1896d0fae..40cef9948 100644 --- a/api/openapi/things.yml +++ b/api/openapi/things.yml @@ -93,6 +93,8 @@ paths: "401": description: | Missing or invalid access token provided. + "403": + description: Failed to perform authorization over the entity. "404": description: A non-existent entity request. "422": @@ -112,7 +114,7 @@ paths: requestBody: $ref: "#/components/requestBodies/ThingsCreateReq" responses: - "201": + "200": $ref: "#/components/responses/ThingPageRes" "400": description: Failed due to malformed JSON. @@ -179,6 +181,8 @@ paths: description: Failed to perform authorization over the entity. "404": description: Failed due to non existing thing. + "409": + description: Failed due to using an existing identity. "415": description: Missing or invalid content type. "422": @@ -534,6 +538,8 @@ paths: description: Failed to perform authorization over the entity. "404": description: Channel does not exist. + "409": + description: Failed due to using an existing identity. "415": description: Missing or invalid content type. "422": @@ -662,7 +668,7 @@ paths: security: - bearerAuth: [] responses: - "200": + "204": description: Thing unshared. "400": description: Failed due to malformed thing's ID. @@ -722,7 +728,7 @@ paths: security: - bearerAuth: [] responses: - "200": + "204": description: Thing unshared. "400": description: Failed due to malformed thing's ID. @@ -1346,7 +1352,7 @@ components: ChannelsPage: type: object properties: - channels: + groups: type: array minItems: 0 uniqueItems: true @@ -1364,9 +1370,9 @@ components: example: 10 description: Maximum number of items to return in one page. required: - - channels + - groups - total - - level + - offset PoliciesPage: type: object diff --git a/things/api/http/responses.go b/things/api/http/responses.go index 3987f182f..de7d8cb81 100644 --- a/things/api/http/responses.go +++ b/things/api/http/responses.go @@ -27,8 +27,8 @@ var ( type pageRes struct { Limit uint64 `json:"limit,omitempty"` - Offset uint64 `json:"offset,omitempty"` - Total uint64 `json:"total,omitempty"` + Offset uint64 `json:"offset"` + Total uint64 `json:"total"` } type createClientRes struct { diff --git a/things/service.go b/things/service.go index 78d331d4d..4e586b745 100644 --- a/things/service.go +++ b/things/service.go @@ -241,7 +241,7 @@ func (svc service) listUserThingPermission(ctx context.Context, userID, thingID ObjectType: auth.ThingType, }) if err != nil { - return []string{}, err + return []string{}, errors.Wrap(svcerr.ErrAuthorization, err) } return lp.GetPermissions(), nil } @@ -607,7 +607,7 @@ func (svc *service) authorize(ctx context.Context, domainID, subjType, subjKind, } res, err := svc.auth.Authorize(ctx, req) if err != nil { - return "", err + return "", errors.Wrap(errors.ErrAuthorization, err) } if !res.GetAuthorized() { return "", errors.Wrap(errors.ErrAuthorization, err) From 8217198ad05ea6e693293918f1da8a8b96860826 Mon Sep 17 00:00:00 2001 From: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> Date: Fri, 15 Dec 2023 18:14:22 +0300 Subject: [PATCH 5/8] Add property based testing to invitations API using schemathesis Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> --- .github/workflows/api-tests.yml | 11 +++++++++++ Makefile | 1 + api/openapi/invitations.yml | 15 +++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index e7bf15846..82c257a83 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -48,6 +48,7 @@ env: DOMAIN_NAME: demo-test USERS_URL: http://localhost:9002 THINGS_URL: http://localhost:9000 + INVITATIONS_URL: http://localhost:9020 jobs: api-test: @@ -155,6 +156,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 Invitations API tests + if: steps.changes.outputs.invitations == 'true' + uses: schemathesis/action@v1 + with: + schema: api/openapi/invitations.yml + base-url: ${{ env.INVITATIONS_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 d225443d3..455506a9a 100644 --- a/Makefile +++ b/Makefile @@ -152,6 +152,7 @@ endef test_api_users: TEST_API_URL := http://localhost:9002 test_api_things: TEST_API_URL := http://localhost:9000 +test_api_invitations: TEST_API_URL := http://localhost:9020 $(TEST_API): $(call test_api_service,$(@),$(TEST_API_URL)) diff --git a/api/openapi/invitations.yml b/api/openapi/invitations.yml index 0dd3c3fc8..372fd6c9f 100644 --- a/api/openapi/invitations.yml +++ b/api/openapi/invitations.yml @@ -29,6 +29,7 @@ tags: paths: /invitations: post: + operationId: sendInvitation tags: - Invitations summary: Send invitation @@ -45,6 +46,8 @@ paths: description: Failed due to malformed JSON. "401": description: Missing or invalid access token provided. + "404": + description: A non-existent entity request. "409": description: Failed due to using an existing identity. "415": @@ -53,6 +56,7 @@ paths: $ref: "#/components/responses/ServiceError" get: + operationId: listInvitations tags: - Invitations summary: List invitations @@ -89,6 +93,7 @@ paths: /invitations/accept: post: + operationId: acceptInvitation summary: Accept invitation description: | Current logged in user accepts invitation to join domain. @@ -105,11 +110,14 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "404": + description: A non-existent entity request. "500": $ref: "#/components/responses/ServiceError" /invitations/{user_id}/{domain_id}: get: + operationId: getInvitation summary: Retrieves a specific invitation description: | Retrieves a specific invitation that is identifier by the user ID and domain ID. @@ -135,6 +143,7 @@ paths: $ref: "#/components/responses/ServiceError" delete: + operationId: deleteInvitation summary: Deletes a specific invitation description: | Deletes a specific invitation that is identifier by the user ID and domain ID. @@ -452,6 +461,12 @@ components: application/json: schema: $ref: "#/components/schemas/Invitation" + links: + delete: + operationId: deleteInvitation + parameters: + user_id: $response.body#/user_id + domain_id: $response.body#/domain_id InvitationPageRes: description: Data retrieved. From 1f6df49e4d9c2fc688e41703acdedc75d5e9dca2 Mon Sep 17 00:00:00 2001 From: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> Date: Fri, 15 Dec 2023 18:24:36 +0300 Subject: [PATCH 6/8] Add property based testing to auth API using schemathesis Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> --- .github/workflows/api-tests.yml | 11 +++++++++++ Makefile | 1 + api/openapi/auth.yml | 20 +++++++++++++++++++- 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 82c257a83..9b6eb9bd2 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -49,6 +49,7 @@ env: USERS_URL: http://localhost:9002 THINGS_URL: http://localhost:9000 INVITATIONS_URL: http://localhost:9020 + AUTH_URL: http://localhost:8189 jobs: api-test: @@ -166,6 +167,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 Auth API tests + if: steps.changes.outputs.auth == 'true' + uses: schemathesis/action@v1 + with: + schema: api/openapi/auth.yml + base-url: ${{ env.AUTH_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 455506a9a..aea5ee280 100644 --- a/Makefile +++ b/Makefile @@ -153,6 +153,7 @@ endef test_api_users: TEST_API_URL := http://localhost:9002 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): $(call test_api_service,$(@),$(TEST_API_URL)) diff --git a/api/openapi/auth.yml b/api/openapi/auth.yml index 520407882..4475019bf 100644 --- a/api/openapi/auth.yml +++ b/api/openapi/auth.yml @@ -319,6 +319,7 @@ paths: $ref: "#/components/responses/ServiceError" /keys: post: + operationId: issueKey tags: - Keys summary: Issue API key @@ -341,6 +342,7 @@ paths: /keys/{keyID}: get: + operationId: getKey summary: Gets API key details. description: | Gets API key details for the given key. @@ -355,10 +357,13 @@ paths: description: Failed due to malformed query parameters. "401": description: Missing or invalid access token provided. + "404": + description: A non-existent entity request. "500": $ref: "#/components/responses/ServiceError" delete: + operationId: revokeKey summary: Revoke API key description: | Revoke API key identified by the given ID. @@ -371,11 +376,14 @@ paths: description: Key revoked. "401": description: Missing or invalid access token provided. + "404": + description: A non-existent entity request. "500": $ref: "#/components/responses/ServiceError" /policies: post: + operationId: addPolicies summary: Creates new policies. description: | Creates new policies. Only admin can use this endpoint. Therefore, you need an authentication token for the admin. @@ -393,6 +401,8 @@ paths: description: Missing or invalid access token provided. "403": description: Unauthorized access token provided. + "404": + description: A non-existent entity request. "409": description: Failed due to using an existing email address. "415": @@ -402,6 +412,7 @@ paths: /policies/delete: post: + operationId: deletePolicies summary: Deletes policies. description: | Deletes policies. Only admin can use this endpoint. Therefore, you need an authentication token for the admin. @@ -415,6 +426,8 @@ paths: description: Policies deleted. "400": description: Failed due to malformed JSON. + "404": + description: A non-existent entity request. "409": description: Failed due to using an existing email address. "415": @@ -867,11 +880,16 @@ components: application/json: schema: $ref: "#/components/schemas/Key" + links: + revoke: + operationId: revokeKey + parameters: + keyID: $response.body#/id HealthRes: description: Service Health Check. content: - application/json: + application/health+json: schema: $ref: "./schemas/HealthInfo.yml" From 87b71db171342663a43d4ca6cef6cd399c4a2333 Mon Sep 17 00:00:00 2001 From: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> Date: Fri, 15 Dec 2023 18:53:31 +0300 Subject: [PATCH 7/8] Add property based testing to boostrap API using schemathesis Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> --- .github/workflows/api-tests.yml | 13 ++- Makefile | 1 + api/openapi/bootstrap.yml | 190 +++++++++++++++++++++----------- bootstrap/api/transport.go | 93 ++-------------- internal/api/common.go | 6 + 5 files changed, 158 insertions(+), 145 deletions(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 9b6eb9bd2..731aa5f8a 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -50,6 +50,7 @@ env: THINGS_URL: http://localhost:9000 INVITATIONS_URL: http://localhost:9020 AUTH_URL: http://localhost:8189 + BOOTSTRAP_URL: http://localhost:9013 jobs: api-test: @@ -68,7 +69,7 @@ jobs: run: make all -j $(nproc) && make dockers_dev -j $(nproc) - name: Start containers - run: make run up args="-d" && sleep 10 + run: make run up args="-d" && make run_addons up args="-d" - name: Set access token run: | @@ -177,6 +178,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 Bootstrap API tests + if: steps.changes.outputs.bootstrap == 'true' + uses: schemathesis/action@v1 + with: + schema: api/openapi/bootstrap.yml + base-url: ${{ env.BOOTSTRAP_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 aea5ee280..0c2e6410e 100644 --- a/Makefile +++ b/Makefile @@ -154,6 +154,7 @@ test_api_users: TEST_API_URL := http://localhost:9002 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): $(call test_api_service,$(@),$(TEST_API_URL)) diff --git a/api/openapi/bootstrap.yml b/api/openapi/bootstrap.yml index 2a8177a54..e2864bdcb 100644 --- a/api/openapi/bootstrap.yml +++ b/api/openapi/bootstrap.yml @@ -29,6 +29,7 @@ tags: paths: /things/configs: post: + operationId: createConfig summary: Adds new config description: | Adds new config to the list of config owned by user identified using @@ -38,17 +39,24 @@ paths: requestBody: $ref: "#/components/requestBodies/ConfigCreateReq" responses: - '201': + "201": $ref: "#/components/responses/ConfigCreateRes" - '400': + "400": description: Failed due to malformed JSON. - '401': + "401": description: Missing or invalid access token provided. - '415': + "403": + description: Failed to perform authorization over the entity. + "415": description: Missing or invalid content type. - '500': + "422": + description: Database can't process request. + "500": $ref: "#/components/responses/ServiceError" + "503": + description: Failed to receive response from the things service. get: + operationId: getConfigs summary: Retrieves managed configs description: | Retrieves a list of managed configs. Due to performance concerns, data @@ -63,31 +71,37 @@ paths: - $ref: "#/components/parameters/State" - $ref: "#/components/parameters/Name" responses: - '200': + "200": $ref: "#/components/responses/ConfigListRes" - '400': + "400": description: Failed due to malformed query parameters. - '401': + "401": description: Missing or invalid access token provided. - '500': + "422": + description: Database can't process request. + "500": $ref: "#/components/responses/ServiceError" /things/configs/{configId}: get: + operationId: getConfig summary: Retrieves config info (with channels). tags: - configs parameters: - $ref: "#/components/parameters/ConfigId" responses: - '200': + "200": $ref: "#/components/responses/ConfigRes" - '401': + "401": description: Missing or invalid access token provided. - '404': + "404": description: Config does not exist. - '500': + "422": + description: Database can't process request. + "500": $ref: "#/components/responses/ServiceError" put: + operationId: updateConfig summary: Updates config info description: | Update is performed by replacing the current resource data with values @@ -98,21 +112,24 @@ paths: parameters: - $ref: "#/components/parameters/ConfigId" requestBody: - $ref: "#/components/requestBodies/ConfigUpdateReq" + $ref: "#/components/requestBodies/ConfigUpdateReq" responses: - '200': + "200": description: Config updated. - '400': + "400": description: Failed due to malformed JSON. - '401': + "401": description: Missing or invalid access token provided. - '404': + "404": description: Config does not exist. - '415': + "415": description: Missing or invalid content type. - '500': + "422": + description: Database can't process request. + "500": $ref: "#/components/responses/ServiceError" delete: + operationId: removeConfig summary: Removes a Config description: | Removes a Config. In case of successful removal the service will ensure @@ -122,16 +139,19 @@ paths: parameters: - $ref: "#/components/parameters/ConfigId" responses: - '204': + "204": description: Config removed. - '400': + "400": description: Failed due to malformed config ID. - '401': + "401": description: Missing or invalid access token provided. - '500': + "422": + description: Database can't process request. + "500": $ref: "#/components/responses/ServiceError" /things/configs/certs/{configId}: patch: + operationId: updateConfigCerts summary: Updates certs description: | Update is performed by replacing the current certificate data with values @@ -143,21 +163,24 @@ paths: requestBody: $ref: "#/components/requestBodies/ConfigCertUpdateReq" responses: - '200': + "200": description: Config updated. $ref: "#/components/responses/ConfigUpdateCertsRes" - '400': + "400": description: Failed due to malformed JSON. - '401': + "401": description: Missing or invalid access token provided. - '404': + "404": description: Config does not exist. - '415': + "415": description: Missing or invalid content type. - '500': + "422": + description: Database can't process request. + "500": $ref: "#/components/responses/ServiceError" /things/configs/connections/{configId}: put: + operationId: updateConfigConnections summary: Updates channels the thing is connected to description: | Update connections performs update of the channel list corresponding @@ -169,20 +192,23 @@ paths: requestBody: $ref: "#/components/requestBodies/ConfigConnUpdateReq" responses: - '200': + "200": description: Config updated. - '400': + "400": description: Failed due to malformed JSON. - '401': + "401": description: Missing or invalid access token provided. - '404': + "404": description: Config does not exist. - '415': + "415": description: Missing or invalid content type. - '500': + "422": + description: Database can't process request. + "500": $ref: "#/components/responses/ServiceError" /things/bootstrap/{externalId}: get: + operationId: getBootstrapConfig summary: Retrieves configuration. description: | Retrieves a configuration with given external ID and external key. @@ -193,18 +219,21 @@ paths: parameters: - $ref: "#/components/parameters/ExternalId" responses: - '200': + "200": $ref: "#/components/responses/BootstrapConfigRes" - '400': + "400": description: Failed due to malformed JSON. - '401': + "401": description: Missing or invalid external key provided. - '404': + "404": description: Failed to retrieve corresponding config. - '500': + "422": + description: Database can't process request. + "500": $ref: "#/components/responses/ServiceError" /things/bootstrap/secure/{externalId}: get: + operationId: getSecureBootstrapConfig summary: Retrieves configuration. description: | Retrieves a configuration with given external ID and encrypted external key. @@ -215,15 +244,20 @@ paths: parameters: - $ref: "#/components/parameters/ExternalId" responses: - '200': + "200": $ref: "#/components/responses/BootstrapConfigRes" - '404': + "401": + description: Missing or invalid access token provided. + "404": description: | Failed to retrieve corresponding config. - '500': + "422": + description: Database can't process request. + "500": $ref: "#/components/responses/ServiceError" /things/state/{configId}: put: + operationId: updateConfigState summary: Updates Config state. description: | Updating state represents enabling/disabling Config, i.e. connecting @@ -233,15 +267,21 @@ paths: parameters: - $ref: "#/components/parameters/ConfigId" requestBody: - $ref: '#/components/requestBodies/ConfigStateUpdateReq' + $ref: "#/components/requestBodies/ConfigStateUpdateReq" responses: - '204': + "204": description: Config removed. - '400': + "400": description: Failed due to malformed config's ID. - '401': + "401": description: Missing or invalid access token provided. - '500': + "404": + description: A non-existent entity request. + "415": + description: Missing or invalid content type. + "422": + description: Database can't process request. + "500": $ref: "#/components/responses/ServiceError" /health: get: @@ -249,9 +289,9 @@ paths: tags: - health responses: - '200': + "200": $ref: "#/components/responses/HealthRes" - '500': + "500": $ref: "#/components/responses/ServiceError" components: @@ -453,12 +493,14 @@ components: description: External key. thing_id: type: string + format: uuid description: ID of the corresponding Magistrala Thing. channels: type: array minItems: 0 items: type: string + format: uuid content: type: string name: @@ -468,7 +510,7 @@ components: description: Thing Certificate. client_key: type: string - description: Thing Private Key. + description: Thing Private Key. ca_cert: type: string required: @@ -513,6 +555,7 @@ components: minItems: 0 items: type: string + format: uuid ConfigStateUpdateReq: description: Update the state of the Config. content: @@ -525,14 +568,14 @@ components: responses: ConfigCreateRes: - description: Config registered. - headers: - Location: - content: - text/plain: - schema: - type: string - description: Created configuration's relative URL (i.e. /things/configs/{configId}). + description: Config registered. + headers: + Location: + content: + text/plain: + schema: + type: string + description: Created configuration's relative URL (i.e. /things/configs/{configId}). ConfigListRes: description: Data retrieved. Configs from this list don't contain channels. content: @@ -545,10 +588,31 @@ components: application/json: schema: $ref: "#/components/schemas/Config" + links: + update: + operationId: updateConfig + parameters: + configId: $response.body#/id + updateCerts: + operationId: updateConfigCerts + parameters: + configId: $response.body#/id + updateConnections: + operationId: updateConfigConnections + parameters: + configId: $response.body#/id + updateState: + operationId: updateConfigState + parameters: + configId: $response.body#/id + delete: + operationId: removeConfig + parameters: + configId: $response.body#/id BootstrapConfigRes: description: | - Data retrieved. If secure, a response is encrypted using - the secret key, so the response is in the binary form. + Data retrieved. If secure, a response is encrypted using + the secret key, so the response is in the binary form. content: application/json: schema: @@ -558,7 +622,7 @@ components: HealthRes: description: Service Health Check. content: - application/json: + application/health+json: schema: $ref: "./schemas/HealthInfo.yml" ConfigUpdateCertsRes: diff --git a/bootstrap/api/transport.go b/bootstrap/api/transport.go index ff3eee6a6..f67d6d99b 100644 --- a/bootstrap/api/transport.go +++ b/bootstrap/api/transport.go @@ -13,9 +13,9 @@ import ( "github.com/absmach/magistrala" "github.com/absmach/magistrala/bootstrap" + "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" @@ -40,7 +40,7 @@ var ( // MakeHandler returns a HTTP handler for API endpoints. func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader, 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() @@ -50,43 +50,43 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader, logger *s r.Post("/", otelhttp.NewHandler(kithttp.NewServer( addEndpoint(svc), decodeAddRequest, - encodeResponse, + api.EncodeResponse, opts...), "add").ServeHTTP) r.Get("/", otelhttp.NewHandler(kithttp.NewServer( listEndpoint(svc), decodeListRequest, - encodeResponse, + api.EncodeResponse, opts...), "list").ServeHTTP) r.Get("/{configID}", otelhttp.NewHandler(kithttp.NewServer( viewEndpoint(svc), decodeEntityRequest, - encodeResponse, + api.EncodeResponse, opts...), "view").ServeHTTP) r.Put("/{configID}", otelhttp.NewHandler(kithttp.NewServer( updateEndpoint(svc), decodeUpdateRequest, - encodeResponse, + api.EncodeResponse, opts...), "update").ServeHTTP) r.Delete("/{configID}", otelhttp.NewHandler(kithttp.NewServer( removeEndpoint(svc), decodeEntityRequest, - encodeResponse, + api.EncodeResponse, opts...), "remove").ServeHTTP) r.Patch("/certs/{certID}", otelhttp.NewHandler(kithttp.NewServer( updateCertEndpoint(svc), decodeUpdateCertRequest, - encodeResponse, + api.EncodeResponse, opts...), "update_cert").ServeHTTP) r.Put("/connections/{connID}", otelhttp.NewHandler(kithttp.NewServer( updateConnEndpoint(svc), decodeUpdateConnRequest, - encodeResponse, + api.EncodeResponse, opts...), "update_connections").ServeHTTP) }) @@ -94,12 +94,12 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader, logger *s r.Get("/", otelhttp.NewHandler(kithttp.NewServer( bootstrapEndpoint(svc, reader, false), decodeBootstrapRequest, - encodeResponse, + api.EncodeResponse, opts...), "bootstrap").ServeHTTP) r.Get("/{externalID}", otelhttp.NewHandler(kithttp.NewServer( bootstrapEndpoint(svc, reader, false), decodeBootstrapRequest, - encodeResponse, + api.EncodeResponse, opts...), "bootstrap").ServeHTTP) r.Get("/secure/{externalID}", otelhttp.NewHandler(kithttp.NewServer( bootstrapEndpoint(svc, reader, true), @@ -111,7 +111,7 @@ func MakeHandler(svc bootstrap.Service, reader bootstrap.ConfigReader, logger *s r.Put("/state/{thingID}", otelhttp.NewHandler(kithttp.NewServer( stateEndpoint(svc), decodeStateRequest, - encodeResponse, + api.EncodeResponse, opts...), "update_state").ServeHTTP) }) r.Get("/health", magistrala.Health("bootstrap", instanceID)) @@ -241,23 +241,6 @@ func decodeEntityRequest(_ 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 encodeSecureRes(_ context.Context, w http.ResponseWriter, response interface{}) error { w.Header().Set("Content-Type", contentType) w.WriteHeader(http.StatusOK) @@ -269,58 +252,6 @@ func encodeSecureRes(_ context.Context, w http.ResponseWriter, response interfac return 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), - errors.Contains(err, apiutil.ErrBearerKey): - w.WriteHeader(http.StatusUnauthorized) - case errors.Contains(err, apiutil.ErrUnsupportedContentType): - w.WriteHeader(http.StatusUnsupportedMediaType) - case errors.Contains(err, apiutil.ErrInvalidQueryParams), - errors.Contains(err, svcerr.ErrMalformedEntity), - errors.Contains(err, apiutil.ErrMissingID), - errors.Contains(err, apiutil.ErrBootstrapState), - errors.Contains(err, apiutil.ErrLimitSize): - w.WriteHeader(http.StatusBadRequest) - case errors.Contains(err, svcerr.ErrNotFound): - w.WriteHeader(http.StatusNotFound) - case errors.Contains(err, bootstrap.ErrExternalKey), - errors.Contains(err, bootstrap.ErrExternalKeySecure), - errors.Contains(err, svcerr.ErrAuthorization): - w.WriteHeader(http.StatusForbidden) - case errors.Contains(err, svcerr.ErrConflict): - w.WriteHeader(http.StatusConflict) - case errors.Contains(err, bootstrap.ErrThings): - w.WriteHeader(http.StatusServiceUnavailable) - - 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) - } - } -} - func parseFilter(values url.Values) bootstrap.Filter { ret := bootstrap.Filter{ FullMatch: make(map[string]string), diff --git a/internal/api/common.go b/internal/api/common.go index 88c3b69bd..956fee190 100644 --- a/internal/api/common.go +++ b/internal/api/common.go @@ -9,6 +9,7 @@ import ( "net/http" "github.com/absmach/magistrala" + "github.com/absmach/magistrala/bootstrap" "github.com/absmach/magistrala/internal/apiutil" "github.com/absmach/magistrala/internal/postgres" mgclients "github.com/absmach/magistrala/pkg/clients" @@ -133,6 +134,7 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) { errors.Contains(err, apiutil.ErrMissingRelation), errors.Contains(err, errors.ErrPasswordFormat), errors.Contains(err, apiutil.ErrInvalidLevel), + errors.Contains(err, apiutil.ErrBootstrapState), errors.Contains(err, apiutil.ErrInvalidQueryParams): w.WriteHeader(http.StatusBadRequest) case errors.Contains(err, svcerr.ErrAuthentication), @@ -149,6 +151,8 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) { w.WriteHeader(http.StatusConflict) case errors.Contains(err, svcerr.ErrAuthorization), errors.Contains(err, errors.ErrAuthorization), + errors.Contains(err, bootstrap.ErrExternalKey), + errors.Contains(err, bootstrap.ErrExternalKeySecure), errors.Contains(err, errors.ErrDomainAuthorization): w.WriteHeader(http.StatusForbidden) case errors.Contains(err, apiutil.ErrUnsupportedContentType): @@ -162,6 +166,8 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) { errors.Contains(err, repoerr.ErrDeletePolicies), errors.Contains(err, svcerr.ErrRemoveEntity): w.WriteHeader(http.StatusUnprocessableEntity) + case errors.Contains(err, bootstrap.ErrThings): + w.WriteHeader(http.StatusServiceUnavailable) default: w.WriteHeader(http.StatusInternalServerError) } From 674e9e10ee31c3cb5798faa1912bf2087f4d7068 Mon Sep 17 00:00:00 2001 From: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> Date: Fri, 15 Dec 2023 19:05:16 +0300 Subject: [PATCH 8/8] Add property based testing to certs API using schemathesis Signed-off-by: Rodney Osodo <28790446+rodneyosodo@users.noreply.github.com> --- .github/workflows/api-tests.yml | 11 +++++ Makefile | 1 + api/openapi/certs.yml | 85 ++++++++++++++++++++++----------- certs/api/transport.go | 71 +++------------------------ internal/api/common.go | 2 + pkg/sdk/go/certs_test.go | 8 ++-- 6 files changed, 82 insertions(+), 96 deletions(-) diff --git a/.github/workflows/api-tests.yml b/.github/workflows/api-tests.yml index 731aa5f8a..55586fd9c 100644 --- a/.github/workflows/api-tests.yml +++ b/.github/workflows/api-tests.yml @@ -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: @@ -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" diff --git a/Makefile b/Makefile index 0c2e6410e..db59a0a4e 100644 --- a/Makefile +++ b/Makefile @@ -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)) diff --git a/api/openapi/certs.yml b/api/openapi/certs.yml index e8a7e88f4..583671a62 100644 --- a/api/openapi/certs.yml +++ b/api/openapi/certs.yml @@ -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" diff --git a/certs/api/transport.go b/certs/api/transport.go index d61ba52c8..012582592 100644 --- a/certs/api/transport.go +++ b/certs/api/transport.go @@ -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) - } - } -} diff --git a/internal/api/common.go b/internal/api/common.go index 956fee190..1b284aa91 100644 --- a/internal/api/common.go +++ b/internal/api/common.go @@ -135,6 +135,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), diff --git a/pkg/sdk/go/certs_test.go b/pkg/sdk/go/certs_test.go index 742f8a0ba..a6b51bd3a 100644 --- a/pkg/sdk/go/certs_test.go +++ b/pkg/sdk/go/certs_test.go @@ -198,7 +198,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{}, }, { @@ -261,7 +261,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{}, }, { @@ -322,7 +322,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", @@ -340,7 +340,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), }, }