Skip to content

feat: search functions for group, project and roles #648

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

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
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