mirror of
https://github.com/interviewstreet/go-jira.git
synced 2025-07-17 01:12:24 +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:
2
.gitignore
vendored
2
.gitignore
vendored
@ -22,3 +22,5 @@ _testmain.go
|
|||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.prof
|
*.prof
|
||||||
|
*.iml
|
||||||
|
.idea
|
||||||
|
@ -26,7 +26,7 @@ type Session struct {
|
|||||||
LastFailedLoginTime string `json:"lastFailedLoginTime"`
|
LastFailedLoginTime string `json:"lastFailedLoginTime"`
|
||||||
PreviousLoginTime string `json:"previousLoginTime"`
|
PreviousLoginTime string `json:"previousLoginTime"`
|
||||||
} `json:"loginInfo"`
|
} `json:"loginInfo"`
|
||||||
SetCoockie []*http.Cookie
|
Cookies []*http.Cookie
|
||||||
}
|
}
|
||||||
|
|
||||||
// AcquireSessionCookie creates a new session for a user in JIRA.
|
// AcquireSessionCookie creates a new session for a user in JIRA.
|
||||||
@ -53,9 +53,7 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
|
|||||||
|
|
||||||
session := new(Session)
|
session := new(Session)
|
||||||
resp, err := s.client.Do(req, session)
|
resp, err := s.client.Do(req, session)
|
||||||
|
session.Cookies = resp.Cookies()
|
||||||
cookies := resp.Cookies()
|
|
||||||
session.SetCoockie = cookies
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
|
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
|
||||||
|
206
issue.go
206
issue.go
@ -6,6 +6,9 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -34,7 +37,7 @@ type Attachment struct {
|
|||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty"`
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
Filename string `json:"filename,omitempty"`
|
Filename string `json:"filename,omitempty"`
|
||||||
Author *Assignee `json:"author,omitempty"`
|
Author *User `json:"author,omitempty"`
|
||||||
Created string `json:"created,omitempty"`
|
Created string `json:"created,omitempty"`
|
||||||
Size int `json:"size,omitempty"`
|
Size int `json:"size,omitempty"`
|
||||||
MimeType string `json:"mimeType,omitempty"`
|
MimeType string `json:"mimeType,omitempty"`
|
||||||
@ -64,12 +67,12 @@ type IssueFields struct {
|
|||||||
Resolutiondate string `json:"resolutiondate,omitempty"`
|
Resolutiondate string `json:"resolutiondate,omitempty"`
|
||||||
Created string `json:"created,omitempty"`
|
Created string `json:"created,omitempty"`
|
||||||
Watches *Watches `json:"watches,omitempty"`
|
Watches *Watches `json:"watches,omitempty"`
|
||||||
Assignee *Assignee `json:"assignee,omitempty"`
|
Assignee *User `json:"assignee,omitempty"`
|
||||||
Updated string `json:"updated,omitempty"`
|
Updated string `json:"updated,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
Creator *Assignee `json:"Creator,omitempty"`
|
Creator *User `json:"Creator,omitempty"`
|
||||||
Reporter *Assignee `json:"reporter,omitempty"`
|
Reporter *User `json:"reporter,omitempty"`
|
||||||
Components []*Component `json:"components,omitempty"`
|
Components []*Component `json:"components,omitempty"`
|
||||||
Status *Status `json:"status,omitempty"`
|
Status *Status `json:"status,omitempty"`
|
||||||
Progress *Progress `json:"progress,omitempty"`
|
Progress *Progress `json:"progress,omitempty"`
|
||||||
@ -92,15 +95,7 @@ type IssueType struct {
|
|||||||
IconURL string `json:"iconUrl,omitempty"`
|
IconURL string `json:"iconUrl,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Subtask bool `json:"subtask,omitempty"`
|
Subtask bool `json:"subtask,omitempty"`
|
||||||
}
|
AvatarID int `json:"avatarId,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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolution represents a resolution of a JIRA issue.
|
// Resolution represents a resolution of a JIRA issue.
|
||||||
@ -128,14 +123,24 @@ type Watches struct {
|
|||||||
IsWatching bool `json:"isWatching,omitempty"`
|
IsWatching bool `json:"isWatching,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assignee represents a user who is this JIRA issue assigned to.
|
// User represents a user who is this JIRA issue assigned to.
|
||||||
type Assignee struct {
|
type User struct {
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
|
Key string `json:"key,omitempty"`
|
||||||
EmailAddress string `json:"emailAddress,omitempty"`
|
EmailAddress string `json:"emailAddress,omitempty"`
|
||||||
AvatarURLs map[string]string `json:"avatarUrls,omitempty"`
|
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"`
|
||||||
DisplayName string `json:"displayName,omitempty"`
|
DisplayName string `json:"displayName,omitempty"`
|
||||||
Active bool `json:"active,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.
|
// Component represents a "component" of a JIRA issue.
|
||||||
@ -174,108 +179,68 @@ type Progress struct {
|
|||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// 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
|
// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
|
||||||
type Worklog struct {
|
type Worklog struct {
|
||||||
StartAt int `json:"startAt"`
|
StartAt int `json:"startAt"`
|
||||||
MaxResults int `json:"maxResults"`
|
MaxResults int `json:"maxResults"`
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
Worklogs []struct {
|
Worklogs []WorklogRecord `json:"worklogs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WorklogRecord represents one entry of a Worklog
|
||||||
|
type WorklogRecord struct {
|
||||||
Self string `json:"self"`
|
Self string `json:"self"`
|
||||||
Author struct {
|
Author User `json:"author"`
|
||||||
Self string `json:"self"`
|
UpdateAuthor User `json:"updateAuthor"`
|
||||||
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"`
|
Comment string `json:"comment"`
|
||||||
Created string `json:"created"`
|
Created Time `json:"created"`
|
||||||
Updated string `json:"updated"`
|
Updated Time `json:"updated"`
|
||||||
Started string `json:"started"`
|
Started Time `json:"started"`
|
||||||
TimeSpent string `json:"timeSpent"`
|
TimeSpent string `json:"timeSpent"`
|
||||||
TimeSpentSeconds int `json:"timeSpentSeconds"`
|
TimeSpentSeconds int `json:"timeSpentSeconds"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
IssueID string `json:"issueId"`
|
IssueID string `json:"issueId"`
|
||||||
} `json:"worklogs"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subtasks represents all issues of a parent issue.
|
||||||
type Subtasks struct {
|
type Subtasks struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Self string `json:"self"`
|
Self string `json:"self"`
|
||||||
Fields struct {
|
Fields IssueFields `json:"fields"`
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssueLink represents a link between two issues in JIRA.
|
// IssueLink represents a link between two issues in JIRA.
|
||||||
type IssueLink struct {
|
type IssueLink struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id,omitempty"`
|
||||||
Self string `json:"self"`
|
Self string `json:"self,omitempty"`
|
||||||
Type IssueLinkType `json:"type"`
|
Type IssueLinkType `json:"type"`
|
||||||
OutwardIssue Issue `json:"outwardIssue"`
|
OutwardIssue *Issue `json:"outwardIssue"`
|
||||||
InwardIssue Issue `json:"inwardIssue"`
|
InwardIssue *Issue `json:"inwardIssue"`
|
||||||
Comment Comment `json:"comment"`
|
Comment *Comment `json:"comment,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssueLinkType represents a type of a link between to issues in JIRA.
|
// IssueLinkType represents a type of a link between to issues in JIRA.
|
||||||
// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc.
|
// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc.
|
||||||
type IssueLinkType struct {
|
type IssueLinkType struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id,omitempty"`
|
||||||
Self string `json:"self"`
|
Self string `json:"self,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Inward string `json:"inward"`
|
Inward string `json:"inward"`
|
||||||
Outward string `json:"outward"`
|
Outward string `json:"outward"`
|
||||||
@ -283,11 +248,12 @@ type IssueLinkType struct {
|
|||||||
|
|
||||||
// Comment represents a comment by a person to an issue in JIRA.
|
// Comment represents a comment by a person to an issue in JIRA.
|
||||||
type Comment struct {
|
type Comment struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty"`
|
||||||
Author Assignee `json:"author,omitempty"`
|
Author User `json:"author,omitempty"`
|
||||||
Body string `json:"body,omitempty"`
|
Body string `json:"body,omitempty"`
|
||||||
UpdateAuthor Assignee `json:"updateAuthor,omitempty"`
|
UpdateAuthor User `json:"updateAuthor,omitempty"`
|
||||||
Updated string `json:"updated,omitempty"`
|
Updated string `json:"updated,omitempty"`
|
||||||
Created string `json:"created,omitempty"`
|
Created string `json:"created,omitempty"`
|
||||||
Visibility CommentVisibility `json:"visibility,omitempty"`
|
Visibility CommentVisibility `json:"visibility,omitempty"`
|
||||||
@ -312,6 +278,16 @@ type CommentVisibility struct {
|
|||||||
Value string `json:"value,omitempty"`
|
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.
|
// 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.
|
// JIRA will attempt to identify the issue by the issueIdOrKey path parameter.
|
||||||
// This can be an issue id, or an issue key.
|
// 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)
|
resp, err := s.client.Do(req, nil)
|
||||||
return resp, err
|
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{
|
Type: IssueLinkType{
|
||||||
Name: "Duplicate",
|
Name: "Duplicate",
|
||||||
},
|
},
|
||||||
InwardIssue: Issue{
|
InwardIssue: &Issue{
|
||||||
Key: "HSP-1",
|
Key: "HSP-1",
|
||||||
},
|
},
|
||||||
OutwardIssue: Issue{
|
OutwardIssue: &Issue{
|
||||||
Key: "MKY-1",
|
Key: "MKY-1",
|
||||||
},
|
},
|
||||||
Comment: Comment{
|
Comment: &Comment{
|
||||||
Body: "Linked related issue!",
|
Body: "Linked related issue!",
|
||||||
Visibility: CommentVisibility{
|
Visibility: CommentVisibility{
|
||||||
Type: "group",
|
Type: "group",
|
||||||
@ -309,3 +309,45 @@ func TestIssuePostAttachment_NoAttachment(t *testing.T) {
|
|||||||
t.Errorf("Error given: %s", err)
|
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
|
// Set session cookie if there is one
|
||||||
if c.session != nil {
|
if c.session != nil {
|
||||||
for _, cookie := range c.session.SetCoockie {
|
for _, cookie := range c.session.Cookies {
|
||||||
req.AddCookie(cookie)
|
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)
|
err := fmt.Errorf("Request failed. Please analyze the request body for more details. Status code: %d", r.StatusCode)
|
||||||
return err
|
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)
|
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) {
|
func TestProjectGetAll(t *testing.T) {
|
||||||
setup()
|
setup()
|
||||||
defer teardown()
|
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 {
|
if err != nil {
|
||||||
t.Error(err.Error())
|
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")
|
testMethod(t, r, "GET")
|
||||||
testRequestURL(t, r, testApiEdpoint)
|
testRequestURL(t, r, testAPIEdpoint)
|
||||||
fmt.Fprint(w, string(raw))
|
fmt.Fprint(w, string(raw))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -34,15 +34,15 @@ func TestProjectGetAll(t *testing.T) {
|
|||||||
func TestProjectGet(t *testing.T) {
|
func TestProjectGet(t *testing.T) {
|
||||||
setup()
|
setup()
|
||||||
defer teardown()
|
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 {
|
if err != nil {
|
||||||
t.Error(err.Error())
|
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")
|
testMethod(t, r, "GET")
|
||||||
testRequestURL(t, r, testApiEdpoint)
|
testRequestURL(t, r, testAPIEdpoint)
|
||||||
fmt.Fprint(w, string(raw))
|
fmt.Fprint(w, string(raw))
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -58,11 +58,11 @@ func TestProjectGet(t *testing.T) {
|
|||||||
func TestProjectGet_NoProject(t *testing.T) {
|
func TestProjectGet_NoProject(t *testing.T) {
|
||||||
setup()
|
setup()
|
||||||
defer teardown()
|
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")
|
testMethod(t, r, "GET")
|
||||||
testRequestURL(t, r, testApiEdpoint)
|
testRequestURL(t, r, testAPIEdpoint)
|
||||||
fmt.Fprint(w, nil)
|
fmt.Fprint(w, nil)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user