mirror of
https://github.com/interviewstreet/go-jira.git
synced 2025-02-13 13:48:28 +02:00
Merge remote-tracking branch 'origin/master' into develop
* origin/master: When creating issue links, the id and self should be omitted along with comment if none is provided Expose comment ID Make issue link direction visible using Time for WorklogRecord.Started Adjusted a few things to be in line with other methods go fmt go fmt, go doc and reuse of Project struct Renamed "json_mocks" into "mocks" Refactored struct types by reusing already existing components Fixed typo in Cookies Moved progect.go to project.go Fix #12: Expose the base JIRA URL update .gitignore using native time.Time updating with search and worklogs # Conflicts: # project_test.go
This commit is contained in:
commit
43018f069b
2
.gitignore
vendored
2
.gitignore
vendored
@ -22,3 +22,5 @@ _testmain.go
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
*.iml
|
||||
.idea
|
||||
|
@ -26,7 +26,7 @@ type Session struct {
|
||||
LastFailedLoginTime string `json:"lastFailedLoginTime"`
|
||||
PreviousLoginTime string `json:"previousLoginTime"`
|
||||
} `json:"loginInfo"`
|
||||
SetCoockie []*http.Cookie
|
||||
Cookies []*http.Cookie
|
||||
}
|
||||
|
||||
// AcquireSessionCookie creates a new session for a user in JIRA.
|
||||
@ -53,9 +53,7 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
|
||||
|
||||
session := new(Session)
|
||||
resp, err := s.client.Do(req, session)
|
||||
|
||||
cookies := resp.Cookies()
|
||||
session.SetCoockie = cookies
|
||||
session.Cookies = resp.Cookies()
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
|
||||
|
262
issue.go
262
issue.go
@ -6,6 +6,9 @@ import (
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -31,15 +34,15 @@ type Issue struct {
|
||||
|
||||
// Attachment represents a JIRA attachment
|
||||
type Attachment struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Author *Assignee `json:"author,omitempty"`
|
||||
Created string `json:"created,omitempty"`
|
||||
Size int `json:"size,omitempty"`
|
||||
MimeType string `json:"mimeType,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Thumbnail string `json:"thumbnail,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Filename string `json:"filename,omitempty"`
|
||||
Author *User `json:"author,omitempty"`
|
||||
Created string `json:"created,omitempty"`
|
||||
Size int `json:"size,omitempty"`
|
||||
MimeType string `json:"mimeType,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Thumbnail string `json:"thumbnail,omitempty"`
|
||||
}
|
||||
|
||||
// IssueFields represents single fields of a JIRA issue.
|
||||
@ -64,12 +67,12 @@ type IssueFields struct {
|
||||
Resolutiondate string `json:"resolutiondate,omitempty"`
|
||||
Created string `json:"created,omitempty"`
|
||||
Watches *Watches `json:"watches,omitempty"`
|
||||
Assignee *Assignee `json:"assignee,omitempty"`
|
||||
Assignee *User `json:"assignee,omitempty"`
|
||||
Updated string `json:"updated,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Summary string `json:"summary"`
|
||||
Creator *Assignee `json:"Creator,omitempty"`
|
||||
Reporter *Assignee `json:"reporter,omitempty"`
|
||||
Creator *User `json:"Creator,omitempty"`
|
||||
Reporter *User `json:"reporter,omitempty"`
|
||||
Components []*Component `json:"components,omitempty"`
|
||||
Status *Status `json:"status,omitempty"`
|
||||
Progress *Progress `json:"progress,omitempty"`
|
||||
@ -92,15 +95,7 @@ type IssueType struct {
|
||||
IconURL string `json:"iconUrl,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Subtask bool `json:"subtask,omitempty"`
|
||||
}
|
||||
|
||||
// Project represents a JIRA Project.
|
||||
type Project struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
AvatarURLs map[string]string `json:"avatarUrls,omitempty"`
|
||||
AvatarID int `json:"avatarId,omitempty"`
|
||||
}
|
||||
|
||||
// Resolution represents a resolution of a JIRA issue.
|
||||
@ -128,14 +123,24 @@ type Watches struct {
|
||||
IsWatching bool `json:"isWatching,omitempty"`
|
||||
}
|
||||
|
||||
// Assignee represents a user who is this JIRA issue assigned to.
|
||||
type Assignee struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
EmailAddress string `json:"emailAddress,omitempty"`
|
||||
AvatarURLs map[string]string `json:"avatarUrls,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Active bool `json:"active,omitempty"`
|
||||
// User represents a user who is this JIRA issue assigned to.
|
||||
type User struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
EmailAddress string `json:"emailAddress,omitempty"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Active bool `json:"active,omitempty"`
|
||||
TimeZone string `json:"timeZone,omitempty"`
|
||||
}
|
||||
|
||||
// AvatarUrls represents different dimensions of avatars / images
|
||||
type AvatarUrls struct {
|
||||
Four8X48 string `json:"48x48,omitempty"`
|
||||
Two4X24 string `json:"24x24,omitempty"`
|
||||
One6X16 string `json:"16x16,omitempty"`
|
||||
Three2X32 string `json:"32x32,omitempty"`
|
||||
}
|
||||
|
||||
// Component represents a "component" of a JIRA issue.
|
||||
@ -174,108 +179,68 @@ type Progress struct {
|
||||
Total int `json:"total"`
|
||||
}
|
||||
|
||||
// Worklog represents the work log of a JIRA issue.
|
||||
// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
|
||||
type Worklog struct {
|
||||
StartAt int `json:"startAt"`
|
||||
MaxResults int `json:"maxResults"`
|
||||
Total int `json:"total"`
|
||||
Worklogs []struct {
|
||||
Self string `json:"self"`
|
||||
Author struct {
|
||||
Self string `json:"self"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
EmailAddress string `json:"emailAddress"`
|
||||
AvatarUrls struct {
|
||||
Four8X48 string `json:"48x48"`
|
||||
Two4X24 string `json:"24x24"`
|
||||
One6X16 string `json:"16x16"`
|
||||
Three2X32 string `json:"32x32"`
|
||||
} `json:"avatarUrls"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Active bool `json:"active"`
|
||||
TimeZone string `json:"timeZone"`
|
||||
} `json:"author"`
|
||||
UpdateAuthor struct {
|
||||
Self string `json:"self"`
|
||||
Name string `json:"name"`
|
||||
Key string `json:"key"`
|
||||
EmailAddress string `json:"emailAddress"`
|
||||
AvatarUrls struct {
|
||||
Four8X48 string `json:"48x48"`
|
||||
Two4X24 string `json:"24x24"`
|
||||
One6X16 string `json:"16x16"`
|
||||
Three2X32 string `json:"32x32"`
|
||||
} `json:"avatarUrls"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Active bool `json:"active"`
|
||||
TimeZone string `json:"timeZone"`
|
||||
} `json:"updateAuthor"`
|
||||
Comment string `json:"comment"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Started string `json:"started"`
|
||||
TimeSpent string `json:"timeSpent"`
|
||||
TimeSpentSeconds int `json:"timeSpentSeconds"`
|
||||
ID string `json:"id"`
|
||||
IssueID string `json:"issueId"`
|
||||
} `json:"worklogs"`
|
||||
// Time represents the Time definition of JIRA as a time.Time of go
|
||||
type Time time.Time
|
||||
|
||||
// UnmarshalJSON will transform the JIRA time into a time.Time
|
||||
// during the transformation of the JIRA JSON response
|
||||
func (t *Time) UnmarshalJSON(b []byte) error {
|
||||
ti, err := time.Parse("\"2006-01-02T15:04:05.999-0700\"", string(b))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*t = Time(ti)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Worklog represents the work log of a JIRA issue.
|
||||
// One Worklog contains zero or n WorklogRecords
|
||||
// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
|
||||
type Worklog struct {
|
||||
StartAt int `json:"startAt"`
|
||||
MaxResults int `json:"maxResults"`
|
||||
Total int `json:"total"`
|
||||
Worklogs []WorklogRecord `json:"worklogs"`
|
||||
}
|
||||
|
||||
// WorklogRecord represents one entry of a Worklog
|
||||
type WorklogRecord struct {
|
||||
Self string `json:"self"`
|
||||
Author User `json:"author"`
|
||||
UpdateAuthor User `json:"updateAuthor"`
|
||||
Comment string `json:"comment"`
|
||||
Created Time `json:"created"`
|
||||
Updated Time `json:"updated"`
|
||||
Started Time `json:"started"`
|
||||
TimeSpent string `json:"timeSpent"`
|
||||
TimeSpentSeconds int `json:"timeSpentSeconds"`
|
||||
ID string `json:"id"`
|
||||
IssueID string `json:"issueId"`
|
||||
}
|
||||
|
||||
// Subtasks represents all issues of a parent issue.
|
||||
type Subtasks struct {
|
||||
ID string `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Self string `json:"self"`
|
||||
Fields struct {
|
||||
Summary string `json:"summary"`
|
||||
Status struct {
|
||||
Self string `json:"self"`
|
||||
Description string `json:"description"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
StatusCategory struct {
|
||||
Self string `json:"self"`
|
||||
ID int `json:"id"`
|
||||
Key string `json:"key"`
|
||||
ColorName string `json:"colorName"`
|
||||
Name string `json:"name"`
|
||||
} `json:"statusCategory"`
|
||||
} `json:"status"`
|
||||
Priority struct {
|
||||
Self string `json:"self"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id"`
|
||||
} `json:"priority"`
|
||||
Issuetype struct {
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id"`
|
||||
Description string `json:"description"`
|
||||
IconURL string `json:"iconUrl"`
|
||||
Name string `json:"name"`
|
||||
Subtask bool `json:"subtask"`
|
||||
AvatarID int `json:"avatarId"`
|
||||
} `json:"issuetype"`
|
||||
} `json:"fields"`
|
||||
ID string `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Self string `json:"self"`
|
||||
Fields IssueFields `json:"fields"`
|
||||
}
|
||||
|
||||
// IssueLink represents a link between two issues in JIRA.
|
||||
type IssueLink struct {
|
||||
ID string `json:"id"`
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
Type IssueLinkType `json:"type"`
|
||||
OutwardIssue Issue `json:"outwardIssue"`
|
||||
InwardIssue Issue `json:"inwardIssue"`
|
||||
Comment Comment `json:"comment"`
|
||||
OutwardIssue *Issue `json:"outwardIssue"`
|
||||
InwardIssue *Issue `json:"inwardIssue"`
|
||||
Comment *Comment `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// IssueLinkType represents a type of a link between to issues in JIRA.
|
||||
// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc.
|
||||
type IssueLinkType struct {
|
||||
ID string `json:"id"`
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Inward string `json:"inward"`
|
||||
Outward string `json:"outward"`
|
||||
@ -283,11 +248,12 @@ type IssueLinkType struct {
|
||||
|
||||
// Comment represents a comment by a person to an issue in JIRA.
|
||||
type Comment struct {
|
||||
ID string `json:"id,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Author Assignee `json:"author,omitempty"`
|
||||
Author User `json:"author,omitempty"`
|
||||
Body string `json:"body,omitempty"`
|
||||
UpdateAuthor Assignee `json:"updateAuthor,omitempty"`
|
||||
UpdateAuthor User `json:"updateAuthor,omitempty"`
|
||||
Updated string `json:"updated,omitempty"`
|
||||
Created string `json:"created,omitempty"`
|
||||
Visibility CommentVisibility `json:"visibility,omitempty"`
|
||||
@ -312,6 +278,16 @@ type CommentVisibility struct {
|
||||
Value string `json:"value,omitempty"`
|
||||
}
|
||||
|
||||
// searchResult is only a small wrapper arround the Search (with JQL) method
|
||||
// to be able to parse the results
|
||||
type searchResult struct {
|
||||
Issues []Issue `json:"issues"`
|
||||
}
|
||||
|
||||
// CustomFields represents custom fields of JIRA
|
||||
// This can heavily differ between JIRA instances
|
||||
type CustomFields map[string]string
|
||||
|
||||
// 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.
|
||||
@ -443,3 +419,49 @@ func (s *IssueService) AddLink(issueLink *IssueLink) (*http.Response, error) {
|
||||
resp, err := s.client.Do(req, nil)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Search will search for tickets according to the jql
|
||||
//
|
||||
// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues
|
||||
func (s *IssueService) Search(jql string) ([]Issue, *http.Response, error) {
|
||||
u := fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql))
|
||||
req, err := s.client.NewRequest("GET", u, nil)
|
||||
if err != nil {
|
||||
return []Issue{}, nil, err
|
||||
}
|
||||
|
||||
v := new(searchResult)
|
||||
resp, err := s.client.Do(req, v)
|
||||
return v.Issues, resp, err
|
||||
}
|
||||
|
||||
// GetCustomFields returns a map of customfield_* keys with string values
|
||||
func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *http.Response, error) {
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
|
||||
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
issue := new(map[string]interface{})
|
||||
resp, err := s.client.Do(req, issue)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
m := *issue
|
||||
f := m["fields"]
|
||||
cf := make(CustomFields)
|
||||
if f == nil {
|
||||
return cf, resp, nil
|
||||
}
|
||||
|
||||
if rec, ok := f.(map[string]interface{}); ok {
|
||||
for key, val := range rec {
|
||||
if strings.Contains(key, "customfield") {
|
||||
cf[key] = fmt.Sprint(val)
|
||||
}
|
||||
}
|
||||
}
|
||||
return cf, resp, nil
|
||||
}
|
||||
|
@ -94,13 +94,13 @@ func TestIssueAddLink(t *testing.T) {
|
||||
Type: IssueLinkType{
|
||||
Name: "Duplicate",
|
||||
},
|
||||
InwardIssue: Issue{
|
||||
InwardIssue: &Issue{
|
||||
Key: "HSP-1",
|
||||
},
|
||||
OutwardIssue: Issue{
|
||||
OutwardIssue: &Issue{
|
||||
Key: "MKY-1",
|
||||
},
|
||||
Comment: Comment{
|
||||
Comment: &Comment{
|
||||
Body: "Linked related issue!",
|
||||
Visibility: CommentVisibility{
|
||||
Type: "group",
|
||||
@ -309,3 +309,45 @@ func TestIssuePostAttachment_NoAttachment(t *testing.T) {
|
||||
t.Errorf("Error given: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue_Search(t *testing.T) {
|
||||
|
||||
setup()
|
||||
defer teardown()
|
||||
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
testRequestURL(t, r, "/rest/api/2/search?jql=something")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, `{"expand": "schema,names","startAt": 0,"maxResults": 50,"total": 6,"issues": [{"expand": "html","id": "10230","self": "http://kelpie9:8081/rest/api/2/issue/BULK-62","key": "BULK-62","fields": {"summary": "testing","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/5","id": "5","description": "The sub-task of the issue","iconUrl": "http://kelpie9:8081/images/icons/issue_subtask.gif","name": "Sub-task","subtask": true},"customfield_10071": null}},{"expand": "html","id": "10004","self": "http://kelpie9:8081/rest/api/2/issue/BULK-47","key": "BULK-47","fields": {"summary": "Cheese v1 2.0 issue","timetracking": null,"issuetype": {"self": "http://kelpie9:8081/rest/api/2/issuetype/3","id": "3","description": "A task that needs to be done.","iconUrl": "http://kelpie9:8081/images/icons/task.gif","name": "Task","subtask": false}}}]}`)
|
||||
})
|
||||
_, resp, err := testClient.Issue.Search("something")
|
||||
|
||||
if resp == nil {
|
||||
t.Errorf("Response given: %+v", resp)
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Error given: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func Test_CustomFields(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testMux.HandleFunc("/rest/api/2/issue/10002", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
testRequestURL(t, r, "/rest/api/2/issue/10002")
|
||||
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":{"customfield_123":"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}]},"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","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.Issue.GetCustomFields("10002")
|
||||
if err != nil {
|
||||
t.Errorf("Error given: %s", err)
|
||||
}
|
||||
if issue == nil {
|
||||
t.Error("Expected Customfields")
|
||||
}
|
||||
cf := issue["customfield_123"]
|
||||
if cf != "test" {
|
||||
t.Error("Expected \"test\" for custom field")
|
||||
}
|
||||
}
|
||||
|
8
jira.go
8
jira.go
@ -87,7 +87,7 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
|
||||
|
||||
// Set session cookie if there is one
|
||||
if c.session != nil {
|
||||
for _, cookie := range c.session.SetCoockie {
|
||||
for _, cookie := range c.session.Cookies {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
}
|
||||
@ -196,3 +196,9 @@ func CheckResponse(r *http.Response) error {
|
||||
err := fmt.Errorf("Request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetBaseURL will return you the Base URL.
|
||||
// This is the same URL as in the NewClient constructor
|
||||
func (c *Client) GetBaseURL() url.URL {
|
||||
return *c.baseURL
|
||||
}
|
||||
|
19
jira_test.go
19
jira_test.go
@ -277,3 +277,22 @@ func TestDo_RedirectLoop(t *testing.T) {
|
||||
t.Errorf("Expected a URL error; got %+v.", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetBaseURL_WithURL(t *testing.T) {
|
||||
u, err := url.Parse(testJIRAInstanceURL)
|
||||
if err != nil {
|
||||
t.Errorf("URL parsing -> Got an error: %s", err)
|
||||
}
|
||||
|
||||
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||
if err != nil {
|
||||
t.Errorf("Client creation -> Got an error: %s", err)
|
||||
}
|
||||
if c == nil {
|
||||
t.Error("Expected a client. Got none")
|
||||
}
|
||||
|
||||
if b := c.GetBaseURL(); !reflect.DeepEqual(b, *u) {
|
||||
t.Errorf("Base URLs are not equal. Expected %+v, got %+v", *u, b)
|
||||
}
|
||||
}
|
||||
|
109
project.go
Normal file
109
project.go
Normal file
@ -0,0 +1,109 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// ProjectService handles projects for the JIRA instance / API.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project
|
||||
type ProjectService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// ProjectList represent a list of Projects
|
||||
type ProjectList []struct {
|
||||
Expand string `json:"expand"`
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls"`
|
||||
ProjectTypeKey string `json:"projectTypeKey"`
|
||||
ProjectCategory ProjectCategory `json:"projectCategory,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectCategory represents a single project category
|
||||
type ProjectCategory struct {
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Project represents a JIRA Project.
|
||||
type Project struct {
|
||||
Expand string `json:"expand,omitempty"`
|
||||
Self string `json:"self,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
Lead User `json:"lead,omitempty"`
|
||||
Components []ProjectComponent `json:"components,omitempty"`
|
||||
IssueTypes []IssueType `json:"issueTypes,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
Email string `json:"email,omitempty"`
|
||||
AssigneeType string `json:"assigneeType,omitempty"`
|
||||
Versions []interface{} `json:"versions,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Roles struct {
|
||||
Developers string `json:"Developers,omitempty"`
|
||||
} `json:"roles,omitempty"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"`
|
||||
ProjectCategory ProjectCategory `json:"projectCategory,omitempty"`
|
||||
}
|
||||
|
||||
// ProjectComponent represents a single component of a project
|
||||
type ProjectComponent struct {
|
||||
Self string `json:"self"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Lead User `json:"lead"`
|
||||
AssigneeType string `json:"assigneeType"`
|
||||
Assignee User `json:"assignee"`
|
||||
RealAssigneeType string `json:"realAssigneeType"`
|
||||
RealAssignee User `json:"realAssignee"`
|
||||
IsAssigneeTypeValid bool `json:"isAssigneeTypeValid"`
|
||||
Project string `json:"project"`
|
||||
ProjectID int `json:"projectId"`
|
||||
}
|
||||
|
||||
// GetList gets all projects form JIRA
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getAllProjects
|
||||
func (s *ProjectService) GetList() (*ProjectList, *http.Response, error) {
|
||||
apiEndpoint := "rest/api/2/project"
|
||||
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
projectList := new(ProjectList)
|
||||
resp, err := s.client.Do(req, projectList)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return projectList, resp, nil
|
||||
}
|
||||
|
||||
// Get returns a full representation of the project for the given issue key.
|
||||
// JIRA will attempt to identify the project by the projectIdOrKey path parameter.
|
||||
// This can be an project id, or an project key.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
|
||||
func (s *ProjectService) Get(projectID string) (*Project, *http.Response, error) {
|
||||
apiEndpoint := fmt.Sprintf("/rest/api/2/project/%s", projectID)
|
||||
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
project := new(Project)
|
||||
resp, err := s.client.Do(req, project)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return project, resp, nil
|
||||
}
|
@ -10,15 +10,15 @@ import (
|
||||
func TestProjectGetAll(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testApiEdpoint := "/rest/api/2/project"
|
||||
testAPIEdpoint := "/rest/api/2/project"
|
||||
|
||||
raw, err := ioutil.ReadFile("./json_mocks/all_projects.json")
|
||||
raw, err := ioutil.ReadFile("./mocks/all_projects.json")
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
testMux.HandleFunc(testApiEdpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMux.HandleFunc(testAPIEdpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
testRequestURL(t, r, testApiEdpoint)
|
||||
testRequestURL(t, r, testAPIEdpoint)
|
||||
fmt.Fprint(w, string(raw))
|
||||
})
|
||||
|
||||
@ -34,15 +34,15 @@ func TestProjectGetAll(t *testing.T) {
|
||||
func TestProjectGet(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testApiEdpoint := "/rest/api/2/project/12310505"
|
||||
testAPIEdpoint := "/rest/api/2/project/12310505"
|
||||
|
||||
raw, err := ioutil.ReadFile("./json_mocks/project.json")
|
||||
raw, err := ioutil.ReadFile("./mocks/project.json")
|
||||
if err != nil {
|
||||
t.Error(err.Error())
|
||||
}
|
||||
testMux.HandleFunc(testApiEdpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMux.HandleFunc(testAPIEdpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
testRequestURL(t, r, testApiEdpoint)
|
||||
testRequestURL(t, r, testAPIEdpoint)
|
||||
fmt.Fprint(w, string(raw))
|
||||
})
|
||||
|
||||
@ -58,11 +58,11 @@ func TestProjectGet(t *testing.T) {
|
||||
func TestProjectGet_NoProject(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testApiEdpoint := "/rest/api/2/project/99999999"
|
||||
testAPIEdpoint := "/rest/api/2/project/99999999"
|
||||
|
||||
testMux.HandleFunc(testApiEdpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMux.HandleFunc(testAPIEdpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
testRequestURL(t, r, testApiEdpoint)
|
||||
testRequestURL(t, r, testAPIEdpoint)
|
||||
fmt.Fprint(w, nil)
|
||||
})
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user