Skip to content
135 changes: 135 additions & 0 deletions cloud/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,19 @@ type groupMembersResult struct {
Members []GroupMember `json:"values"`
}

// Response body of https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-groups/#api-rest-api-2-group-member-get
type getGroupMembersResult struct {
IsLast bool `json:"isLast"`
MaxResults int `json:"maxResults"`
NextPage string `json:"nextPage"`
Total int `json:"total"`
StartAt int `json:"startAt"`
Values []GroupMember `json:"values"`
}

// Group represents a Jira group
type Group struct {
ID string `json:"groupId,omitempty" structs:"groupId,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
Users GroupMembers `json:"users,omitempty" structs:"users,omitempty"`
Expand Down Expand Up @@ -58,6 +69,12 @@ type GroupSearchOptions struct {
IncludeInactiveUsers bool
}

type Groups struct {
Groups []Group `json:"groups,omitempty"`
Header string `json:"header,omitempty"`
Total int `json:"total,omitempty"`
}

// Get returns a paginated list of members of the specified group and its subgroups.
// Users in the page are ordered by user names.
// User of this resource is required to have sysadmin or admin permissions.
Expand All @@ -68,6 +85,7 @@ type GroupSearchOptions struct {
//
// TODO Double check this method if this works as expected, is using the latest API and the response is complete
// This double check effort is done for v2 - Remove this two lines if this is completed.
// Deprecated: Use GetGroupMembers instead
func (s *GroupService) Get(ctx context.Context, name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) {
var apiEndpoint string
if options == nil {
Expand Down Expand Up @@ -144,3 +162,120 @@ func (s *GroupService) RemoveUserByGroupName(ctx context.Context, groupName stri

return resp, nil
}

// Sets case insensitive search
func WithCaseInsensitive() searchF {
return func(s search) search {
s = append(s, searchParam{name: "caseInsensitive", value: "true"})
return s
}
}

// Sets query string for filtering group names.
func WithGroupNameContains(contains string) searchF {
return func(s search) search {
s = append(s, searchParam{name: "query", value: contains})
return s
}
}

// Sets excluded group names.
func WithExcludedGroupNames(excluded []string) searchF {
return func(s search) search {
for _, name := range excluded {
s = append(s, searchParam{name: "exclude", value: name})
}

return s
}
}

// Sets excluded group ids.
func WithExcludedGroupsIds(excluded []string) searchF {
return func(s search) search {
for _, id := range excluded {
s = append(s, searchParam{name: "excludeId", value: id})
}

return s
}
}

// Search for the groups
// It can search by groupId, accountId or userName
// Apart from returning groups it also returns total number of groups
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-groups/#api-rest-api-3-groups-picker-get
func (s *GroupService) Find(ctx context.Context, tweaks ...searchF) ([]Group, *Response, error) {
search := []searchParam{}
for _, f := range tweaks {
search = f(search)
}

apiEndpoint := "/rest/api/3/groups/picker"

queryString := ""
for _, param := range search {
queryString += fmt.Sprintf("%s=%s&", param.name, param.value)
}

if queryString != "" {
apiEndpoint += "?" + queryString
}

req, err := s.client.NewRequest(ctx, http.MethodGet, apiEndpoint, nil)
if err != nil {
return nil, nil, err
}

groups := Groups{}
resp, err := s.client.Do(req, &groups)
if err != nil {
return nil, resp, NewJiraError(resp, err)
}

return groups.Groups, resp, nil
}

func WithInactiveUsers() searchF {
return func(s search) search {
s = append(s, searchParam{name: "includeInactiveUsers", value: "true"})
return s
}
}

// Search for the group members
// It can filter out inactive users
// Apart from returning group members it also returns total number of group members
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-groups/#api-rest-api-3-group-member-get
func (s *GroupService) GetGroupMembers(ctx context.Context, groupId string, tweaks ...searchF) ([]GroupMember, *Response, error) {
search := []searchParam{}
for _, f := range tweaks {
search = f(search)
}

apiEndpoint := fmt.Sprintf("/rest/api/3/group/member?groupId=%s", groupId)

queryString := ""
for _, param := range search {
queryString += fmt.Sprintf("%s=%s&", param.name, param.value)
}

if queryString != "" {
apiEndpoint += "&" + queryString
}

req, err := s.client.NewRequest(ctx, http.MethodGet, apiEndpoint, nil)
if err != nil {
return nil, nil, err
}

group := new(getGroupMembersResult)
resp, err := s.client.Do(req, group)
if err != nil {
return nil, resp, NewJiraError(resp, err)
}

return group.Values, resp, nil
}
120 changes: 120 additions & 0 deletions cloud/group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,123 @@ func TestGroupService_Remove(t *testing.T) {
t.Errorf("Error given: %s", err)
}
}

func TestGroupService_Find_Success(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/api/3/groups/picker", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
testRequestURL(t, r, "/rest/api/3/groups/picker")

fmt.Fprint(w, `{"header": "Showing 2 of 2 matching groups",
"total": 2,
"groups": [{
"name": "jdog-developers",
"html": "<b>j</b>dog-developers",
"groupId": "276f955c-63d7-42c8-9520-92d01dca0625"
},
{
"name": "juvenal-bot",
"html": "<b>j</b>uvenal-bot",
"groupId": "6e87dc72-4f1f-421f-9382-2fee8b652487"
}]}`)
})

if group, _, err := testClient.Group.Find(context.Background()); err != nil {
t.Errorf("Error given: %s", err)
} else if group == nil {
t.Error("Expected group. Group is nil")
} else if len(group) != 2 {
t.Errorf("Expected 2 groups. Group is %d", len(group))
}
}

func TestGroupService_Find_SuccessParams(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/api/3/groups/picker", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
testRequestURL(t, r, "/rest/api/3/groups/picker?maxResults=2&caseInsensitive=true&excludeId=1&excludeId=2&exclude=test&query=test&accountId=123")

fmt.Fprint(w, `{"header": "Showing 2 of 2 matching groups",
"total": 2,
"groups": [{
"name": "jdog-developers",
"html": "<b>j</b>dog-developers",
"groupId": "276f955c-63d7-42c8-9520-92d01dca0625"
},
{
"name": "juvenal-bot",
"html": "<b>j</b>uvenal-bot",
"groupId": "6e87dc72-4f1f-421f-9382-2fee8b652487"
}]}`)
})

if group, _, err := testClient.Group.Find(
context.Background(),
WithMaxResults(2),
WithCaseInsensitive(),
WithExcludedGroupsIds([]string{"1", "2"}),
WithExcludedGroupNames([]string{"test"}),
WithGroupNameContains("test"),
WithAccountId("123"),
); err != nil {
t.Errorf("Error given: %s", err)
} else if group == nil {
t.Error("Expected group. Group is nil")
} else if len(group) != 2 {
t.Errorf("Expected 2 groups. Group is %d", len(group))
}
}

func TestGroupService_GetGroupMembers_Success(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/api/3/group/member", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, http.MethodGet)
testRequestURL(t, r, "/rest/api/3/group/member?groupId=1&startAt=0&maxResults=2&includeInactiveUsers=true")

fmt.Fprint(w, `{
"self": "https://your-domain.atlassian.net/rest/api/3/group/member?groupname=jira-administrators&includeInactiveUsers=false&startAt=2&maxResults=2",
"nextPage": "https://your-domain.atlassian.net/rest/api/3/group/member?groupname=jira-administrators&includeInactiveUsers=false&startAt=4&maxResults=2",
"maxResults": 2,
"startAt": 3,
"total": 5,
"isLast": false,
"values": [
{
"self": "https://your-domain.atlassian.net/rest/api/3/user?accountId=5b10a2844c20165700ede21g",
"name": "",
"key": "",
"accountId": "5b10a2844c20165700ede21g",
"emailAddress": "[email protected]",
"avatarUrls": {},
"displayName": "Mia",
"active": true,
"timeZone": "Australia/Sydney",
"accountType": "atlassian"
},
{
"self": "https://your-domain.atlassian.net/rest/api/3/user?accountId=5b10a0effa615349cb016cd8",
"name": "",
"key": "",
"accountId": "5b10a0effa615349cb016cd8",
"emailAddress": "[email protected]",
"avatarUrls": {},
"displayName": "Will",
"active": false,
"timeZone": "Australia/Sydney",
"accountType": "atlassian"
}
]
}`)
})

if members, _, err := testClient.Group.GetGroupMembers(context.Background(), "1", WithStartAt(0), WithMaxResults(2), WithInactiveUsers()); err != nil {
t.Errorf("Error given: %s", err)
} else if len(members) != 2 {
t.Errorf("Expected 2 members. Members is %d", len(members))
} else if members[0].AccountID != "5b10a2844c20165700ede21g" {
t.Errorf("Expected 5b10a2844c20165700ede21g. Members[0].AccountId is %s", members[0].AccountID)
}
}
48 changes: 48 additions & 0 deletions cloud/project.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,17 @@ type ProjectList []struct {
IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"`
}

