1
0
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:
Evgen Kostenko
2016-06-15 12:25:10 +03:00
10 changed files with 337 additions and 139 deletions

2
.gitignore vendored
View File

@ -22,3 +22,5 @@ _testmain.go
*.exe *.exe
*.test *.test
*.prof *.prof
*.iml
.idea

View File

@ -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
View File

@ -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
}

View File

@ -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")
}
}

View File

@ -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
}

View File

@ -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
View 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
}

View File

@ -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)
}) })