Skip to content

Commit

Permalink
Patch User (#444)
Browse files Browse the repository at this point in the history
  • Loading branch information
nmacianx authored Jul 3, 2024
1 parent 51e8227 commit bff4957
Show file tree
Hide file tree
Showing 6 changed files with 270 additions and 0 deletions.
10 changes: 10 additions & 0 deletions descope/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ var (
userCreate: "mgmt/user/create",
userCreateBatch: "mgmt/user/create/batch",
userUpdate: "mgmt/user/update",
userPatch: "mgmt/user/patch",
userDelete: "mgmt/user/delete",
userDeleteAllTestUsers: "mgmt/user/test/delete/all",
userImport: "mgmt/user/import",
Expand Down Expand Up @@ -291,6 +292,7 @@ type mgmtEndpoints struct {
userCreate string
userCreateBatch string
userUpdate string
userPatch string
userDelete string
userDeleteAllTestUsers string
userImport string
Expand Down Expand Up @@ -691,6 +693,10 @@ func (e *endpoints) ManagementUserUpdate() string {
return path.Join(e.version, e.mgmt.userUpdate)
}

func (e *endpoints) ManagementUserPatch() string {
return path.Join(e.version, e.mgmt.userPatch)
}

func (e *endpoints) ManagementUserDelete() string {
return path.Join(e.version, e.mgmt.userDelete)
}
Expand Down Expand Up @@ -1191,6 +1197,10 @@ func (c *Client) DoPostRequest(ctx context.Context, uri string, body interface{}
return c.doRequestWithBody(ctx, http.MethodPost, uri, body, options, pswd)
}

func (c *Client) DoPatchRequest(ctx context.Context, uri string, body interface{}, options *HTTPRequest, pswd string) (*HTTPResponse, error) {
return c.doRequestWithBody(ctx, http.MethodPatch, uri, body, options, pswd)
}

func (c *Client) doRequestWithBody(ctx context.Context, method string, uri string, body interface{}, options *HTTPRequest, pswd string) (*HTTPResponse, error) {
if options == nil {
options = &HTTPRequest{}
Expand Down
61 changes: 61 additions & 0 deletions descope/internal/mgmt/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,21 @@ func (u *user) Update(ctx context.Context, loginID string, user *descope.UserReq
return unmarshalUserResponse(res)
}

func (u *user) Patch(ctx context.Context, loginID string, user *descope.PatchUserRequest) (*descope.UserResponse, error) {
if loginID == "" {
return nil, utils.NewInvalidArgumentError("loginID")
}
if user == nil {
return nil, utils.NewInvalidArgumentError("user")
}
req := makePatchUserRequest(loginID, user)
res, err := u.client.DoPatchRequest(ctx, api.Routes.ManagementUserPatch(), req, nil, u.conf.ManagementKey)
if err != nil {
return nil, err
}
return unmarshalUserResponse(res)
}

func (u *user) Delete(ctx context.Context, loginID string) error {
if loginID == "" {
return utils.NewInvalidArgumentError("loginID")
Expand Down Expand Up @@ -759,6 +774,52 @@ func makeUpdateUserRequest(req *createUserRequest) map[string]any {
return res
}

func makePatchUserRequest(loginID string, req *descope.PatchUserRequest) map[string]any {
res := map[string]interface{}{
"loginId": loginID,
}
if req.Name != nil {
res["name"] = *req.Name
}
if req.GivenName != nil {
res["givenName"] = *req.GivenName
}
if req.MiddleName != nil {
res["middleName"] = *req.MiddleName
}
if req.FamilyName != nil {
res["familyName"] = *req.FamilyName
}
if req.Phone != nil {
res["phone"] = *req.Phone
}
if req.Email != nil {
res["email"] = *req.Email
}
if req.Roles != nil {
res["roleNames"] = *req.Roles
}
if req.Tenants != nil {
res["userTenants"] = makeAssociatedTenantList(*req.Tenants)
}
if req.CustomAttributes != nil {
res["customAttributes"] = req.CustomAttributes
}
if req.Picture != nil {
res["picture"] = *req.Picture
}
if req.VerifiedEmail != nil {
res["verifiedEmail"] = *req.VerifiedEmail
}
if req.VerifiedPhone != nil {
res["verifiedPhone"] = *req.VerifiedPhone
}
if req.SSOAppIDs != nil {
res["ssoAppIds"] = *req.SSOAppIDs
}
return res
}

func makeUpdateUserTenantRequest(loginID, tenantID string) map[string]any {
return map[string]any{
"loginId": loginID,
Expand Down
166 changes: 166 additions & 0 deletions descope/internal/mgmt/user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,172 @@ func TestUserUpdateError(t *testing.T) {
require.Error(t, err)
}

func TestUserPatchError(t *testing.T) {
m := newTestMgmt(nil, helpers.DoOk(nil))
user := &descope.PatchUserRequest{}
email := "[email protected]"
user.Email = &email
_, err := m.User().Patch(context.Background(), "", user)
require.Error(t, err)
_, err = m.User().Patch(context.Background(), "abc", nil)
require.Error(t, err)
}

func TestUserPatchSuccess(t *testing.T) {
response := map[string]any{
"user": map[string]any{
"name": "name1",
"middleName": "middleName1",
"phone": "+9724567890",
"verifiedPhone": true,
"picture": "https://test.com",
"roleNames": []string{"foo", "bar"},
}}
m := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) {
require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key")
req := map[string]any{}
require.NoError(t, helpers.ReadBody(r, &req))
require.Equal(t, "abc", req["loginId"])

require.Equal(t, "name1", req["name"])
require.Equal(t, "middleName1", req["middleName"])
require.Equal(t, "+9724567890", req["phone"])
reqVerifiedPhone := req["verifiedPhone"].(bool)
require.True(t, reqVerifiedPhone)
require.Equal(t, "https://test.com", req["picture"])
roles := req["roleNames"].([]any)
require.EqualValues(t, []any{"foo", "bar"}, roles)

_, ok := req["givenName"]
require.False(t, ok)
_, ok = req["familyName"]
require.False(t, ok)
_, ok = req["email"]
require.False(t, ok)
_, ok = req["userTenants"]
require.False(t, ok)
_, ok = req["customAttributes"]
require.False(t, ok)
_, ok = req["verifiedEmail"]
require.False(t, ok)
_, ok = req["ssoAppIds"]
require.False(t, ok)
}, response))
user := &descope.PatchUserRequest{}
patchedName := "name1"
patchedMiddleName := "middleName1"
patchedPhone := "+9724567890"
patchedVerifiedPhone := true
patchedPicture := "https://test.com"
patchedRoles := []string{"foo", "bar"}
user.Name = &patchedName
user.MiddleName = &patchedMiddleName
user.Phone = &patchedPhone
user.VerifiedPhone = &patchedVerifiedPhone
user.Picture = &patchedPicture
user.Roles = &patchedRoles
res, err := m.User().Patch(context.Background(), "abc", user)
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, "name1", res.Name)
require.Equal(t, "middleName1", res.MiddleName)
require.Equal(t, "+9724567890", res.Phone)
require.True(t, res.VerifiedPhone)
require.Equal(t, "https://test.com", res.Picture)
require.EqualValues(t, patchedRoles, res.RoleNames)
}

func TestUserPatchSuccess2(t *testing.T) {
response := map[string]any{
"user": map[string]any{
"givenName": "givenName1",
"familyName": "familyName1",
"email": "[email protected]",
"verifiedEmail": true,
"customAttributes": map[string]interface{}{
"ca1": "cavalue1",
},
"ssoAppIds": []string{"app1", "app2"},
}}
m := newTestMgmt(nil, helpers.DoOkWithBody(func(r *http.Request) {
require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key")
req := map[string]any{}
require.NoError(t, helpers.ReadBody(r, &req))
require.Equal(t, "abc", req["loginId"])

require.Equal(t, "givenName1", req["givenName"])
require.Equal(t, "familyName1", req["familyName"])
require.Equal(t, "[email protected]", req["email"])
reqVerifiedEmail := req["verifiedEmail"].(bool)
require.True(t, reqVerifiedEmail)
require.EqualValues(t, map[string]interface{}{
"ca1": "cavalue1",
}, req["customAttributes"])
ssoAppIDs := req["ssoAppIds"].([]any)
require.EqualValues(t, []any{"app1", "app2"}, ssoAppIDs)
userTenants := req["userTenants"].([]any)
require.Len(t, userTenants, 2)
for i := range userTenants {
tenant := userTenants[i].(map[string]any)
roleNames := tenant["roleNames"].([]any)
if i == 0 {
require.Equal(t, "t1", tenant["tenantId"])
require.Len(t, roleNames, 2)
require.Equal(t, "foo", roleNames[0])
require.Equal(t, "foo2", roleNames[1])
} else {
require.Equal(t, "t2", tenant["tenantId"])
require.Len(t, roleNames, 1)
require.Equal(t, "bar", roleNames[0])
}
}

_, ok := req["name"]
require.False(t, ok)
_, ok = req["middleName"]
require.False(t, ok)
_, ok = req["phone"]
require.False(t, ok)
_, ok = req["picture"]
require.False(t, ok)
_, ok = req["verifiedPhone"]
require.False(t, ok)
_, ok = req["roleNames"]
require.False(t, ok)
}, response))
user := &descope.PatchUserRequest{}
patchedGivenName := "givenName1"
patchedFamilyName := "familyName1"
patchedEmail := "[email protected]"
patchedVerifiedEmail := true
patchedCustomAttributes := map[string]interface{}{
"ca1": "cavalue1",
}
patchedTenants := []*descope.AssociatedTenant{
{TenantID: "t1", Roles: []string{"foo", "foo2"}},
{TenantID: "t2", Roles: []string{"bar"}},
}
patchedSSOAppIDs := []string{"app1", "app2"}
user.GivenName = &patchedGivenName
user.FamilyName = &patchedFamilyName
user.Email = &patchedEmail
user.VerifiedEmail = &patchedVerifiedEmail
user.CustomAttributes = patchedCustomAttributes
user.Tenants = &patchedTenants
user.SSOAppIDs = &patchedSSOAppIDs
res, err := m.User().Patch(context.Background(), "abc", user)
require.NoError(t, err)
require.NotNil(t, res)
require.Equal(t, "givenName1", res.GivenName)
require.Equal(t, "familyName1", res.FamilyName)
require.Equal(t, "[email protected]", res.Email)
require.True(t, res.VerifiedEmail)
require.EqualValues(t, map[string]interface{}{
"ca1": "cavalue1",
}, res.CustomAttributes)
require.EqualValues(t, []string{"app1", "app2"}, res.SSOAppIDs)
}

func TestUserDeleteSuccess(t *testing.T) {
m := newTestMgmt(nil, helpers.DoOk(func(r *http.Request) {
require.Equal(t, r.Header.Get("Authorization"), "Bearer a:key")
Expand Down
6 changes: 6 additions & 0 deletions descope/sdk/mgmt.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,14 @@ type User interface {
//
// IMPORTANT: All parameters will override whatever values are currently set
// in the existing user. Use carefully.
// Instead, use Patch if you don't want to pass all parameters.
Update(ctx context.Context, loginID string, user *descope.UserRequest) (*descope.UserResponse, error)

// Patches an existing user.
//
// Only the fields that are set in the request will be updated.
Patch(ctx context.Context, loginID string, user *descope.PatchUserRequest) (*descope.UserResponse, error)

// Delete an existing user.
//
// IMPORTANT: This action is irreversible. Use carefully.
Expand Down
11 changes: 11 additions & 0 deletions descope/tests/mocks/mgmt/managementmock.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,10 @@ type MockUser struct {
UpdateResponse *descope.UserResponse
UpdateError error

PatchAssert func(loginID string, user *descope.PatchUserRequest)
PatchResponse *descope.UserResponse
PatchError error

DeleteAssert func(loginID string)
DeleteError error

Expand Down Expand Up @@ -434,6 +438,13 @@ func (m *MockUser) Update(_ context.Context, loginID string, user *descope.UserR
return m.UpdateResponse, m.UpdateError
}

func (m *MockUser) Patch(_ context.Context, loginID string, user *descope.PatchUserRequest) (*descope.UserResponse, error) {
if m.PatchAssert != nil {
m.PatchAssert(loginID, user)
}
return m.PatchResponse, m.PatchError
}

func (m *MockUser) Delete(_ context.Context, loginID string) error {
if m.DeleteAssert != nil {
m.DeleteAssert(loginID)
Expand Down
16 changes: 16 additions & 0 deletions descope/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,22 @@ type UserRequest struct {
SSOAppIDs []string `json:"ssoAppIDs,omitempty"`
}

type PatchUserRequest struct {
Name *string `json:"name,omitempty"`
GivenName *string `json:"givenName,omitempty"`
MiddleName *string `json:"middleName,omitempty"`
FamilyName *string `json:"familyName,omitempty"`
Phone *string `json:"phone,omitempty"`
Email *string `json:"email,omitempty"`
Roles *[]string `json:"roles,omitempty"`
Tenants *[]*AssociatedTenant `json:"tenants,omitempty"`
CustomAttributes map[string]any `json:"customAttributes,omitempty"`
Picture *string `json:"picture,omitempty"`
VerifiedEmail *bool `json:"verifiedEmail,omitempty"`
VerifiedPhone *bool `json:"verifiedPhone,omitempty"`
SSOAppIDs *[]string `json:"ssoAppIds,omitempty"`
}

type BatchUser struct {
LoginID string `json:"loginId,omitempty"`
Password *BatchUserPassword `json:"password,omitempty"`
Expand Down

0 comments on commit bff4957

Please sign in to comment.