// Response body of https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-projects/#api-rest-api-2-project-search-get
type searchProjectsResponse struct {
Self string `json:"self,omitempty" structs:"self,omitempty"`
NextPage string `json:"nextPage,omitempty" structs:"nextPage,omitempty"`
MaxResults int `json:"maxResult,omitempty" structs:"maxResults,omitempty"`
StartAt int `json:"startAt,omitempty" structs:"startAt,omitempty"`
Total int `json:"total,omitempty" structs:"total,omitempty"`
IsLast bool `json:"isLast,omitempty" structs:"isLast,omitempty"`
Values []Project `json:"values,omitempty" structs:"values,omitempty"`
}

// ProjectCategory represents a single project category
type ProjectCategory struct {
Self string `json:"self" structs:"self,omitempty"`
Expand All @@ -52,6 +63,7 @@ type Project struct {
Roles map[string]string `json:"roles,omitempty" structs:"roles,omitempty"`
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"`
ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectCategory,omitempty"`
IsPrivate bool `json:"isPrivate,omitempty" structs:"isPrivate,omitempty"`
}

// ProjectComponent represents a single component of a project
Expand Down Expand Up @@ -87,6 +99,7 @@ type PermissionScheme struct {
//
// TODO Double check this method if this works as expected, is using the latest API and the response is complete
// This double check effort is done for v2 - Remove this two lines if this is completed.
// DEPRECATED: use Find instead
func (s *ProjectService) GetAll(ctx context.Context, options *GetQueryOptions) (*ProjectList, *Response, error) {
apiEndpoint := "rest/api/2/project"
req, err := s.client.NewRequest(ctx, http.MethodGet, apiEndpoint, nil)
Expand Down Expand Up @@ -161,3 +174,38 @@ func (s *ProjectService) GetPermissionScheme(ctx context.Context, projectID stri

return ps, resp, nil
}

// Find searches for project paginated info from Jira
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-projects/#api-rest-api-2-project-search-get
func (s *ProjectService) Find(ctx context.Context, tweaks ...searchF) ([]Project, *Response, error) {
apiEndpoint := "rest/api/2/project/search"

search := []searchParam{}
for _, f := range tweaks {
search = f(search)
}

queryString := ""
for _, param := range search {
queryString += fmt.Sprintf("%s=%s&", param.name, param.value)
}

if queryString != "" {
apiEndpoint += "?" + queryString
}

req, err := s.client.NewRequest(ctx, http.MethodGet, apiEndpoint, nil)
if err != nil {
return nil, nil, err
}

response := new(searchProjectsResponse)
resp, err := s.client.Do(req, response)
if err != nil {
jerr := NewJiraError(resp, err)
return nil, resp, jerr
}

return response.Values, resp, nil
}
Loading