diff --git a/group.go b/group.go index 72cd9ce0..9e2ecafb 100644 --- a/group.go +++ b/group.go @@ -2,6 +2,7 @@ package jira import ( "fmt" + "net/url" ) // GroupService handles Groups for the JIRA instance / API. @@ -48,13 +49,21 @@ type GroupMember struct { TimeZone string `json:"timeZone,omitempty"` } +type GroupSearchOptions struct { + StartAt int + MaxResults int + IncludeInactiveUsers bool +} + // Get returns a paginated list of users who are 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. // // JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup +// +// WARNING: This API only returns the first page of group members func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) { - apiEndpoint := fmt.Sprintf("/rest/api/2/group/member?groupname=%s", name) + apiEndpoint := fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name)) req, err := s.client.NewRequest("GET", apiEndpoint, nil) if err != nil { return nil, nil, err @@ -69,6 +78,37 @@ func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) { return group.Members, resp, nil } +// 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. +// +// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup +func (s *GroupService) GetWithOptions(name string, options *GroupSearchOptions) ([]GroupMember, *Response, error) { + var apiEndpoint string + if options == nil { + apiEndpoint = fmt.Sprintf("/rest/api/2/group/member?groupname=%s", url.QueryEscape(name)) + } else { + apiEndpoint = fmt.Sprintf( + "/rest/api/2/group/member?groupname=%s&startAt=%d&maxResults=%d&includeInactiveUsers=%t", + url.QueryEscape(name), + options.StartAt, + options.MaxResults, + options.IncludeInactiveUsers, + ) + } + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + if err != nil { + return nil, nil, err + } + + group := new(groupMembersResult) + resp, err := s.client.Do(req, group) + if err != nil { + return nil, resp, err + } + return group.Members, resp, nil +} + // Add adds user to group // // JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/group-addUserToGroup diff --git a/group_test.go b/group_test.go index d6698bce..e4503ddd 100644 --- a/group_test.go +++ b/group_test.go @@ -21,6 +21,61 @@ func TestGroupService_Get(t *testing.T) { } } +func TestGroupService_GetPage(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/api/2/group/member", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testRequestURL(t, r, "/rest/api/2/group/member?groupname=default") + startAt := r.URL.Query().Get("startAt") + if startAt == "0" { + fmt.Fprint(w, `{"self":"http://www.example.com/jira/rest/api/2/group/member?includeInactiveUsers=false&maxResults=2&groupname=default&startAt=0","nextPage":"`+testServer.URL+`/rest/api/2/group/member?groupname=default&includeInactiveUsers=false&maxResults=2&startAt=2","maxResults":2,"startAt":0,"total":4,"isLast":false,"values":[{"self":"http://www.example.com/jira/rest/api/2/user?username=michael","name":"michael","key":"michael","emailAddress":"michael@example.com","displayName":"MichaelScofield","active":true,"timeZone":"Australia/Sydney"},{"self":"http://www.example.com/jira/rest/api/2/user?username=alex","name":"alex","key":"alex","emailAddress":"alex@example.com","displayName":"AlexanderMahone","active":true,"timeZone":"Australia/Sydney"}]}`) + } else if startAt == "2" { + fmt.Fprint(w, `{"self":"http://www.example.com/jira/rest/api/2/group/member?includeInactiveUsers=false&maxResults=2&groupname=default&startAt=2","maxResults":2,"startAt":2,"total":4,"isLast":true,"values":[{"self":"http://www.example.com/jira/rest/api/2/user?username=michael","name":"michael","key":"michael","emailAddress":"michael@example.com","displayName":"MichaelScofield","active":true,"timeZone":"Australia/Sydney"},{"self":"http://www.example.com/jira/rest/api/2/user?username=alex","name":"alex","key":"alex","emailAddress":"alex@example.com","displayName":"AlexanderMahone","active":true,"timeZone":"Australia/Sydney"}]}`) + } else { + t.Errorf("startAt %s", startAt) + } + }) + if page, resp, err := testClient.Group.GetWithOptions("default", &GroupSearchOptions{ + StartAt: 0, + MaxResults: 2, + IncludeInactiveUsers: false, + }); err != nil { + t.Errorf("Error given: %s %s", err, testServer.URL) + } else if page == nil || len(page) != 2 { + t.Error("Expected members. Group.Members is not 2 or is nil") + } else { + if resp.StartAt != 0 { + t.Errorf("Expect Result StartAt to be 0, but is %d", resp.StartAt) + } + if resp.MaxResults != 2 { + t.Errorf("Expect Result MaxResults to be 2, but is %d", resp.MaxResults) + } + if resp.Total != 4 { + t.Errorf("Expect Result Total to be 4, but is %d", resp.Total) + } + if page, resp, err := testClient.Group.GetWithOptions("default", &GroupSearchOptions{ + StartAt: 2, + MaxResults: 2, + IncludeInactiveUsers: false, + }); err != nil { + t.Errorf("Error give: %s %s", err, testServer.URL) + } else if page == nil || len(page) != 2 { + t.Error("Expected members. Group.Members is not 2 or is nil") + } else { + if resp.StartAt != 2 { + t.Errorf("Expect Result StartAt to be 2, but is %d", resp.StartAt) + } + if resp.MaxResults != 2 { + t.Errorf("Expect Result MaxResults to be 2, but is %d", resp.MaxResults) + } + if resp.Total != 4 { + t.Errorf("Expect Result Total to be 4, but is %d", resp.Total) + } + } + } +} + func TestGroupService_Add(t *testing.T) { setup() defer teardown() diff --git a/issue.go b/issue.go index ee586e08..c29b7b0b 100644 --- a/issue.go +++ b/issue.go @@ -127,6 +127,7 @@ type IssueFields struct { Subtasks []*Subtasks `json:"subtasks,omitempty" structs:"subtasks,omitempty"` Attachments []*Attachment `json:"attachment,omitempty" structs:"attachment,omitempty"` Epic *Epic `json:"epic,omitempty" structs:"epic,omitempty"` + Sprint *Sprint `json:"sprint,omitempty" structs:"sprint,omitempty"` Parent *Parent `json:"parent,omitempty" structs:"parent,omitempty"` Unknowns tcontainer.MarshalMap } diff --git a/jira.go b/jira.go index f0699243..10703795 100644 --- a/jira.go +++ b/jira.go @@ -280,6 +280,10 @@ func (r *Response) populatePageValues(v interface{}) { r.StartAt = value.StartAt r.MaxResults = value.MaxResults r.Total = value.Total + case *groupMembersResult: + r.StartAt = value.StartAt + r.MaxResults = value.MaxResults + r.Total = value.Total } return } diff --git a/sprint.go b/sprint.go index a617e017..65ba873a 100644 --- a/sprint.go +++ b/sprint.go @@ -2,6 +2,7 @@ package jira import ( "fmt" + "github.com/google/go-querystring/query" ) // SprintService handles sprints in JIRA Agile API. @@ -65,3 +66,41 @@ func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, er return result.Issues, resp, err } + +// Get returns a full representation of the issue for the given issue key. +// JIRA will attempt to identify the issue by the issueIdOrKey path parameter. +// This can be an issue id, or an issue key. +// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved. +// +// The given options will be appended to the query string +// +// JIRA API docs: https://docs.atlassian.com/jira-software/REST/7.3.1/#agile/1.0/issue-getIssue +// +// TODO: create agile service for holding all agile apis' implementation +func (s *SprintService) GetIssue(issueID string, options *GetQueryOptions) (*Issue, *Response, error) { + apiEndpoint := fmt.Sprintf("rest/agile/1.0/issue/%s", issueID) + + req, err := s.client.NewRequest("GET", apiEndpoint, nil) + + if err != nil { + return nil, nil, err + } + + if options != nil { + q, err := query.Values(options) + if err != nil { + return nil, nil, err + } + req.URL.RawQuery = q.Encode() + } + + issue := new(Issue) + resp, err := s.client.Do(req, issue) + + if err != nil { + jerr := NewJiraError(resp, err) + return nil, resp, jerr + } + + return issue, resp, nil +} diff --git a/sprint_test.go b/sprint_test.go index 40f05465..6b5c5bdd 100644 --- a/sprint_test.go +++ b/sprint_test.go @@ -5,6 +5,7 @@ import ( "fmt" "io/ioutil" "net/http" + "reflect" "testing" ) @@ -65,3 +66,31 @@ func TestSprintService_GetIssuesForSprint(t *testing.T) { } } + +func TestSprintService_GetIssue(t *testing.T) { + setup() + defer teardown() + + testAPIEndpoint := "/rest/agile/1.0/issue/10002" + + testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + testRequestURL(t, r, testAPIEndpoint) + fmt.Fprint(w, `{"expand":"renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations","id":"10002","self":"http://www.example.com/jira/rest/api/2/issue/10002","key":"EX-1","fields":{"labels":["test"],"watcher":{"self":"http://www.example.com/jira/rest/api/2/issue/EX-1/watchers","isWatching":false,"watchCount":1,"watchers":[{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false}]},"sprint": {"id": 37,"self": "http://www.example.com/jira/rest/agile/1.0/sprint/13", "state": "future", "name": "sprint 2"}, "epic": {"id": 19415,"key": "EPIC-77","self": "https://example.atlassian.net/rest/agile/1.0/epic/19415","name": "Epic Name","summary": "Do it","color": {"key": "color_11"},"done": false},"attachment":[{"self":"http://www.example.com/jira/rest/api/2.0/attachments/10000","filename":"picture.jpg","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","avatarUrls":{"48x48":"http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred","24x24":"http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred","16x16":"http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred","32x32":"http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred"},"displayName":"Fred F. User","active":false},"created":"2016-03-16T04:22:37.461+0000","size":23123,"mimeType":"image/jpeg","content":"http://www.example.com/jira/attachments/10000","thumbnail":"http://www.example.com/jira/secure/thumbnail/10000"}],"sub-tasks":[{"id":"10000","type":{"id":"10000","name":"","inward":"Parent","outward":"Sub-task"},"outwardIssue":{"id":"10003","key":"EX-2","self":"http://www.example.com/jira/rest/api/2/issue/EX-2","fields":{"status":{"iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png","name":"Open"}}}}],"description":"example bug report","project":{"self":"http://www.example.com/jira/rest/api/2/project/EX","id":"10000","key":"EX","name":"Example","avatarUrls":{"48x48":"http://www.example.com/jira/secure/projectavatar?size=large&pid=10000","24x24":"http://www.example.com/jira/secure/projectavatar?size=small&pid=10000","16x16":"http://www.example.com/jira/secure/projectavatar?size=xsmall&pid=10000","32x32":"http://www.example.com/jira/secure/projectavatar?size=medium&pid=10000"},"projectCategory":{"self":"http://www.example.com/jira/rest/api/2/projectCategory/10000","id":"10000","name":"FIRST","description":"First Project Category"}},"comment":{"comments":[{"self":"http://www.example.com/jira/rest/api/2/issue/10010/comment/10000","id":"10000","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque eget venenatis elit. Duis eu justo eget augue iaculis fermentum. Sed semper quam laoreet nisi egestas at posuere augue semper.","updateAuthor":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"created":"2016-03-16T04:22:37.356+0000","updated":"2016-03-16T04:22:37.356+0000","visibility":{"type":"role","value":"Administrators"}}]},"issuelinks":[{"id":"10001","type":{"id":"10000","name":"Dependent","inward":"depends on","outward":"is depended by"},"outwardIssue":{"id":"10004L","key":"PRJ-2","self":"http://www.example.com/jira/rest/api/2/issue/PRJ-2","fields":{"status":{"iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png","name":"Open"}}}},{"id":"10002","type":{"id":"10000","name":"Dependent","inward":"depends on","outward":"is depended by"},"inwardIssue":{"id":"10004","key":"PRJ-3","self":"http://www.example.com/jira/rest/api/2/issue/PRJ-3","fields":{"status":{"iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png","name":"Open"}}}}],"worklog":{"worklogs":[{"self":"http://www.example.com/jira/rest/api/2/issue/10010/worklog/10000","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"updateAuthor":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"comment":"I did some work here.","updated":"2016-03-16T04:22:37.471+0000","visibility":{"type":"group","value":"jira-developers"},"started":"2016-03-16T04:22:37.471+0000","timeSpent":"3h 20m","timeSpentSeconds":12000,"id":"100028","issueId":"10002"}]},"updated":"2016-04-06T02:36:53.594-0700","duedate":"2018-01-19","timetracking":{"originalEstimate":"10m","remainingEstimate":"3m","timeSpent":"6m","originalEstimateSeconds":600,"remainingEstimateSeconds":200,"timeSpentSeconds":400}},"names":{"watcher":"watcher","attachment":"attachment","sub-tasks":"sub-tasks","description":"description","project":"project","comment":"comment","issuelinks":"issuelinks","worklog":"worklog","updated":"updated","timetracking":"timetracking"},"schema":{}}`) + }) + + issue, _, err := testClient.Sprint.GetIssue("10002", nil) + if issue == nil { + t.Errorf("Expected issue. Issue is nil %v", err) + } + if !reflect.DeepEqual(issue.Fields.Labels, []string{"test"}) { + t.Error("Expected labels for the returned issue") + } + if len(issue.Fields.Comments.Comments) != 1 { + t.Errorf("Expected one comment, %v found", len(issue.Fields.Comments.Comments)) + } + if err != nil { + t.Errorf("Error given: %s", err) + } + +}