diff --git a/api/openapi/users.yml b/api/openapi/users.yml index 8aac147eb..6cb178605 100644 --- a/api/openapi/users.yml +++ b/api/openapi/users.yml @@ -1458,15 +1458,6 @@ components: required: true example: bb7edb32-2eac-4aad-aebe-ed96fe073879 - Visibility: - name: visibility - description: The visibility specifier when listing users. Either all, shared or mine. - in: path - schema: - type: string - required: true - example: all - UserName: name: name description: User's name. @@ -1505,15 +1496,6 @@ components: required: false example: bb7edb32-2eac-4aad-aebe-ed96fe073879 - UserVisibility: - name: visibility - description: visibility to list either users I own or users that are shared with me or both users I own and shared with me - in: query - schema: - type: string - required: false - example: shared - Status: name: status description: User account status. diff --git a/users/api/clients.go b/users/api/clients.go index c3b45a490..0f9075c3e 100644 --- a/users/api/clients.go +++ b/users/api/clients.go @@ -189,7 +189,6 @@ func decodeViewProfile(_ context.Context, r *http.Request) (interface{}, error) } func decodeListClients(_ context.Context, r *http.Request) (interface{}, error) { - var sharedID, ownerID string s, err := apiutil.ReadStringQuery(r, api.StatusKey, api.DefClientStatus) if err != nil { return nil, errors.Wrap(apiutil.ErrValidation, err) @@ -222,10 +221,6 @@ func decodeListClients(_ context.Context, r *http.Request) (interface{}, error) if err != nil { return nil, err } - visibility, err := apiutil.ReadStringQuery(r, api.VisibilityKey, "") - 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) @@ -234,18 +229,6 @@ func decodeListClients(_ context.Context, r *http.Request) (interface{}, error) if err != nil { return nil, errors.Wrap(apiutil.ErrValidation, err) } - switch visibility { - case api.MyVisibility: - ownerID = api.MyVisibility - case api.SharedVisibility: - sharedID = api.MyVisibility - case api.AllVisibility: - sharedID = api.MyVisibility - ownerID = api.MyVisibility - } - if oid != "" { - ownerID = oid - } st, err := mgclients.ToStatus(s) if err != nil { return nil, errors.Wrap(apiutil.ErrValidation, err) @@ -259,8 +242,7 @@ func decodeListClients(_ context.Context, r *http.Request) (interface{}, error) name: n, identity: i, tag: t, - sharedBy: sharedID, - owner: ownerID, + owner: oid, order: order, dir: dir, } @@ -473,13 +455,7 @@ func decodeListMembersByDomain(_ context.Context, r *http.Request) (interface{}, if err != nil { return nil, err } - // For domains default permission in membership, In "queryPageParams" default is view, - // so overwriting the permission given by queryPageParams function with default membership permission. - p, err := apiutil.ReadStringQuery(r, api.PermissionKey, auth.MembershipPermission) - if err != nil { - return mgclients.Page{}, errors.Wrap(apiutil.ErrValidation, err) - } - page.Permission = p + req := listMembersByObjectReq{ token: apiutil.ExtractBearerToken(r), Page: page, diff --git a/users/api/endpoint_test.go b/users/api/endpoint_test.go new file mode 100644 index 000000000..4a0d4d663 --- /dev/null +++ b/users/api/endpoint_test.go @@ -0,0 +1,3097 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package api_test + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/absmach/magistrala" + authmocks "github.com/absmach/magistrala/auth/mocks" + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/internal/apiutil" + "github.com/absmach/magistrala/internal/groups" + "github.com/absmach/magistrala/internal/testsutil" + mglog "github.com/absmach/magistrala/logger" + mgclients "github.com/absmach/magistrala/pkg/clients" + "github.com/absmach/magistrala/pkg/errors" + svcerr "github.com/absmach/magistrala/pkg/errors/service" + gmocks "github.com/absmach/magistrala/pkg/groups/mocks" + "github.com/absmach/magistrala/pkg/uuid" + httpapi "github.com/absmach/magistrala/users/api" + "github.com/absmach/magistrala/users/mocks" + "github.com/go-chi/chi/v5" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +var ( + idProvider = uuid.New() + secret = "strongsecret" + validCMetadata = mgclients.Metadata{"role": "client"} + client = mgclients.Client{ + ID: testsutil.GenerateUUID(&testing.T{}), + Name: "clientname", + Tags: []string{"tag1", "tag2"}, + Credentials: mgclients.Credentials{Identity: "clientidentity@example.com", Secret: secret}, + Metadata: validCMetadata, + Status: mgclients.EnabledStatus, + } + validToken = "valid" + inValidToken = "invalid" + inValid = "invalid" + validID = "d4ebb847-5d0e-4e46-bdd9-b6aceaaa3a22" + ErrPasswordFormat = errors.New("password does not meet the requirements") +) + +const contentType = "application/json" + +type testRequest struct { + client *http.Client + method string + url string + contentType string + token string + body io.Reader +} + +func (tr testRequest) make() (*http.Response, error) { + req, err := http.NewRequest(tr.method, tr.url, tr.body) + if err != nil { + return nil, err + } + + if tr.token != "" { + req.Header.Set("Authorization", apiutil.BearerPrefix+tr.token) + } + + if tr.contentType != "" { + req.Header.Set("Content-Type", tr.contentType) + } + + req.Header.Set("Referer", "http://localhost") + + return tr.client.Do(req) +} + +func newUsersServer() (*httptest.Server, *mocks.Service) { + gRepo := new(gmocks.Repository) + auth := new(authmocks.Service) + + svc := new(mocks.Service) + gsvc := groups.NewService(gRepo, idProvider, auth) + + logger := mglog.NewMock() + mux := chi.NewRouter() + httpapi.MakeHandler(svc, gsvc, mux, logger, "") + + return httptest.NewServer(mux), svc +} + +func toJSON(data interface{}) string { + jsonData, err := json.Marshal(data) + if err != nil { + return "" + } + return string(jsonData) +} + +func TestRegisterClient(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + client mgclients.Client + token string + contentType string + status int + err error + }{ + { + desc: "register a new user with a valid token", + client: client, + token: validToken, + contentType: contentType, + status: http.StatusCreated, + err: nil, + }, + { + desc: "register an existing user", + client: client, + token: validToken, + contentType: contentType, + status: http.StatusConflict, + err: errors.ErrConflict, + }, + { + desc: "register a new user with an empty token", + client: client, + token: "", + contentType: contentType, + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "register a user with an invalid ID", + client: mgclients.Client{ + ID: inValid, + Credentials: mgclients.Credentials{ + Identity: "user@example.com", + Secret: "12345678", + }, + }, + token: validToken, + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "register a user that can't be marshalled", + client: mgclients.Client{ + Credentials: mgclients.Credentials{ + Identity: "user@example.com", + Secret: "12345678", + }, + Metadata: map[string]interface{}{ + "test": make(chan int), + }, + }, + token: validToken, + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "register user with invalid status", + client: mgclients.Client{ + Credentials: mgclients.Credentials{ + Identity: "newclientwithinvalidstatus@example.com", + Secret: secret, + }, + Status: mgclients.AllStatus, + }, + token: validToken, + contentType: contentType, + status: http.StatusBadRequest, + err: svcerr.ErrInvalidStatus, + }, + { + desc: "register a user with name too long", + client: mgclients.Client{ + Name: strings.Repeat("a", 1025), + Credentials: mgclients.Credentials{ + Identity: "newclientwithinvalidname@example.com", + Secret: secret, + }, + }, + token: validToken, + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "register user with invalid content type", + client: client, + token: validToken, + contentType: "application/xml", + status: http.StatusUnsupportedMediaType, + err: apiutil.ErrValidation, + }, + { + desc: "register user with empty request body", + client: mgclients.Client{}, + token: validToken, + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + data := toJSON(tc.client) + req := testRequest{ + client: us.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/users/", us.URL), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(data), + } + + repoCall := svc.On("RegisterClient", mock.Anything, tc.token, tc.client).Return(tc.client, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var errRes respBody + err = json.NewDecoder(res.Body).Decode(&errRes) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if errRes.Err != "" || errRes.Message != "" { + err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestViewClient(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + token string + id string + status int + err error + }{ + { + desc: "view user with valid token", + token: validToken, + id: client.ID, + status: http.StatusOK, + err: nil, + }, + { + desc: "view user with invalid token", + token: inValidToken, + id: client.ID, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "view user with empty token", + token: "", + id: client.ID, + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodGet, + url: fmt.Sprintf("%s/users/%s", us.URL, tc.id), + token: tc.token, + } + + repoCall := svc.On("ViewClient", mock.Anything, tc.token, tc.id).Return(mgclients.Client{}, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var errRes respBody + err = json.NewDecoder(res.Body).Decode(&errRes) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if errRes.Err != "" || errRes.Message != "" { + err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestViewProfile(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + token string + id string + status int + err error + }{ + { + desc: "view profile with valid token", + token: validToken, + id: client.ID, + status: http.StatusOK, + err: nil, + }, + { + desc: "view profile with invalid token", + token: inValidToken, + id: client.ID, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "view profile with empty token", + token: "", + id: client.ID, + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodGet, + url: fmt.Sprintf("%s/users/profile", us.URL), + token: tc.token, + } + + repoCall := svc.On("ViewProfile", mock.Anything, tc.token).Return(mgclients.Client{}, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var errRes respBody + err = json.NewDecoder(res.Body).Decode(&errRes) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if errRes.Err != "" || errRes.Message != "" { + err = errors.Wrap(errors.New(errRes.Err), errors.New(errRes.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestListClients(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + query string + token string + listUsersResponse mgclients.ClientsPage + status int + err error + }{ + { + desc: "list users with valid token", + token: validToken, + status: http.StatusOK, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + err: nil, + }, + { + desc: "list users with empty token", + token: "", + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "list users with invalid token", + token: inValidToken, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "list users with offset", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Offset: 1, + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "offset=1", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid offset", + token: validToken, + query: "offset=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with limit", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Limit: 1, + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "limit=1", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid limit", + token: validToken, + query: "limit=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with limit greater than max", + token: validToken, + query: fmt.Sprintf("limit=%d", api.MaxLimitSize+1), + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with owner_id", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: fmt.Sprintf("owner_id=%s", validID), + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with duplicate owner_id", + token: validToken, + query: "owner_id=1&owner_id=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with invalid owner_id", + token: validToken, + query: "owner_id=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with name", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "name=clientname", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid name", + token: validToken, + query: "name=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate name", + token: validToken, + query: "name=1&name=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with status", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "status=enabled", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid status", + token: validToken, + query: "status=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate status", + token: validToken, + query: "status=enabled&status=disabled", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with tags", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "tag=tag1,tag2", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid tags", + token: validToken, + query: "tag=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate tags", + token: validToken, + query: "tag=tag1&tag=tag2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with metadata", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "metadata=%7B%22domain%22%3A%20%22example.com%22%7D&", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid metadata", + token: validToken, + query: "metadata=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate metadata", + token: validToken, + query: "metadata=%7B%22domain%22%3A%20%22example.com%22%7D&metadata=%7B%22domain%22%3A%20%22example.com%22%7D", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with permissions", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "permission=view", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid permissions", + token: validToken, + query: "permission=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate permissions", + token: validToken, + query: "permission=view&permission=view", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with list perms", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "list_perms=true", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid list perms", + token: validToken, + query: "list_perms=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate list perms", + token: validToken, + query: "list_perms=true&list_perms=true", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with identity", + token: validToken, + query: fmt.Sprintf("identity=%s", client.Credentials.Identity), + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{ + client, + }, + }, + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid identity", + token: validToken, + query: "identity=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate identity", + token: validToken, + query: "identity=1&identity=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with order", + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{ + client, + }, + }, + token: validToken, + query: "order=name", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid order", + token: validToken, + query: "order=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate order", + token: validToken, + query: "order=name&order=name", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with invalid order direction", + token: validToken, + query: "dir=invalid", + status: http.StatusInternalServerError, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate order direction", + token: validToken, + query: "dir=asc&dir=asc", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodGet, + url: us.URL + "/users?" + tc.query, + contentType: contentType, + token: tc.token, + } + + repoCall := svc.On("ListClients", mock.Anything, tc.token, mock.Anything, mock.Anything).Return(tc.listUsersResponse, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var bodyRes respBody + err = json.NewDecoder(res.Body).Decode(&bodyRes) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if bodyRes.Err != "" || bodyRes.Message != "" { + err = errors.Wrap(errors.New(bodyRes.Err), errors.New(bodyRes.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestUpdateClient(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + newName := "newname" + newMetadata := mgclients.Metadata{"newkey": "newvalue"} + + cases := []struct { + desc string + id string + data string + clientResponse mgclients.Client + token string + contentType string + status int + err error + }{ + { + desc: "update user with valid token", + id: client.ID, + data: fmt.Sprintf(`{"name":"%s","metadata":%s}`, newName, toJSON(newMetadata)), + token: validToken, + contentType: contentType, + clientResponse: mgclients.Client{ + ID: client.ID, + Name: newName, + Metadata: newMetadata, + }, + status: http.StatusOK, + err: nil, + }, + { + desc: "update user with invalid token", + id: client.ID, + data: fmt.Sprintf(`{"name":"%s","metadata":%s}`, newName, toJSON(newMetadata)), + token: inValidToken, + contentType: contentType, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "update user with empty token", + id: client.ID, + data: fmt.Sprintf(`{"name":"%s","metadata":%s}`, newName, toJSON(newMetadata)), + token: "", + contentType: contentType, + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "update user with invalid id", + id: inValid, + data: fmt.Sprintf(`{"name":"%s","metadata":%s}`, newName, toJSON(newMetadata)), + token: validToken, + contentType: contentType, + status: http.StatusForbidden, + err: svcerr.ErrAuthorization, + }, + { + desc: "update user with invalid contentype", + id: client.ID, + data: fmt.Sprintf(`{"name":"%s","metadata":%s}`, newName, toJSON(newMetadata)), + token: validToken, + contentType: "application/xml", + status: http.StatusUnsupportedMediaType, + err: apiutil.ErrValidation, + }, + { + desc: "update user with malformed data", + id: client.ID, + data: fmt.Sprintf(`{"name":%s}`, "invalid"), + token: validToken, + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "update user with empty id", + id: " ", + data: fmt.Sprintf(`{"name":"%s","metadata":%s}`, newName, toJSON(newMetadata)), + token: validToken, + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodPatch, + url: fmt.Sprintf("%s/users/%s", us.URL, tc.id), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.data), + } + repoCall := svc.On("UpdateClient", mock.Anything, tc.token, mock.Anything).Return(tc.clientResponse, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestUpdateClientTags(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + newTag := "newtag" + + cases := []struct { + desc string + id string + data string + contentType string + clientResponse mgclients.Client + token string + status int + err error + }{ + { + desc: "update user tags with valid token", + id: client.ID, + data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), + contentType: contentType, + clientResponse: mgclients.Client{ + ID: client.ID, + Tags: []string{newTag}, + }, + token: validToken, + status: http.StatusOK, + err: nil, + }, + { + desc: "update user tags with empty token", + id: client.ID, + data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), + contentType: contentType, + token: "", + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "update user tags with invalid token", + id: client.ID, + data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), + contentType: contentType, + token: inValidToken, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "update user tags with invalid id", + id: client.ID, + data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), + contentType: contentType, + token: validToken, + status: http.StatusForbidden, + err: svcerr.ErrAuthorization, + }, + { + desc: "update user tags with invalid contentype", + id: client.ID, + data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), + contentType: "application/xml", + token: validToken, + status: http.StatusUnsupportedMediaType, + err: apiutil.ErrValidation, + }, + { + desc: "update user tags with empty id", + id: "", + data: fmt.Sprintf(`{"tags":["%s"]}`, newTag), + contentType: contentType, + token: validToken, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "update user with malfomed data", + id: client.ID, + data: fmt.Sprintf(`{"tags":%s}`, newTag), + contentType: contentType, + token: validToken, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodPatch, + url: fmt.Sprintf("%s/users/%s/tags", us.URL, tc.id), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.data), + } + + repoCall := svc.On("UpdateClientTags", mock.Anything, tc.token, mock.Anything).Return(tc.clientResponse, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + if err == nil { + assert.Equal(t, tc.clientResponse.Tags, resBody.Tags, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.clientResponse.Tags, resBody.Tags)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestUpdateClientIdentity(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + data string + client mgclients.Client + contentType string + token string + status int + err error + }{ + { + desc: "update user identity with valid token", + data: fmt.Sprintf(`{"identity": "%s"}`, "newclientidentity@example.com"), + client: mgclients.Client{ + ID: client.ID, + Credentials: mgclients.Credentials{ + Identity: "newclientidentity@example.com", + Secret: "secret", + }, + }, + contentType: contentType, + token: validToken, + status: http.StatusOK, + err: nil, + }, + { + desc: "update user identity with empty token", + data: fmt.Sprintf(`{"identity": "%s"}`, "newclientidentity@example.com"), + client: mgclients.Client{ + ID: client.ID, + Credentials: mgclients.Credentials{ + Identity: "newclientidentity@example.com", + Secret: "secret", + }, + }, + contentType: contentType, + token: "", + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "update user identity with invalid token", + data: fmt.Sprintf(`{"identity": "%s"}`, "newclientidentity@example.com"), + client: mgclients.Client{ + ID: client.ID, + Credentials: mgclients.Credentials{ + Identity: "newclientidentity@example.com", + Secret: "secret", + }, + }, + contentType: contentType, + token: inValid, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "update user identity with empty id", + data: fmt.Sprintf(`{"identity": "%s"}`, "newclientidentity@example.com"), + client: mgclients.Client{ + ID: "", + Credentials: mgclients.Credentials{ + Identity: "newclientidentity@example.com", + Secret: "secret", + }, + }, + contentType: contentType, + token: validToken, + status: http.StatusBadRequest, + err: apiutil.ErrMissingID, + }, + { + desc: "update user identity with invalid contentype", + data: fmt.Sprintf(`{"identity": "%s"}`, ""), + client: mgclients.Client{ + ID: client.ID, + Credentials: mgclients.Credentials{ + Identity: "newclientidentity@example.com", + Secret: "secret", + }, + }, + contentType: "application/xml", + token: validToken, + status: http.StatusUnsupportedMediaType, + err: apiutil.ErrValidation, + }, + { + desc: "update user identity with malformed data", + data: fmt.Sprintf(`{"identity": %s}`, "invalid"), + client: mgclients.Client{ + ID: client.ID, + Credentials: mgclients.Credentials{ + Identity: "", + Secret: "secret", + }, + }, + contentType: contentType, + token: validToken, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodPatch, + url: fmt.Sprintf("%s/users/%s/identity", us.URL, tc.client.ID), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.data), + } + + repoCall := svc.On("UpdateClientIdentity", mock.Anything, tc.token, mock.Anything, mock.Anything).Return(mgclients.Client{}, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestPasswordResetRequest(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + testemail := "test@example.com" + testhost := "example.com" + + cases := []struct { + desc string + data string + contentType string + status int + err error + }{ + { + desc: "password reset request with valid email", + data: fmt.Sprintf(`{"email": "%s", "host": "%s"}`, testemail, testhost), + contentType: contentType, + status: http.StatusCreated, + err: nil, + }, + { + desc: "password reset request with empty email", + data: fmt.Sprintf(`{"email": "%s", "host": "%s"}`, "", testhost), + contentType: contentType, + status: http.StatusInternalServerError, + err: apiutil.ErrValidation, + }, + { + desc: "password reset request with empty host", + data: fmt.Sprintf(`{"email": "%s", "host": "%s"}`, testemail, ""), + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "password reset request with invalid email", + data: fmt.Sprintf(`{"email": "%s", "host": "%s"}`, "invalid", testhost), + contentType: contentType, + status: http.StatusNotFound, + err: errors.ErrNotFound, + }, + { + desc: "password reset with malformed data", + data: fmt.Sprintf(`{"email": %s, "host": %s}`, testemail, testhost), + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "password reset with invalid contentype", + data: fmt.Sprintf(`{"email": "%s", "host": "%s"}`, testemail, testhost), + contentType: "application/xml", + status: http.StatusUnsupportedMediaType, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/users/password/reset-request", us.URL), + contentType: tc.contentType, + body: strings.NewReader(tc.data), + } + + repoCall := svc.On("GenerateResetToken", mock.Anything, mock.Anything, mock.Anything).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestPasswordReset(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + strongPass := "StrongPassword" + + cases := []struct { + desc string + data string + token string + contentType string + status int + err error + }{ + { + desc: "password reset with valid token", + data: fmt.Sprintf(`{"token": "%s", "password": "%s", "confirm_password": "%s"}`, validToken, strongPass, strongPass), + token: validToken, + contentType: contentType, + status: http.StatusCreated, + err: nil, + }, + { + desc: "password reset with invalid token", + data: fmt.Sprintf(`{"token": "%s", "password": "%s", "confirm_password": "%s"}`, inValidToken, strongPass, strongPass), + token: inValidToken, + contentType: contentType, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "password reset to weak password", + data: fmt.Sprintf(`{"token": "%s", "password": "%s", "confirm_password": "%s"}`, validToken, "weak", "weak"), + token: validToken, + contentType: contentType, + status: http.StatusInternalServerError, + err: ErrPasswordFormat, + }, + { + desc: "password reset with empty token", + data: fmt.Sprintf(`{"token": "%s", "password": "%s", "confirm_password": "%s"}`, "", strongPass, strongPass), + token: "", + contentType: contentType, + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "password reset with empty password", + data: fmt.Sprintf(`{"token": "%s", "password": "%s", "confirm_password": "%s"}`, validToken, "", ""), + token: validToken, + contentType: contentType, + status: http.StatusInternalServerError, + err: apiutil.ErrValidation, + }, + { + desc: "password reset with malformed data", + data: fmt.Sprintf(`{"token": "%s", "password": %s, "confirm_password": %s}`, validToken, strongPass, strongPass), + token: validToken, + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "password reset with invalid contentype", + data: fmt.Sprintf(`{"token": "%s", "password": "%s", "confirm_password": "%s"}`, validToken, strongPass, strongPass), + token: validToken, + status: http.StatusUnsupportedMediaType, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodPut, + url: fmt.Sprintf("%s/users/password/reset", us.URL), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.data), + } + + repoCall := svc.On("ResetSecret", mock.Anything, mock.Anything, mock.Anything).Return(tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestUpdateClientRole(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + data string + clientID string + token string + contentType string + status int + err error + }{ + { + desc: "update client role with valid token", + data: fmt.Sprintf(`{"role": "%s"}`, "admin"), + clientID: client.ID, + token: validToken, + contentType: contentType, + status: http.StatusOK, + err: nil, + }, + { + desc: "update client role with invalid token", + data: fmt.Sprintf(`{"role": "%s"}`, "admin"), + clientID: client.ID, + token: inValidToken, + contentType: contentType, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "update client role with invalid id", + data: fmt.Sprintf(`{"role": "%s"}`, "admin"), + clientID: inValid, + token: validToken, + contentType: contentType, + status: http.StatusForbidden, + err: svcerr.ErrAuthorization, + }, + { + desc: "update client role with empty token", + data: fmt.Sprintf(`{"role": "%s"}`, "admin"), + clientID: client.ID, + token: "", + contentType: contentType, + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "update client with invalid role", + data: fmt.Sprintf(`{"role": "%s"}`, "invalid"), + clientID: client.ID, + token: validToken, + contentType: contentType, + status: http.StatusInternalServerError, + err: svcerr.ErrInvalidRole, + }, + { + desc: "update client with invalid contentype", + data: fmt.Sprintf(`{"role": "%s"}`, "admin"), + clientID: client.ID, + token: validToken, + contentType: "application/xml", + status: http.StatusUnsupportedMediaType, + err: apiutil.ErrValidation, + }, + { + desc: "update client with malformed data", + data: fmt.Sprintf(`{"role": %s}`, "admin"), + clientID: client.ID, + token: validToken, + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodPatch, + url: fmt.Sprintf("%s/users/%s/role", us.URL, tc.clientID), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.data), + } + + repoCall := svc.On("UpdateClientRole", mock.Anything, tc.token, mock.Anything).Return(mgclients.Client{}, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestUpdateClientSecret(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + data string + client mgclients.Client + contentType string + token string + status int + err error + }{ + { + desc: "update user secret with valid token", + data: fmt.Sprintf(`{"secret": "%s"}`, "strongersecret"), + client: mgclients.Client{ + ID: client.ID, + Credentials: mgclients.Credentials{ + Identity: "clientname", + Secret: "strongersecret", + }, + }, + contentType: contentType, + token: validToken, + status: http.StatusOK, + err: nil, + }, + { + desc: "update user secret with empty token", + data: fmt.Sprintf(`{"secret": "%s"}`, "strongersecret"), + client: mgclients.Client{ + ID: client.ID, + Credentials: mgclients.Credentials{ + Identity: "clientname", + Secret: "strongersecret", + }, + }, + contentType: contentType, + token: "", + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "update user secret with invalid token", + data: fmt.Sprintf(`{"secret": "%s"}`, "strongersecret"), + client: mgclients.Client{ + ID: client.ID, + Credentials: mgclients.Credentials{ + Identity: "clientname", + Secret: "strongersecret", + }, + }, + contentType: contentType, + token: inValid, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + + { + desc: "update user secret with empty secret", + data: fmt.Sprintf(`{"secret": "%s"}`, ""), + client: mgclients.Client{ + ID: client.ID, + Credentials: mgclients.Credentials{ + Identity: "clientname", + Secret: "", + }, + }, + contentType: contentType, + token: validToken, + status: http.StatusBadRequest, + err: apiutil.ErrBearerKey, + }, + { + desc: "update user secret with invalid contentype", + data: fmt.Sprintf(`{"secret": "%s"}`, ""), + client: mgclients.Client{ + ID: client.ID, + Credentials: mgclients.Credentials{ + Identity: "clientname", + Secret: "", + }, + }, + contentType: "application/xml", + token: validToken, + status: http.StatusUnsupportedMediaType, + err: apiutil.ErrValidation, + }, + { + desc: "update user secret with malformed data", + data: fmt.Sprintf(`{"secret": %s}`, "invalid"), + client: mgclients.Client{ + ID: client.ID, + Credentials: mgclients.Credentials{ + Identity: "clientname", + Secret: "", + }, + }, + contentType: contentType, + token: validToken, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodPatch, + url: fmt.Sprintf("%s/users/secret", us.URL), + contentType: tc.contentType, + token: tc.token, + body: strings.NewReader(tc.data), + } + + repoCall := svc.On("UpdateClientSecret", mock.Anything, tc.token, mock.Anything, mock.Anything).Return(tc.client, tc.err) + + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestIssueToken(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + validIdentity := "valid" + + cases := []struct { + desc string + data string + contentType string + status int + err error + }{ + { + desc: "issue token with valid identity and secret", + data: fmt.Sprintf(`{"identity": "%s", "secret": "%s", "domainID": "%s"}`, validIdentity, secret, validID), + contentType: contentType, + status: http.StatusCreated, + err: nil, + }, + { + desc: "issue token with empty identity", + data: fmt.Sprintf(`{"identity": "%s", "secret": "%s", "domainID": "%s"}`, "", secret, validID), + contentType: contentType, + status: http.StatusInternalServerError, + err: apiutil.ErrValidation, + }, + { + desc: "issue token with empty secret", + data: fmt.Sprintf(`{"identity": "%s", "secret": "%s", "domainID": "%s"}`, validIdentity, "", validID), + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "issue token with empty domain", + data: fmt.Sprintf(`{"identity": "%s", "secret": "%s", "domainID": "%s"}`, validIdentity, secret, ""), + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "issue token with invalid identity", + data: fmt.Sprintf(`{"identity": "%s", "secret": "%s", "domainID": "%s"}`, "invalid", secret, validID), + contentType: contentType, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "issues token with malformed data", + data: fmt.Sprintf(`{"identity": %s, "secret": %s, "domainID": %s}`, validIdentity, secret, validID), + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "issue token with invalid contentype", + data: fmt.Sprintf(`{"identity": "%s", "secret": "%s", "domainID": "%s"}`, "invalid", secret, validID), + contentType: "application/xml", + status: http.StatusUnsupportedMediaType, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/users/tokens/issue", us.URL), + contentType: tc.contentType, + body: strings.NewReader(tc.data), + } + + repoCall := svc.On("IssueToken", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(&magistrala.Token{}, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + if tc.err != nil { + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestRefreshToken(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + data string + contentType string + status int + err error + }{ + { + desc: "refresh token with valid token", + data: fmt.Sprintf(`{"refresh_token": "%s", "domain_id": "%s"}`, validToken, validID), + contentType: contentType, + status: http.StatusCreated, + err: nil, + }, + { + desc: "refresh token with invalid token", + data: fmt.Sprintf(`{"refresh_token": "%s", "domain_id": "%s"}`, inValidToken, validID), + contentType: contentType, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "refresh token with empty token", + data: fmt.Sprintf(`{"refresh_token": "%s", "domain_id": "%s"}`, "", validID), + contentType: contentType, + status: http.StatusUnauthorized, + err: apiutil.ErrValidation, + }, + { + desc: "refresh token with invalid domain", + data: fmt.Sprintf(`{"refresh_token": "%s", "domain_id": "%s"}`, validToken, "invalid"), + contentType: contentType, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "refresh token with malformed data", + data: fmt.Sprintf(`{"refresh_token": %s, "domain_id": %s}`, validToken, validID), + contentType: contentType, + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "refresh token with invalid contentype", + data: fmt.Sprintf(`{"refresh_token": "%s", "domain_id": "%s"}`, validToken, validID), + contentType: "application/xml", + status: http.StatusUnsupportedMediaType, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/users/tokens/refresh", us.URL), + contentType: tc.contentType, + body: strings.NewReader(tc.data), + } + + repoCall := svc.On("RefreshToken", mock.Anything, mock.Anything, mock.Anything).Return(&magistrala.Token{}, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + if tc.err != nil { + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestEnableClient(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + cases := []struct { + desc string + client mgclients.Client + response mgclients.Client + token string + status int + err error + }{ + { + desc: "enable client with valid token", + client: client, + response: mgclients.Client{ + ID: client.ID, + Status: mgclients.EnabledStatus, + }, + token: validToken, + status: http.StatusOK, + err: nil, + }, + { + desc: "enable client with invalid token", + client: client, + token: inValidToken, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "enable client with empty id", + client: mgclients.Client{ + ID: "", + }, + token: validToken, + status: http.StatusBadRequest, + err: apiutil.ErrMissingID, + }, + { + desc: "enable client with invalid id", + client: mgclients.Client{ + ID: "invalid", + }, + token: validToken, + status: http.StatusForbidden, + err: svcerr.ErrAuthorization, + }, + } + + for _, tc := range cases { + data := toJSON(tc.client) + req := testRequest{ + client: us.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/users/%s/enable", us.URL, tc.client.ID), + contentType: contentType, + token: tc.token, + body: strings.NewReader(data), + } + + repoCall := svc.On("EnableClient", mock.Anything, mock.Anything, mock.Anything).Return(mgclients.Client{}, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + if tc.err != nil { + var resBody respBody + err = json.NewDecoder(res.Body).Decode(&resBody) + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error while decoding response body: %s", tc.desc, err)) + if resBody.Err != "" || resBody.Message != "" { + err = errors.Wrap(errors.New(resBody.Err), errors.New(resBody.Message)) + } + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestDisableClient(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + client mgclients.Client + response mgclients.Client + token string + status int + err error + }{ + { + desc: "disable user with valid token", + client: client, + response: mgclients.Client{ + ID: client.ID, + Status: mgclients.DisabledStatus, + }, + token: validToken, + status: http.StatusOK, + err: nil, + }, + { + desc: "disable user with invalid token", + client: client, + token: inValidToken, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "disable user with empty id", + client: mgclients.Client{ + ID: "", + }, + token: validToken, + status: http.StatusBadRequest, + err: apiutil.ErrMissingID, + }, + { + desc: "disable user with invalid id", + client: mgclients.Client{ + ID: "invalid", + }, + token: validToken, + status: http.StatusForbidden, + err: svcerr.ErrAuthorization, + }, + } + + for _, tc := range cases { + data := toJSON(tc.client) + req := testRequest{ + client: us.Client(), + method: http.MethodPost, + url: fmt.Sprintf("%s/users/%s/disable", us.URL, tc.client.ID), + contentType: contentType, + token: tc.token, + body: strings.NewReader(data), + } + + repoCall := svc.On("DisableClient", mock.Anything, mock.Anything, mock.Anything).Return(mgclients.Client{}, tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestListUsersByUserGroupId(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + token string + groupID string + page mgclients.Page + status int + query string + listUsersResponse mgclients.ClientsPage + err error + }{ + { + desc: "list users with valid token", + token: validToken, + groupID: validID, + status: http.StatusOK, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + err: nil, + }, + { + desc: "list users with empty id", + token: validToken, + groupID: "", + status: http.StatusBadRequest, + err: apiutil.ErrMissingID, + }, + { + desc: "list users with empty token", + token: "", + groupID: validID, + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "list users with invalid token", + token: inValidToken, + groupID: validID, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "list users with offset", + token: validToken, + groupID: validID, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Offset: 1, + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "offset=1", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid offset", + token: validToken, + groupID: validID, + query: "offset=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with limit", + token: validToken, + groupID: validID, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Limit: 1, + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "limit=1", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid limit", + token: validToken, + groupID: validID, + query: "limit=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with limit greater than max", + token: validToken, + groupID: validID, + query: fmt.Sprintf("limit=%d", api.MaxLimitSize+1), + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with owner_id", + token: validToken, + groupID: validID, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: fmt.Sprintf("owner_id=%s", validID), + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with duplicate owner_id", + token: validToken, + groupID: validID, + query: "owner_id=1&owner_id=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with invalid owner_id", + token: validToken, + groupID: validID, + query: "owner_id=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with name", + token: validToken, + groupID: validID, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "name=clientname", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid name", + token: validToken, + groupID: validID, + query: "name=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate name", + token: validToken, + groupID: validID, + query: "name=1&name=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with status", + token: validToken, + groupID: validID, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "status=enabled", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid status", + token: validToken, + groupID: validID, + query: "status=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate status", + token: validToken, + groupID: validID, + query: "status=enabled&status=disabled", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with tags", + token: validToken, + groupID: validID, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "tag=tag1,tag2", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid tags", + token: validToken, + groupID: validID, + query: "tag=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate tags", + token: validToken, + groupID: validID, + query: "tag=tag1&tag=tag2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with metadata", + token: validToken, + groupID: validID, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "metadata=%7B%22domain%22%3A%20%22example.com%22%7D&", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid metadata", + token: validToken, + groupID: validID, + query: "metadata=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate metadata", + token: validToken, + groupID: validID, + query: "metadata=%7B%22domain%22%3A%20%22example.com%22%7D&metadata=%7B%22domain%22%3A%20%22example.com%22%7D", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with permissions", + token: validToken, + groupID: validID, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "permission=view", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid permissions", + token: validToken, + groupID: validID, + query: "permission=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate permissions", + token: validToken, + groupID: validID, + query: "permission=view&permission=view", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with identity", + token: validToken, + groupID: validID, + query: fmt.Sprintf("identity=%s", client.Credentials.Identity), + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{ + client, + }, + }, + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid identity", + token: validToken, + groupID: validID, + query: "identity=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate identity", + token: validToken, + groupID: validID, + query: "identity=1&identity=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodGet, + url: fmt.Sprintf("%s/groups/%s/users?", us.URL, tc.groupID) + tc.query, + token: tc.token, + } + + repoCall := svc.On("ListMembers", mock.Anything, tc.token, mock.Anything, mock.Anything, mock.Anything).Return( + mgclients.MembersPage{ + Page: tc.listUsersResponse.Page, + Members: tc.listUsersResponse.Clients, + }, + tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestListUsersByChannelID(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + token string + groupID string + page mgclients.Page + status int + query string + listUsersResponse mgclients.ClientsPage + err error + }{ + { + desc: "list users with valid token", + token: validToken, + status: http.StatusOK, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + err: nil, + }, + { + desc: "list users with empty token", + token: "", + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "list users with invalid token", + token: inValidToken, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "list users with offset", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Offset: 1, + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "offset=1", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid offset", + token: validToken, + query: "offset=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with limit", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Limit: 1, + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "limit=1", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid limit", + token: validToken, + query: "limit=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with limit greater than max", + token: validToken, + query: fmt.Sprintf("limit=%d", api.MaxLimitSize+1), + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with owner_id", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: fmt.Sprintf("owner_id=%s", validID), + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with duplicate owner_id", + token: validToken, + query: "owner_id=1&owner_id=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with invalid owner_id", + token: validToken, + query: "owner_id=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with name", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "name=clientname", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid name", + token: validToken, + query: "name=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate name", + token: validToken, + query: "name=1&name=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with status", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "status=enabled", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid status", + token: validToken, + query: "status=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate status", + token: validToken, + query: "status=enabled&status=disabled", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with tags", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "tag=tag1,tag2", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid tags", + token: validToken, + query: "tag=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate tags", + token: validToken, + query: "tag=tag1&tag=tag2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with metadata", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "metadata=%7B%22domain%22%3A%20%22example.com%22%7D&", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid metadata", + token: validToken, + query: "metadata=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate metadata", + token: validToken, + query: "metadata=%7B%22domain%22%3A%20%22example.com%22%7D&metadata=%7B%22domain%22%3A%20%22example.com%22%7D", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with permissions", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "permission=view", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid permissions", + token: validToken, + query: "permission=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate permissions", + token: validToken, + query: "permission=view&permission=view", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with identity", + token: validToken, + query: fmt.Sprintf("identity=%s", client.Credentials.Identity), + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{ + client, + }, + }, + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid identity", + token: validToken, + query: "identity=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate identity", + token: validToken, + query: "identity=1&identity=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with list_perms", + token: validToken, + query: "list_perms=true", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid list_perms", + token: validToken, + query: "list_perms=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate list_perms", + token: validToken, + query: "list_perms=true&list_perms=false", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodGet, + url: fmt.Sprintf("%s/channels/%s/users?", us.URL, validID) + tc.query, + token: tc.token, + } + + repoCall := svc.On("ListMembers", mock.Anything, tc.token, mock.Anything, mock.Anything, mock.Anything).Return( + mgclients.MembersPage{ + Page: tc.listUsersResponse.Page, + Members: tc.listUsersResponse.Clients, + }, + tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode, fmt.Sprintf("%s: expected status code %d got %d", tc.desc, tc.status, res.StatusCode)) + repoCall.Unset() + } +} + +func TestListUsersByDomainID(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + token string + groupID string + page mgclients.Page + status int + query string + listUsersResponse mgclients.ClientsPage + err error + }{ + { + desc: "list users with valid token", + token: validToken, + status: http.StatusOK, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + err: nil, + }, + { + desc: "list users with empty token", + token: "", + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "list users with invalid token", + token: inValidToken, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "list users with offset", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Offset: 1, + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "offset=1", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid offset", + token: validToken, + query: "offset=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with limit", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Limit: 1, + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "limit=1", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid limit", + token: validToken, + query: "limit=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with limit greater than max", + token: validToken, + query: fmt.Sprintf("limit=%d", api.MaxLimitSize+1), + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with owner_id", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: fmt.Sprintf("owner_id=%s", validID), + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with duplicate owner_id", + token: validToken, + query: "owner_id=1&owner_id=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with invalid owner_id", + token: validToken, + query: "owner_id=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with name", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "name=clientname", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid name", + token: validToken, + query: "name=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate name", + token: validToken, + query: "name=1&name=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with status", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "status=enabled", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid status", + token: validToken, + query: "status=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate status", + token: validToken, + query: "status=enabled&status=disabled", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with tags", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "tag=tag1,tag2", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid tags", + token: validToken, + query: "tag=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate tags", + token: validToken, + query: "tag=tag1&tag=tag2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with metadata", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "metadata=%7B%22domain%22%3A%20%22example.com%22%7D&", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid metadata", + token: validToken, + query: "metadata=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate metadata", + token: validToken, + query: "metadata=%7B%22domain%22%3A%20%22example.com%22%7D&metadata=%7B%22domain%22%3A%20%22example.com%22%7D", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with permissions", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "permission=membership", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid permissions", + token: validToken, + query: "permission=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate permissions", + token: validToken, + query: "permission=view&permission=view", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with identity", + token: validToken, + query: fmt.Sprintf("identity=%s", client.Credentials.Identity), + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{ + client, + }, + }, + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid identity", + token: validToken, + query: "identity=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate identity", + token: validToken, + query: "identity=1&identity=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users wiith list permissions", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{ + client, + }, + }, + query: "list_perms=true", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid list_perms", + token: validToken, + query: "list_perms=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate list_perms", + token: validToken, + query: "list_perms=true&list_perms=false", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodGet, + url: fmt.Sprintf("%s/domains/%s/users?", us.URL, validID) + tc.query, + token: tc.token, + } + + repoCall := svc.On("ListMembers", mock.Anything, tc.token, mock.Anything, mock.Anything, mock.Anything).Return( + mgclients.MembersPage{ + Page: tc.listUsersResponse.Page, + Members: tc.listUsersResponse.Clients, + }, + tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode) + repoCall.Unset() + } +} + +func TestListUsersByThingID(t *testing.T) { + us, svc := newUsersServer() + defer us.Close() + + cases := []struct { + desc string + token string + groupID string + page mgclients.Page + status int + query string + listUsersResponse mgclients.ClientsPage + err error + }{ + { + desc: "list users with valid token", + token: validToken, + status: http.StatusOK, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + err: nil, + }, + { + desc: "list users with empty token", + token: "", + status: http.StatusUnauthorized, + err: apiutil.ErrBearerToken, + }, + { + desc: "list users with invalid token", + token: inValidToken, + status: http.StatusUnauthorized, + err: svcerr.ErrAuthentication, + }, + { + desc: "list users with offset", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Offset: 1, + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "offset=1", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid offset", + token: validToken, + query: "offset=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with limit", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Limit: 1, + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "limit=1", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid limit", + token: validToken, + query: "limit=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with limit greater than max", + token: validToken, + query: fmt.Sprintf("limit=%d", api.MaxLimitSize+1), + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with owner_id", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: fmt.Sprintf("owner_id=%s", validID), + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with duplicate owner_id", + token: validToken, + query: "owner_id=1&owner_id=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with invalid owner_id", + token: validToken, + query: "owner_id=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with name", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "name=clientname", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid name", + token: validToken, + query: "name=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate name", + token: validToken, + query: "name=1&name=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with status", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "status=enabled", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid status", + token: validToken, + query: "status=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate status", + token: validToken, + query: "status=enabled&status=disabled", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with tags", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "tag=tag1,tag2", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid tags", + token: validToken, + query: "tag=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate tags", + token: validToken, + query: "tag=tag1&tag=tag2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with metadata", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "metadata=%7B%22domain%22%3A%20%22example.com%22%7D&", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid metadata", + token: validToken, + query: "metadata=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate metadata", + token: validToken, + query: "metadata=%7B%22domain%22%3A%20%22example.com%22%7D&metadata=%7B%22domain%22%3A%20%22example.com%22%7D", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with permissions", + token: validToken, + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{client}, + }, + query: "permission=view", + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid permissions", + token: validToken, + query: "permission=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate permissions", + token: validToken, + query: "permission=view&permission=view", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + { + desc: "list users with identity", + token: validToken, + query: fmt.Sprintf("identity=%s", client.Credentials.Identity), + listUsersResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + }, + Clients: []mgclients.Client{ + client, + }, + }, + status: http.StatusOK, + err: nil, + }, + { + desc: "list users with invalid identity", + token: validToken, + query: "identity=invalid", + status: http.StatusBadRequest, + err: apiutil.ErrValidation, + }, + { + desc: "list users with duplicate identity", + token: validToken, + query: "identity=1&identity=2", + status: http.StatusBadRequest, + err: apiutil.ErrInvalidQueryParams, + }, + } + + for _, tc := range cases { + req := testRequest{ + client: us.Client(), + method: http.MethodGet, + url: fmt.Sprintf("%s/things/%s/users?", us.URL, validID) + tc.query, + token: tc.token, + } + + repoCall := svc.On("ListMembers", mock.Anything, tc.token, mock.Anything, mock.Anything, mock.Anything).Return( + mgclients.MembersPage{ + Page: tc.listUsersResponse.Page, + Members: tc.listUsersResponse.Clients, + }, + tc.err) + res, err := req.make() + assert.Nil(t, err, fmt.Sprintf("%s: unexpected error %s", tc.desc, err)) + assert.Equal(t, tc.status, res.StatusCode) + repoCall.Unset() + } +} + +type respBody struct { + Err string `json:"error"` + Message string `json:"message"` + Total int `json:"total"` + ID string `json:"id"` + Tags []string `json:"tags"` + Role mgclients.Role `json:"role"` + Status mgclients.Status `json:"status"` +} diff --git a/users/api/requests.go b/users/api/requests.go index dfd51147c..9c52064f3 100644 --- a/users/api/requests.go +++ b/users/api/requests.go @@ -53,19 +53,17 @@ func (req viewProfileReq) validate() error { } type listClientsReq struct { - token string - status mgclients.Status - offset uint64 - limit uint64 - name string - tag string - identity string - visibility string - owner string - sharedBy string - metadata mgclients.Metadata - order string - dir string + token string + status mgclients.Status + offset uint64 + limit uint64 + name string + tag string + identity string + owner string + metadata mgclients.Metadata + order string + dir string } func (req listClientsReq) validate() error { @@ -75,12 +73,6 @@ func (req listClientsReq) validate() error { if req.limit > maxLimitSize || req.limit < 1 { return apiutil.ErrLimitSize } - if req.visibility != "" && - req.visibility != api.AllVisibility && - req.visibility != api.MyVisibility && - req.visibility != api.SharedVisibility { - return apiutil.ErrInvalidVisibilityType - } if req.dir != "" && (req.dir != api.AscDir && req.dir != api.DescDir) { return apiutil.ErrInvalidDirection } diff --git a/users/api/requests_test.go b/users/api/requests_test.go new file mode 100644 index 000000000..571233ae8 --- /dev/null +++ b/users/api/requests_test.go @@ -0,0 +1,853 @@ +// Copyright (c) Abstract Machines +// SPDX-License-Identifier: Apache-2.0 + +package api + +import ( + "strings" + "testing" + + "github.com/absmach/magistrala/internal/api" + "github.com/absmach/magistrala/internal/apiutil" + "github.com/absmach/magistrala/internal/testsutil" + mgclients "github.com/absmach/magistrala/pkg/clients" + "github.com/stretchr/testify/assert" +) + +const ( + valid = "valid" + invalid = "invalid" +) + +var validID = testsutil.GenerateUUID(&testing.T{}) + +func TestCreateClientReqValidate(t *testing.T) { + cases := []struct { + desc string + req createClientReq + err error + }{ + { + desc: "valid request", + req: createClientReq{ + token: valid, + client: mgclients.Client{ + ID: validID, + Name: valid, + Credentials: mgclients.Credentials{ + Identity: "example@example.com", + Secret: valid, + }, + }, + }, + err: nil, + }, + { + desc: "empty token", + req: createClientReq{ + token: "", + client: mgclients.Client{ + ID: validID, + Name: valid, + Credentials: mgclients.Credentials{ + Identity: "example@example.com", + Secret: valid, + }, + }, + }, + }, + { + desc: "name too long", + req: createClientReq{ + token: valid, + client: mgclients.Client{ + ID: validID, + Name: strings.Repeat("a", api.MaxNameSize+1), + }, + }, + err: apiutil.ErrNameSize, + }, + } + for _, tc := range cases { + err := tc.req.validate() + assert.Equal(t, tc.err, err) + } +} + +func TestViewClientReqValidate(t *testing.T) { + cases := []struct { + desc string + req viewClientReq + err error + }{ + { + desc: "valid request", + req: viewClientReq{ + token: valid, + id: validID, + }, + err: nil, + }, + { + desc: "empty token", + req: viewClientReq{ + token: "", + id: validID, + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty id", + req: viewClientReq{ + token: valid, + id: "", + }, + err: apiutil.ErrMissingID, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestViewProfileReqValidate(t *testing.T) { + cases := []struct { + desc string + req viewProfileReq + err error + }{ + { + desc: "valid request", + req: viewProfileReq{ + token: valid, + }, + err: nil, + }, + { + desc: "empty token", + req: viewProfileReq{ + token: "", + }, + err: apiutil.ErrBearerToken, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err) + } +} + +func TestListClientsReqValidate(t *testing.T) { + cases := []struct { + desc string + req listClientsReq + err error + }{ + { + desc: "valid request", + req: listClientsReq{ + token: valid, + limit: 10, + }, + err: nil, + }, + { + desc: "empty token", + req: listClientsReq{ + token: "", + limit: 10, + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "limit too big", + req: listClientsReq{ + token: valid, + limit: api.MaxLimitSize + 1, + }, + err: apiutil.ErrLimitSize, + }, + { + desc: "limit too small", + req: listClientsReq{ + token: valid, + limit: 0, + }, + err: apiutil.ErrLimitSize, + }, + { + desc: "invalid direction", + req: listClientsReq{ + token: valid, + limit: 10, + dir: "invalid", + }, + err: apiutil.ErrInvalidDirection, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestListMembersByObjectReqValidate(t *testing.T) { + cases := []struct { + desc string + req listMembersByObjectReq + err error + }{ + { + desc: "valid request", + req: listMembersByObjectReq{ + token: valid, + objectKind: "group", + objectID: validID, + }, + err: nil, + }, + { + desc: "empty token", + req: listMembersByObjectReq{ + token: "", + objectKind: "group", + objectID: validID, + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty object kind", + req: listMembersByObjectReq{ + token: valid, + objectKind: "", + objectID: validID, + }, + err: apiutil.ErrMissingMemberKind, + }, + { + desc: "empty object id", + req: listMembersByObjectReq{ + token: valid, + objectKind: "group", + objectID: "", + }, + err: apiutil.ErrMissingID, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err) + } +} + +func TestUpdateClientReqValidate(t *testing.T) { + cases := []struct { + desc string + req updateClientReq + err error + }{ + { + desc: "valid request", + req: updateClientReq{ + token: valid, + id: validID, + Name: valid, + }, + err: nil, + }, + { + desc: "empty token", + req: updateClientReq{ + token: "", + id: validID, + Name: valid, + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty id", + req: updateClientReq{ + token: valid, + id: "", + Name: valid, + }, + err: apiutil.ErrMissingID, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestUpdateClientTagsReqValidate(t *testing.T) { + cases := []struct { + desc string + req updateClientTagsReq + err error + }{ + { + desc: "valid request", + req: updateClientTagsReq{ + token: valid, + id: validID, + Tags: []string{"tag1", "tag2"}, + }, + err: nil, + }, + { + desc: "empty token", + req: updateClientTagsReq{ + token: "", + id: validID, + Tags: []string{"tag1", "tag2"}, + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty id", + req: updateClientTagsReq{ + token: valid, + id: "", + Tags: []string{"tag1", "tag2"}, + }, + err: apiutil.ErrMissingID, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestUpdateClientRoleReqValidate(t *testing.T) { + cases := []struct { + desc string + req updateClientRoleReq + err error + }{ + { + desc: "valid request", + req: updateClientRoleReq{ + token: valid, + id: validID, + Role: "admin", + }, + err: nil, + }, + { + desc: "empty token", + req: updateClientRoleReq{ + token: "", + id: validID, + Role: "admin", + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty id", + req: updateClientRoleReq{ + token: valid, + id: "", + Role: "admin", + }, + err: apiutil.ErrMissingID, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestUpdateClientIdentityReqValidate(t *testing.T) { + cases := []struct { + desc string + req updateClientIdentityReq + err error + }{ + { + desc: "valid request", + req: updateClientIdentityReq{ + token: valid, + id: validID, + Identity: "example@example.com", + }, + err: nil, + }, + { + desc: "empty token", + req: updateClientIdentityReq{ + token: "", + id: validID, + Identity: "example@example.com", + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty id", + req: updateClientIdentityReq{ + token: valid, + id: "", + Identity: "example@example.com", + }, + err: apiutil.ErrMissingID, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestUpdateClientSecretReqValidate(t *testing.T) { + cases := []struct { + desc string + req updateClientSecretReq + err error + }{ + { + desc: "valid request", + req: updateClientSecretReq{ + token: valid, + OldSecret: valid, + NewSecret: valid, + }, + err: nil, + }, + { + desc: "empty token", + req: updateClientSecretReq{ + token: "", + OldSecret: valid, + NewSecret: valid, + }, + err: apiutil.ErrBearerToken, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err) + } +} + +func TestChangeClientStatusReqValidate(t *testing.T) { + cases := []struct { + desc string + req changeClientStatusReq + err error + }{ + { + desc: "valid request", + req: changeClientStatusReq{ + token: valid, + id: validID, + }, + err: nil, + }, + { + desc: "empty token", + req: changeClientStatusReq{ + token: "", + id: validID, + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty id", + req: changeClientStatusReq{ + token: valid, + id: "", + }, + err: apiutil.ErrMissingID, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestLoginClientReqValidate(t *testing.T) { + cases := []struct { + desc string + req loginClientReq + err error + }{ + { + desc: "valid request", + req: loginClientReq{ + Identity: "eaxmple,example.com", + Secret: valid, + }, + err: nil, + }, + { + desc: "empty identity", + req: loginClientReq{ + Identity: "", + Secret: valid, + }, + err: apiutil.ErrMissingIdentity, + }, + { + desc: "empty secret", + req: loginClientReq{ + Identity: "eaxmple,example.com", + Secret: "", + }, + err: apiutil.ErrMissingSecret, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestTokenReqValidate(t *testing.T) { + cases := []struct { + desc string + req tokenReq + err error + }{ + { + desc: "valid request", + req: tokenReq{ + RefreshToken: valid, + }, + err: nil, + }, + { + desc: "empty token", + req: tokenReq{ + RefreshToken: "", + }, + err: apiutil.ErrBearerToken, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestPasswResetReqValidate(t *testing.T) { + cases := []struct { + desc string + req passwResetReq + err error + }{ + { + desc: "valid request", + req: passwResetReq{ + Email: "example@example.com", + Host: "example.com", + }, + err: nil, + }, + { + desc: "empty email", + req: passwResetReq{ + Email: "", + Host: "example.com", + }, + err: apiutil.ErrMissingEmail, + }, + { + desc: "empty host", + req: passwResetReq{ + Email: "example@example.com", + Host: "", + }, + err: apiutil.ErrMissingHost, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestResetTokenReqValidate(t *testing.T) { + cases := []struct { + desc string + req resetTokenReq + err error + }{ + { + desc: "valid request", + req: resetTokenReq{ + Token: valid, + Password: valid, + ConfPass: valid, + }, + err: nil, + }, + { + desc: "empty token", + req: resetTokenReq{ + Token: "", + Password: valid, + ConfPass: valid, + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty password", + req: resetTokenReq{ + Token: valid, + Password: "", + ConfPass: valid, + }, + err: apiutil.ErrMissingPass, + }, + { + desc: "empty confpass", + req: resetTokenReq{ + Token: valid, + Password: valid, + ConfPass: "", + }, + err: apiutil.ErrMissingConfPass, + }, + { + desc: "mismatching password and confpass", + req: resetTokenReq{ + Token: valid, + Password: "valid2", + ConfPass: valid, + }, + err: apiutil.ErrInvalidResetPass, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err) + } +} + +func TestAssignUsersRequestValidate(t *testing.T) { + cases := []struct { + desc string + req assignUsersReq + err error + }{ + { + desc: "valid request", + req: assignUsersReq{ + token: valid, + groupID: validID, + UserIDs: []string{validID}, + Relation: valid, + }, + err: nil, + }, + { + desc: "empty token", + req: assignUsersReq{ + token: "", + groupID: validID, + UserIDs: []string{validID}, + Relation: valid, + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty id", + req: assignUsersReq{ + token: valid, + groupID: "", + UserIDs: []string{validID}, + Relation: valid, + }, + err: apiutil.ErrMissingID, + }, + { + desc: "empty users", + req: assignUsersReq{ + token: valid, + groupID: validID, + UserIDs: []string{}, + Relation: valid, + }, + err: apiutil.ErrEmptyList, + }, + { + desc: "empty relation", + req: assignUsersReq{ + token: valid, + groupID: validID, + UserIDs: []string{validID}, + Relation: "", + }, + err: apiutil.ErrMissingRelation, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestUnassignUsersRequestValidate(t *testing.T) { + cases := []struct { + desc string + req unassignUsersReq + err error + }{ + { + desc: "valid request", + req: unassignUsersReq{ + token: valid, + groupID: validID, + UserIDs: []string{validID}, + Relation: valid, + }, + err: nil, + }, + { + desc: "empty token", + req: unassignUsersReq{ + token: "", + groupID: validID, + UserIDs: []string{validID}, + Relation: valid, + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty id", + req: unassignUsersReq{ + token: valid, + groupID: "", + UserIDs: []string{validID}, + Relation: valid, + }, + err: apiutil.ErrMissingID, + }, + { + desc: "empty users", + req: unassignUsersReq{ + token: valid, + groupID: validID, + UserIDs: []string{}, + Relation: valid, + }, + err: apiutil.ErrEmptyList, + }, + { + desc: "empty relation", + req: unassignUsersReq{ + token: valid, + groupID: validID, + UserIDs: []string{validID}, + Relation: "", + }, + err: apiutil.ErrMissingRelation, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestAssignGroupsRequestValidate(t *testing.T) { + cases := []struct { + desc string + req assignGroupsReq + err error + }{ + { + desc: "valid request", + req: assignGroupsReq{ + token: valid, + groupID: validID, + GroupIDs: []string{validID}, + }, + err: nil, + }, + { + desc: "empty token", + req: assignGroupsReq{ + token: "", + groupID: validID, + GroupIDs: []string{validID}, + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty group id", + req: assignGroupsReq{ + token: valid, + groupID: "", + GroupIDs: []string{validID}, + }, + err: apiutil.ErrMissingID, + }, + { + desc: "empty user group ids", + req: assignGroupsReq{ + token: valid, + groupID: validID, + GroupIDs: []string{}, + }, + err: apiutil.ErrEmptyList, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} + +func TestUnassignGroupsRequestValidate(t *testing.T) { + cases := []struct { + desc string + req unassignGroupsReq + err error + }{ + { + desc: "valid request", + req: unassignGroupsReq{ + token: valid, + groupID: validID, + GroupIDs: []string{validID}, + }, + err: nil, + }, + { + desc: "empty token", + req: unassignGroupsReq{ + token: "", + groupID: validID, + GroupIDs: []string{validID}, + }, + err: apiutil.ErrBearerToken, + }, + { + desc: "empty group id", + req: unassignGroupsReq{ + token: valid, + groupID: "", + GroupIDs: []string{validID}, + }, + err: apiutil.ErrMissingID, + }, + { + desc: "empty user group ids", + req: unassignGroupsReq{ + token: valid, + groupID: validID, + GroupIDs: []string{}, + }, + err: apiutil.ErrEmptyList, + }, + } + for _, c := range cases { + err := c.req.validate() + assert.Equal(t, c.err, err, "%s: expected %s got %s\n", c.desc, c.err, err) + } +} diff --git a/users/postgres/clients.go b/users/postgres/clients.go index e22dad596..dd1abfae9 100644 --- a/users/postgres/clients.go +++ b/users/postgres/clients.go @@ -5,7 +5,6 @@ package postgres import ( "context" - "database/sql" "fmt" "github.com/absmach/magistrala/internal/postgres" @@ -79,19 +78,17 @@ func (repo clientRepo) CheckSuperAdmin(ctx context.Context, adminID string) erro q := "SELECT 1 FROM clients WHERE id = $1 AND role = $2" rows, err := repo.DB.QueryContext(ctx, q, adminID, mgclients.AdminRole) if err != nil { - if err == sql.ErrNoRows { - return errors.ErrAuthorization - } return errors.Wrap(errors.ErrAuthorization, err) } defer rows.Close() - if !rows.Next() { - return errors.ErrAuthorization - } - if err := rows.Err(); err != nil { - return errors.Wrap(errors.ErrAuthorization, err) + + if rows.Next() { + if err := rows.Err(); err != nil { + return errors.Wrap(errors.ErrAuthorization, err) + } + return nil } - return nil + return errors.ErrAuthorization } func (repo clientRepo) RetrieveByID(ctx context.Context, id string) (mgclients.Client, error) { @@ -102,22 +99,27 @@ func (repo clientRepo) RetrieveByID(ctx context.Context, id string) (mgclients.C ID: id, } - row, err := repo.DB.NamedQueryContext(ctx, q, dbc) + rows, err := repo.DB.NamedQueryContext(ctx, q, dbc) if err != nil { - if err == sql.ErrNoRows { - return mgclients.Client{}, errors.Wrap(errors.ErrNotFound, err) - } - return mgclients.Client{}, errors.Wrap(errors.ErrViewEntity, err) + return mgclients.Client{}, postgres.HandleError(repoerr.ErrViewEntity, err) } + defer rows.Close() - defer row.Close() - row.Next() dbc = pgclients.DBClient{} - if err := row.StructScan(&dbc); err != nil { - return mgclients.Client{}, errors.Wrap(errors.ErrNotFound, err) + if rows.Next() { + if err = rows.StructScan(&dbc); err != nil { + return mgclients.Client{}, postgres.HandleError(repoerr.ErrViewEntity, err) + } + + client, err := pgclients.ToClient(dbc) + if err != nil { + return mgclients.Client{}, errors.Wrap(repoerr.ErrFailedOpDB, err) + } + + return client, nil } - return pgclients.ToClient(dbc) + return mgclients.Client{}, repoerr.ErrNotFound } func (repo clientRepo) RetrieveAll(ctx context.Context, pm mgclients.Page) (mgclients.ClientsPage, error) { diff --git a/users/postgres/clients_test.go b/users/postgres/clients_test.go index e4670cf54..4a5d7f045 100644 --- a/users/postgres/clients_test.go +++ b/users/postgres/clients_test.go @@ -192,7 +192,23 @@ func TestClientsSave(t *testing.T) { }, err: nil, }, + { + desc: "add a client with invalid metadata", + client: mgclients.Client{ + ID: testsutil.GenerateUUID(t), + Name: namesgen.Generate(), + Credentials: mgclients.Credentials{ + Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()), + Secret: password, + }, + Metadata: map[string]interface{}{ + "key": make(chan int), + }, + }, + err: errors.ErrMalformedEntity, + }, } + for _, tc := range cases { rClient, err := repo.Save(context.Background(), tc.client) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) @@ -254,3 +270,553 @@ func TestIsPlatformAdmin(t *testing.T) { assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) } } + +func TestRetrieveByID(t *testing.T) { + t.Cleanup(func() { + _, err := db.Exec("DELETE FROM clients") + require.Nil(t, err, fmt.Sprintf("clean clients unexpected error: %s", err)) + }) + repo := cpostgres.NewRepository(database) + + client := mgclients.Client{ + ID: testsutil.GenerateUUID(t), + Name: namesgen.Generate(), + Credentials: mgclients.Credentials{ + Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()), + Secret: password, + }, + Metadata: mgclients.Metadata{}, + Status: mgclients.EnabledStatus, + } + + _, err := repo.Save(context.Background(), client) + require.Nil(t, err, fmt.Sprintf("failed to save client %s", client.ID)) + + cases := []struct { + desc string + clientID string + err error + }{ + { + desc: "retrieve existing client", + clientID: client.ID, + err: nil, + }, + { + desc: "retrieve non-existing client", + clientID: invalidName, + err: errors.ErrNotFound, + }, + { + desc: "retrieve with empty client id", + clientID: "", + err: errors.ErrNotFound, + }, + } + + for _, tc := range cases { + _, err := repo.RetrieveByID(context.Background(), tc.clientID) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.err, err)) + } +} + +func TestRetrieveAll(t *testing.T) { + t.Cleanup(func() { + _, err := db.Exec("DELETE FROM clients") + require.Nil(t, err, fmt.Sprintf("clean clients unexpected error: %s", err)) + }) + + repo := cpostgres.NewRepository(database) + + ownerID := testsutil.GenerateUUID(t) + + num := 200 + var items, enabledClients []mgclients.Client + for i := 0; i < num; i++ { + client := mgclients.Client{ + ID: testsutil.GenerateUUID(t), + Name: namesgen.Generate(), + Credentials: mgclients.Credentials{ + Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()), + Secret: "", + }, + Metadata: mgclients.Metadata{}, + Status: mgclients.EnabledStatus, + Tags: []string{"tag1"}, + } + if i%50 == 0 { + client.Owner = ownerID + client.Metadata = map[string]interface{}{ + "key": "value", + } + client.Role = mgclients.AdminRole + client.Status = mgclients.DisabledStatus + } + _, err := repo.Save(context.Background(), client) + require.Nil(t, err, fmt.Sprintf("failed to save client %s", client.ID)) + items = append(items, client) + if client.Status == mgclients.EnabledStatus { + enabledClients = append(enabledClients, client) + } + } + + cases := []struct { + desc string + pageMeta mgclients.Page + page mgclients.ClientsPage + err error + }{ + { + desc: "retrieve first page of clients", + pageMeta: mgclients.Page{ + Offset: 0, + Limit: 50, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 200, + Offset: 0, + Limit: 50, + }, + Clients: items[0:50], + }, + err: nil, + }, + { + desc: "retrieve second page of clients", + pageMeta: mgclients.Page{ + Offset: 50, + Limit: 200, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 200, + Offset: 50, + Limit: 200, + }, + Clients: items[50:200], + }, + err: nil, + }, + { + desc: "retrieve clients with limit", + pageMeta: mgclients.Page{ + Offset: 0, + Limit: 50, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: uint64(num), + Offset: 0, + Limit: 50, + }, + Clients: items[:50], + }, + }, + { + desc: "retrieve with offset out of range", + pageMeta: mgclients.Page{ + Offset: 1000, + Limit: 200, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 200, + Offset: 1000, + Limit: 200, + }, + Clients: []mgclients.Client{}, + }, + err: nil, + }, + { + desc: "retrieve with limit out of range", + pageMeta: mgclients.Page{ + Offset: 0, + Limit: 1000, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 200, + Offset: 0, + Limit: 1000, + }, + Clients: items, + }, + err: nil, + }, + { + desc: "retrieve with empty page", + pageMeta: mgclients.Page{}, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 196, // No of enabled clients. + Offset: 0, + Limit: 0, + }, + Clients: []mgclients.Client{}, + }, + err: nil, + }, + { + desc: "retrieve with client id", + pageMeta: mgclients.Page{ + IDs: []string{items[0].ID}, + Offset: 0, + Limit: 3, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + Offset: 0, + Limit: 3, + }, + Clients: []mgclients.Client{items[0]}, + }, + err: nil, + }, + { + desc: "retrieve with invalid client id", + pageMeta: mgclients.Page{ + IDs: []string{invalidName}, + Offset: 0, + Limit: 3, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 0, + Offset: 0, + Limit: 3, + }, + Clients: []mgclients.Client{}, + }, + err: nil, + }, + { + desc: "retrieve with client name", + pageMeta: mgclients.Page{ + Name: items[0].Name, + Offset: 0, + Limit: 3, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + Offset: 0, + Limit: 3, + }, + Clients: []mgclients.Client{items[0]}, + }, + err: nil, + }, + { + desc: "retrieve with enabled status", + pageMeta: mgclients.Page{ + Status: mgclients.EnabledStatus, + Offset: 0, + Limit: 200, + Role: mgclients.AllRole, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 196, + Offset: 0, + Limit: 200, + }, + Clients: enabledClients, + }, + err: nil, + }, + { + desc: "retrieve with disabled status", + pageMeta: mgclients.Page{ + Status: mgclients.DisabledStatus, + Offset: 0, + Limit: 200, + Role: mgclients.AllRole, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 4, + Offset: 0, + Limit: 200, + }, + Clients: []mgclients.Client{items[0], items[50], items[100], items[150]}, + }, + }, + { + desc: "retrieve with all status", + pageMeta: mgclients.Page{ + Status: mgclients.AllStatus, + Offset: 0, + Limit: 200, + Role: mgclients.AllRole, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 200, + Offset: 0, + Limit: 200, + }, + Clients: items, + }, + }, + { + desc: "retrieve with owner id", + pageMeta: mgclients.Page{ + Owner: ownerID, + Offset: 0, + Limit: 5, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 4, + Offset: 0, + Limit: 5, + }, + Clients: []mgclients.Client{items[0], items[50], items[100], items[150]}, + }, + err: nil, + }, + { + desc: "retrieve with invalid owner id", + pageMeta: mgclients.Page{ + Owner: invalidName, + Offset: 0, + Limit: 200, + Role: mgclients.AllRole, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 0, + Offset: 0, + Limit: 200, + }, + Clients: []mgclients.Client{}, + }, + err: nil, + }, + { + desc: "retrieve by tags", + pageMeta: mgclients.Page{ + Tag: "tag1", + Offset: 0, + Limit: 200, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 200, + Offset: 0, + Limit: 200, + }, + Clients: items, + }, + err: nil, + }, + { + desc: "retrieve with invalid client name", + pageMeta: mgclients.Page{ + Name: invalidName, + Offset: 0, + Limit: 3, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 0, + Offset: 0, + Limit: 3, + }, + Clients: []mgclients.Client{}, + }, + }, + { + desc: "retrieve with metadata", + pageMeta: mgclients.Page{ + Metadata: map[string]interface{}{ + "key": "value", + }, + Offset: 0, + Limit: 200, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 4, + Offset: 0, + Limit: 200, + }, + Clients: []mgclients.Client{items[0], items[50], items[100], items[150]}, + }, + err: nil, + }, + { + desc: "retrieve with invalid metadata", + pageMeta: mgclients.Page{ + Metadata: map[string]interface{}{ + "key": "value1", + }, + Offset: 0, + Limit: 200, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 0, + Offset: 0, + Limit: 200, + }, + Clients: []mgclients.Client{}, + }, + err: nil, + }, + { + desc: "retrieve with role", + pageMeta: mgclients.Page{ + Role: mgclients.AdminRole, + Offset: 0, + Limit: 200, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 4, + Offset: 0, + Limit: 200, + }, + Clients: []mgclients.Client{items[0], items[50], items[100], items[150]}, + }, + err: nil, + }, + { + desc: "retrieve with invalid role", + pageMeta: mgclients.Page{ + Role: mgclients.AdminRole + 2, + Offset: 0, + Limit: 200, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 0, + Offset: 0, + Limit: 200, + }, + Clients: []mgclients.Client{}, + }, + err: nil, + }, + { + desc: "retrieve with identity", + pageMeta: mgclients.Page{ + Identity: items[0].Credentials.Identity, + Offset: 0, + Limit: 3, + Role: mgclients.AllRole, + Status: mgclients.AllStatus, + }, + page: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + Offset: 0, + Limit: 3, + }, + Clients: []mgclients.Client{items[0]}, + }, + err: nil, + }, + } + + for _, tc := range cases { + page, err := repo.RetrieveAll(context.Background(), tc.pageMeta) + assert.Equal(t, tc.page.Total, page.Total, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.page.Total, page.Total)) + assert.Equal(t, tc.page.Offset, page.Offset, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.page.Offset, page.Offset)) + assert.Equal(t, tc.page.Limit, page.Limit, fmt.Sprintf("%s: expected %d got %d\n", tc.desc, tc.page.Limit, page.Limit)) + assert.Equal(t, tc.page.Page, page.Page, fmt.Sprintf("%s: expected %v, got %v", tc.desc, tc.page, page)) + assert.ElementsMatch(t, tc.page.Clients, page.Clients, fmt.Sprintf("%s: expected %v, got %v", tc.desc, tc.page.Clients, page.Clients)) + assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + } +} + +func TestUpdateRole(t *testing.T) { + t.Cleanup(func() { + _, err := db.Exec("DELETE FROM clients") + require.Nil(t, err, fmt.Sprintf("clean clients unexpected error: %s", err)) + }) + + repo := cpostgres.NewRepository(database) + + client := mgclients.Client{ + ID: testsutil.GenerateUUID(t), + Name: namesgen.Generate(), + Credentials: mgclients.Credentials{ + Identity: fmt.Sprintf("%s@example.com", namesgen.Generate()), + Secret: password, + }, + Metadata: mgclients.Metadata{}, + Status: mgclients.EnabledStatus, + Role: mgclients.UserRole, + } + + _, err := repo.Save(context.Background(), client) + require.Nil(t, err, fmt.Sprintf("failed to save client %s", client.ID)) + + cases := []struct { + desc string + client mgclients.Client + newRole mgclients.Role + err error + }{ + { + desc: "update role to admin", + client: client, + newRole: mgclients.AdminRole, + err: nil, + }, + { + desc: "update role to user", + client: client, + newRole: mgclients.UserRole, + err: nil, + }, + { + desc: "update role with invalid client id", + client: mgclients.Client{ID: invalidName}, + newRole: mgclients.AdminRole, + err: errors.ErrNotFound, + }, + } + + for _, tc := range cases { + tc.client.Role = tc.newRole + client, err := repo.UpdateRole(context.Background(), tc.client) + if err != nil { + assert.Equal(t, err, tc.err, fmt.Sprintf("%s: expected error %v, got %v", tc.desc, tc.err, err)) + } else { + assert.Equal(t, tc.newRole, client.Role, fmt.Sprintf("%s: expected role %v, got %v", tc.desc, tc.newRole, client.Role)) + } + } +} diff --git a/users/service_test.go b/users/service_test.go index 131a36d7d..93be53385 100644 --- a/users/service_test.go +++ b/users/service_test.go @@ -7,9 +7,11 @@ import ( "context" "fmt" "regexp" + "strings" "testing" "github.com/absmach/magistrala" + authsvc "github.com/absmach/magistrala/auth" authmocks "github.com/absmach/magistrala/auth/mocks" "github.com/absmach/magistrala/internal/testsutil" mgclients "github.com/absmach/magistrala/pkg/clients" @@ -38,38 +40,54 @@ var ( Metadata: validCMetadata, Status: mgclients.EnabledStatus, } - passRegex = regexp.MustCompile("^.{8,}$") - myKey = "mine" - validToken = "token" - inValidToken = "invalid" - validID = "d4ebb847-5d0e-4e46-bdd9-b6aceaaa3a22" - domainID = testsutil.GenerateUUID(&testing.T{}) - wrongID = testsutil.GenerateUUID(&testing.T{}) + passRegex = regexp.MustCompile("^.{8,}$") + validToken = "token" + inValidToken = "invalid" + validID = "d4ebb847-5d0e-4e46-bdd9-b6aceaaa3a22" + wrongID = testsutil.GenerateUUID(&testing.T{}) + errHashPassword = errors.New("generate hash from password failed") + errAddPolicies = errors.New("failed to add policies") + errDeletePolicies = errors.New("failed to delete policies") ) -func TestRegisterClient(t *testing.T) { +func newService(selfRegister bool) (users.Service, *mocks.Repository, *authmocks.Service, users.Emailer) { cRepo := new(mocks.Repository) auth := new(authmocks.Service) e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) + return users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, selfRegister), cRepo, auth, e +} + +func TestRegisterClient(t *testing.T) { + svc, cRepo, auth, _ := newService(true) cases := []struct { - desc string - client mgclients.Client - token string - err error + desc string + client mgclients.Client + identifyResponse *magistrala.IdentityRes + addPoliciesResponse *magistrala.AddPoliciesRes + deletePoliciesResponse *magistrala.DeletePoliciesRes + token string + identifyErr error + addPoliciesResponseErr error + deletePoliciesResponseErr error + saveErr error + err error }{ { - desc: "register new client", - client: client, - token: validToken, - err: nil, + desc: "register new client successfully", + client: client, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + token: validToken, + err: nil, }, { - desc: "register existing client", - client: client, - token: validToken, - err: errors.ErrConflict, + desc: "register existing client", + client: client, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true}, + token: validToken, + saveErr: repoerr.ErrConflict, + err: errors.ErrConflict, }, { desc: "register a new enabled client with name", @@ -81,8 +99,9 @@ func TestRegisterClient(t *testing.T) { }, Status: mgclients.EnabledStatus, }, - err: nil, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + err: nil, + token: validToken, }, { desc: "register a new disabled client with name", @@ -93,156 +112,250 @@ func TestRegisterClient(t *testing.T) { Secret: secret, }, }, - err: nil, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + err: nil, + token: validToken, }, { - desc: "register a new enabled client with tags", + desc: "register a new client with all fields", client: mgclients.Client{ + Name: "newclientwithallfields", Tags: []string{"tag1", "tag2"}, Credentials: mgclients.Credentials{ - Identity: "newclientwithtags@example.com", + Identity: "newclientwithallfields@example.com", Secret: secret, }, + Metadata: mgclients.Metadata{ + "name": "newclientwithallfields", + }, Status: mgclients.EnabledStatus, }, - err: nil, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + err: nil, + token: validToken, }, { - desc: "register a new disabled client with tags", + desc: "register a new client with missing identity", client: mgclients.Client{ - Tags: []string{"tag1", "tag2"}, + Name: "clientWithMissingIdentity", Credentials: mgclients.Credentials{ - Identity: "newclientwithtags@example.com", - Secret: secret, + Secret: secret, }, - Status: mgclients.DisabledStatus, }, - err: nil, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true}, + saveErr: errors.ErrMalformedEntity, + err: errors.ErrMalformedEntity, + token: validToken, }, { - desc: "register a new enabled client with metadata", + desc: "register a new client with missing secret", client: mgclients.Client{ + Name: "clientWithMissingSecret", Credentials: mgclients.Credentials{ - Identity: "newclientwithmetadata@example.com", - Secret: secret, + Identity: "clientwithmissingsecret@example.com", + Secret: "", }, - Metadata: validCMetadata, - Status: mgclients.EnabledStatus, }, - err: nil, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true}, + err: repoerr.ErrMissingSecret, }, { - desc: "register a new disabled client with metadata", + desc: "register a new client with a weak secret", client: mgclients.Client{ + Name: "clientWithWeakSecret", Credentials: mgclients.Credentials{ - Identity: "newclientwithmetadata@example.com", - Secret: secret, + Identity: "clientwithweaksecret@example.com", + Secret: "weak", }, - Metadata: validCMetadata, }, - err: nil, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true}, + err: nil, }, { - desc: "register a new disabled client", + desc: " register a client with a secret that is too long", client: mgclients.Client{ + Name: "clientWithLongSecret", Credentials: mgclients.Credentials{ - Identity: "newclientwithvalidstatus@example.com", - Secret: secret, + Identity: "clientwithlongsecret@example.com", + Secret: strings.Repeat("a", 73), }, }, - err: nil, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true}, + err: repoerr.ErrMalformedEntity, }, { - desc: "register a new client with valid disabled status", + desc: "register a new client with invalid status", client: mgclients.Client{ + Name: "clientWithInvalidStatus", Credentials: mgclients.Credentials{ - Identity: "newclientwithvalidstatus@example.com", + Identity: "client with invalid status", Secret: secret, }, - Status: mgclients.DisabledStatus, + Status: mgclients.AllStatus, }, - err: nil, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true}, + err: svcerr.ErrInvalidStatus, }, { - desc: "register a new client with all fields", + desc: "register a new client with invalid role", client: mgclients.Client{ - Name: "newclientwithallfields", - Tags: []string{"tag1", "tag2"}, + Name: "clientWithInvalidRole", Credentials: mgclients.Credentials{ - Identity: "newclientwithallfields@example.com", + Identity: "clientwithinvalidrole@example.com", Secret: secret, }, - Metadata: mgclients.Metadata{ - "name": "newclientwithallfields", - }, - Status: mgclients.EnabledStatus, + Role: 2, }, - err: nil, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: true}, + err: svcerr.ErrInvalidRole, }, { - desc: "register a new client with missing identity", + desc: "register a new client with failed to authorize add policies", client: mgclients.Client{ - Name: "clientWithMissingIdentity", + Name: "clientWithFailedToAddPolicies", Credentials: mgclients.Credentials{ - Secret: secret, + Identity: "clientwithfailedpolicies@example.com", + Secret: secret, }, + Role: mgclients.AdminRole, }, - err: errors.ErrMalformedEntity, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: false}, + err: errors.ErrAuthorization, }, { - desc: "register a new client with invalid owner", + desc: "register a new client with failed to add policies with err", client: mgclients.Client{ - Owner: wrongID, + Name: "clientWithFailedToAddPolicies", Credentials: mgclients.Credentials{ - Identity: "newclientwithinvalidowner@example.com", + Identity: "clientwithfailedpolicies@example.com", Secret: secret, }, + Role: mgclients.AdminRole, }, - err: errors.ErrMalformedEntity, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + addPoliciesResponseErr: errAddPolicies, + err: errAddPolicies, }, { - desc: "register a new client with empty secret", + desc: "register a new client with failed to delete policies with err", client: mgclients.Client{ - Owner: testsutil.GenerateUUID(t), + Name: "clientWithFailedToDeletePolicies", Credentials: mgclients.Credentials{ - Identity: "newclientwithemptysecret@example.com", + Identity: "clientwithfailedtodelete@example.com", + Secret: secret, }, + Role: mgclients.AdminRole, }, - err: repoerr.ErrMissingSecret, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: false}, + deletePoliciesResponseErr: errDeletePolicies, + saveErr: repoerr.ErrConflict, + err: errDeletePolicies, }, { - desc: "register a new client with invalid status", + desc: "register a new client with failed to delete policies with failed to delete", client: mgclients.Client{ + Name: "clientWithFailedToDeletePolicies", Credentials: mgclients.Credentials{ - Identity: "newclientwithinvalidstatus@example.com", + Identity: "clientwithfailedtodelete@example.com", Secret: secret, }, - Status: mgclients.AllStatus, + Role: mgclients.AdminRole, }, - err: svcerr.ErrInvalidStatus, - token: validToken, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + deletePoliciesResponse: &magistrala.DeletePoliciesRes{Deleted: false}, + saveErr: repoerr.ErrConflict, + err: svcerr.ErrAuthorization, }, } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{Id: validID}, nil) - if tc.token == inValidToken { - repoCall = auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: inValidToken}).Return(&magistrala.IdentityRes{}, svcerr.ErrAuthentication) + repoCall := auth.On("AddPolicies", mock.Anything, mock.Anything).Return(tc.addPoliciesResponse, tc.addPoliciesResponseErr) + repoCall1 := auth.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePoliciesResponse, tc.deletePoliciesResponseErr) + repoCall2 := cRepo.On("Save", context.Background(), mock.Anything).Return(tc.client, tc.saveErr) + expected, err := svc.RegisterClient(context.Background(), tc.token, tc.client) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + if err == nil { + tc.client.ID = expected.ID + tc.client.CreatedAt = expected.CreatedAt + tc.client.UpdatedAt = expected.UpdatedAt + tc.client.Credentials.Secret = expected.Credentials.Secret + tc.client.Owner = expected.Owner + tc.client.UpdatedBy = expected.UpdatedBy + assert.Equal(t, tc.client, expected, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.client, expected)) + ok := repoCall2.Parent.AssertCalled(t, "Save", context.Background(), mock.Anything) + assert.True(t, ok, fmt.Sprintf("Save was not called on %s", tc.desc)) } - repoCall1 := auth.On("AddPolicies", mock.Anything, mock.Anything).Return(&magistrala.AddPoliciesRes{Authorized: true}, nil) - repoCall2 := auth.On("DeletePolicies", mock.Anything, mock.Anything).Return(&magistrala.DeletePoliciesRes{Deleted: true}, nil) - repoCall3 := cRepo.On("Save", context.Background(), mock.Anything).Return(tc.client, tc.err) + repoCall2.Unset() + repoCall1.Unset() + repoCall.Unset() + } + + svc, cRepo, auth, _ = newService(false) + + cases2 := []struct { + desc string + client mgclients.Client + identifyResponse *magistrala.IdentityRes + authorizeResponse *magistrala.AuthorizeRes + addPoliciesResponse *magistrala.AddPoliciesRes + deletePoliciesResponse *magistrala.DeletePoliciesRes + token string + identifyErr error + authorizeErr error + addPoliciesResponseErr error + deletePoliciesResponseErr error + saveErr error + checkSuperAdminErr error + err error + }{ + { + desc: "register new client successfully as admin", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: validID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + addPoliciesResponse: &magistrala.AddPoliciesRes{Authorized: true}, + token: validToken, + err: nil, + }, + { + desc: "register a new clinet as admin with invalid token", + client: client, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: svcerr.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "register a new client as admin with failed to authorize", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: wrongID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + token: validToken, + err: errors.ErrAuthorization, + }, + { + desc: "register a new client as admin with failed check on super admin", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: validID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + token: validToken, + checkSuperAdminErr: errors.ErrAuthorization, + err: errors.ErrAuthorization, + }, + } + for _, tc := range cases2 { + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := cRepo.On("CheckSuperAdmin", mock.Anything, mock.Anything).Return(tc.checkSuperAdminErr) + repoCall3 := auth.On("AddPolicies", mock.Anything, mock.Anything).Return(tc.addPoliciesResponse, tc.addPoliciesResponseErr) + repoCall4 := auth.On("DeletePolicies", mock.Anything, mock.Anything).Return(tc.deletePoliciesResponse, tc.deletePoliciesResponseErr) + repoCall5 := cRepo.On("Save", context.Background(), mock.Anything).Return(tc.client, tc.saveErr) expected, err := svc.RegisterClient(context.Background(), tc.token, tc.client) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) if err == nil { @@ -253,9 +366,12 @@ func TestRegisterClient(t *testing.T) { tc.client.Owner = expected.Owner tc.client.UpdatedBy = expected.UpdatedBy assert.Equal(t, tc.client, expected, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.client, expected)) - ok := repoCall3.Parent.AssertCalled(t, "Save", context.Background(), mock.Anything) + ok := repoCall5.Parent.AssertCalled(t, "Save", context.Background(), mock.Anything) assert.True(t, ok, fmt.Sprintf("Save was not called on %s", tc.desc)) } + + repoCall5.Unset() + repoCall4.Unset() repoCall3.Unset() repoCall2.Unset() repoCall1.Unset() @@ -264,66 +380,100 @@ func TestRegisterClient(t *testing.T) { } func TestViewClient(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) + svc, cRepo, auth, _ := newService(true) + adminID := testsutil.GenerateUUID(t) cases := []struct { - desc string - token string - clientID string - response mgclients.Client - err error + desc string + token string + clientID string + identifyResponse *magistrala.IdentityRes + authorizeResponse *magistrala.AuthorizeRes + retrieveByIDResponse mgclients.Client + response mgclients.Client + identifyErr error + authorizeErr error + retrieveByIDErr error + checkSuperAdminErr error + err error }{ { - desc: "view client successfully", - response: client, - token: validToken, - clientID: client.ID, - err: nil, + desc: "view client as normal user successfully", + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: client, + response: client, + token: validToken, + clientID: client.ID, + err: nil, }, { - desc: "view client with an invalid token", - response: mgclients.Client{}, - token: inValidToken, - clientID: client.ID, - err: svcerr.ErrAuthentication, + desc: "view client with an invalid token", + identifyResponse: &magistrala.IdentityRes{}, + response: mgclients.Client{}, + token: inValidToken, + err: svcerr.ErrAuthentication, }, { - desc: "view client with valid token and invalid client id", - response: mgclients.Client{}, - token: validToken, - clientID: wrongID, - err: svcerr.ErrNotFound, + desc: "view client as normal user with failed to retrieve client", + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: mgclients.Client{}, + token: validToken, + clientID: client.ID, + retrieveByIDErr: errors.ErrNotFound, + err: svcerr.ErrNotFound, + }, + { + desc: "view client as admin user successfully", + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + retrieveByIDResponse: client, + response: client, + token: validToken, + clientID: client.ID, + err: nil, }, { - desc: "view client with an invalid token and invalid client id", - response: mgclients.Client{}, - token: inValidToken, - clientID: wrongID, - err: svcerr.ErrAuthentication, + desc: "view client as admin user with invalid token", + identifyResponse: &magistrala.IdentityRes{}, + token: inValidToken, + identifyErr: errors.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "view client as admin user with invalid ID", + identifyResponse: &magistrala.IdentityRes{UserId: wrongID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + token: validToken, + clientID: client.ID, + err: errors.ErrAuthorization, + }, + { + desc: "view client as admin user with failed check on super admin", + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + token: validToken, + clientID: client.ID, + checkSuperAdminErr: errors.ErrAuthorization, + err: errors.ErrAuthorization, }, } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{UserId: validID}, nil) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) - if tc.token == inValidToken { - repoCall = auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: inValidToken}).Return(&magistrala.IdentityRes{}, errors.ErrAuthentication) - repoCall1 = auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: false}, errors.ErrAuthorization) - } - repoCall2 := cRepo.On("RetrieveByID", context.Background(), tc.clientID).Return(tc.response, tc.err) + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := cRepo.On("CheckSuperAdmin", mock.Anything, mock.Anything).Return(tc.checkSuperAdminErr) + repoCall3 := cRepo.On("RetrieveByID", context.Background(), tc.clientID).Return(tc.retrieveByIDResponse, tc.retrieveByIDErr) rClient, err := svc.ViewClient(context.Background(), tc.token, tc.clientID) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) tc.response.Credentials.Secret = "" assert.Equal(t, tc.response, rClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, rClient)) if tc.err == nil { - ok := repoCall2.Parent.AssertCalled(t, "RetrieveByID", context.Background(), tc.clientID) + ok := repoCall3.Parent.AssertCalled(t, "RetrieveByID", context.Background(), tc.clientID) assert.True(t, ok, fmt.Sprintf("RetrieveByID was not called on %s", tc.desc)) } + repoCall3.Unset() repoCall2.Unset() repoCall1.Unset() repoCall.Unset() @@ -331,358 +481,262 @@ func TestViewClient(t *testing.T) { } func TestListClients(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) - - nClients := uint64(200) - aClients := []mgclients.Client{} - OwnerID := testsutil.GenerateUUID(t) - for i := uint64(1); i < nClients; i++ { - identity := fmt.Sprintf("TestListClients_%d@example.com", i) - client := mgclients.Client{ - Name: identity, - Credentials: mgclients.Credentials{ - Identity: identity, - Secret: "password", - }, - Tags: []string{"tag1", "tag2"}, - Metadata: mgclients.Metadata{"role": "client"}, - } - if i%50 == 0 { - client.Owner = OwnerID - client.Owner = testsutil.GenerateUUID(t) - } - aClients = append(aClients, client) - } + svc, cRepo, auth, _ := newService(true) cases := []struct { - desc string - token string - page mgclients.Page - response mgclients.ClientsPage - size uint64 - err error + desc string + token string + page mgclients.Page + identifyResponse *magistrala.IdentityRes + authorizeResponse *magistrala.AuthorizeRes + retrieveAllResponse mgclients.ClientsPage + response mgclients.ClientsPage + size uint64 + identifyErr error + authorizeErr error + retrieveAllErr error + superAdminErr error + err error }{ { - desc: "list clients with authorized token", - token: validToken, - - page: mgclients.Page{ - Status: mgclients.AllStatus, - }, - size: 0, - response: mgclients.ClientsPage{ - Page: mgclients.Page{ - Total: 0, - Offset: 0, - Limit: 0, - }, - Clients: []mgclients.Client{}, - }, - err: nil, - }, - { - desc: "list clients with an invalid token", - token: inValidToken, + desc: "list clients as admin successfully", page: mgclients.Page{ - Status: mgclients.AllStatus, + Total: 1, }, - size: 0, - response: mgclients.ClientsPage{ + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + retrieveAllResponse: mgclients.ClientsPage{ Page: mgclients.Page{ - Total: 0, - Offset: 0, - Limit: 0, + Total: 1, }, - }, - err: svcerr.ErrAuthentication, - }, - { - desc: "list clients that are shared with me", - token: validToken, - page: mgclients.Page{ - Offset: 6, - Limit: nClients, - Status: mgclients.EnabledStatus, + Clients: []mgclients.Client{client}, }, response: mgclients.ClientsPage{ Page: mgclients.Page{ - Total: 4, - Offset: 0, - Limit: 0, + Total: 1, }, - Clients: []mgclients.Client{aClients[0], aClients[50], aClients[100], aClients[150]}, + Clients: []mgclients.Client{client}, }, - size: 4, - }, - { - desc: "list clients that are shared with me with a specific name", token: validToken, - page: mgclients.Page{ - Offset: 6, - Limit: nClients, - Name: "TestListClients3", - Status: mgclients.EnabledStatus, - }, - response: mgclients.ClientsPage{ - Page: mgclients.Page{ - Total: 4, - Offset: 0, - Limit: 0, - }, - Clients: []mgclients.Client{aClients[0], aClients[50], aClients[100], aClients[150]}, - }, - size: 4, + err: nil, }, { - desc: "list clients that are shared with me with an invalid name", - token: validToken, + desc: "list clients as admin with invalid token", page: mgclients.Page{ - Offset: 6, - Limit: nClients, - Name: "notpresentclient", - Status: mgclients.EnabledStatus, - }, - response: mgclients.ClientsPage{ - Page: mgclients.Page{ - Total: 0, - Offset: 0, - Limit: 0, - }, - Clients: []mgclients.Client{}, + Total: 1, }, - size: 0, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: svcerr.ErrAuthentication, + err: svcerr.ErrAuthentication, }, { - desc: "list clients that I own", - token: validToken, + desc: "list clients as admin with invalid ID", page: mgclients.Page{ - Offset: 6, - Limit: nClients, - Owner: myKey, - Status: mgclients.EnabledStatus, + Total: 1, }, - response: mgclients.ClientsPage{ - Page: mgclients.Page{ - Total: 4, - Offset: 0, - Limit: 0, - }, - Clients: []mgclients.Client{aClients[0], aClients[50], aClients[100], aClients[150]}, - }, - size: 4, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + token: validToken, + authorizeErr: svcerr.ErrAuthorization, + err: nil, }, { - desc: "list clients that I own with a specific name", - token: validToken, + desc: "list clients as admin with failed to retrieve clients", page: mgclients.Page{ - Offset: 6, - Limit: nClients, - Owner: myKey, - Name: "TestListClients3", - Status: mgclients.AllStatus, - }, - response: mgclients.ClientsPage{ - Page: mgclients.Page{ - Total: 4, - Offset: 0, - Limit: 0, - }, - Clients: []mgclients.Client{aClients[0], aClients[50], aClients[100], aClients[150]}, + Total: 1, }, - size: 4, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + retrieveAllResponse: mgclients.ClientsPage{}, + token: validToken, + retrieveAllErr: errors.ErrNotFound, + err: svcerr.ErrNotFound, }, { - desc: "list clients that I own with an invalid name", - token: validToken, + desc: "list clients as admin with failed check on super admin", page: mgclients.Page{ - Offset: 6, - Limit: nClients, - Owner: myKey, - Name: "notpresentclient", - Status: mgclients.AllStatus, + Total: 1, }, - response: mgclients.ClientsPage{ - Page: mgclients.Page{ - Total: 0, - Offset: 0, - Limit: 0, - }, - Clients: []mgclients.Client{}, - }, - size: 0, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + token: validToken, + superAdminErr: errors.ErrAuthorization, + err: nil, }, { - desc: "list clients that I own and are shared with me", - token: validToken, + desc: "list clients as normal user successfully", page: mgclients.Page{ - Offset: 6, - Limit: nClients, - Owner: myKey, - Status: mgclients.AllStatus, + Total: 1, }, - response: mgclients.ClientsPage{ + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + retrieveAllResponse: mgclients.ClientsPage{ Page: mgclients.Page{ - Total: 4, - Offset: 0, - Limit: 0, + Total: 1, }, - Clients: []mgclients.Client{aClients[0], aClients[50], aClients[100], aClients[150]}, - }, - size: 4, - }, - { - desc: "list clients that I own and are shared with me with a specific name", - token: validToken, - page: mgclients.Page{ - Offset: 6, - Limit: nClients, - Owner: myKey, - Name: "TestListClients3", - Status: mgclients.AllStatus, + Clients: []mgclients.Client{client}, }, response: mgclients.ClientsPage{ Page: mgclients.Page{ - Total: 4, - Offset: 0, - Limit: 0, + Total: 1, }, - Clients: []mgclients.Client{aClients[0], aClients[50], aClients[100], aClients[150]}, + Clients: []mgclients.Client{client}, }, - size: 4, - }, - { - desc: "list clients that I own and are shared with me with an invalid name", token: validToken, - page: mgclients.Page{ - Offset: 6, - Limit: nClients, - Owner: myKey, - Name: "notpresentclient", - Status: mgclients.AllStatus, - }, - response: mgclients.ClientsPage{ - Page: mgclients.Page{ - Total: 0, - Offset: 0, - Limit: 0, - }, - Clients: []mgclients.Client{}, - }, - size: 0, + err: nil, }, { - desc: "list clients with offset and limit", - token: validToken, - + desc: "list clients as normal user with failed to retrieve clients", page: mgclients.Page{ - Offset: 6, - Limit: nClients, - Status: mgclients.AllStatus, - }, - response: mgclients.ClientsPage{ - Page: mgclients.Page{ - Total: nClients - 6, - Offset: 0, - Limit: 0, - }, - Clients: aClients[6:nClients], + Total: 1, }, - size: nClients - 6, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + retrieveAllResponse: mgclients.ClientsPage{}, + token: validToken, + retrieveAllErr: errors.ErrNotFound, + err: svcerr.ErrNotFound, }, } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{UserId: validID}, nil) - if tc.token == inValidToken { - repoCall = auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: inValidToken}).Return(&magistrala.IdentityRes{}, svcerr.ErrAuthentication) - } - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) - repoCall2 := cRepo.On("RetrieveAll", context.Background(), mock.Anything).Return(tc.response, tc.err) + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := cRepo.On("CheckSuperAdmin", mock.Anything, mock.Anything).Return(tc.superAdminErr) + repoCall3 := cRepo.On("RetrieveAll", context.Background(), mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr) page, err := svc.ListClients(context.Background(), tc.token, tc.page) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) assert.Equal(t, tc.response, page, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page)) if tc.err == nil { - ok := repoCall2.Parent.AssertCalled(t, "RetrieveAll", context.Background(), mock.Anything) + ok := repoCall3.Parent.AssertCalled(t, "RetrieveAll", context.Background(), mock.Anything) assert.True(t, ok, fmt.Sprintf("RetrieveAll was not called on %s", tc.desc)) } repoCall.Unset() repoCall1.Unset() repoCall2.Unset() + repoCall3.Unset() } } func TestUpdateClient(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) + svc, cRepo, auth, _ := newService(true) client1 := client client2 := client client1.Name = "Updated client" client2.Metadata = mgclients.Metadata{"role": "test"} + adminID := testsutil.GenerateUUID(t) cases := []struct { - desc string - client mgclients.Client - response mgclients.Client - token string - err error + desc string + client mgclients.Client + identifyResponse *magistrala.IdentityRes + authorizeResponse *magistrala.AuthorizeRes + updateResponse mgclients.Client + token string + identifyErr error + authorizeErr error + updateErr error + checkSuperAdminErr error + err error }{ { - desc: "update client name with valid token", - client: client1, - response: client1, - token: validToken, - err: nil, - }, - { - desc: "update client name with invalid token", - client: client1, - response: mgclients.Client{}, - token: inValidToken, - err: svcerr.ErrAuthentication, - }, - { - desc: "update client name with invalid ID", - client: mgclients.Client{ - ID: wrongID, - Name: "Updated Client", - }, - response: mgclients.Client{}, - token: inValidToken, - err: svcerr.ErrAuthentication, - }, - { - desc: "update client metadata with valid token", - client: client2, - response: client2, - token: validToken, - err: nil, - }, - { - desc: "update client metadata with invalid token", - client: client2, - response: mgclients.Client{}, - token: inValidToken, - err: svcerr.ErrAuthentication, + desc: "update client name successfully as normal user", + client: client1, + identifyResponse: &magistrala.IdentityRes{UserId: client1.ID}, + updateResponse: client1, + token: validToken, + err: nil, + }, + { + desc: "update metadata successfully as normal user", + client: client2, + identifyResponse: &magistrala.IdentityRes{UserId: client2.ID}, + updateResponse: client2, + token: validToken, + err: nil, + }, + { + desc: "update client name as normal user with invalid token", + client: client1, + identifyResponse: &magistrala.IdentityRes{}, + token: inValidToken, + identifyErr: errors.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "update client name as normal user with repo error on update", + client: client1, + identifyResponse: &magistrala.IdentityRes{UserId: client1.ID}, + updateResponse: mgclients.Client{}, + token: validToken, + updateErr: errors.ErrMalformedEntity, + err: svcerr.ErrUpdateEntity, + }, + { + desc: "update client name as admin successfully", + client: client1, + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + updateResponse: client1, + token: validToken, + err: nil, + }, + { + desc: "update client metadata as admin successfully", + client: client2, + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + updateResponse: client2, + token: validToken, + err: nil, + }, + { + desc: "update client name as admin with invalid token", + client: client1, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: errors.ErrAuthentication, + token: inValidToken, + err: svcerr.ErrAuthentication, + }, + { + desc: "update cient name as admin with invalid ID", + client: client1, + identifyResponse: &magistrala.IdentityRes{UserId: wrongID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + token: validToken, + err: errors.ErrAuthorization, + }, + { + desc: "update client with failed check on super admin", + client: client1, + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + token: validToken, + checkSuperAdminErr: errors.ErrAuthorization, + err: errors.ErrAuthorization, + }, + { + desc: "update client name as admin with repo error on update", + client: client1, + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + updateResponse: mgclients.Client{}, + token: validToken, + updateErr: errors.ErrMalformedEntity, + err: svcerr.ErrUpdateEntity, }, } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{UserId: validID}, nil) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) - if tc.token == inValidToken { - repoCall = auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: inValidToken}).Return(&magistrala.IdentityRes{}, errors.ErrAuthentication) - repoCall1 = auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: false}, errors.ErrAuthorization) - } - repoCall2 := cRepo.On("Update", context.Background(), mock.Anything).Return(tc.response, tc.err) + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := cRepo.On("CheckSuperAdmin", mock.Anything, mock.Anything).Return(tc.checkSuperAdminErr) + repoCall3 := cRepo.On("Update", context.Background(), mock.Anything).Return(tc.updateResponse, tc.err) updatedClient, err := svc.UpdateClient(context.Background(), tc.token, tc.client) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.response, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, updatedClient)) + assert.Equal(t, tc.updateResponse, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateResponse, updatedClient)) + if tc.err == nil { ok := repoCall2.Parent.AssertCalled(t, "Update", context.Background(), mock.Anything) assert.True(t, ok, fmt.Sprintf("Update was not called on %s", tc.desc)) @@ -690,191 +744,381 @@ func TestUpdateClient(t *testing.T) { repoCall.Unset() repoCall1.Unset() repoCall2.Unset() + repoCall3.Unset() } } func TestUpdateClientTags(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) + svc, cRepo, auth, _ := newService(true) client.Tags = []string{"updated"} + adminID := testsutil.GenerateUUID(t) cases := []struct { - desc string - client mgclients.Client - response mgclients.Client - token string - err error + desc string + client mgclients.Client + identifyResponse *magistrala.IdentityRes + authorizeResponse *magistrala.AuthorizeRes + updateClientTagsResponse mgclients.Client + token string + identifyErr error + authorizeErr error + updateClientTagsErr error + checkSuperAdminErr error + err error }{ { - desc: "update client tags with valid token", - client: client, - token: validToken, - response: client, - err: nil, - }, - { - desc: "update client tags with invalid token", - client: client, - token: inValidToken, - response: mgclients.Client{}, - err: svcerr.ErrAuthentication, - }, - { - desc: "update client name with invalid ID", - client: mgclients.Client{ - ID: wrongID, - Name: "Updated name", - }, - response: mgclients.Client{}, - token: inValidToken, - err: svcerr.ErrAuthentication, + desc: "update client tags as normal user successfully", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + updateClientTagsResponse: client, + token: validToken, + err: nil, + }, + { + desc: "update client tags as normal user with invalid token", + client: client, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: errors.ErrAuthentication, + token: inValidToken, + err: svcerr.ErrAuthentication, + }, + { + desc: "update client tags as normal user with repo error on update", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + updateClientTagsResponse: mgclients.Client{}, + token: validToken, + updateClientTagsErr: errors.ErrMalformedEntity, + err: svcerr.ErrUpdateEntity, + }, + { + desc: "update client tags as admin successfully", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + token: validToken, + err: nil, + }, + { + desc: "update client tags as admin with invalid token", + client: client, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: errors.ErrAuthentication, + token: inValidToken, + err: svcerr.ErrAuthentication, + }, + { + desc: "update client tags as admin with invalid ID", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: wrongID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + token: validToken, + err: errors.ErrAuthorization, + }, + { + desc: "update client tags as admin with failed check on super admin", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + checkSuperAdminErr: errors.ErrAuthorization, + token: validToken, + err: errors.ErrAuthorization, + }, + { + desc: "update client tags as admin with repo error on update", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + updateClientTagsResponse: mgclients.Client{}, + token: validToken, + updateClientTagsErr: errors.ErrMalformedEntity, + err: svcerr.ErrUpdateEntity, }, } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{UserId: validID}, nil) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) - if tc.token == inValidToken { - repoCall = auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: inValidToken}).Return(&magistrala.IdentityRes{}, errors.ErrAuthentication) - repoCall1 = auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: false}, errors.ErrAuthorization) - } - repoCall2 := cRepo.On("UpdateTags", context.Background(), mock.Anything).Return(tc.response, tc.err) + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := cRepo.On("CheckSuperAdmin", mock.Anything, mock.Anything).Return(tc.checkSuperAdminErr) + repoCall3 := cRepo.On("UpdateTags", context.Background(), mock.Anything).Return(tc.updateClientTagsResponse, tc.updateClientTagsErr) updatedClient, err := svc.UpdateClientTags(context.Background(), tc.token, tc.client) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.response, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, updatedClient)) + assert.Equal(t, tc.updateClientTagsResponse, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateClientTagsResponse, updatedClient)) if tc.err == nil { - ok := repoCall2.Parent.AssertCalled(t, "UpdateTags", context.Background(), mock.Anything) + ok := repoCall3.Parent.AssertCalled(t, "UpdateTags", context.Background(), mock.Anything) assert.True(t, ok, fmt.Sprintf("UpdateTags was not called on %s", tc.desc)) } repoCall.Unset() repoCall1.Unset() repoCall2.Unset() + repoCall3.Unset() } } func TestUpdateClientIdentity(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) + svc, cRepo, auth, _ := newService(true) client2 := client client2.Credentials.Identity = "updated@example.com" + adminID := testsutil.GenerateUUID(t) cases := []struct { - desc string - identity string - response mgclients.Client - token string - id string - err error + desc string + identity string + token string + id string + identifyResponse *magistrala.IdentityRes + authorizeResponse *magistrala.AuthorizeRes + updateClientIdentityResponse mgclients.Client + identifyErr error + authorizeErr error + updateClientIdentityErr error + checkSuperAdminErr error + err error }{ { - desc: "update client identity with valid token", - identity: "updated@example.com", - token: validToken, - id: client.ID, - response: client2, - err: nil, - }, - { - desc: "update client identity with invalid id", - identity: "updated@example.com", - token: validToken, - id: wrongID, - response: mgclients.Client{}, - err: repoerr.ErrNotFound, - }, - { - desc: "update client identity with invalid token", - identity: "updated@example.com", - token: inValidToken, - id: client2.ID, - response: mgclients.Client{}, - err: svcerr.ErrAuthentication, + desc: "update client as normal user successfully", + identity: "updated@example.com", + token: validToken, + id: client.ID, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + updateClientIdentityResponse: client2, + err: nil, + }, + { + desc: "update client identity as normal user with invalid token", + identity: "updated@example.com", + token: inValidToken, + id: client.ID, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: errors.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "update client identity as normal user with repo error on update", + identity: "updated@example.com", + token: validToken, + id: client.ID, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + updateClientIdentityResponse: mgclients.Client{}, + updateClientIdentityErr: errors.ErrMalformedEntity, + err: svcerr.ErrUpdateEntity, + }, + { + desc: "update client identity as admin successfully", + identity: "updated@example.com", + token: validToken, + id: client.ID, + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + err: nil, + }, + { + desc: "update client identity as admin with invalid token", + identity: "updated@example.com", + token: inValidToken, + id: client.ID, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: errors.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "update client identity as admin with invalid ID", + identity: "updated@example.com", + token: validToken, + id: client.ID, + identifyResponse: &magistrala.IdentityRes{UserId: wrongID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + err: errors.ErrAuthorization, + }, + { + desc: "update client identity as admin with failed check on super admin", + identity: "updated@example.com", + token: validToken, + id: client.ID, + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + checkSuperAdminErr: errors.ErrAuthorization, + err: errors.ErrAuthorization, + }, + { + desc: "update client identity as admin with repo error on update", + identity: "updated@exmaple.com", + token: validToken, + id: client.ID, + identifyResponse: &magistrala.IdentityRes{UserId: adminID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + updateClientIdentityResponse: mgclients.Client{}, + updateClientIdentityErr: errors.ErrMalformedEntity, + err: svcerr.ErrUpdateEntity, }, } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{UserId: validID}, nil) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) - if tc.token == inValidToken { - repoCall = auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: inValidToken}).Return(&magistrala.IdentityRes{}, errors.ErrAuthentication) - repoCall1 = auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: false}, errors.ErrAuthorization) - } - repoCall2 := cRepo.On("UpdateIdentity", context.Background(), mock.Anything).Return(tc.response, tc.err) + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := cRepo.On("CheckSuperAdmin", mock.Anything, mock.Anything).Return(tc.checkSuperAdminErr) + repoCall3 := cRepo.On("UpdateIdentity", context.Background(), mock.Anything).Return(tc.updateClientIdentityResponse, tc.updateClientIdentityErr) updatedClient, err := svc.UpdateClientIdentity(context.Background(), tc.token, tc.id, tc.identity) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.response, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, updatedClient)) + assert.Equal(t, tc.updateClientIdentityResponse, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateClientIdentityResponse, updatedClient)) if tc.err == nil { - ok := repoCall2.Parent.AssertCalled(t, "UpdateIdentity", context.Background(), mock.Anything) + ok := repoCall3.Parent.AssertCalled(t, "UpdateIdentity", context.Background(), mock.Anything) assert.True(t, ok, fmt.Sprintf("UpdateIdentity was not called on %s", tc.desc)) } repoCall.Unset() repoCall1.Unset() repoCall2.Unset() + repoCall3.Unset() } } func TestUpdateClientRole(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) + svc, cRepo, auth, _ := newService(true) + client2 := client client.Role = mgclients.AdminRole + client2.Role = mgclients.UserRole cases := []struct { - desc string - client mgclients.Client - response mgclients.Client - token string - err error + desc string + client mgclients.Client + identifyResponse *magistrala.IdentityRes + authorizeResponse *magistrala.AuthorizeRes + deletePolicyResponse *magistrala.DeletePolicyRes + addPolicyResponse *magistrala.AddPolicyRes + updateRoleResponse mgclients.Client + token string + identifyErr error + authorizeErr error + deletePolicyErr error + addPolicyErr error + updateRoleErr error + checkSuperAdminErr error + err error }{ - { - desc: "update client role with valid token", - client: client, - token: validToken, - response: client, - err: nil, - }, - { - desc: "update client role with invalid token", - client: client, - token: inValidToken, - response: mgclients.Client{}, - err: svcerr.ErrAuthentication, - }, - { - desc: "update client role with invalid ID", - client: mgclients.Client{ - ID: wrongID, - Role: mgclients.AdminRole, - }, - response: mgclients.Client{}, - token: inValidToken, - err: svcerr.ErrAuthentication, + { + desc: "update client role successfully", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + addPolicyResponse: &magistrala.AddPolicyRes{Authorized: true}, + updateRoleResponse: client, + token: validToken, + err: nil, + }, + { + desc: "update client role with invalid token", + client: client, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: errors.ErrAuthentication, + token: inValidToken, + err: svcerr.ErrAuthentication, + }, + { + desc: "update client role with invalid ID", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: wrongID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + token: validToken, + err: errors.ErrAuthorization, + }, + { + desc: "update client role with failed check on super admin", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + checkSuperAdminErr: errors.ErrAuthorization, + token: validToken, + err: errors.ErrAuthorization, + }, + { + desc: "update client role with failed authorization on add policy", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + addPolicyResponse: &magistrala.AddPolicyRes{Authorized: false}, + token: validToken, + err: errors.ErrAuthorization, + }, + { + desc: "update client role with failed to add policy", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + addPolicyResponse: &magistrala.AddPolicyRes{}, + addPolicyErr: errors.ErrMalformedEntity, + token: validToken, + err: errAddPolicies, + }, + { + desc: "update client role to user role successfully ", + client: client2, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + deletePolicyResponse: &magistrala.DeletePolicyRes{Deleted: true}, + updateRoleResponse: client2, + token: validToken, + err: nil, + }, + { + desc: "update client role to user role with failed to delete policy", + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + deletePolicyResponse: &magistrala.DeletePolicyRes{Deleted: false}, + updateRoleResponse: mgclients.Client{}, + token: validToken, + err: errDeletePolicies, + }, + { + desc: "update client role to user role with failed to delete policy with error", + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + deletePolicyResponse: &magistrala.DeletePolicyRes{Deleted: false}, + updateRoleResponse: mgclients.Client{}, + token: validToken, + deletePolicyErr: svcerr.ErrMalformedEntity, + err: errDeletePolicies, + }, + { + desc: "Update client with failed repo update and roll back", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + addPolicyResponse: &magistrala.AddPolicyRes{Authorized: true}, + deletePolicyResponse: &magistrala.DeletePolicyRes{Deleted: true}, + updateRoleResponse: mgclients.Client{}, + token: validToken, + updateRoleErr: svcerr.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "Update client with failed repo update and failedroll back", + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + addPolicyResponse: &magistrala.AddPolicyRes{Authorized: true}, + deletePolicyResponse: &magistrala.DeletePolicyRes{Deleted: false}, + updateRoleResponse: mgclients.Client{}, + token: validToken, + updateRoleErr: svcerr.ErrAuthentication, + err: svcerr.ErrAuthentication, }, } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{UserId: validID}, nil) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) - repoCall2 := auth.On("DeletePolicy", mock.Anything, mock.Anything).Return(&magistrala.DeletePolicyRes{Deleted: true}, nil) - repoCall3 := auth.On("AddPolicy", mock.Anything, mock.Anything).Return(&magistrala.AddPolicyRes{Authorized: true}, nil) - if tc.token == inValidToken { - repoCall = auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: inValidToken}).Return(&magistrala.IdentityRes{}, errors.ErrAuthentication) - repoCall1 = auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: false}, errors.ErrAuthorization) - } - repoCall4 := cRepo.On("UpdateRole", context.Background(), mock.Anything).Return(tc.response, tc.err) + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := cRepo.On("CheckSuperAdmin", mock.Anything, mock.Anything).Return(tc.checkSuperAdminErr) + repoCall3 := auth.On("AddPolicy", mock.Anything, mock.Anything).Return(tc.addPolicyResponse, tc.addPolicyErr) + repoCall4 := auth.On("DeletePolicy", mock.Anything, mock.Anything).Return(tc.deletePolicyResponse, tc.deletePolicyErr) + repoCall5 := cRepo.On("UpdateRole", context.Background(), mock.Anything).Return(tc.updateRoleResponse, tc.updateRoleErr) updatedClient, err := svc.UpdateClientRole(context.Background(), tc.token, tc.client) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) - assert.Equal(t, tc.response, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, updatedClient)) + assert.Equal(t, tc.updateRoleResponse, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.updateRoleResponse, updatedClient)) if tc.err == nil { - ok := repoCall4.Parent.AssertCalled(t, "UpdateRole", context.Background(), mock.Anything) + ok := repoCall5.Parent.AssertCalled(t, "UpdateRole", context.Background(), mock.Anything) assert.True(t, ok, fmt.Sprintf("UpdateRole was not called on %s", tc.desc)) } repoCall.Unset() @@ -882,61 +1126,128 @@ func TestUpdateClientRole(t *testing.T) { repoCall2.Unset() repoCall3.Unset() repoCall4.Unset() + repoCall5.Unset() } } func TestUpdateClientSecret(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) + svc, cRepo, auth, _ := newService(true) + newSecret := "newstrongSecret" rClient := client rClient.Credentials.Secret, _ = phasher.Hash(client.Credentials.Secret) + responseClient := client + responseClient.Credentials.Secret = newSecret cases := []struct { - desc string - oldSecret string - newSecret string - token string - response mgclients.Client - err error + desc string + oldSecret string + newSecret string + token string + identifyResponse *magistrala.IdentityRes + retrieveByIDResponse mgclients.Client + retrieveByIdentityResponse mgclients.Client + updateSecretResponse mgclients.Client + issueResponse *magistrala.Token + response mgclients.Client + identifyErr error + retrieveByIDErr error + retrieveByIdentityErr error + updateSecretErr error + issueErr error + err error }{ { - desc: "update client secret with valid token", - oldSecret: client.Credentials.Secret, - newSecret: "newSecret", - token: validToken, - response: rClient, - err: nil, - }, - { - desc: "update client secret with invalid token", - oldSecret: client.Credentials.Secret, - newSecret: "newPassword", - token: inValidToken, - response: mgclients.Client{}, - err: svcerr.ErrAuthentication, - }, - { - desc: "update client secret with wrong old secret", - oldSecret: "oldSecret", - newSecret: "newSecret", - token: validToken, - response: mgclients.Client{}, - err: repoerr.ErrInvalidSecret, + desc: "update client secret with valid token", + oldSecret: client.Credentials.Secret, + newSecret: newSecret, + token: validToken, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIdentityResponse: rClient, + retrieveByIDResponse: client, + updateSecretResponse: responseClient, + issueResponse: &magistrala.Token{AccessToken: validToken}, + response: responseClient, + err: nil, + }, + { + desc: "update client secret with invalid token", + oldSecret: client.Credentials.Secret, + newSecret: newSecret, + token: inValidToken, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: errors.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "update client secret with weak secret", + oldSecret: client.Credentials.Secret, + newSecret: "weak", + token: validToken, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + err: users.ErrPasswordFormat, + }, + { + desc: "update client secret with failed to retrieve client by ID", + oldSecret: client.Credentials.Secret, + newSecret: newSecret, + token: validToken, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: mgclients.Client{}, + retrieveByIDErr: repoerr.ErrNotFound, + err: repoerr.ErrNotFound, + }, + { + desc: "update client secret with failed to retrieve client by identity", + oldSecret: client.Credentials.Secret, + newSecret: newSecret, + token: validToken, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: client, + retrieveByIdentityResponse: mgclients.Client{}, + retrieveByIdentityErr: repoerr.ErrNotFound, + err: repoerr.ErrNotFound, + }, + { + desc: "update client secret with invalod old secret", + oldSecret: "invalid", + newSecret: newSecret, + token: validToken, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: client, + retrieveByIdentityResponse: rClient, + err: errors.ErrLogin, + }, + { + desc: "update client secret with too long new secret", + oldSecret: client.Credentials.Secret, + newSecret: strings.Repeat("a", 73), + token: validToken, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: client, + retrieveByIdentityResponse: rClient, + err: repoerr.ErrMalformedEntity, + }, + { + desc: "update client secret with failed to update secret", + oldSecret: client.Credentials.Secret, + newSecret: newSecret, + token: validToken, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: client, + retrieveByIdentityResponse: rClient, + updateSecretResponse: mgclients.Client{}, + updateSecretErr: repoerr.ErrMalformedEntity, + err: svcerr.ErrUpdateEntity, }, } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{UserId: client.ID}, nil) - if tc.token == inValidToken { - repoCall = auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: inValidToken}).Return(&magistrala.IdentityRes{}, svcerr.ErrAuthentication) - } - repoCall1 := cRepo.On("RetrieveByID", context.Background(), client.ID).Return(tc.response, tc.err) - repoCall2 := cRepo.On("RetrieveByIdentity", context.Background(), client.Credentials.Identity).Return(tc.response, tc.err) - repoCall3 := cRepo.On("UpdateSecret", context.Background(), mock.Anything).Return(tc.response, tc.err) - repoCall4 := auth.On("Issue", mock.Anything, mock.Anything).Return(&magistrala.Token{}, nil) + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := cRepo.On("RetrieveByID", context.Background(), client.ID).Return(tc.retrieveByIDResponse, tc.retrieveByIDErr) + repoCall2 := cRepo.On("RetrieveByIdentity", context.Background(), client.Credentials.Identity).Return(tc.retrieveByIdentityResponse, tc.retrieveByIdentityErr) + repoCall3 := cRepo.On("UpdateSecret", context.Background(), mock.Anything).Return(tc.updateSecretResponse, tc.updateSecretErr) + repoCall4 := auth.On("Issue", mock.Anything, mock.Anything).Return(tc.issueResponse, tc.issueErr) updatedClient, err := svc.UpdateClientSecret(context.Background(), tc.token, tc.oldSecret, tc.newSecret) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) assert.Equal(t, tc.response, updatedClient, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, updatedClient)) @@ -957,10 +1268,7 @@ func TestUpdateClientSecret(t *testing.T) { } func TestEnableClient(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) + svc, cRepo, auth, _ := newService(true) enabledClient1 := mgclients.Client{ID: testsutil.GenerateUUID(t), Credentials: mgclients.Credentials{Identity: "client1@example.com", Secret: "password"}, Status: mgclients.EnabledStatus} disabledClient1 := mgclients.Client{ID: testsutil.GenerateUUID(t), Credentials: mgclients.Credentials{Identity: "client3@example.com", Secret: "password"}, Status: mgclients.DisabledStatus} @@ -968,56 +1276,116 @@ func TestEnableClient(t *testing.T) { endisabledClient1.Status = mgclients.EnabledStatus cases := []struct { - desc string - id string - token string - client mgclients.Client - response mgclients.Client - err error + desc string + id string + token string + client mgclients.Client + identifyResponse *magistrala.IdentityRes + authorizeResponse *magistrala.AuthorizeRes + retrieveByIDResponse mgclients.Client + changeStatusResponse mgclients.Client + response mgclients.Client + identifyErr error + authorizeErr error + retrieveByIDErr error + changeStatusErr error + checkSuperAdminErr error + err error }{ { - desc: "enable disabled client", - id: disabledClient1.ID, - token: validToken, - client: disabledClient1, - response: endisabledClient1, - err: nil, - }, - { - desc: "enable enabled client", - id: enabledClient1.ID, - token: validToken, - client: enabledClient1, - response: enabledClient1, - err: mgclients.ErrStatusAlreadyAssigned, - }, - { - desc: "enable non-existing client", - id: wrongID, - token: validToken, - client: mgclients.Client{}, - response: mgclients.Client{}, - err: repoerr.ErrNotFound, + desc: "enable disabled client", + id: disabledClient1.ID, + token: validToken, + client: disabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: disabledClient1.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + retrieveByIDResponse: disabledClient1, + changeStatusResponse: endisabledClient1, + response: endisabledClient1, + err: nil, + }, + { + desc: "enable disabled client with invalid token", + id: disabledClient1.ID, + token: inValidToken, + client: disabledClient1, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: errors.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "enable disabled client with failed to authorize", + id: disabledClient1.ID, + token: validToken, + client: disabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: disabledClient1.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + err: errors.ErrAuthorization, + }, + { + desc: "enable disabled client with normal user token", + id: disabledClient1.ID, + token: validToken, + client: disabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: validID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + checkSuperAdminErr: errors.ErrAuthorization, + err: errors.ErrAuthorization, + }, + { + desc: "enable disabled client with failed to retrieve client by ID", + id: disabledClient1.ID, + token: validToken, + client: disabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: disabledClient1.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + retrieveByIDResponse: mgclients.Client{}, + retrieveByIDErr: repoerr.ErrNotFound, + err: repoerr.ErrNotFound, + }, + { + desc: "enable already enabled client", + id: enabledClient1.ID, + token: validToken, + client: enabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: enabledClient1.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + retrieveByIDResponse: enabledClient1, + err: mgclients.ErrStatusAlreadyAssigned, + }, + { + desc: "enable disabled client with failed to change status", + id: disabledClient1.ID, + token: validToken, + client: disabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: disabledClient1.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + retrieveByIDResponse: disabledClient1, + changeStatusResponse: mgclients.Client{}, + changeStatusErr: repoerr.ErrMalformedEntity, + err: svcerr.ErrUpdateEntity, }, } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{UserId: validID}, nil) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) - repoCall2 := cRepo.On("RetrieveByID", context.Background(), tc.id).Return(tc.client, tc.err) - repoCall3 := cRepo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.response, tc.err) + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := cRepo.On("CheckSuperAdmin", mock.Anything, mock.Anything).Return(tc.checkSuperAdminErr) + repoCall3 := cRepo.On("RetrieveByID", context.Background(), tc.id).Return(tc.retrieveByIDResponse, tc.retrieveByIDErr) + repoCall4 := cRepo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeStatusResponse, tc.changeStatusErr) _, err := svc.EnableClient(context.Background(), tc.token, tc.id) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) if tc.err == nil { - ok := repoCall2.Parent.AssertCalled(t, "RetrieveByID", context.Background(), tc.id) + ok := repoCall3.Parent.AssertCalled(t, "RetrieveByID", context.Background(), tc.id) assert.True(t, ok, fmt.Sprintf("RetrieveByID was not called on %s", tc.desc)) - ok = repoCall3.Parent.AssertCalled(t, "ChangeStatus", context.Background(), mock.Anything) + ok = repoCall4.Parent.AssertCalled(t, "ChangeStatus", context.Background(), mock.Anything) assert.True(t, ok, fmt.Sprintf("ChangeStatus was not called on %s", tc.desc)) } repoCall.Unset() repoCall1.Unset() repoCall2.Unset() repoCall3.Unset() + repoCall4.Unset() } cases2 := []struct { @@ -1087,10 +1455,7 @@ func TestEnableClient(t *testing.T) { } func TestDisableClient(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) + svc, cRepo, auth, _ := newService(true) enabledClient1 := mgclients.Client{ID: testsutil.GenerateUUID(t), Credentials: mgclients.Credentials{Identity: "client1@example.com", Secret: "password"}, Status: mgclients.EnabledStatus} disabledClient1 := mgclients.Client{ID: testsutil.GenerateUUID(t), Credentials: mgclients.Credentials{Identity: "client3@example.com", Secret: "password"}, Status: mgclients.DisabledStatus} @@ -1098,56 +1463,116 @@ func TestDisableClient(t *testing.T) { disenabledClient1.Status = mgclients.DisabledStatus cases := []struct { - desc string - id string - token string - client mgclients.Client - response mgclients.Client - err error + desc string + id string + token string + client mgclients.Client + identifyResponse *magistrala.IdentityRes + authorizeResponse *magistrala.AuthorizeRes + retrieveByIDResponse mgclients.Client + changeStatusResponse mgclients.Client + response mgclients.Client + identifyErr error + authorizeErr error + retrieveByIDErr error + changeStatusErr error + checkSuperAdminErr error + err error }{ { - desc: "disable enabled client", - id: enabledClient1.ID, - token: validToken, - client: enabledClient1, - response: disenabledClient1, - err: nil, - }, - { - desc: "disable disabled client", - id: disabledClient1.ID, - token: validToken, - client: disabledClient1, - response: mgclients.Client{}, - err: mgclients.ErrStatusAlreadyAssigned, - }, - { - desc: "disable non-existing client", - id: wrongID, - client: mgclients.Client{}, - token: validToken, - response: mgclients.Client{}, - err: repoerr.ErrNotFound, + desc: "disable enabled client", + id: enabledClient1.ID, + token: validToken, + client: enabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: enabledClient1.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + retrieveByIDResponse: enabledClient1, + changeStatusResponse: disenabledClient1, + response: disenabledClient1, + err: nil, + }, + { + desc: "disable enabled client with invalid token", + id: enabledClient1.ID, + token: inValidToken, + client: enabledClient1, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: errors.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "disable enabled client with failed to authorize", + id: enabledClient1.ID, + token: validToken, + client: enabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: disabledClient1.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + err: errors.ErrAuthorization, + }, + { + desc: "disable enabled client with normal user token", + id: enabledClient1.ID, + token: validToken, + client: enabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: enabledClient1.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + checkSuperAdminErr: errors.ErrAuthorization, + err: errors.ErrAuthorization, + }, + { + desc: "disable enabled client with failed to retrieve client by ID", + id: enabledClient1.ID, + token: validToken, + client: enabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: enabledClient1.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + retrieveByIDResponse: mgclients.Client{}, + retrieveByIDErr: repoerr.ErrNotFound, + err: repoerr.ErrNotFound, + }, + { + desc: "disable already disabled client", + id: disabledClient1.ID, + token: validToken, + client: disabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: disabledClient1.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + retrieveByIDResponse: disabledClient1, + err: mgclients.ErrStatusAlreadyAssigned, + }, + { + desc: "disable enabled client with failed to change status", + id: enabledClient1.ID, + token: validToken, + client: enabledClient1, + identifyResponse: &magistrala.IdentityRes{UserId: enabledClient1.ID}, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + retrieveByIDResponse: enabledClient1, + changeStatusResponse: mgclients.Client{}, + changeStatusErr: repoerr.ErrMalformedEntity, + err: svcerr.ErrUpdateEntity, }, } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: validToken}).Return(&magistrala.IdentityRes{UserId: validID}, nil) - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true}, nil) - repoCall2 := cRepo.On("RetrieveByID", context.Background(), tc.id).Return(tc.client, tc.err) - repoCall3 := cRepo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.response, tc.err) + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := cRepo.On("CheckSuperAdmin", mock.Anything, mock.Anything).Return(tc.checkSuperAdminErr) + repoCall3 := cRepo.On("RetrieveByID", context.Background(), tc.id).Return(tc.retrieveByIDResponse, tc.retrieveByIDErr) + repoCall4 := cRepo.On("ChangeStatus", context.Background(), mock.Anything).Return(tc.changeStatusResponse, tc.changeStatusErr) _, err := svc.DisableClient(context.Background(), tc.token, tc.id) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) if tc.err == nil { - ok := repoCall2.Parent.AssertCalled(t, "RetrieveByID", context.Background(), tc.id) + ok := repoCall3.Parent.AssertCalled(t, "RetrieveByID", context.Background(), tc.id) assert.True(t, ok, fmt.Sprintf("RetrieveByID was not called on %s", tc.desc)) - ok = repoCall3.Parent.AssertCalled(t, "ChangeStatus", context.Background(), mock.Anything) + ok = repoCall4.Parent.AssertCalled(t, "ChangeStatus", context.Background(), mock.Anything) assert.True(t, ok, fmt.Sprintf("ChangeStatus was not called on %s", tc.desc)) } repoCall.Unset() repoCall1.Unset() repoCall2.Unset() repoCall3.Unset() + repoCall4.Unset() } cases2 := []struct { @@ -1217,133 +1642,475 @@ func TestDisableClient(t *testing.T) { } func TestListMembers(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) - - nClients := uint64(10) - aClients := []mgclients.Client{} - owner := testsutil.GenerateUUID(t) - for i := uint64(0); i < nClients; i++ { - identity := fmt.Sprintf("member_%d@example.com", i) - client := mgclients.Client{ - ID: testsutil.GenerateUUID(t), - Name: identity, - Credentials: mgclients.Credentials{ - Identity: identity, - Secret: "password", - }, - Tags: []string{"tag1", "tag2"}, - Metadata: mgclients.Metadata{"role": "client"}, - } - if i%3 == 0 { - client.Owner = owner - } - aClients = append(aClients, client) - } + svc, cRepo, auth, _ := newService(true) + + validPolicy := fmt.Sprintf("%s_%s", validID, client.ID) + permissionsClient := client + permissionsClient.Permissions = []string{"read"} cases := []struct { - desc string - token string - groupID string - page mgclients.Page - response mgclients.MembersPage - err error + desc string + token string + groupID string + objectKind string + objectID string + page mgclients.Page + identifyResponse *magistrala.IdentityRes + authorizeReq *magistrala.AuthorizeReq + listAllSubjectsReq *magistrala.ListSubjectsReq + authorizeResponse *magistrala.AuthorizeRes + listAllSubjectsResponse *magistrala.ListSubjectsRes + retrieveAllResponse mgclients.ClientsPage + listPermissionsResponse *magistrala.ListPermissionsRes + response mgclients.MembersPage + authorizeErr error + listAllSubjectsErr error + retrieveAllErr error + identifyErr error + listPermissionErr error + err error }{ { - desc: "list clients with authorized token", - token: validToken, - groupID: testsutil.GenerateUUID(t), - page: mgclients.Page{ - IDs: clientsToUUIDs(aClients), + desc: "list members with no policies successfully of the things kind", + token: validToken, + groupID: validID, + objectKind: authsvc.ThingsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read"}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + listAllSubjectsResponse: &magistrala.ListSubjectsRes{}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.ThingType, + Object: validID, }, + listAllSubjectsReq: &magistrala.ListSubjectsReq{ + SubjectType: authsvc.UserType, + Permission: "read", + Object: validID, + ObjectType: authsvc.ThingType, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, response: mgclients.MembersPage{ Page: mgclients.Page{ Total: 0, Offset: 0, - Limit: 10, + Limit: 100, }, - Members: aClients, }, err: nil, }, { - desc: "list clients with offset and limit", - token: validToken, - groupID: testsutil.GenerateUUID(t), - page: mgclients.Page{ - Offset: 6, - Limit: nClients, - Status: mgclients.AllStatus, - IDs: clientsToUUIDs(aClients[6 : nClients-1]), + desc: "list members with policies successsfully of the things kind", + token: validToken, + groupID: validID, + objectKind: authsvc.ThingsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read"}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.ThingType, + Object: validID, + }, + listAllSubjectsReq: &magistrala.ListSubjectsReq{ + SubjectType: authsvc.UserType, + Permission: "read", + Object: validID, + ObjectType: authsvc.ThingType, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + listAllSubjectsResponse: &magistrala.ListSubjectsRes{ + Policies: []string{validPolicy}, }, + retrieveAllResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + Offset: 0, + Limit: 100, + }, + Clients: []mgclients.Client{client}, + }, + response: mgclients.MembersPage{ + Page: mgclients.Page{ + Total: 1, + Offset: 0, + Limit: 100, + }, + Members: []mgclients.Client{client}, + }, + err: nil, + }, + { + desc: "list members with policies successsfully of the things kind with permissions", + token: validToken, + groupID: validID, + objectKind: authsvc.ThingsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read", ListPerms: true}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.ThingType, + Object: validID, + }, + listAllSubjectsReq: &magistrala.ListSubjectsReq{ + SubjectType: authsvc.UserType, + Permission: "read", + Object: validID, + ObjectType: authsvc.ThingType, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + listAllSubjectsResponse: &magistrala.ListSubjectsRes{ + Policies: []string{validPolicy}, + }, + retrieveAllResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + Offset: 0, + Limit: 100, + }, + Clients: []mgclients.Client{client}, + }, + listPermissionsResponse: &magistrala.ListPermissionsRes{Permissions: []string{"read"}}, response: mgclients.MembersPage{ Page: mgclients.Page{ - Total: nClients - 6 - 1, + Total: 1, + Offset: 0, + Limit: 100, }, - Members: aClients[6 : nClients-1], + Members: []mgclients.Client{permissionsClient}, }, + err: nil, }, { - desc: "list clients with an invalid token", - token: inValidToken, - groupID: testsutil.GenerateUUID(t), - page: mgclients.Page{}, + desc: "list members with policies of the things kind with permissionswith failed list permissions", + token: validToken, + groupID: validID, + objectKind: authsvc.ThingsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read", ListPerms: true}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.ThingType, + Object: validID, + }, + listAllSubjectsReq: &magistrala.ListSubjectsReq{ + SubjectType: authsvc.UserType, + Permission: "read", + Object: validID, + ObjectType: authsvc.ThingType, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + listAllSubjectsResponse: &magistrala.ListSubjectsRes{ + Policies: []string{validPolicy}, + }, + retrieveAllResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + Offset: 0, + Limit: 100, + }, + Clients: []mgclients.Client{client}, + }, + listPermissionsResponse: &magistrala.ListPermissionsRes{}, + response: mgclients.MembersPage{}, + listPermissionErr: svcerr.ErrNotFound, + err: svcerr.ErrNotFound, + }, + { + desc: "list members with of the things kind with failed to authorize", + token: validToken, + groupID: validID, + objectKind: authsvc.ThingsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read"}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.ThingType, + Object: validID, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + err: errors.ErrAuthorization, + }, + { + desc: "list members with of the things kind with failed to list all subjects", + token: validToken, + groupID: validID, + objectKind: authsvc.ThingsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read"}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.ThingType, + Object: validID, + }, + listAllSubjectsReq: &magistrala.ListSubjectsReq{ + SubjectType: authsvc.UserType, + Permission: "read", + Object: validID, + ObjectType: authsvc.ThingType, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + listAllSubjectsErr: errors.ErrNotFound, + listAllSubjectsResponse: &magistrala.ListSubjectsRes{}, + err: errors.ErrNotFound, + }, + { + desc: "list members with of the things kind with failed to retrieve all", + token: validToken, + groupID: validID, + objectKind: authsvc.ThingsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read"}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.ThingType, + Object: validID, + }, + listAllSubjectsReq: &magistrala.ListSubjectsReq{ + SubjectType: authsvc.UserType, + Permission: "read", + Object: validID, + ObjectType: authsvc.ThingType, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + listAllSubjectsResponse: &magistrala.ListSubjectsRes{ + Policies: []string{validPolicy}, + }, + retrieveAllResponse: mgclients.ClientsPage{}, + response: mgclients.MembersPage{}, + retrieveAllErr: repoerr.ErrNotFound, + err: repoerr.ErrNotFound, + }, + { + desc: "list members with no policies successfully of the domain kind", + token: validToken, + groupID: validID, + objectKind: authsvc.DomainsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read"}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + listAllSubjectsResponse: &magistrala.ListSubjectsRes{}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.DomainType, + Object: validID, + }, + listAllSubjectsReq: &magistrala.ListSubjectsReq{ + SubjectType: authsvc.UserType, + Permission: "read", + Object: validID, + ObjectType: authsvc.DomainType, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, response: mgclients.MembersPage{ Page: mgclients.Page{ Total: 0, Offset: 0, - Limit: 0, + Limit: 100, }, }, - err: svcerr.ErrAuthentication, + err: nil, }, { - desc: "list clients for an owner", - token: validToken, - groupID: testsutil.GenerateUUID(t), - page: mgclients.Page{ - IDs: clientsToUUIDs([]mgclients.Client{aClients[0], aClients[3], aClients[6], aClients[9]}), + desc: "list members with policies successsfully of the domains kind", + token: validToken, + groupID: validID, + objectKind: authsvc.DomainsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read"}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.DomainType, + Object: validID, + }, + listAllSubjectsReq: &magistrala.ListSubjectsReq{ + SubjectType: authsvc.UserType, + Permission: "read", + Object: validID, + ObjectType: authsvc.DomainType, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + listAllSubjectsResponse: &magistrala.ListSubjectsRes{ + Policies: []string{validPolicy}, + }, + retrieveAllResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + Offset: 0, + Limit: 100, + }, + Clients: []mgclients.Client{client}, + }, + response: mgclients.MembersPage{ + Page: mgclients.Page{ + Total: 1, + Offset: 0, + Limit: 100, + }, + Members: []mgclients.Client{client}, + }, + err: nil, + }, + { + desc: "list members with of the domains kind with failed to authorize", + token: validToken, + groupID: validID, + objectKind: authsvc.DomainsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read"}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.DomainType, + Object: validID, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: false}, + err: errors.ErrAuthorization, + }, + { + desc: "list members with no policies successfully of the groups kind", + token: validToken, + groupID: validID, + objectKind: authsvc.GroupsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read"}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + listAllSubjectsResponse: &magistrala.ListSubjectsRes{}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.GroupType, + Object: validID, + }, + listAllSubjectsReq: &magistrala.ListSubjectsReq{ + SubjectType: authsvc.UserType, + Permission: "read", + Object: validID, + ObjectType: authsvc.GroupType, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + response: mgclients.MembersPage{ + Page: mgclients.Page{ + Total: 0, + Offset: 0, + Limit: 100, + }, + }, + err: nil, + }, + { + desc: "list members with policies successsfully of the domains kind", + token: validToken, + groupID: validID, + objectKind: authsvc.GroupsKind, + objectID: validID, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read"}, + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + authorizeReq: &magistrala.AuthorizeReq{ + SubjectType: authsvc.UserType, + SubjectKind: authsvc.TokenKind, + Subject: validToken, + Permission: "read", + ObjectType: authsvc.GroupType, + Object: validID, + }, + listAllSubjectsReq: &magistrala.ListSubjectsReq{ + SubjectType: authsvc.UserType, + Permission: "read", + Object: validID, + ObjectType: authsvc.GroupType, + }, + authorizeResponse: &magistrala.AuthorizeRes{Authorized: true}, + listAllSubjectsResponse: &magistrala.ListSubjectsRes{ + Policies: []string{validPolicy}, + }, + retrieveAllResponse: mgclients.ClientsPage{ + Page: mgclients.Page{ + Total: 1, + Offset: 0, + Limit: 100, + }, + Clients: []mgclients.Client{client}, }, response: mgclients.MembersPage{ Page: mgclients.Page{ - Total: 4, + Total: 1, + Offset: 0, + Limit: 100, }, - Members: []mgclients.Client{aClients[0], aClients[3], aClients[6], aClients[9]}, + Members: []mgclients.Client{client}, }, err: nil, }, + { + desc: "list members with invalid token", + token: inValidToken, + page: mgclients.Page{Offset: 0, Limit: 100, Permission: "read"}, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: errors.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, } for _, tc := range cases { - repoCall := auth.On("Identify", mock.Anything, mock.Anything).Return(&magistrala.IdentityRes{}, nil) - if tc.token == inValidToken { - repoCall = auth.On("Identify", mock.Anything, mock.Anything).Return(&magistrala.IdentityRes{}, errors.ErrAuthentication) - } - repoCall1 := auth.On("Authorize", mock.Anything, mock.Anything).Return(&magistrala.AuthorizeRes{Authorized: true, Id: validID}, nil) - - repoCall2 := auth.On("ListAllSubjects", mock.Anything, mock.Anything).Return(&magistrala.ListSubjectsRes{Policies: prefixClientUUIDSWithDomain(tc.response.Members)}, nil) - repoCall3 := cRepo.On("RetrieveAll", context.Background(), tc.page).Return(mgclients.ClientsPage{Page: tc.response.Page, Clients: tc.response.Members}, tc.err) - page, err := svc.ListMembers(context.Background(), tc.token, "groups", tc.groupID, tc.page) + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := auth.On("Authorize", mock.Anything, tc.authorizeReq).Return(tc.authorizeResponse, tc.authorizeErr) + repoCall2 := auth.On("ListAllSubjects", mock.Anything, tc.listAllSubjectsReq).Return(tc.listAllSubjectsResponse, tc.listAllSubjectsErr) + repoCall3 := cRepo.On("RetrieveAll", context.Background(), mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr) + repoCall4 := auth.On("ListPermissions", mock.Anything, mock.Anything).Return(tc.listPermissionsResponse, tc.listPermissionErr) + page, err := svc.ListMembers(context.Background(), tc.token, tc.objectKind, tc.objectID, tc.page) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) assert.Equal(t, tc.response, page, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page)) - if tc.err == nil { - ok := repoCall3.Parent.AssertCalled(t, "RetrieveAll", context.Background(), tc.page) - assert.True(t, ok, fmt.Sprintf("RetrieveAll was not called on %s", tc.desc)) - } + repoCall.Unset() repoCall1.Unset() repoCall2.Unset() repoCall3.Unset() + repoCall4.Unset() } } func TestIssueToken(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) + svc, cRepo, auth, _ := newService(true) rClient := client rClient2 := client @@ -1353,40 +2120,54 @@ func TestIssueToken(t *testing.T) { rClient3.Credentials.Secret, _ = phasher.Hash("wrongsecret") cases := []struct { - desc string - client mgclients.Client - rClient mgclients.Client - err error + desc string + DomainID string + client mgclients.Client + retrieveByIdentityResponse mgclients.Client + issueResponse *magistrala.Token + retrieveByIdentityErr error + issueErr error + err error }{ { - desc: "issue token for an existing client", - client: client, - rClient: rClient, - err: nil, + desc: "issue token for an existing client", + client: client, + retrieveByIdentityResponse: rClient, + issueResponse: &magistrala.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, + err: nil, }, { - desc: "issue token for a non-existing client", - client: client, - rClient: mgclients.Client{}, - err: svcerr.ErrAuthentication, + desc: "issue token for a non-existing client", + client: client, + retrieveByIdentityResponse: mgclients.Client{}, + retrieveByIdentityErr: errors.ErrNotFound, + err: repoerr.ErrNotFound, }, { - desc: "issue token for a client with wrong secret", - client: rClient2, - rClient: rClient3, - err: errors.ErrAuthentication, + desc: "issue token for a client with wrong secret", + client: client, + retrieveByIdentityResponse: rClient3, + err: errors.ErrLogin, + }, + { + desc: "issue token with non-empty domain id", + DomainID: "domain", + client: client, + retrieveByIdentityResponse: rClient, + issueResponse: &magistrala.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, + err: nil, }, } for _, tc := range cases { - repoCall := auth.On("Issue", mock.Anything, mock.Anything).Return(&magistrala.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, tc.err) - repoCall1 := cRepo.On("RetrieveByIdentity", context.Background(), tc.client.Credentials.Identity).Return(tc.rClient, tc.err) - token, err := svc.IssueToken(context.Background(), tc.client.Credentials.Identity, tc.client.Credentials.Secret, "") + repoCall := cRepo.On("RetrieveByIdentity", context.Background(), tc.client.Credentials.Identity).Return(tc.retrieveByIdentityResponse, tc.retrieveByIdentityErr) + repoCall1 := auth.On("Issue", mock.Anything, mock.Anything).Return(tc.issueResponse, tc.issueErr) + token, err := svc.IssueToken(context.Background(), tc.client.Credentials.Identity, tc.client.Credentials.Secret, tc.DomainID) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) if err == nil { assert.NotEmpty(t, token.GetAccessToken(), fmt.Sprintf("%s: expected %s not to be empty\n", tc.desc, token.GetAccessToken())) assert.NotEmpty(t, token.GetRefreshToken(), fmt.Sprintf("%s: expected %s not to be empty\n", tc.desc, token.GetRefreshToken())) - ok := repoCall1.Parent.AssertCalled(t, "RetrieveByIdentity", context.Background(), tc.client.Credentials.Identity) + ok := repoCall.Parent.AssertCalled(t, "RetrieveByIdentity", context.Background(), tc.client.Credentials.Identity) assert.True(t, ok, fmt.Sprintf("RetrieveByIdentity was not called on %s", tc.desc)) } repoCall.Unset() @@ -1395,19 +2176,17 @@ func TestIssueToken(t *testing.T) { } func TestRefreshToken(t *testing.T) { - cRepo := new(mocks.Repository) - auth := new(authmocks.Service) - e := mocks.NewEmailer() - svc := users.NewService(cRepo, auth, e, phasher, idProvider, passRegex, true) + svc, _, auth, _ := newService(true) rClient := client rClient.Credentials.Secret, _ = phasher.Hash(client.Credentials.Secret) cases := []struct { - desc string - token string - client mgclients.Client - err error + desc string + token string + domainID string + client mgclients.Client + err error }{ { desc: "refresh token with refresh token for an existing client", @@ -1439,11 +2218,18 @@ func TestRefreshToken(t *testing.T) { client: client, err: svcerr.ErrAuthentication, }, + { + desc: "refresh token with non-empty domain id", + token: validToken, + domainID: validID, + client: client, + err: nil, + }, } for _, tc := range cases { repoCall := auth.On("Refresh", mock.Anything, mock.Anything).Return(&magistrala.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, tc.err) - token, err := svc.RefreshToken(context.Background(), tc.token, "") + token, err := svc.RefreshToken(context.Background(), tc.token, tc.domainID) assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) if err == nil { assert.NotEmpty(t, token.GetAccessToken(), fmt.Sprintf("%s: expected %s not to be empty\n", tc.desc, token.GetAccessToken())) @@ -1453,18 +2239,230 @@ func TestRefreshToken(t *testing.T) { } } -func clientsToUUIDs(clients []mgclients.Client) []string { - ids := []string{} - for _, c := range clients { - ids = append(ids, c.ID) +func TestGenerateResetToken(t *testing.T) { + svc, cRepo, auth, _ := newService(true) + + cases := []struct { + desc string + email string + host string + retrieveByIdentityResponse mgclients.Client + issueResponse *magistrala.Token + retrieveByIdentityErr error + issueErr error + err error + }{ + { + desc: "generate reset token for existing client", + email: "existingemail@example.com", + host: "examplehost", + retrieveByIdentityResponse: client, + issueResponse: &magistrala.Token{AccessToken: validToken, RefreshToken: &validToken, AccessType: "3"}, + err: nil, + }, + { + desc: "generate reset token for client with non-existing client", + email: "example@example.com", + host: "examplehost", + retrieveByIdentityResponse: mgclients.Client{ + ID: testsutil.GenerateUUID(t), + Credentials: mgclients.Credentials{ + Identity: "", + }, + }, + retrieveByIdentityErr: errors.ErrNotFound, + err: errors.ErrNotFound, + }, + { + desc: "generate reset token with failed to issue token", + email: "existingemail@example.com", + host: "examplehost", + retrieveByIdentityResponse: client, + issueResponse: &magistrala.Token{}, + issueErr: svcerr.ErrAuthorization, + err: users.ErrRecoveryToken, + }, + } + + for _, tc := range cases { + repoCall := cRepo.On("RetrieveByIdentity", context.Background(), tc.email).Return(tc.retrieveByIdentityResponse, tc.retrieveByIdentityErr) + repoCall1 := auth.On("Issue", mock.Anything, mock.Anything).Return(tc.issueResponse, tc.issueErr) + err := svc.GenerateResetToken(context.Background(), tc.email, tc.host) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + repoCall.Parent.AssertCalled(t, "RetrieveByIdentity", context.Background(), tc.email) + repoCall.Unset() + repoCall1.Unset() + } +} + +func TestResetSecret(t *testing.T) { + svc, cRepo, auth, _ := newService(true) + + client := mgclients.Client{ + ID: "clientID", + Credentials: mgclients.Credentials{ + Identity: "test@example.com", + Secret: "Strongsecret", + }, + } + + cases := []struct { + desc string + token string + newSecret string + identifyResponse *magistrala.IdentityRes + retrieveByIDResponse mgclients.Client + updateSecretResponse mgclients.Client + identifyErr error + retrieveByIDErr error + updateSecretErr error + err error + }{ + { + desc: "reset secret with successfully", + token: validToken, + newSecret: "newStrongSecret", + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: client, + updateSecretResponse: mgclients.Client{ + ID: "clientID", + Credentials: mgclients.Credentials{ + Identity: "test@example.com", + Secret: "newStrongSecret", + }, + }, + err: nil, + }, + { + desc: "reset secret with invalid token", + token: inValidToken, + newSecret: "newStrongSecret", + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: svcerr.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "reset secret with invalid ID", + token: validToken, + newSecret: "newStrongSecret", + identifyResponse: &magistrala.IdentityRes{UserId: wrongID}, + retrieveByIDResponse: mgclients.Client{}, + retrieveByIDErr: repoerr.ErrNotFound, + err: repoerr.ErrNotFound, + }, + { + desc: "reset secret with empty identity", + token: validToken, + newSecret: "newStrongSecret", + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: mgclients.Client{ + ID: "clientID", + Credentials: mgclients.Credentials{ + Identity: "", + }, + }, + err: errors.ErrNotFound, + }, + { + desc: "reset secret with invalid secret format", + token: validToken, + newSecret: "weak", + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: client, + err: users.ErrPasswordFormat, + }, + { + desc: "reset secret with failed to update secret", + token: validToken, + newSecret: "newStrongSecret", + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: client, + updateSecretResponse: mgclients.Client{}, + updateSecretErr: svcerr.ErrUpdateEntity, + err: svcerr.ErrAuthorization, + }, + { + desc: "reset secret with a too long secret", + token: validToken, + newSecret: strings.Repeat("strongSecret", 10), + identifyResponse: &magistrala.IdentityRes{UserId: client.ID}, + retrieveByIDResponse: client, + err: errHashPassword, + }, + } + + for _, tc := range cases { + repoCall := auth.On("Identify", mock.Anything, &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := cRepo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.retrieveByIDResponse, tc.retrieveByIDErr) + repoCall2 := cRepo.On("UpdateSecret", context.Background(), mock.Anything).Return(tc.updateSecretResponse, tc.updateSecretErr) + err := svc.ResetSecret(context.Background(), tc.token, tc.newSecret) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + + repoCall2.Parent.AssertCalled(t, "UpdateSecret", context.Background(), mock.Anything) + repoCall1.Parent.AssertCalled(t, "RetrieveByID", context.Background(), client.ID) + repoCall.Parent.AssertCalled(t, "Identify", mock.Anything, mock.Anything) + repoCall.Unset() + repoCall2.Unset() + repoCall1.Unset() } - return ids } -func prefixClientUUIDSWithDomain(clients []mgclients.Client) []string { - ids := []string{} - for _, c := range clients { - ids = append(ids, fmt.Sprintf("%s_%s", domainID, c.ID)) +func TestViewProfile(t *testing.T) { + svc, cRepo, auth, _ := newService(true) + + client := mgclients.Client{ + ID: "clientID", + Credentials: mgclients.Credentials{ + Identity: "existingIdentity", + Secret: "Strongsecret", + }, + } + cases := []struct { + desc string + token string + client mgclients.Client + identifyResponse *magistrala.IdentityRes + retrieveByIDResponse mgclients.Client + identifyErr error + retrieveByIDErr error + err error + }{ + { + desc: "view profile successfully", + token: validToken, + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: validID}, + retrieveByIDResponse: client, + err: nil, + }, + { + desc: "view profile with invalid token", + token: inValidToken, + client: client, + identifyResponse: &magistrala.IdentityRes{}, + identifyErr: svcerr.ErrAuthentication, + err: svcerr.ErrAuthentication, + }, + { + desc: "view profile with invalid ID", + token: validToken, + client: client, + identifyResponse: &magistrala.IdentityRes{UserId: wrongID}, + retrieveByIDResponse: mgclients.Client{}, + retrieveByIDErr: repoerr.ErrNotFound, + err: repoerr.ErrNotFound, + }, + } + + for _, tc := range cases { + repoCall := auth.On("Identify", mock.Anything, mock.Anything).Return(tc.identifyResponse, tc.identifyErr) + repoCall1 := cRepo.On("RetrieveByID", context.Background(), mock.Anything).Return(tc.retrieveByIDResponse, tc.retrieveByIDErr) + _, err := svc.ViewProfile(context.Background(), tc.token) + assert.True(t, errors.Contains(err, tc.err), fmt.Sprintf("%s: expected %s got %s\n", tc.desc, tc.err, err)) + + repoCall.Parent.AssertCalled(t, "Identify", mock.Anything, mock.Anything) + repoCall1.Parent.AssertCalled(t, "RetrieveByID", context.Background(), mock.Anything) + repoCall.Unset() + repoCall1.Unset() } - return ids }