From 60e9c50d6e36aaed860afdeac72fae79fc410fbc Mon Sep 17 00:00:00 2001 From: Roman Mingazeev Date: Thu, 12 Nov 2020 20:33:53 +0300 Subject: [PATCH 1/3] support user model and listing groups --- teamcity/group.go | 37 ++++++-- teamcity/group_test.go | 54 ++++++++++++ teamcity/locator.go | 7 +- teamcity/rest_helper.go | 1 + teamcity/teamcity.go | 2 + teamcity/user.go | 183 ++++++++++++++++++++++++++++++++++++++++ teamcity/user_test.go | 157 ++++++++++++++++++++++++++++++++++ 7 files changed, 435 insertions(+), 6 deletions(-) create mode 100644 teamcity/user.go create mode 100644 teamcity/user_test.go diff --git a/teamcity/group.go b/teamcity/group.go index 87d35d0..06fafd1 100644 --- a/teamcity/group.go +++ b/teamcity/group.go @@ -9,9 +9,18 @@ import ( // Group is the model for group entities in TeamCity type Group struct { - Key string `json:"key,omitempty" xml:"key"` - Description string `json:"description,omitempty" xml:"description"` - Name string `json:"name,omitempty" xml:"name"` + Key string `json:"key,omitempty" xml:"key"` + Description string `json:"description,omitempty" xml:"description"` + Name string `json:"name,omitempty" xml:"name"` + Users *UserList `json:"users,omitempty" xml:"users"` + Roles *roleAssignmentsJSON `json:"roles,omitempty" xml:"roles"` + Properties *Properties `json:"properties,omitempty" xml:"properties"` +} + +// GroupList is the model for group list in TeamCity +type GroupList struct { + Count int `json:"count,omitempty" xml:"count"` + Items []Group `json:"group, omitempty" xml:"group"` } // NewGroup returns an instance of a Group. A non-empty Key and Name is required. @@ -62,9 +71,17 @@ func (s *GroupService) Create(group *Group) (*Group, error) { // GetByKey - Get a group by its group key func (s *GroupService) GetByKey(key string) (*Group, error) { + return s.getByLocator(LocatorKey(key)) +} + +// GetByName - Get a group by its group name +func (s *GroupService) GetByName(name string) (*Group, error) { + return s.getByLocator(LocatorName(name)) +} + +func (s *GroupService) getByLocator(locator Locator) (*Group, error) { var out Group - locator := LocatorKey(key).String() - err := s.restHelper.get(locator, &out, "group") + err := s.restHelper.get(locator.String(), &out, "group") if err != nil { return nil, err } @@ -78,3 +95,13 @@ func (s *GroupService) Delete(key string) error { err := s.restHelper.delete(locator, "group") return err } + +// List - List of all groups +func (s *GroupService) List() (*GroupList, error) { + var out GroupList + err := s.restHelper.get("", &out, "group") + if err != nil { + return nil, err + } + return &out, nil +} diff --git a/teamcity/group_test.go b/teamcity/group_test.go index 38b76fa..4073e15 100644 --- a/teamcity/group_test.go +++ b/teamcity/group_test.go @@ -1,6 +1,7 @@ package teamcity_test import ( + "fmt" "testing" "github.com/cvbarros/go-teamcity/teamcity" @@ -42,6 +43,24 @@ func TestGroup_GetByKey(t *testing.T) { assert.Equal(t, newGroup.Description, actual.Description) } +func TestGroup_GetByName(t *testing.T) { + newGroup, _ := teamcity.NewGroup("TESTGROUPKEY2", "Test Group Name 2", "Test Group Description 2") + client := setup() + client.Groups.Create(newGroup) + + actual, err := client.Groups.GetByName(newGroup.Name) + + require.NoError(t, err) + require.NotNil(t, actual) + require.NotEmpty(t, actual.Key) + + cleanUpGroup(t, client, actual.Key) + + assert.Equal(t, newGroup.Key, actual.Key) + assert.Equal(t, newGroup.Name, actual.Name) + assert.Equal(t, newGroup.Description, actual.Description) +} + func TestGroup_Delete(t *testing.T) { newGroup, _ := teamcity.NewGroup("TESTGROUPKEY", "Test Group Name", "Test Group Description") client := setup() @@ -58,6 +77,41 @@ func TestGroup_Delete(t *testing.T) { assert.Contains(t, err.Error(), "404") } +func TestGroup_List(t *testing.T) { + groups := []*teamcity.Group{} + client := setup() + + groupListBefore, err := client.Groups.List() + require.NoError(t, err) + for i := 0; i < 5; i++ { + group, err := teamcity.NewGroup( + fmt.Sprint("TESTGROUPLIST", i), + fmt.Sprint("Test Group List ", i), + fmt.Sprint("Test Group Description List ", i), + ) + require.NoError(t, err) + groups = append(groups, group) + client.Groups.Create(group) + } + groupList, err := client.Groups.List() + + require.NoError(t, err) + assert.Equal(t, groupListBefore.Count+5, groupList.Count) + + for _, group := range groupList.Items { + if group.Key == "ALL_USERS_GROUP" { + continue + } + _, err := client.Groups.GetByKey(group.Key) + require.NoError(t, err) + + cleanUpGroup(t, client, group.Key) + + _, err = client.Groups.GetByKey(group.Key) + require.Error(t, err) + } +} + func cleanUpGroup(t *testing.T, client *teamcity.Client, key string) { client.Groups.Delete(key) } diff --git a/teamcity/locator.go b/teamcity/locator.go index 6c1e61b..c3703c2 100644 --- a/teamcity/locator.go +++ b/teamcity/locator.go @@ -19,11 +19,16 @@ func LocatorIDInt(id int) Locator { return Locator(url.QueryEscape("id:") + fmt.Sprintf("%d", id)) } -//LocatorName creates a locator for Project/BuildType by Name +//LocatorName creates a locator for User/Project/BuildType by Name func LocatorName(name string) Locator { return Locator(url.QueryEscape("name:") + url.PathEscape(name)) } +//LocatorUsername creates a locator for User by Username +func LocatorUsername(name string) Locator { + return Locator(url.QueryEscape("username:") + url.PathEscape(name)) +} + //LocatorKey creates a locator for Group by Key func LocatorKey(key string) Locator { return Locator(url.QueryEscape("key:") + url.PathEscape(key)) diff --git a/teamcity/rest_helper.go b/teamcity/rest_helper.go index f098107..94e49b3 100644 --- a/teamcity/rest_helper.go +++ b/teamcity/rest_helper.go @@ -150,6 +150,7 @@ func (r *restHelper) putTextPlain(path string, data string, resourceDescription return "", r.handleRestError(bodyBytes, resp.StatusCode, "PUT", resourceDescription) } + func (r *restHelper) post(path string, data interface{}, out interface{}, resourceDescription string) error { request, _ := r.sling.New().Post(path).BodyJSON(data).Request() response, err := r.httpClient.Do(request) diff --git a/teamcity/teamcity.go b/teamcity/teamcity.go index ca1002a..ebcadd8 100644 --- a/teamcity/teamcity.go +++ b/teamcity/teamcity.go @@ -67,6 +67,7 @@ type Client struct { Server *ServerService VcsRoots *VcsRootService Groups *GroupService + Users *UserService RoleAssignments *RoleAssignmentService } @@ -116,6 +117,7 @@ func newClientInstance(auth Auth, address string, httpClient *http.Client) (*Cli Server: newServerService(sharedClient.New()), VcsRoots: newVcsRootService(sharedClient.New(), httpClient), Groups: newGroupService(sharedClient.New(), httpClient), + Users: newUserService(sharedClient.New(), httpClient), RoleAssignments: newRoleAssignmentService(sharedClient.New(), httpClient), }, nil } diff --git a/teamcity/user.go b/teamcity/user.go new file mode 100644 index 0000000..4256828 --- /dev/null +++ b/teamcity/user.go @@ -0,0 +1,183 @@ +package teamcity + +import ( + "fmt" + "net/http" + + "github.com/dghubble/sling" +) + +// User is the model for User entities in TeamCity +type User struct { + Username string `json:"username,omitempty" xml:"username"` + Name string `json:"name,omitempty" xml:"name"` + ID int `json:"id,omitempty" xml:"id"` + Email string `json:"email,omitempty" xml:"email"` + Properties *Properties `json:"properties,omitempty" xml:"properties"` + Roles *roleAssignmentsJSON `json:"roles,omitempty" xml:"roles"` + Groups *groupAssignments `json:"groups,omitempty" xml:"groups"` +} + +type groupAssignments struct { + Count int `json:"count,omitempty" xml:"count"` + Items []Group `json:"group,omitempty" xml:"groups"` +} + +// UserList contains list of users +type UserList struct { + Count int `json:"count,omitempty" xml:"count"` + Items []User `json:"user,omitempty" xml:"user"` +} + +// NewUser returns an instance of a User. A non-empty Username, Name and Email is required. +func NewUser(username string, name string, email string) (*User, error) { + if username == "" { + return nil, fmt.Errorf("Key is required") + } + + if name == "" { + return nil, fmt.Errorf("Name is required") + } + + if email == "" { + return nil, fmt.Errorf("Email is required") + } + + return &User{ + Username: username, + Name: name, + Email: email, + }, nil +} + +// UserService has operations for handling Users +type UserService struct { + sling *sling.Sling + httpClient *http.Client + restHelper *restHelper +} + +func newUserService(base *sling.Sling, httpClient *http.Client) *UserService { + sling := base.Path("users/") + return &UserService{ + httpClient: httpClient, + sling: sling, + restHelper: newRestHelperWithSling(httpClient, sling), + } +} + +// Create - Creates a new User +func (s *UserService) Create(user *User) (*User, error) { + var created User + err := s.restHelper.post("", user, &created, "User") + + if err != nil { + return nil, err + } + + return &created, nil +} + +// GetByID - Get a User by its User ID +func (s *UserService) GetByID(ID int) (*User, error) { + return s.getByLocator(LocatorID(fmt.Sprint(ID))) +} + +// GetByUsername - Get a User by its User Username +func (s *UserService) GetByUsername(username string) (*User, error) { + return s.getByLocator(LocatorUsername(username)) +} + +// GetByName - Get a User by its User Name +func (s *UserService) GetByName(name string) (*User, error) { + return s.getByLocator(LocatorName(name)) +} + +func (s *UserService) getByLocator(locator Locator) (*User, error) { + var out User + err := s.restHelper.get(locator.String(), &out, "User") + if err != nil { + return nil, err + } + + return &out, err +} + +// DeleteByID - Deletes a User by its User ID +func (s *UserService) DeleteByID(id int) error { + return s.deleteByLocator(LocatorID(fmt.Sprint(id))) +} + +// DeleteByName - Deletes a User by its User Name +func (s *UserService) DeleteByName(name string) error { + return s.deleteByLocator(LocatorName(name)) +} + +// DeleteByUsername - Deletes a User by its User Username +func (s *UserService) DeleteByUsername(username string) error { + return s.deleteByLocator(LocatorUsername(username)) +} + +func (s *UserService) deleteByLocator(locator Locator) error { + err := s.restHelper.delete(locator.String(), "User") + return err +} + +// List - Get list of all User +func (s *UserService) List() (*UserList, error) { + var out UserList + err := s.restHelper.get("", &out, "Users") + if err != nil { + return nil, err + } + return &out, err +} + +// GroupAddByID - Add User with userID to Group with groupKey +func (s *UserService) GroupAddByID(userID int, groupKey string) (*Group, error) { + return s.groupAddByKey(LocatorID(fmt.Sprint(userID)), groupKey) +} + +// GroupAddByUsername - Add User with username to Group with groupKey +func (s *UserService) GroupAddByUsername(username, groupKey string) (*Group, error) { + return s.groupAddByKey(LocatorUsername(username), groupKey) +} + +// GroupAddByName - Add User with name to Group with groupKey +func (s *UserService) GroupAddByName(userName, groupKey string) (*Group, error) { + return s.groupAddByKey(LocatorName(userName), groupKey) +} + +func (s *UserService) groupAddByKey(locator Locator, groupKey string) (*Group, error) { + var out Group + + err := s.restHelper.post(fmt.Sprintf("%s/groups", locator), Group{Key: groupKey}, &out, "User") + if err != nil { + return nil, err + } + return &out, nil +} + +// GroupDeleteByID - Add User with userID to Group with groupKey +func (s *UserService) GroupDeleteByID(userID int, groupKey string) (*Group, error) { + return s.groupDeleteByKey(LocatorID(fmt.Sprint(userID)), groupKey) +} + +// GroupDeleteByUsername - Add User with username to Group with groupKey +func (s *UserService) GroupDeleteByUsername(username, groupKey string) (*Group, error) { + return s.groupDeleteByKey(LocatorUsername(username), groupKey) +} + +// GroupDeleteByName - Add User with name to Group with groupKey +func (s *UserService) GroupDeleteByName(userName, groupKey string) (*Group, error) { + return s.groupDeleteByKey(LocatorName(userName), groupKey) +} + +func (s *UserService) groupDeleteByKey(locator Locator, groupKey string) (*Group, error) { + var out Group + err := s.restHelper.delete(fmt.Sprintf("%s/groups/%s", locator, groupKey), "User") + if err != nil { + return nil, err + } + return &out, nil +} diff --git a/teamcity/user_test.go b/teamcity/user_test.go new file mode 100644 index 0000000..59deb9b --- /dev/null +++ b/teamcity/user_test.go @@ -0,0 +1,157 @@ +package teamcity_test + +import ( + "fmt" + "testing" + + "github.com/cvbarros/go-teamcity/teamcity" + "github.com/stretchr/testify/require" +) + +func TestUser_Create(t *testing.T) { + newUser, _ := teamcity.NewUser("username", "First Middle Last", "test@test.test") + client := setup() + actual, err := client.Users.Create(newUser) + + require.NoError(t, err) + require.NotNil(t, actual) + require.NotEmpty(t, actual.Name) + require.NotEmpty(t, actual.Username) + + cleanUpUser(t, client, actual.ID) + + require.NotEqual(t, newUser.ID, actual.ID) + require.Equal(t, newUser.Name, actual.Name) + require.Equal(t, newUser.Email, actual.Email) +} + +func TestUser_Get(t *testing.T) { + newUser, _ := teamcity.NewUser("usernameGet", "First Middle Last", "test@test.test") + + client := setup() + actual, err := client.Users.Create(newUser) + + require.NoError(t, err) + require.NotNil(t, actual) + require.NotEmpty(t, actual.Name) + require.NotEmpty(t, actual.Username) + require.NotZero(t, actual.ID) + + userByUsername, err := client.Users.GetByUsername(newUser.Username) + + require.NoError(t, err) + require.NotNil(t, userByUsername) + require.NotEmpty(t, userByUsername.Name) + require.NotEmpty(t, userByUsername.Username) + require.NotZero(t, userByUsername.ID) + + userByName, err := client.Users.GetByName(newUser.Name) + + require.NoError(t, err) + require.NotNil(t, userByName) + require.NotEmpty(t, userByName.Name) + require.NotEmpty(t, userByName.Username) + require.NotZero(t, userByName.ID) + + userByID, err := client.Users.GetByID(actual.ID) + + require.NoError(t, err) + require.NotNil(t, userByID) + require.NotEmpty(t, userByID.Name) + require.NotEmpty(t, userByID.Username) + require.NotZero(t, userByID.ID) + + cleanUpUser(t, client, actual.ID) + + require.NotEqual(t, newUser.ID, actual.ID) + require.Equal(t, newUser.Name, actual.Name) + require.Equal(t, newUser.Email, actual.Email) + + require.Equal(t, actual, userByID) + require.Equal(t, actual, userByUsername) +} + +func TestUser_Delete(t *testing.T) { + newUser, _ := teamcity.NewUser("usernameDel", "First Middle Last", "test@test.test") + client := setup() + actual, err := client.Users.Create(newUser) + + err = client.Users.DeleteByID(actual.ID) + + require.NoError(t, err) + + _, err = client.Users.GetByUsername(newUser.Username) + + // User is deleted, so expect error, and message to contain 404 (NOT FOUND) + require.Error(t, err) + require.Contains(t, err.Error(), "404") +} + +func TestUser_List(t *testing.T) { + users := []*teamcity.User{} + client := setup() + + for i := 0; i < 5; i++ { + user, err := teamcity.NewUser( + fmt.Sprint("TESTUSERNAME", i), + fmt.Sprint("Test User List ", i), + fmt.Sprint("Test User Description List ", i), + ) + require.NoError(t, err) + users = append(users, user) + _, err = client.Users.Create(user) + require.NoError(t, err) + } + userList, err := client.Users.List() + + require.NoError(t, err) + require.Equal(t, 5, userList.Count-1) + + for _, user := range userList.Items { + if user.Username == "admin" { + continue + } + actual, err := client.Users.GetByUsername(user.Username) + require.NoError(t, err) + + cleanUpUser(t, client, actual.ID) + + _, err = client.Users.GetByName(user.Name) + require.Error(t, err) + } + userList, err = client.Users.List() + require.NoError(t, err) + require.Equal(t, 1, userList.Count) +} + +func TestUser_Group(t *testing.T) { + client := setup() + + newGroup, _ := teamcity.NewGroup("TESTGROUPMEMBER", "Test Group Member", "") + actualGroup, err := client.Groups.Create(newGroup) + require.NoError(t, err) + require.Zero(t, actualGroup.Users.Count) + + newUser, _ := teamcity.NewUser("testusermember", "Test User Member", "test@member.com") + actualUser, err := client.Users.Create(newUser) + require.NoError(t, err) + + actualGroup, err = client.Users.GroupAddByID(actualUser.ID, actualGroup.Key) + require.NoError(t, err) + require.NotNil(t, actualGroup) + require.NotZero(t, actualGroup.Users.Count) + + actualGroup, err = client.Users.GroupDeleteByID(actualUser.ID, actualGroup.Key) + + cleanUpGroup(t, client, newGroup.Key) + cleanUpUser(t, client, actualUser.ID) + + require.NoError(t, err) + require.NotNil(t, actualGroup) + require.Nil(t, actualGroup.Users) + +} + +func cleanUpUser(t *testing.T, client *teamcity.Client, id int) { + client.Users.DeleteByID(id) +} From 107f470c1ea541d138ee8e97d33ad4e2701c1295 Mon Sep 17 00:00:00 2001 From: Roman Mingazeev Date: Mon, 16 Nov 2020 18:03:58 +0300 Subject: [PATCH 2/3] add func for check membership --- teamcity/user.go | 29 +++++++++++++++++++++++++++++ teamcity/user_test.go | 20 ++++++++++++++++++++ 2 files changed, 49 insertions(+) diff --git a/teamcity/user.go b/teamcity/user.go index 4256828..0279bc8 100644 --- a/teamcity/user.go +++ b/teamcity/user.go @@ -3,6 +3,7 @@ package teamcity import ( "fmt" "net/http" + "strings" "github.com/dghubble/sling" ) @@ -181,3 +182,31 @@ func (s *UserService) groupDeleteByKey(locator Locator, groupKey string) (*Group } return &out, nil } + +// IsGroupMemberByID - checks the user's group membership by ID +func (s *UserService) IsGroupMemberByID(id int, key string) (bool, error) { + return s.isGroupMemberByLocator(LocatorID(fmt.Sprint(id)), key) +} + +// IsGroupMemberByUsername - checks the user's group membership by Username +func (s *UserService) IsGroupMemberByUsername(username, key string) (bool, error) { + return s.isGroupMemberByLocator(LocatorUsername(username), key) +} + +// IsGroupMemberByName - checks the user's group membership by Name +func (s *UserService) IsGroupMemberByName(name, key string) (bool, error) { + return s.isGroupMemberByLocator(LocatorName(name), key) +} + +func (s *UserService) isGroupMemberByLocator(locator Locator, key string) (bool, error) { + var out Group + err := s.restHelper.get(fmt.Sprintf("%s/groups/%s", locator, LocatorKey(key)), &out, "User") + if err != nil { + strErr := err.Error() + if strings.Contains(strErr, "status code: 404") { + return false, nil + } + return false, err + } + return true, nil +} diff --git a/teamcity/user_test.go b/teamcity/user_test.go index 59deb9b..bc91583 100644 --- a/teamcity/user_test.go +++ b/teamcity/user_test.go @@ -152,6 +152,26 @@ func TestUser_Group(t *testing.T) { } +func TestUser_Member(t *testing.T) { + client := setup() + admin, err := client.Users.GetByUsername("admin") + require.NoError(t, err) + groupKey := "ALL_USERS_GROUP" + + member, err := client.Users.IsGroupMemberByID(admin.ID, groupKey) + require.NoError(t, err) + require.True(t, member) + + member, err = client.Users.IsGroupMemberByID(admin.ID, "INVALID_GROUP") + require.NoError(t, err) + require.False(t, member) + + member, err = client.Users.IsGroupMemberByUsername("INVALIDUSERNAME", groupKey) + require.NoError(t, err) + require.False(t, member) + +} + func cleanUpUser(t *testing.T, client *teamcity.Client, id int) { client.Users.DeleteByID(id) } From 59aadd45a30fe5e267d59834acc47abcf43c8371 Mon Sep 17 00:00:00 2001 From: Roman Mingazeev Date: Tue, 9 Mar 2021 15:27:45 +0300 Subject: [PATCH 3/3] new service UserGroupMemberShip --- teamcity/group.go | 17 ++- teamcity/group_test.go | 4 +- teamcity/locator.go | 12 +- teamcity/query_locator.go | 17 +++ teamcity/query_locator_test.go | 22 ++++ teamcity/rest_helper.go | 16 ++- teamcity/teamcity.go | 40 +++--- teamcity/user.go | 73 ++--------- teamcity/user_group_member_ship_test.go | 97 +++++++++++++++ teamcity/user_group_membership.go | 156 ++++++++++++++++++++++++ teamcity/user_test.go | 52 +------- 11 files changed, 365 insertions(+), 141 deletions(-) create mode 100644 teamcity/query_locator.go create mode 100644 teamcity/query_locator_test.go create mode 100644 teamcity/user_group_member_ship_test.go create mode 100644 teamcity/user_group_membership.go diff --git a/teamcity/group.go b/teamcity/group.go index 06fafd1..f2f637f 100644 --- a/teamcity/group.go +++ b/teamcity/group.go @@ -12,7 +12,6 @@ type Group struct { Key string `json:"key,omitempty" xml:"key"` Description string `json:"description,omitempty" xml:"description"` Name string `json:"name,omitempty" xml:"name"` - Users *UserList `json:"users,omitempty" xml:"users"` Roles *roleAssignmentsJSON `json:"roles,omitempty" xml:"roles"` Properties *Properties `json:"properties,omitempty" xml:"properties"` } @@ -20,7 +19,7 @@ type Group struct { // GroupList is the model for group list in TeamCity type GroupList struct { Count int `json:"count,omitempty" xml:"count"` - Items []Group `json:"group, omitempty" xml:"group"` + Items []Group `json:"group,omitempty" xml:"group"` } // NewGroup returns an instance of a Group. A non-empty Key and Name is required. @@ -96,12 +95,20 @@ func (s *GroupService) Delete(key string) error { return err } -// List - List of all groups -func (s *GroupService) List() (*GroupList, error) { +// List - List of groups in range [offset:limit) +func (s *GroupService) List(offset, limit int) (*GroupList, error) { var out GroupList - err := s.restHelper.get("", &out, "group") + err := s.restHelper.get("", &out, "group", buildQueryLocator( + LocatorStart(offset), + LocatorCount(limit), + )) if err != nil { return nil, err } return &out, nil } + +// ListAll returns all groups +func (s *GroupService) ListAll() (*GroupList, error) { + return s.List(0, -1) +} diff --git a/teamcity/group_test.go b/teamcity/group_test.go index 4073e15..8c3eb6e 100644 --- a/teamcity/group_test.go +++ b/teamcity/group_test.go @@ -81,7 +81,7 @@ func TestGroup_List(t *testing.T) { groups := []*teamcity.Group{} client := setup() - groupListBefore, err := client.Groups.List() + groupListBefore, err := client.Groups.ListAll() require.NoError(t, err) for i := 0; i < 5; i++ { group, err := teamcity.NewGroup( @@ -93,7 +93,7 @@ func TestGroup_List(t *testing.T) { groups = append(groups, group) client.Groups.Create(group) } - groupList, err := client.Groups.List() + groupList, err := client.Groups.ListAll() require.NoError(t, err) assert.Equal(t, groupListBefore.Count+5, groupList.Count) diff --git a/teamcity/locator.go b/teamcity/locator.go index c3703c2..b4549f9 100644 --- a/teamcity/locator.go +++ b/teamcity/locator.go @@ -16,7 +16,7 @@ func LocatorID(id string) Locator { //LocatorIDInt creates a locator for a Project/BuildType by Id where the Id's an integer func LocatorIDInt(id int) Locator { - return Locator(url.QueryEscape("id:") + fmt.Sprintf("%d", id)) + return Locator(url.QueryEscape("id:") + fmt.Sprint(id)) } //LocatorName creates a locator for User/Project/BuildType by Name @@ -39,6 +39,16 @@ func LocatorType(id string) Locator { return Locator(url.QueryEscape("type:") + id) } +//LocatorStart creates a locator to set offset +func LocatorStart(start int) Locator { + return Locator(url.QueryEscape("start:") + fmt.Sprint(start)) +} + +//LocatorCount creates a locator to set number of answers +func LocatorCount(count int) Locator { + return Locator(url.QueryEscape("count:") + fmt.Sprint(count)) +} + func (l Locator) String() string { return string(l) } diff --git a/teamcity/query_locator.go b/teamcity/query_locator.go new file mode 100644 index 0000000..60ffa73 --- /dev/null +++ b/teamcity/query_locator.go @@ -0,0 +1,17 @@ +package teamcity + +func buildQueryLocator(locators ...Locator) *queryStruct { + locatorQuery := "" + if len(locators) > 1 { + for _, locator := range locators[1:] { + locatorQuery += "," + locator.String() + } + } + if len(locators) >= 1 { + locatorQuery = locators[0].String() + locatorQuery + } + return &queryStruct{ + key: "locator", + value: locatorQuery, + } +} diff --git a/teamcity/query_locator_test.go b/teamcity/query_locator_test.go new file mode 100644 index 0000000..3040000 --- /dev/null +++ b/teamcity/query_locator_test.go @@ -0,0 +1,22 @@ +package teamcity + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBuildQueryLocator_Empty(t *testing.T) { + q := buildQueryLocator() + require.Equal(t, "", q.value) +} + +func TestBuildQueryLocator_OneElement(t *testing.T) { + q := buildQueryLocator(LocatorStart(0)) + require.Equal(t, "locator=start%3A0", q.String()) +} + +func TestBuildQueryLocator_TwoElements(t *testing.T) { + q := buildQueryLocator(LocatorStart(0), LocatorCount(1)) + require.Equal(t, "locator=start%3A0,count%3A1", q.String()) +} diff --git a/teamcity/rest_helper.go b/teamcity/rest_helper.go index 94e49b3..dd6ac86 100644 --- a/teamcity/rest_helper.go +++ b/teamcity/rest_helper.go @@ -20,6 +20,14 @@ type restHelper struct { sling *sling.Sling } +type queryStruct struct { + key, value string +} + +func (q *queryStruct) String() string { + return fmt.Sprintf("%s=%s", q.key, q.value) +} + func newRestHelper(httpClient *http.Client) *restHelper { return newRestHelperWithSling(httpClient, nil) } @@ -54,8 +62,13 @@ func (r *restHelper) getCustom(path string, out interface{}, resourceDescription return r.handleRestError(bodyBytes, response.StatusCode, "GET", resourceDescription) } -func (r *restHelper) get(path string, out interface{}, resourceDescription string) error { +func (r *restHelper) get(path string, out interface{}, resourceDescription string, query ...*queryStruct) error { request, _ := r.sling.New().Get(path).Request() + for _, q := range query { + if q != nil { + request.URL.Query().Set(q.key, q.value) + } + } response, err := r.httpClient.Do(request) if err != nil { return err @@ -150,7 +163,6 @@ func (r *restHelper) putTextPlain(path string, data string, resourceDescription return "", r.handleRestError(bodyBytes, resp.StatusCode, "PUT", resourceDescription) } - func (r *restHelper) post(path string, data interface{}, out interface{}, resourceDescription string) error { request, _ := r.sling.New().Post(path).BodyJSON(data).Request() response, err := r.httpClient.Do(request) diff --git a/teamcity/teamcity.go b/teamcity/teamcity.go index ebcadd8..8d3812e 100644 --- a/teamcity/teamcity.go +++ b/teamcity/teamcity.go @@ -61,14 +61,15 @@ type Client struct { commonBase *sling.Sling - AgentPools *AgentPoolsService - Projects *ProjectService - BuildTypes *BuildTypeService - Server *ServerService - VcsRoots *VcsRootService - Groups *GroupService - Users *UserService - RoleAssignments *RoleAssignmentService + AgentPools *AgentPoolsService + Projects *ProjectService + BuildTypes *BuildTypeService + Server *ServerService + VcsRoots *VcsRootService + Groups *GroupService + Users *UserService + UserGroupMemberShip *UserGroupMemberShipService + RoleAssignments *RoleAssignmentService } func NewClient(auth Auth, httpClient *http.Client) (*Client, error) { @@ -108,17 +109,18 @@ func newClientInstance(auth Auth, address string, httpClient *http.Client) (*Cli } return &Client{ - address: address, - HTTPClient: httpClient, - commonBase: sharedClient, - AgentPools: newAgentPoolsService(sharedClient.New(), httpClient), - Projects: newProjectService(sharedClient.New(), httpClient), - BuildTypes: newBuildTypeService(sharedClient.New(), httpClient), - Server: newServerService(sharedClient.New()), - VcsRoots: newVcsRootService(sharedClient.New(), httpClient), - Groups: newGroupService(sharedClient.New(), httpClient), - Users: newUserService(sharedClient.New(), httpClient), - RoleAssignments: newRoleAssignmentService(sharedClient.New(), httpClient), + address: address, + HTTPClient: httpClient, + commonBase: sharedClient, + AgentPools: newAgentPoolsService(sharedClient.New(), httpClient), + Projects: newProjectService(sharedClient.New(), httpClient), + BuildTypes: newBuildTypeService(sharedClient.New(), httpClient), + Server: newServerService(sharedClient.New()), + VcsRoots: newVcsRootService(sharedClient.New(), httpClient), + Groups: newGroupService(sharedClient.New(), httpClient), + Users: newUserService(sharedClient.New(), httpClient), + UserGroupMemberShip: newUserGroupMembershipService(sharedClient.New(), httpClient), + RoleAssignments: newRoleAssignmentService(sharedClient.New(), httpClient), }, nil } diff --git a/teamcity/user.go b/teamcity/user.go index 0279bc8..abc4491 100644 --- a/teamcity/user.go +++ b/teamcity/user.go @@ -3,7 +3,6 @@ package teamcity import ( "fmt" "net/http" - "strings" "github.com/dghubble/sling" ) @@ -16,12 +15,6 @@ type User struct { Email string `json:"email,omitempty" xml:"email"` Properties *Properties `json:"properties,omitempty" xml:"properties"` Roles *roleAssignmentsJSON `json:"roles,omitempty" xml:"roles"` - Groups *groupAssignments `json:"groups,omitempty" xml:"groups"` -} - -type groupAssignments struct { - Count int `json:"count,omitempty" xml:"count"` - Items []Group `json:"group,omitempty" xml:"groups"` } // UserList contains list of users @@ -124,16 +117,24 @@ func (s *UserService) deleteByLocator(locator Locator) error { return err } -// List - Get list of all User -func (s *UserService) List() (*UserList, error) { +// List - Get list of users in range [offset:limit) +func (s *UserService) List(offset, limit int) (*UserList, error) { var out UserList - err := s.restHelper.get("", &out, "Users") + err := s.restHelper.get("", &out, "Users", buildQueryLocator( + LocatorStart(offset), + LocatorCount(limit), + )) if err != nil { return nil, err } return &out, err } +// ListAll returns list of all users +func (s *UserService) ListAll() (*UserList, error) { + return s.List(0, -1) +} + // GroupAddByID - Add User with userID to Group with groupKey func (s *UserService) GroupAddByID(userID int, groupKey string) (*Group, error) { return s.groupAddByKey(LocatorID(fmt.Sprint(userID)), groupKey) @@ -158,55 +159,3 @@ func (s *UserService) groupAddByKey(locator Locator, groupKey string) (*Group, e } return &out, nil } - -// GroupDeleteByID - Add User with userID to Group with groupKey -func (s *UserService) GroupDeleteByID(userID int, groupKey string) (*Group, error) { - return s.groupDeleteByKey(LocatorID(fmt.Sprint(userID)), groupKey) -} - -// GroupDeleteByUsername - Add User with username to Group with groupKey -func (s *UserService) GroupDeleteByUsername(username, groupKey string) (*Group, error) { - return s.groupDeleteByKey(LocatorUsername(username), groupKey) -} - -// GroupDeleteByName - Add User with name to Group with groupKey -func (s *UserService) GroupDeleteByName(userName, groupKey string) (*Group, error) { - return s.groupDeleteByKey(LocatorName(userName), groupKey) -} - -func (s *UserService) groupDeleteByKey(locator Locator, groupKey string) (*Group, error) { - var out Group - err := s.restHelper.delete(fmt.Sprintf("%s/groups/%s", locator, groupKey), "User") - if err != nil { - return nil, err - } - return &out, nil -} - -// IsGroupMemberByID - checks the user's group membership by ID -func (s *UserService) IsGroupMemberByID(id int, key string) (bool, error) { - return s.isGroupMemberByLocator(LocatorID(fmt.Sprint(id)), key) -} - -// IsGroupMemberByUsername - checks the user's group membership by Username -func (s *UserService) IsGroupMemberByUsername(username, key string) (bool, error) { - return s.isGroupMemberByLocator(LocatorUsername(username), key) -} - -// IsGroupMemberByName - checks the user's group membership by Name -func (s *UserService) IsGroupMemberByName(name, key string) (bool, error) { - return s.isGroupMemberByLocator(LocatorName(name), key) -} - -func (s *UserService) isGroupMemberByLocator(locator Locator, key string) (bool, error) { - var out Group - err := s.restHelper.get(fmt.Sprintf("%s/groups/%s", locator, LocatorKey(key)), &out, "User") - if err != nil { - strErr := err.Error() - if strings.Contains(strErr, "status code: 404") { - return false, nil - } - return false, err - } - return true, nil -} diff --git a/teamcity/user_group_member_ship_test.go b/teamcity/user_group_member_ship_test.go new file mode 100644 index 0000000..8e9e12c --- /dev/null +++ b/teamcity/user_group_member_ship_test.go @@ -0,0 +1,97 @@ +package teamcity_test + +import ( + "testing" + + "github.com/cvbarros/go-teamcity/teamcity" + "github.com/stretchr/testify/require" +) + +func TestUserGroupMemberShip_IsGroupMember(t *testing.T) { + client := setup() + admin, err := client.Users.GetByUsername("admin") + require.NoError(t, err) + groupKey := "ALL_USERS_GROUP" + + isMember, err := client.UserGroupMemberShip.IsGroupMemberByID(admin.ID, groupKey) + require.NoError(t, err) + require.True(t, isMember) + + isMember, err = client.UserGroupMemberShip.IsGroupMemberByID(admin.ID, "INVALID_GROUP") + require.NoError(t, err) + require.False(t, isMember) + + isMember, err = client.UserGroupMemberShip.IsGroupMemberByUsername("INVALIDUSERNAME", groupKey) + require.NoError(t, err) + require.False(t, isMember) + +} + +func TestUserGropMemberShip_GetGroupMembers(t *testing.T) { + client := setup() + + newGroup, _ := teamcity.NewGroup("TESTGROUPMEMBER", "Test Group Member", "") + actualGroup, err := client.Groups.Create(newGroup) + require.NoError(t, err) + + newUser, _ := teamcity.NewUser("testusermember", "Test User Member", "test@member.com") + actualUser, err := client.Users.Create(newUser) + require.NoError(t, err) + + actualGroup, err = client.Users.GroupAddByID(actualUser.ID, actualGroup.Key) + require.NoError(t, err) + require.NotNil(t, actualGroup) + + memberList, err := client.UserGroupMemberShip.GetGroupMembersListAllByKey(actualGroup.Key) + require.NoError(t, err) + require.NotEmpty(t, memberList.Items) + require.Equal(t, memberList.Count, 1) + require.Equal(t, actualUser.ID, memberList.Items[0].ID) + + require.NoError(t, client.UserGroupMemberShip.GroupDeleteMemberByID(actualUser.ID, actualGroup.Key)) + + memberList, err = client.UserGroupMemberShip.GetGroupMembersListAllByKey(actualGroup.Key) + require.NoError(t, err) + require.Empty(t, memberList.Items) + require.Equal(t, memberList.Count, 0) + + cleanUpGroup(t, client, newGroup.Key) + cleanUpUser(t, client, actualUser.ID) + + require.NoError(t, err) + require.NotNil(t, actualGroup) +} +func TestUserGropMemberShip_GetUserGroups(t *testing.T) { + client := setup() + + newGroup, _ := teamcity.NewGroup("TESTGUSERGROUPS", "Test User groups", "") + actualGroup, err := client.Groups.Create(newGroup) + require.NoError(t, err) + + newUser, _ := teamcity.NewUser("testusergroups", "Test User Groups", "testgroups@member.com") + actualUser, err := client.Users.Create(newUser) + require.NoError(t, err) + + actualGroup, err = client.Users.GroupAddByID(actualUser.ID, actualGroup.Key) + require.NoError(t, err) + require.NotNil(t, actualGroup) + + groupsList, err := client.UserGroupMemberShip.GetUserGroupsListAllByID(actualUser.ID) + require.NoError(t, err) + require.NotEmpty(t, groupsList.Items) + // first is ALL_USERS_GROUP + require.Equal(t, groupsList.Count, 2) + require.Equal(t, actualGroup.Key, groupsList.Items[1].Key) + + require.NoError(t, client.UserGroupMemberShip.GroupDeleteMemberByID(actualUser.ID, actualGroup.Key)) + + groupsList, err = client.UserGroupMemberShip.GetUserGroupsListAllByID(actualUser.ID) + require.NoError(t, err) + require.Equal(t, groupsList.Count, 1) + + cleanUpGroup(t, client, newGroup.Key) + cleanUpUser(t, client, actualUser.ID) + + require.NoError(t, err) + require.NotNil(t, actualGroup) +} diff --git a/teamcity/user_group_membership.go b/teamcity/user_group_membership.go new file mode 100644 index 0000000..472438b --- /dev/null +++ b/teamcity/user_group_membership.go @@ -0,0 +1,156 @@ +package teamcity + +import ( + "fmt" + "net/http" + "strings" + + "github.com/dghubble/sling" +) + +// UserGroupMemberShipService has operations for handling UserGroupMemberships +type UserGroupMemberShipService struct { + // sling *sling.Sling + httpClient *http.Client + userRestHelper *restHelper + groupRestHelper *restHelper +} + +func newUserGroupMembershipService(base *sling.Sling, httpClient *http.Client) *UserGroupMemberShipService { + return &UserGroupMemberShipService{ + httpClient: httpClient, + // sling: sling, + userRestHelper: newRestHelperWithSling(httpClient, base.New().Path("users/")), + groupRestHelper: newRestHelperWithSling(httpClient, base.New().Path("userGroups/")), + } +} + +// GroupAddByID - Add User with UserID to Group with groupKey +func (s *UserGroupMemberShipService) GroupAddByID(userID int, groupKey string) (*Group, error) { + return s.groupAddByKey(LocatorID(fmt.Sprint(userID)), groupKey) +} + +// GroupAddByUsername - Add User with Username to Group with groupKey +func (s *UserGroupMemberShipService) GroupAddByUsername(username, groupKey string) (*Group, error) { + return s.groupAddByKey(LocatorUsername(username), groupKey) +} + +// GroupAddByName - Add User with name to Group with groupKey +func (s *UserGroupMemberShipService) GroupAddByName(name, groupKey string) (*Group, error) { + return s.groupAddByKey(LocatorName(name), groupKey) +} + +func (s *UserGroupMemberShipService) groupAddByKey(locator Locator, groupKey string) (*Group, error) { + var out Group + + err := s.userRestHelper.post(fmt.Sprintf("%s/groups", locator), Group{Key: groupKey}, &out, "UserGroupMemberShip") + if err != nil { + return nil, err + } + return &out, nil +} + +// GroupDeleteMemberByID - Delete User with UserID from the Group with groupKey +func (s *UserGroupMemberShipService) GroupDeleteMemberByID(userID int, groupKey string) error { + return s.groupDeleteMemberByKey(LocatorID(fmt.Sprint(userID)), groupKey) +} + +// GroupDeleteMemberByUsername - Delete User with username from the Group with groupKey +func (s *UserGroupMemberShipService) GroupDeleteMemberByUsername(username, groupKey string) error { + return s.groupDeleteMemberByKey(LocatorUsername(username), groupKey) +} + +// GroupDeleteMemberByName - Delete User with name from the Group with groupKey +func (s *UserGroupMemberShipService) GroupDeleteMemberByName(name, groupKey string) error { + return s.groupDeleteMemberByKey(LocatorName(name), groupKey) +} + +func (s *UserGroupMemberShipService) groupDeleteMemberByKey(locator Locator, groupKey string) error { + err := s.userRestHelper.delete(fmt.Sprintf("%s/groups/%s", locator, groupKey), "UserGroupMemberShip") + if err != nil { + return err + } + return nil +} + +// IsGroupMemberByID - checks the UserGroupMembership's group membership by ID +func (s *UserGroupMemberShipService) IsGroupMemberByID(id int, groupKey string) (bool, error) { + return s.isGroupMemberByLocator(LocatorIDInt(id), groupKey) +} + +// IsGroupMemberByUsername - checks the User's group membership by Username +func (s *UserGroupMemberShipService) IsGroupMemberByUsername(username, groupKey string) (bool, error) { + return s.isGroupMemberByLocator(LocatorUsername(username), groupKey) +} + +// IsGroupMemberByName - checks the User's group membership by Name +func (s *UserGroupMemberShipService) IsGroupMemberByName(name, key string) (bool, error) { + return s.isGroupMemberByLocator(LocatorName(name), key) +} + +func (s *UserGroupMemberShipService) isGroupMemberByLocator(locator Locator, groupKey string) (bool, error) { + var out Group + err := s.userRestHelper.get(fmt.Sprintf("%s/groups/%s", locator, LocatorKey(groupKey)), &out, "UserGroupMemberShip") + if err != nil { + strErr := err.Error() + if strings.Contains(strErr, "status code: 404") { + return false, nil + } + return false, err + } + return true, nil +} + +func (s *UserGroupMemberShipService) GetUserGroupsListByUsername(username string, offset, limit int) (*GroupList, error) { + return s.getUserGroupsList(LocatorUsername(username), offset, limit) +} + +func (s *UserGroupMemberShipService) GetUserGroupsListAllByUsername(username string) (*GroupList, error) { + return s.getUserGroupsList(LocatorUsername(username), 0, -1) +} + +func (s *UserGroupMemberShipService) GetUserGroupsListByID(id, offset, limit int) (*GroupList, error) { + return s.getUserGroupsList(LocatorIDInt(id), offset, limit) +} +func (s *UserGroupMemberShipService) GetUserGroupsListAllByID(id int) (*GroupList, error) { + return s.getUserGroupsList(LocatorIDInt(id), 0, -1) +} +func (s *UserGroupMemberShipService) getUserGroupsList(locator Locator, offset, limit int) (*GroupList, error) { + var out GroupList + err := s.userRestHelper.get(fmt.Sprintf("%s/groups/", locator), &out, "UserGroupMemberShip", + buildQueryLocator( + LocatorStart(offset), + LocatorCount(limit), + )) + if err != nil { + return nil, err + } + return &out, nil +} + +func (s *UserGroupMemberShipService) GetGroupMembersListByName(groupName string, offset, limit int) (*UserList, error) { + return s.getGroupMembersList(LocatorName(groupName), offset, limit) +} +func (s *UserGroupMemberShipService) GetGroupMembersListAllByName(groupName string) (*UserList, error) { + return s.getGroupMembersList(LocatorName(groupName), 0, -1) +} +func (s *UserGroupMemberShipService) GetGroupMembersListByKey(groupKey string, offset, limit int) (*UserList, error) { + return s.getGroupMembersList(LocatorKey(groupKey), offset, limit) +} +func (s *UserGroupMemberShipService) GetGroupMembersListAllByKey(groupKey string) (*UserList, error) { + return s.getGroupMembersList(LocatorKey(groupKey), 0, -1) +} +func (s *UserGroupMemberShipService) getGroupMembersList(locator Locator, offset, limit int) (*UserList, error) { + var out struct { + Users UserList + } + err := s.groupRestHelper.get(locator.String(), &out, "UserGroupMemberShip list members", + buildQueryLocator( + LocatorStart(offset), + LocatorCount(limit), + )) + if err != nil { + return nil, err + } + return &out.Users, nil +} diff --git a/teamcity/user_test.go b/teamcity/user_test.go index bc91583..571b308 100644 --- a/teamcity/user_test.go +++ b/teamcity/user_test.go @@ -102,7 +102,7 @@ func TestUser_List(t *testing.T) { _, err = client.Users.Create(user) require.NoError(t, err) } - userList, err := client.Users.List() + userList, err := client.Users.List(0, 0) require.NoError(t, err) require.Equal(t, 5, userList.Count-1) @@ -119,59 +119,11 @@ func TestUser_List(t *testing.T) { _, err = client.Users.GetByName(user.Name) require.Error(t, err) } - userList, err = client.Users.List() + userList, err = client.Users.List(0, 0) require.NoError(t, err) require.Equal(t, 1, userList.Count) } -func TestUser_Group(t *testing.T) { - client := setup() - - newGroup, _ := teamcity.NewGroup("TESTGROUPMEMBER", "Test Group Member", "") - actualGroup, err := client.Groups.Create(newGroup) - require.NoError(t, err) - require.Zero(t, actualGroup.Users.Count) - - newUser, _ := teamcity.NewUser("testusermember", "Test User Member", "test@member.com") - actualUser, err := client.Users.Create(newUser) - require.NoError(t, err) - - actualGroup, err = client.Users.GroupAddByID(actualUser.ID, actualGroup.Key) - require.NoError(t, err) - require.NotNil(t, actualGroup) - require.NotZero(t, actualGroup.Users.Count) - - actualGroup, err = client.Users.GroupDeleteByID(actualUser.ID, actualGroup.Key) - - cleanUpGroup(t, client, newGroup.Key) - cleanUpUser(t, client, actualUser.ID) - - require.NoError(t, err) - require.NotNil(t, actualGroup) - require.Nil(t, actualGroup.Users) - -} - -func TestUser_Member(t *testing.T) { - client := setup() - admin, err := client.Users.GetByUsername("admin") - require.NoError(t, err) - groupKey := "ALL_USERS_GROUP" - - member, err := client.Users.IsGroupMemberByID(admin.ID, groupKey) - require.NoError(t, err) - require.True(t, member) - - member, err = client.Users.IsGroupMemberByID(admin.ID, "INVALID_GROUP") - require.NoError(t, err) - require.False(t, member) - - member, err = client.Users.IsGroupMemberByUsername("INVALIDUSERNAME", groupKey) - require.NoError(t, err) - require.False(t, member) - -} - func cleanUpUser(t *testing.T, client *teamcity.Client, id int) { client.Users.DeleteByID(id) }