MG-2330 - Fix non-admin users search with identity (#2331)

Signed-off-by: nyagamunene <stevenyaga2014@gmail.com>
This commit is contained in:
Steve Munene
2024-07-22 14:12:43 +03:00
committed by GitHub
parent c4c037f839
commit c398908d50
22 changed files with 752 additions and 40 deletions
+26
View File
@@ -397,6 +397,32 @@ paths:
"500":
$ref: "#/components/responses/ServiceError"
/users/search:
get:
operationId: searchUsers
summary: Search users
description: |
Search users by name and identity.
tags:
- Users
parameters:
- $ref: "#/components/parameters/Limit"
- $ref: "#/components/parameters/Offset"
- $ref: "#/components/parameters/UserName"
- $ref: "#/components/parameters/UserIdentity"
- $ref: "#/components/parameters/UserID"
security:
- bearerAuth: []
responses:
"200":
$ref: "#/components/responses/UserPageRes"
"400":
description: Failed due to malformed query parameters.
"401":
description: Missing or invalid access token provided.
"500":
$ref: "#/components/responses/ServiceError"
/password/reset-request:
post:
operationId: requestPasswordReset
+46 -1
View File
@@ -5,6 +5,9 @@ package cli
import (
"encoding/json"
"fmt"
"net/url"
"strconv"
mgclients "github.com/absmach/magistrala/pkg/clients"
mgxsdk "github.com/absmach/magistrala/pkg/sdk/go"
@@ -463,12 +466,54 @@ var cmdUsers = []cobra.Command{
logJSONCmd(*cmd, users)
},
},
{
Use: "search <query> <user_auth_token>",
Short: "Search users",
Long: "Search users by query\n" +
"Usage:\n" +
"\tmagistrala-cli users search <query> <user_auth_token>\n",
Run: func(cmd *cobra.Command, args []string) {
if len(args) != 2 {
logUsageCmd(*cmd, cmd.Use)
return
}
values, err := url.ParseQuery(args[0])
if err != nil {
logErrorCmd(*cmd, fmt.Errorf("failed to parse query: %s", err))
}
pm := mgxsdk.PageMetadata{
Offset: Offset,
Limit: Limit,
Name: values.Get("name"),
ID: values.Get("id"),
}
if off, err := strconv.Atoi(values.Get("offset")); err == nil {
pm.Offset = uint64(off)
}
if lim, err := strconv.Atoi(values.Get("limit")); err == nil {
pm.Limit = uint64(lim)
}
users, err := sdk.SearchUsers(pm, args[1])
if err != nil {
logErrorCmd(*cmd, err)
return
}
logJSONCmd(*cmd, users)
},
},
}
// NewUsersCmd returns users command.
func NewUsersCmd() *cobra.Command {
cmd := cobra.Command{
Use: "users [create | get | update | token | password | enable | disable | delete | channels | things | groups]",
Use: "users [create | get | update | token | password | enable | disable | delete | channels | things | groups | search]",
Short: "Users management",
Long: `Users management: create accounts and tokens"`,
}
+3 -1
View File
@@ -160,7 +160,9 @@ func EncodeError(_ context.Context, err error, w http.ResponseWriter) {
errors.Contains(err, apiutil.ErrInvalidEntityType),
errors.Contains(err, apiutil.ErrMissingEntityType),
errors.Contains(err, apiutil.ErrInvalidTimeFormat),
errors.Contains(err, svcerr.ErrSearch):
errors.Contains(err, svcerr.ErrSearch),
errors.Contains(err, apiutil.ErrEmptySearchQuery),
errors.Contains(err, apiutil.ErrLenSearchQuery):
err = unwrap(err)
w.WriteHeader(http.StatusBadRequest)
+6
View File
@@ -179,4 +179,10 @@ var (
// ErrInvalidTimeFormat indicates invalid time format i.e not unix time.
ErrInvalidTimeFormat = errors.New("invalid time format use unix time")
// ErrEmptySearchQuery indicates search query should not be empty.
ErrEmptySearchQuery = errors.New("search query must not be empty")
// ErrLenSearchQuery indicates search query length.
ErrLenSearchQuery = errors.New("search query must be at least 3 characters")
)
+12
View File
@@ -336,6 +336,18 @@ type SDK interface {
// fmt.Println(things)
ListUserThings(userID string, pm PageMetadata, token string) (ThingsPage, errors.SDKError)
// SeachUsers filters users and returns a page result.
//
// example:
// pm := sdk.PageMetadata{
// Offset: 0,
// Limit: 10,
// Name: "John Doe",
// }
// users, _ := sdk.SearchUsers(pm, "token")
// fmt.Println(users)
SearchUsers(pm PageMetadata, token string) (UsersPage, errors.SDKError)
// CreateThing registers new thing and returns its id.
//
// example:
+19
View File
@@ -328,6 +328,25 @@ func (sdk mgSDK) ListUserThings(userID string, pm PageMetadata, token string) (T
return tp, nil
}
func (sdk mgSDK) SearchUsers(pm PageMetadata, token string) (UsersPage, errors.SDKError) {
url, err := sdk.withQueryParams(sdk.usersURL, fmt.Sprintf("%s/search", usersEndpoint), pm)
if err != nil {
return UsersPage{}, errors.NewSDKError(err)
}
_, body, sdkerr := sdk.processRequest(http.MethodGet, url, token, nil, nil, http.StatusOK)
if sdkerr != nil {
return UsersPage{}, sdkerr
}
var cp UsersPage
if err := json.Unmarshal(body, &cp); err != nil {
return UsersPage{}, errors.NewSDKError(err)
}
return cp, nil
}
func (sdk mgSDK) EnableUser(id, token string) (User, errors.SDKError) {
return sdk.changeClientStatus(token, id, enableEndpoint)
}
+121
View File
@@ -542,6 +542,127 @@ func TestListUsers(t *testing.T) {
}
}
func TestSearchClients(t *testing.T) {
ts, svc := setupUsers()
defer ts.Close()
var cls []sdk.User
conf := sdk.Config{
UsersURL: ts.URL,
}
mgsdk := sdk.NewSDK(conf)
for i := 10; i < 100; i++ {
cl := sdk.User{
ID: generateUUID(t),
Name: fmt.Sprintf("client_%d", i),
Credentials: sdk.Credentials{
Identity: fmt.Sprintf("identity_%d", i),
Secret: fmt.Sprintf("password_%d", i),
},
Metadata: sdk.Metadata{"name": fmt.Sprintf("client_%d", i)},
Status: mgclients.EnabledStatus.String(),
}
if i == 50 {
cl.Status = mgclients.DisabledStatus.String()
cl.Tags = []string{"tag1", "tag2"}
}
cls = append(cls, cl)
}
cases := []struct {
desc string
token string
page sdk.PageMetadata
response []sdk.User
searchreturn mgclients.ClientsPage
err errors.SDKError
identifyErr error
}{
{
desc: "search for users",
token: validToken,
err: nil,
page: sdk.PageMetadata{
Offset: offset,
Limit: limit,
Name: "client_10",
},
response: []sdk.User{cls[10]},
searchreturn: mgclients.ClientsPage{
Clients: []mgclients.Client{convertClient(cls[10])},
Page: mgclients.Page{
Total: 1,
Offset: offset,
Limit: limit,
},
},
},
{
desc: "search for users with invalid token",
token: invalidToken,
page: sdk.PageMetadata{
Offset: offset,
Limit: limit,
Name: "client_10",
},
err: errors.NewSDKErrorWithStatus(svcerr.ErrAuthentication, http.StatusUnauthorized),
response: nil,
identifyErr: svcerr.ErrAuthentication,
},
{
desc: "search for users with empty token",
token: "",
page: sdk.PageMetadata{
Offset: offset,
Limit: limit,
Name: "client_10",
},
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, apiutil.ErrBearerToken), http.StatusUnauthorized),
response: nil,
identifyErr: svcerr.ErrAuthentication,
},
{
desc: "search for users with empty query",
token: validToken,
page: sdk.PageMetadata{
Offset: offset,
Limit: limit,
Name: "",
},
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, apiutil.ErrEmptySearchQuery), http.StatusBadRequest),
},
{
desc: "search for users with invalid length of query",
token: validToken,
page: sdk.PageMetadata{
Offset: offset,
Limit: limit,
Name: "a",
},
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrLenSearchQuery, apiutil.ErrValidation), http.StatusBadRequest),
},
{
desc: "search for users with invalid limit",
token: validToken,
page: sdk.PageMetadata{
Offset: offset,
Limit: 0,
Name: "client_10",
},
err: errors.NewSDKErrorWithStatus(errors.Wrap(apiutil.ErrValidation, apiutil.ErrLimitSize), http.StatusBadRequest),
},
}
for _, tc := range cases {
repoCall := svc.On("SearchUsers", mock.Anything, mock.Anything, mock.Anything).Return(tc.searchreturn, tc.err)
page, err := mgsdk.SearchUsers(tc.page, tc.token)
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected error %s, got %s", tc.desc, tc.err, err))
assert.Equal(t, tc.response, page.Users, fmt.Sprintf("%s: expected %v got %v\n", tc.desc, tc.response, page))
repoCall.Unset()
}
}
func TestViewUser(t *testing.T) {
ts, svc := setupUsers()
defer ts.Close()
+30
View File
@@ -2050,6 +2050,36 @@ func (_m *SDK) RevokeCert(thingID string, token string) (time.Time, errors.SDKEr
return r0, r1
}
// SearchUsers provides a mock function with given fields: pm, token
func (_m *SDK) SearchUsers(pm sdk.PageMetadata, token string) (sdk.UsersPage, errors.SDKError) {
ret := _m.Called(pm, token)
if len(ret) == 0 {
panic("no return value specified for SearchUsers")
}
var r0 sdk.UsersPage
var r1 errors.SDKError
if rf, ok := ret.Get(0).(func(sdk.PageMetadata, string) (sdk.UsersPage, errors.SDKError)); ok {
return rf(pm, token)
}
if rf, ok := ret.Get(0).(func(sdk.PageMetadata, string) sdk.UsersPage); ok {
r0 = rf(pm, token)
} else {
r0 = ret.Get(0).(sdk.UsersPage)
}
if rf, ok := ret.Get(1).(func(sdk.PageMetadata, string) errors.SDKError); ok {
r1 = rf(pm, token)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(errors.SDKError)
}
}
return r0, r1
}
// SendInvitation provides a mock function with given fields: invitation, token
func (_m *SDK) SendInvitation(invitation sdk.Invitation, token string) error {
ret := _m.Called(invitation, token)
+55
View File
@@ -62,6 +62,13 @@ func clientsHandler(svc users.Service, r *chi.Mux, logger *slog.Logger, pr *rege
opts...,
), "list_clients").ServeHTTP)
r.Get("/search", otelhttp.NewHandler(kithttp.NewServer(
searchClientsEndpoint(svc),
decodeSearchClients,
api.EncodeResponse,
opts...,
), "search_clients").ServeHTTP)
r.Patch("/secret", otelhttp.NewHandler(kithttp.NewServer(
updateClientSecretEndpoint(svc),
decodeUpdateClientSecret,
@@ -272,6 +279,54 @@ func decodeListClients(_ context.Context, r *http.Request) (interface{}, error)
return req, nil
}
func decodeSearchClients(_ context.Context, r *http.Request) (interface{}, error) {
o, err := apiutil.ReadNumQuery[uint64](r, api.OffsetKey, api.DefOffset)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
l, err := apiutil.ReadNumQuery[uint64](r, api.LimitKey, api.DefLimit)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
n, err := apiutil.ReadStringQuery(r, api.NameKey, "")
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
id, err := apiutil.ReadStringQuery(r, api.IDOrder, "")
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)
}
dir, err := apiutil.ReadStringQuery(r, api.DirKey, api.DefDir)
if err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
req := searchClientsReq{
token: apiutil.ExtractBearerToken(r),
Offset: o,
Limit: l,
Name: n,
Id: id,
Order: order,
Dir: dir,
}
for _, field := range []string{req.Name, req.Id} {
if field != "" && len(field) < 3 {
req = searchClientsReq{
token: apiutil.ExtractBearerToken(r),
}
return req, errors.Wrap(apiutil.ErrLenSearchQuery, apiutil.ErrValidation)
}
}
return req, nil
}
func decodeUpdateClient(_ context.Context, r *http.Request) (interface{}, error) {
if !strings.Contains(r.Header.Get("Content-Type"), api.ContentType) {
return nil, errors.Wrap(apiutil.ErrValidation, apiutil.ErrUnsupportedContentType)
+119
View File
@@ -656,6 +656,125 @@ func TestListClients(t *testing.T) {
}
}
func TestSearchUsers(t *testing.T) {
us, svc, _ := newUsersServer()
defer us.Close()
cases := []struct {
desc string
token string
page mgclients.Page
status int
query string
listUsersResponse mgclients.ClientsPage
err error
}{
{
desc: "search users with valid token",
token: validToken,
status: http.StatusOK,
query: "name=clientname",
listUsersResponse: mgclients.ClientsPage{
Page: mgclients.Page{
Total: 1,
},
Clients: []mgclients.Client{client},
},
err: nil,
},
{
desc: "search users with empty token",
token: "",
query: "name=clientname",
status: http.StatusUnauthorized,
err: apiutil.ErrBearerToken,
},
{
desc: "search users with invalid token",
token: inValidToken,
query: "name=clientname",
status: http.StatusUnauthorized,
err: svcerr.ErrAuthentication,
},
{
desc: "search users with offset",
token: validToken,
listUsersResponse: mgclients.ClientsPage{
Page: mgclients.Page{
Offset: 1,
Total: 1,
},
Clients: []mgclients.Client{client},
},
query: "name=clientname&offset=1",
status: http.StatusOK,
err: nil,
},
{
desc: "search users with invalid offset",
token: validToken,
query: "name=clientname&offset=invalid",
status: http.StatusBadRequest,
err: apiutil.ErrValidation,
},
{
desc: "search users with limit",
token: validToken,
listUsersResponse: mgclients.ClientsPage{
Page: mgclients.Page{
Limit: 1,
Total: 1,
},
Clients: []mgclients.Client{client},
},
query: "name=clientname&limit=1",
status: http.StatusOK,
err: nil,
},
{
desc: "search users with invalid limit",
token: validToken,
query: "name=clientname&limit=invalid",
status: http.StatusBadRequest,
err: apiutil.ErrValidation,
},
{
desc: "search users with empty query",
token: validToken,
query: "",
status: http.StatusBadRequest,
err: apiutil.ErrEmptySearchQuery,
},
{
desc: "search users with invalid length of query",
token: validToken,
query: "name=a",
status: http.StatusBadRequest,
err: apiutil.ErrLenSearchQuery,
},
}
for _, tc := range cases {
req := testRequest{
client: us.Client(),
method: http.MethodGet,
url: fmt.Sprintf("%s/users/search?", us.URL) + tc.query,
token: tc.token,
}
svcCall := svc.On("SearchUsers", mock.Anything, tc.token, mock.Anything).Return(
mgclients.ClientsPage{
Page: tc.listUsersResponse.Page,
Clients: 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))
svcCall.Unset()
}
}
func TestUpdateClient(t *testing.T) {
us, svc, _ := newUsersServer()
defer us.Close()
+36
View File
@@ -104,6 +104,42 @@ func listClientsEndpoint(svc users.Service) endpoint.Endpoint {
}
}
func searchClientsEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(searchClientsReq)
if err := req.validate(); err != nil {
return nil, errors.Wrap(apiutil.ErrValidation, err)
}
pm := mgclients.Page{
Offset: req.Offset,
Limit: req.Limit,
Name: req.Name,
Id: req.Id,
Order: req.Order,
Dir: req.Dir,
}
page, err := svc.SearchUsers(ctx, req.token, pm)
if err != nil {
return nil, err
}
res := clientsPageRes{
pageRes: pageRes{
Total: page.Total,
Offset: page.Offset,
Limit: page.Limit,
},
Clients: []viewClientRes{},
}
for _, client := range page.Clients {
res.Clients = append(res.Clients, viewClientRes{Client: client})
}
return res, nil
}
}
func listMembersByGroupEndpoint(svc users.Service) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
req := request.(listMembersByObjectReq)
+21
View File
@@ -152,6 +152,27 @@ func (lm *loggingMiddleware) ListClients(ctx context.Context, token string, pm m
return lm.svc.ListClients(ctx, token, pm)
}
// SearchUsers logs the search_users request. It logs the page metadata and the time it took to complete the request.
func (lm *loggingMiddleware) SearchUsers(ctx context.Context, token string, cp mgclients.Page) (mp mgclients.ClientsPage, err error) {
defer func(begin time.Time) {
args := []any{
slog.String("duration", time.Since(begin).String()),
slog.Group("page",
slog.Uint64("limit", cp.Limit),
slog.Uint64("offset", cp.Offset),
slog.Uint64("total", mp.Total),
),
}
if err != nil {
args = append(args, slog.Any("error", err))
lm.logger.Warn("Search clients failed to complete successfully", args...)
return
}
lm.logger.Info("Search clients completed successfully", args...)
}(time.Now())
return lm.svc.SearchUsers(ctx, token, cp)
}
// UpdateClient logs the update_client request. It logs the client id and the time it took to complete the request.
// If the request fails, it logs the error.
func (lm *loggingMiddleware) UpdateClient(ctx context.Context, token string, client mgclients.Client) (c mgclients.Client, err error) {
+9
View File
@@ -84,6 +84,15 @@ func (ms *metricsMiddleware) ListClients(ctx context.Context, token string, pm m
return ms.svc.ListClients(ctx, token, pm)
}
// SearchUsers instruments SearchClients method with metrics.
func (ms *metricsMiddleware) SearchUsers(ctx context.Context, token string, pm mgclients.Page) (mp mgclients.ClientsPage, err error) {
defer func(begin time.Time) {
ms.counter.With("method", "search_users").Add(1)
ms.latency.With("method", "search_users").Observe(time.Since(begin).Seconds())
}(time.Now())
return ms.svc.SearchUsers(ctx, token, pm)
}
// UpdateClient instruments UpdateClient method with metrics.
func (ms *metricsMiddleware) UpdateClient(ctx context.Context, token string, client mgclients.Client) (mgclients.Client, error) {
defer func(begin time.Time) {
+22
View File
@@ -89,6 +89,28 @@ func (req listClientsReq) validate() error {
return nil
}
type searchClientsReq struct {
token string
Offset uint64
Limit uint64
Name string
Id string
Order string
Dir string
}
func (req searchClientsReq) validate() error {
if req.token == "" {
return apiutil.ErrBearerToken
}
if req.Name == "" && req.Id == "" {
return apiutil.ErrEmptySearchQuery
}
return nil
}
type listMembersByObjectReq struct {
mgclients.Page
token string
+36
View File
@@ -237,6 +237,42 @@ func TestListClientsReqValidate(t *testing.T) {
}
}
func TestSearchClientsReqValidate(t *testing.T) {
cases := []struct {
desc string
req searchClientsReq
err error
}{
{
desc: "valid request",
req: searchClientsReq{
token: valid,
Name: name,
},
err: nil,
},
{
desc: "empty token",
req: searchClientsReq{
token: "",
Name: name,
},
err: apiutil.ErrBearerToken,
},
{
desc: "empty query",
req: searchClientsReq{
token: valid,
},
err: apiutil.ErrEmptySearchQuery,
},
}
for _, c := range cases {
err := c.req.validate()
assert.Equal(t, c.err, err)
}
}
func TestListMembersByObjectReqValidate(t *testing.T) {
cases := []struct {
desc string
+3
View File
@@ -31,6 +31,9 @@ type Service interface {
// ListMembers retrieves everything that is assigned to a group/thing identified by objectID.
ListMembers(ctx context.Context, token, objectKind, objectID string, pm clients.Page) (clients.MembersPage, error)
// SearchClients searches for users with provided filters for a valid auth token.
SearchUsers(ctx context.Context, token string, pm clients.Page) (clients.ClientsPage, error)
// UpdateClient updates the client's name and metadata.
UpdateClient(ctx context.Context, token string, client clients.Client) (clients.Client, error)
+26
View File
@@ -18,6 +18,7 @@ const (
clientView = clientPrefix + "view"
profileView = clientPrefix + "view_profile"
clientList = clientPrefix + "list"
clientSearch = clientPrefix + "search"
clientListByGroup = clientPrefix + "list_by_group"
clientIdentify = clientPrefix + "identify"
generateResetToken = clientPrefix + "generate_reset_token"
@@ -37,6 +38,7 @@ var (
_ events.Event = (*viewProfileEvent)(nil)
_ events.Event = (*listClientEvent)(nil)
_ events.Event = (*listClientByGroupEvent)(nil)
_ events.Event = (*searchClientEvent)(nil)
_ events.Event = (*identifyClientEvent)(nil)
_ events.Event = (*generateResetTokenEvent)(nil)
_ events.Event = (*issueTokenEvent)(nil)
@@ -307,6 +309,30 @@ func (lcge listClientByGroupEvent) Encode() (map[string]interface{}, error) {
return val, nil
}
type searchClientEvent struct {
mgclients.Page
}
func (sce searchClientEvent) Encode() (map[string]interface{}, error) {
val := map[string]interface{}{
"operation": clientSearch,
"total": sce.Total,
"offset": sce.Offset,
"limit": sce.Limit,
}
if sce.Name != "" {
val["name"] = sce.Name
}
if sce.Identity != "" {
val["identity"] = sce.Identity
}
if sce.Id != "" {
val["id"] = sce.Id
}
return val, nil
}
type identifyClientEvent struct {
userID string
}
+16
View File
@@ -160,6 +160,22 @@ func (es *eventStore) ListClients(ctx context.Context, token string, pm mgclient
return cp, nil
}
func (es *eventStore) SearchUsers(ctx context.Context, token string, pm mgclients.Page) (mgclients.ClientsPage, error) {
cp, err := es.svc.SearchUsers(ctx, token, pm)
if err != nil {
return cp, err
}
event := searchClientEvent{
pm,
}
if err := es.Publish(ctx, event); err != nil {
return cp, err
}
return cp, nil
}
func (es *eventStore) ListMembers(ctx context.Context, token, objectKind, objectID string, pm mgclients.Page) (mgclients.MembersPage, error) {
mp, err := es.svc.ListMembers(ctx, token, objectKind, objectID, pm)
if err != nil {
+28
View File
@@ -331,6 +331,34 @@ func (_m *Service) ResetSecret(ctx context.Context, resetToken string, secret st
return r0
}
// SearchUsers provides a mock function with given fields: ctx, token, pm
func (_m *Service) SearchUsers(ctx context.Context, token string, pm clients.Page) (clients.ClientsPage, error) {
ret := _m.Called(ctx, token, pm)
if len(ret) == 0 {
panic("no return value specified for SearchUsers")
}
var r0 clients.ClientsPage
var r1 error
if rf, ok := ret.Get(0).(func(context.Context, string, clients.Page) (clients.ClientsPage, error)); ok {
return rf(ctx, token, pm)
}
if rf, ok := ret.Get(0).(func(context.Context, string, clients.Page) clients.ClientsPage); ok {
r0 = rf(ctx, token, pm)
} else {
r0 = ret.Get(0).(clients.ClientsPage)
}
if rf, ok := ret.Get(1).(func(context.Context, string, clients.Page) error); ok {
r1 = rf(ctx, token, pm)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// SendPasswordReset provides a mock function with given fields: ctx, host, email, user, token
func (_m *Service) SendPasswordReset(ctx context.Context, host string, email string, user string, token string) error {
ret := _m.Called(ctx, host, email, user, token)
+25 -11
View File
@@ -180,24 +180,38 @@ func (svc service) ListClients(ctx context.Context, token string, pm mgclients.P
if err != nil {
return mgclients.ClientsPage{}, err
}
if err := svc.checkSuperAdmin(ctx, userID); err == nil {
pm.Role = mgclients.AllRole
pg, err := svc.clients.RetrieveAll(ctx, pm)
if err != nil {
return mgclients.ClientsPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
return pg, err
if err := svc.checkSuperAdmin(ctx, userID); err != nil {
return mgclients.ClientsPage{}, err
}
pg, err := svc.clients.SearchClients(ctx, pm)
pm.Role = mgclients.AllRole
pg, err := svc.clients.RetrieveAll(ctx, pm)
if err != nil {
return mgclients.ClientsPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
for i, c := range pg.Clients {
pg.Clients[i] = mgclients.Client{ID: c.ID, Name: c.Name}
return pg, err
}
func (svc service) SearchUsers(ctx context.Context, token string, pm mgclients.Page) (mgclients.ClientsPage, error) {
_, err := svc.Identify(ctx, token)
if err != nil {
return mgclients.ClientsPage{}, err
}
return pg, nil
page := mgclients.Page{
Offset: pm.Offset,
Limit: pm.Limit,
Name: pm.Name,
Id: pm.Id,
Role: mgclients.UserRole,
}
cp, err := svc.clients.SearchClients(ctx, page)
if err != nil {
return mgclients.ClientsPage{}, errors.Wrap(svcerr.ErrViewEntity, err)
}
return cp, nil
}
func (svc service) UpdateClient(ctx context.Context, token string, cli mgclients.Client) (mgclients.Client, error) {
+85 -27
View File
@@ -534,7 +534,7 @@ func TestListClients(t *testing.T) {
authorizeResponse: &magistrala.AuthorizeRes{Authorized: false},
token: validToken,
authorizeErr: svcerr.ErrAuthorization,
err: nil,
err: svcerr.ErrAuthorization,
},
{
desc: "list clients as admin with failed to retrieve clients",
@@ -557,29 +557,7 @@ func TestListClients(t *testing.T) {
authorizeResponse: &magistrala.AuthorizeRes{Authorized: false},
token: validToken,
superAdminErr: svcerr.ErrAuthorization,
err: nil,
},
{
desc: "list clients as normal user successfully",
page: mgclients.Page{
Total: 1,
},
identifyResponse: &magistrala.IdentityRes{UserId: client.ID},
authorizeResponse: &magistrala.AuthorizeRes{Authorized: false},
retrieveAllResponse: mgclients.ClientsPage{
Page: mgclients.Page{
Total: 1,
},
Clients: []mgclients.Client{basicClient},
},
response: mgclients.ClientsPage{
Page: mgclients.Page{
Total: 1,
},
Clients: []mgclients.Client{basicClient},
},
token: validToken,
err: nil,
err: svcerr.ErrAuthorization,
},
{
desc: "list clients as normal user with failed to retrieve clients",
@@ -591,7 +569,7 @@ func TestListClients(t *testing.T) {
retrieveAllResponse: mgclients.ClientsPage{},
token: validToken,
retrieveAllErr: repoerr.ErrNotFound,
err: svcerr.ErrViewEntity,
err: svcerr.ErrAuthorization,
},
}
@@ -600,7 +578,6 @@ func TestListClients(t *testing.T) {
authCall1 := auth.On("Authorize", context.Background(), mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr)
repoCall := cRepo.On("CheckSuperAdmin", context.Background(), mock.Anything).Return(tc.superAdminErr)
repoCall1 := cRepo.On("RetrieveAll", context.Background(), mock.Anything).Return(tc.retrieveAllResponse, tc.retrieveAllErr)
repoCall2 := cRepo.On("SearchClients", context.Background(), mock.Anything).Return(tc.response, tc.err)
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))
@@ -612,7 +589,88 @@ func TestListClients(t *testing.T) {
authCall1.Unset()
repoCall.Unset()
repoCall1.Unset()
repoCall2.Unset()
}
}
func TestSearchUsers(t *testing.T) {
svc, cRepo, auth, _ := newService(true)
cases := []struct {
desc string
token string
page mgclients.Page
identifyResp *magistrala.IdentityRes
authorizeResponse *magistrala.AuthorizeRes
response mgclients.ClientsPage
responseErr error
identifyErr error
authorizeErr error
checkSuperAdminErr error
err error
}{
{
desc: "search clients with valid token",
token: validToken,
page: mgclients.Page{Offset: 0, Name: "clientname", Limit: 100},
response: mgclients.ClientsPage{
Page: mgclients.Page{Total: 1, Offset: 0, Limit: 100},
Clients: []mgclients.Client{client},
},
identifyResp: &magistrala.IdentityRes{UserId: client.ID},
authorizeResponse: &magistrala.AuthorizeRes{Authorized: true},
},
{
desc: "search clients with invalid token",
token: inValidToken,
page: mgclients.Page{Offset: 0, Name: "clientname", Limit: 100},
response: mgclients.ClientsPage{},
responseErr: svcerr.ErrAuthentication,
err: svcerr.ErrAuthentication,
},
{
desc: "search clients with id",
token: validToken,
page: mgclients.Page{Offset: 0, Id: "d8dd12ef-aa2a-43fe-8ef2-2e4fe514360f", Limit: 100},
response: mgclients.ClientsPage{
Page: mgclients.Page{Total: 1, Offset: 0, Limit: 100},
Clients: []mgclients.Client{client},
},
identifyResp: &magistrala.IdentityRes{UserId: client.ID},
authorizeResponse: &magistrala.AuthorizeRes{Authorized: true},
},
{
desc: "search clients with random name",
token: validToken,
page: mgclients.Page{Offset: 0, Name: "randomname", Limit: 100},
response: mgclients.ClientsPage{
Page: mgclients.Page{Total: 0, Offset: 0, Limit: 100},
Clients: []mgclients.Client{},
},
identifyResp: &magistrala.IdentityRes{UserId: client.ID},
authorizeResponse: &magistrala.AuthorizeRes{Authorized: true},
},
{
desc: "search clients as a normal user",
token: validToken,
page: mgclients.Page{Offset: 0, Identity: "clientidentity", Limit: 100},
response: mgclients.ClientsPage{},
authorizeResponse: &magistrala.AuthorizeRes{Authorized: false},
checkSuperAdminErr: svcerr.ErrAuthorization,
responseErr: nil,
},
}
for _, tc := range cases {
authCall := auth.On("Identify", context.Background(), &magistrala.IdentityReq{Token: tc.token}).Return(tc.identifyResp, tc.identifyErr)
authCall1 := auth.On("Authorize", context.Background(), mock.Anything).Return(tc.authorizeResponse, tc.authorizeErr)
repoCall := cRepo.On("CheckSuperAdmin", context.Background(), mock.Anything).Return(tc.checkSuperAdminErr)
repoCall1 := cRepo.On("SearchClients", context.Background(), mock.Anything).Return(tc.response, tc.responseErr)
page, err := svc.SearchUsers(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))
authCall.Unset()
authCall1.Unset()
repoCall.Unset()
repoCall1.Unset()
}
}
+8
View File
@@ -71,6 +71,14 @@ func (tm *tracingMiddleware) ListClients(ctx context.Context, token string, pm m
return tm.svc.ListClients(ctx, token, pm)
}
// SearchUsers traces the "SearchUsers" operation of the wrapped clients.Service.
func (tm *tracingMiddleware) SearchUsers(ctx context.Context, token string, pm mgclients.Page) (mgclients.ClientsPage, error) {
ctx, span := tm.tracer.Start(ctx, "svc_search_clients", trace.WithAttributes(attribute.String("token", token)))
defer span.End()
return tm.svc.SearchUsers(ctx, token, pm)
}
// UpdateClient traces the "UpdateClient" operation of the wrapped clients.Service.
func (tm *tracingMiddleware) UpdateClient(ctx context.Context, token string, cli mgclients.Client) (mgclients.Client, error) {
ctx, span := tm.tracer.Start(ctx, "svc_update_client_name_and_metadata", trace.WithAttributes(