Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[SDP-974] data/httphandler/validators: Adding sorting to GET /users endpoint. #104

Merged
merged 10 commits into from
Nov 29, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Add the ability to filter supported assets by wallets. [#35](https://github.com/stellar/stellar-disbursement-platform-backend/pull/35)
- Add wallets filtering by `enabled` flag [#72](https://github.com/stellar/stellar-disbursement-platform-backend/pull/72)
- Return SMS templates in `GET /organization` endpoint. [#63](https://github.com/stellar/stellar-disbursement-platform-backend/pull/63)
- Add sorting to `GET /users` endpoint [#104](https://github.com/stellar/stellar-disbursement-platform-backend/pull/104)
ceciliaromao marked this conversation as resolved.
Show resolved Hide resolved

### Fixed

Expand Down
2 changes: 2 additions & 0 deletions internal/data/query_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ type SortField string

const (
SortFieldName SortField = "name"
SortFieldEmail SortField = "email"
SortFieldIsActive SortField = "is_active"
SortFieldCreatedAt SortField = "created_at"
SortFieldUpdatedAt SortField = "updated_at"
)
Expand Down
9 changes: 8 additions & 1 deletion internal/serve/httphandler/user_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,13 @@ func (h UserHandler) UpdateUserRoles(rw http.ResponseWriter, req *http.Request)
}

func (h UserHandler) GetAllUsers(rw http.ResponseWriter, req *http.Request) {
validator := validators.NewUserQueryValidator()
queryParams := validator.ParseParametersFromRequest(req)
if validator.HasErrors() {
httperror.BadRequest("request invalid", nil, validator.Errors).Render(rw)
return
}

ctx := req.Context()

token, ok := ctx.Value(middleware.TokenContextKey).(string)
Expand All @@ -284,7 +291,7 @@ func (h UserHandler) GetAllUsers(rw http.ResponseWriter, req *http.Request) {
return
}

users, err := h.AuthManager.GetAllUsers(ctx, token)
users, err := h.AuthManager.GetAllUsers(ctx, token, queryParams)
ceciliaromao marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
if errors.Is(err, auth.ErrInvalidToken) {
httperror.Unauthorized("", err, nil).Render(rw)
Expand Down
9 changes: 8 additions & 1 deletion internal/serve/httphandler/user_handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1246,7 +1246,14 @@ func Test_UserHandler_GetAllUsers(t *testing.T) {
Once()

authenticatorMock.
On("GetAllUsers", req.Context()).
On("GetAllUsers", req.Context(), &data.QueryParams{
Query: "",
Page: 1,
PageLimit: 20,
SortBy: data.SortFieldEmail,
SortOrder: data.SortOrderASC,
Filters: map[data.FilterKey]interface{}{},
}).
Return([]auth.User{
{
ID: "user1-ID",
Expand Down
21 changes: 21 additions & 0 deletions internal/serve/validators/user_query_validator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package validators

import (
"github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/pkg/auth"
)

type UserQueryValidator struct {
QueryValidator
}

// NewUserQueryValidator creates a new UserQueryValidator with the provided configuration.
func NewUserQueryValidator() *UserQueryValidator {
return &UserQueryValidator{
QueryValidator: QueryValidator{
Validator: NewValidator(),
DefaultSortField: auth.DefaultUserSortField,
DefaultSortOrder: auth.DefaultUserSortOrder,
AllowedSortFields: auth.AllowedUserSorts,
},
}
}
7 changes: 4 additions & 3 deletions stellar-auth/pkg/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"time"

"github.com/stellar/stellar-disbursement-platform-backend/internal/data"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/internal/db"
)

Expand All @@ -25,7 +26,7 @@ type AuthManager interface {
UpdatePassword(ctx context.Context, token, currentPassword, newPassword string) error
GetUser(ctx context.Context, tokenString string) (*User, error)
GetUserID(ctx context.Context, tokenString string) (string, error)
GetAllUsers(ctx context.Context, tokenString string) ([]User, error)
GetAllUsers(ctx context.Context, tokenString string, queryParams *data.QueryParams) ([]User, error)
ceciliaromao marked this conversation as resolved.
Show resolved Hide resolved
UpdateUserRoles(ctx context.Context, tokenString, userID string, roles []string) error
DeactivateUser(ctx context.Context, tokenString, userID string) error
ActivateUser(ctx context.Context, tokenString, userID string) error
Expand Down Expand Up @@ -283,7 +284,7 @@ func (am *defaultAuthManager) UpdateUserRoles(ctx context.Context, tokenString,
return nil
}

func (am *defaultAuthManager) GetAllUsers(ctx context.Context, tokenString string) ([]User, error) {
func (am *defaultAuthManager) GetAllUsers(ctx context.Context, tokenString string, queryParams *data.QueryParams) ([]User, error) {
isValid, err := am.ValidateToken(ctx, tokenString)
if err != nil {
return nil, fmt.Errorf("validating token: %w", err)
Expand All @@ -293,7 +294,7 @@ func (am *defaultAuthManager) GetAllUsers(ctx context.Context, tokenString strin
return nil, ErrInvalidToken
}

users, err := am.authenticator.GetAllUsers(ctx)
users, err := am.authenticator.GetAllUsers(ctx, queryParams)
if err != nil {
return nil, fmt.Errorf("error getting all users: %w", err)
}
Expand Down
41 changes: 33 additions & 8 deletions stellar-auth/pkg/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"time"

"github.com/stellar/stellar-disbursement-platform-backend/internal/data"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -1108,7 +1109,10 @@ func Test_AuthManager_GetAllUsers(t *testing.T) {
Return(false, errUnexpectedError).
Once()

users, err := authManager.GetAllUsers(ctx, token)
users, err := authManager.GetAllUsers(ctx, token, &data.QueryParams{
SortBy: data.SortFieldEmail,
SortOrder: data.SortOrderASC,
})

assert.Nil(t, users)
assert.EqualError(t, err, "validating token: validating token: unexpected error")
Expand All @@ -1122,7 +1126,10 @@ func Test_AuthManager_GetAllUsers(t *testing.T) {
Return(false, nil).
Once()

users, err := authManager.GetAllUsers(ctx, token)
users, err := authManager.GetAllUsers(ctx, token, &data.QueryParams{
SortBy: data.SortFieldEmail,
SortOrder: data.SortOrderASC,
})

assert.Nil(t, users)
assert.EqualError(t, err, "invalid token")
Expand All @@ -1137,11 +1144,17 @@ func Test_AuthManager_GetAllUsers(t *testing.T) {
Once()

authenticatorMock.
On("GetAllUsers", ctx).
On("GetAllUsers", ctx, &data.QueryParams{
SortBy: data.SortFieldEmail,
SortOrder: data.SortOrderASC,
}).
Return(nil, errUnexpectedError).
Once()

users, err := authManager.GetAllUsers(ctx, token)
users, err := authManager.GetAllUsers(ctx, token, &data.QueryParams{
SortBy: data.SortFieldEmail,
SortOrder: data.SortOrderASC,
})

assert.EqualError(t, err, "error getting all users: unexpected error")
assert.Nil(t, users)
Expand All @@ -1156,11 +1169,17 @@ func Test_AuthManager_GetAllUsers(t *testing.T) {
Twice()

authenticatorMock.
On("GetAllUsers", ctx).
On("GetAllUsers", ctx, &data.QueryParams{
SortBy: data.SortFieldEmail,
SortOrder: data.SortOrderASC,
}).
Return([]User{}, nil).
Once()

users, err := authManager.GetAllUsers(ctx, token)
users, err := authManager.GetAllUsers(ctx, token, &data.QueryParams{
SortBy: data.SortFieldEmail,
SortOrder: data.SortOrderASC,
})
require.NoError(t, err)
assert.Empty(t, users)

Expand All @@ -1186,11 +1205,17 @@ func Test_AuthManager_GetAllUsers(t *testing.T) {
}

authenticatorMock.
On("GetAllUsers", ctx).
On("GetAllUsers", ctx, &data.QueryParams{
SortBy: data.SortFieldEmail,
SortOrder: data.SortOrderASC,
}).
Return(expectedUsers, nil).
Once()

users, err = authManager.GetAllUsers(ctx, token)
users, err = authManager.GetAllUsers(ctx, token, &data.QueryParams{
SortBy: data.SortFieldEmail,
SortOrder: data.SortOrderASC,
})
require.NoError(t, err)
assert.Equal(t, expectedUsers, users)
})
Expand Down
17 changes: 13 additions & 4 deletions stellar-auth/pkg/auth/authenticator.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

"github.com/lib/pq"
"github.com/stellar/stellar-disbursement-platform-backend/internal/data"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/internal/db"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/pkg/utils"
)
Expand All @@ -28,6 +29,12 @@ const (
resetTokenLength = 10
)

var (
DefaultUserSortField = data.SortFieldEmail
DefaultUserSortOrder = data.SortOrderASC
AllowedUserSorts = []data.SortField{data.SortFieldEmail, data.SortFieldIsActive}
)
ceciliaromao marked this conversation as resolved.
Show resolved Hide resolved

type Authenticator interface {
ValidateCredentials(ctx context.Context, email, password string) (*User, error)
// CreateUser creates a new user it receives a user object and the password
Expand All @@ -38,7 +45,7 @@ type Authenticator interface {
ForgotPassword(ctx context.Context, email string) (string, error)
ResetPassword(ctx context.Context, resetToken, password string) error
UpdatePassword(ctx context.Context, user *User, currentPassword, newPassword string) error
GetAllUsers(ctx context.Context) ([]User, error)
GetAllUsers(ctx context.Context, queryParams *data.QueryParams) ([]User, error)
GetUser(ctx context.Context, userID string) (*User, error)
}

Expand Down Expand Up @@ -404,8 +411,8 @@ func (a *defaultAuthenticator) invalidateResetPasswordToken(ctx context.Context,
return nil
}

func (a *defaultAuthenticator) GetAllUsers(ctx context.Context) ([]User, error) {
const query = `
func (a *defaultAuthenticator) GetAllUsers(ctx context.Context, queryParams *data.QueryParams) ([]User, error) {
query := fmt.Sprintf(`
SELECT
id,
first_name,
Expand All @@ -416,7 +423,8 @@ func (a *defaultAuthenticator) GetAllUsers(ctx context.Context) ([]User, error)
is_active
FROM
auth_users
`
ORDER BY %s %s
`, queryParams.SortBy, queryParams.SortOrder)

dbUsers := []struct {
ID string `db:"id"`
Expand All @@ -429,6 +437,7 @@ func (a *defaultAuthenticator) GetAllUsers(ctx context.Context) ([]User, error)
}{}
err := a.dbConnectionPool.SelectContext(ctx, &dbUsers, query)
if err != nil {
fmt.Println(queryParams)
ceciliaromao marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("error querying all users in the database: %w", err)
}

Expand Down
20 changes: 17 additions & 3 deletions stellar-auth/pkg/auth/authenticator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import (
"context"
"database/sql"
"errors"
"sort"
"testing"
"time"

"github.com/stellar/stellar-disbursement-platform-backend/internal/data"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/internal/db"
"github.com/stellar/stellar-disbursement-platform-backend/stellar-auth/internal/db/dbtest"
"github.com/stretchr/testify/assert"
Expand All @@ -16,6 +18,12 @@ import (

var errUnexpectedError = errors.New("unexpected error")

type UserSorter []User

func (a UserSorter) Len() int { return len(a) }
func (a UserSorter) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a UserSorter) Less(i, j int) bool { return a[i].Email < a[j].Email }
ceciliaromao marked this conversation as resolved.
Show resolved Hide resolved

func assertUserIsActive(t *testing.T, ctx context.Context, dbConnectionPool db.DBConnectionPool, userID string, expectedIsActive bool) {
const query = "SELECT is_active FROM auth_users WHERE id = $1"

Expand Down Expand Up @@ -612,7 +620,10 @@ func Test_DefaultAuthenticator_GetAllUsers(t *testing.T) {
ctx := context.Background()

t.Run("returns an empty array if no users are registered", func(t *testing.T) {
users, err := authenticator.GetAllUsers(ctx)
users, err := authenticator.GetAllUsers(ctx, &data.QueryParams{
SortBy: data.SortFieldEmail,
SortOrder: data.SortOrderASC,
})
require.NoError(t, err)

assert.Empty(t, users)
Expand All @@ -627,15 +638,18 @@ func Test_DefaultAuthenticator_GetAllUsers(t *testing.T) {
randUser2 := CreateRandomAuthUserFixture(t, ctx, dbConnectionPool, passwordEncrypterMock, true, "role1", "role2")
randUser3 := CreateRandomAuthUserFixture(t, ctx, dbConnectionPool, passwordEncrypterMock, false, "role3")

users, err := authenticator.GetAllUsers(ctx)
users, err := authenticator.GetAllUsers(ctx, &data.QueryParams{
SortBy: data.SortFieldEmail,
SortOrder: data.SortOrderASC,
})
require.NoError(t, err)

expectedUsers := []User{
*randUser1.ToUser(),
*randUser2.ToUser(),
*randUser3.ToUser(),
}

sort.Sort(UserSorter(expectedUsers))
assert.Equal(t, expectedUsers, users)
})

Expand Down
9 changes: 5 additions & 4 deletions stellar-auth/pkg/auth/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"time"

"github.com/stellar/stellar-disbursement-platform-backend/internal/data"
"github.com/stretchr/testify/mock"
)

Expand Down Expand Up @@ -105,8 +106,8 @@ func (am *AuthenticatorMock) UpdatePassword(ctx context.Context, user *User, cur
return args.Error(0)
}

func (am *AuthenticatorMock) GetAllUsers(ctx context.Context) ([]User, error) {
args := am.Called(ctx)
func (am *AuthenticatorMock) GetAllUsers(ctx context.Context, queryParams *data.QueryParams) ([]User, error) {
args := am.Called(ctx, queryParams)
if args.Get(0) == nil {
return nil, args.Error(1)
}
Expand Down Expand Up @@ -255,8 +256,8 @@ func (am *AuthManagerMock) GetUserID(ctx context.Context, tokenString string) (s
return args.Get(0).(string), args.Error(1)
}

func (am *AuthManagerMock) GetAllUsers(ctx context.Context, tokenString string) ([]User, error) {
args := am.Called(ctx, tokenString)
func (am *AuthManagerMock) GetAllUsers(ctx context.Context, tokenString string, queryParams *data.QueryParams) ([]User, error) {
args := am.Called(ctx, tokenString, queryParams)
if args.Get(0) == nil {
return nil, args.Error(1)
}
Expand Down
Loading