1
0
mirror of https://github.com/interviewstreet/go-jira.git synced 2025-02-09 13:36:58 +02:00

Adding tests

This commit is contained in:
Douglas Chimento 2016-06-03 20:51:44 -04:00
commit 31fcf682f6
13 changed files with 10996 additions and 200 deletions

View File

@ -71,6 +71,9 @@ func main() {
### Authenticate with session cookie
Some actions require an authenticated user.
Here is an example with a session cookie authentification.
```go
package main
@ -102,6 +105,8 @@ func main() {
### Call a not implemented API endpoint
Not all API endpoints of the JIRA API are implemented into *go-jira*.
But you can call them anyway:
Lets get all public projects of [Atlassian`s JIRA instance](https://jira.atlassian.com/).
```go

View File

@ -2,6 +2,7 @@ package jira
import (
"fmt"
"net/http"
)
// AuthenticationService handles authentication for the JIRA instance / API.
@ -25,6 +26,7 @@ type Session struct {
LastFailedLoginTime string `json:"lastFailedLoginTime"`
PreviousLoginTime string `json:"previousLoginTime"`
} `json:"loginInfo"`
Cookies []*http.Cookie
}
// AcquireSessionCookie creates a new session for a user in JIRA.
@ -51,6 +53,8 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
session := new(Session)
resp, err := s.client.Do(req, session)
session.Cookies = resp.Cookies()
if err != nil {
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
}
@ -63,6 +67,14 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
return true, nil
}
// Authenticated reports if the current Client has an authenticated session with JIRA
func (s *AuthenticationService) Authenticated() bool {
if s != nil {
return s.client.session != nil
}
return false
}
// TODO Missing API Call GET (Returns information about the currently authenticated user's session)
// See https://docs.atlassian.com/jira/REST/latest/#auth/1/session
// TODO Missing API Call DELETE (Logs the current user out of JIRA, destroying the existing session, if any.)

View File

@ -36,6 +36,10 @@ func TestAcquireSessionCookie_Fail(t *testing.T) {
if res == true {
t.Error("Expected error, but result was true")
}
if testClient.Authentication.Authenticated() != false {
t.Error("Expected false, but result was true")
}
}
func TestAcquireSessionCookie_Success(t *testing.T) {
@ -65,4 +69,18 @@ func TestAcquireSessionCookie_Success(t *testing.T) {
if res == false {
t.Error("Expected result was true. Got false")
}
if testClient.Authentication.Authenticated() != true {
t.Error("Expected true, but result was false")
}
}
func TestAuthenticated_NotInit(t *testing.T) {
// Skip setup() because we don't want a fully setup client
testClient = new(Client)
// Test before we've attempted to authenticate
if testClient.Authentication.Authenticated() != false {
t.Error("Expected false, but result was true")
}
}

View File

@ -1,22 +0,0 @@
package jira
import (
"fmt"
"net/http"
)
// ErrorResponse reports one or more errors caused by an API request.
type ErrorResponse struct {
Response *http.Response // HTTP response that caused this error
ErrorMessages []string `json:"errorMessages,omitempty"`
Errors map[string]string `json:"errors,omitempty"`
}
func (r *ErrorResponse) Error() string {
if r.Response == nil {
return fmt.Sprintf("%v %+v", r.ErrorMessages, r.Errors)
}
return fmt.Sprintf("%v %v: %d %v %+v",
r.Response.Request.Method, r.Response.Request.URL,
r.Response.StatusCode, r.ErrorMessages, r.Errors)
}

View File

@ -1,82 +0,0 @@
package jira
import (
"net/http"
"net/url"
"testing"
)
func TestErrorResponse_Empty(t *testing.T) {
u, _ := url.Parse("https://issues.apache.org/jira/browse/MESOS-5040")
r := &http.Response{
Request: &http.Request{
Method: "POST",
URL: u,
},
StatusCode: 200,
}
mockData := []struct {
Response ErrorResponse
Expected string
}{
{
Response: ErrorResponse{},
Expected: "[] map[]",
},
{
Response: ErrorResponse{
ErrorMessages: []string{"foo", "bar"},
},
Expected: "[foo bar] map[]",
},
{
Response: ErrorResponse{
Errors: map[string]string{"Foo": "Bar"},
},
Expected: "[] map[Foo:Bar]",
},
{
Response: ErrorResponse{
ErrorMessages: []string{"foo", "bar"},
Errors: map[string]string{"Foo": "Bar"},
},
Expected: "[foo bar] map[Foo:Bar]",
},
{
Response: ErrorResponse{
Response: r,
},
Expected: "POST https://issues.apache.org/jira/browse/MESOS-5040: 200 [] map[]",
},
{
Response: ErrorResponse{
Response: r,
ErrorMessages: []string{"foo", "bar"},
},
Expected: "POST https://issues.apache.org/jira/browse/MESOS-5040: 200 [foo bar] map[]",
},
{
Response: ErrorResponse{
Response: r,
Errors: map[string]string{"Foo": "Bar"},
},
Expected: "POST https://issues.apache.org/jira/browse/MESOS-5040: 200 [] map[Foo:Bar]",
},
{
Response: ErrorResponse{
Response: r,
ErrorMessages: []string{"foo", "bar"},
Errors: map[string]string{"Foo": "Bar"},
},
Expected: "POST https://issues.apache.org/jira/browse/MESOS-5040: 200 [foo bar] map[Foo:Bar]",
},
}
for _, data := range mockData {
got := data.Response.Error()
if got != data.Expected {
t.Errorf("Response is different as expected. Expected \"%s\". Got \"%s\"", data.Expected, got)
}
}
}

252
issue.go
View File

@ -1,7 +1,10 @@
package jira
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/url"
"strings"
@ -29,7 +32,18 @@ type Issue struct {
Fields *IssueFields `json:"fields,omitempty"`
}
type Issues []*Issue
// Attachment represents a JIRA attachment
type Attachment struct {
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.
// Every JIRA issue has several fields attached.
@ -43,7 +57,6 @@ type IssueFields struct {
// * "aggregatetimeoriginalestimate": null,
// * "timeoriginalestimate": null,
// * "timetracking": {},
// * "attachment": [],
// * "aggregatetimeestimate": null,
// * "environment": null,
// * "duedate": null,
@ -54,22 +67,23 @@ 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"`
AggregateProgress *Progress `json:"aggregateprogress,omitempty"`
WorklogPage *WorklogPage `json:"worklog,omitempty"`
Worklog *Worklog `json:"worklog,omitempty"`
IssueLinks []*IssueLink `json:"issuelinks,omitempty"`
Comments []*Comment `json:"comment.comments,omitempty"`
FixVersions []*FixVersion `json:"fixVersions,omitempty"`
Labels []string `json:"labels,omitempty"`
SubTasks Issues `json:"subtasks,omitempty"`
Subtasks []*Subtasks `json:"subtasks,omitempty"`
Attachments []*Attachment `json:"attachment,omitempty"`
}
// IssueType represents a type of a JIRA issue.
@ -81,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.
@ -117,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.
@ -163,32 +179,8 @@ type Progress struct {
Total int `json:"total"`
}
type WorklogPage struct {
StartAt uint `json:"startAt"`
MaxResults uint `json:"maxResults"`
Total uint `json:"total"`
Worklogs []*Worklog `json:"worklogs"`
}
// 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 {
ID string `json:"id"`
Self string `json:"self"`
IssueId string `json:"issueId"`
TimeSpent string `json:"timeSpent"`
TimeSpentSeconds uint64 `json:"timeSpentSeconds"`
Comment string `json:"comment"`
Updated JiraTime `json:"updated"`
Created JiraTime `json:"created"`
Started JiraTime `json:"started"`
Author *Assignee `json:"author"`
}
type JiraTime time.Time
type CustomFields map[string]string
func (t *JiraTime) UnmarshalJSON(b []byte) error {
ti, err := time.Parse("\"2006-01-02T15:04:05.999-0700\"", string(b))
if err != nil {
@ -198,6 +190,39 @@ func (t *JiraTime) UnmarshalJSON(b []byte) error {
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 JiraTime `json:"created"`
Updated JiraTime `json:"updated"`
Started string `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 IssueFields `json:"fields"`
}
// IssueLink represents a link between two issues in JIRA.
type IssueLink struct {
ID string `json:"id"`
@ -220,14 +245,14 @@ type IssueLinkType struct {
// Comment represents a comment by a person to an issue in JIRA.
type Comment struct {
Self string `json:"self"`
Name string `json:"name"`
Author Assignee `json:"author"`
Body string `json:"body"`
UpdateAuthor Assignee `json:"updateAuthor"`
Updated string `json:"updated"`
Created string `json:"created"`
Visibility CommentVisibility `json:"visibility"`
Self string `json:"self,omitempty"`
Name string `json:"name,omitempty"`
Author User `json:"author,omitempty"`
Body string `json:"body,omitempty"`
UpdateAuthor User `json:"updateAuthor,omitempty"`
Updated string `json:"updated,omitempty"`
Created string `json:"created,omitempty"`
Visibility CommentVisibility `json:"visibility,omitempty"`
}
// FixVersion represents a software release in which an issue is fixed.
@ -245,12 +270,8 @@ type FixVersion struct {
// CommentVisibility represents he visibility of a comment.
// E.g. Type could be "role" and Value "Administrators"
type CommentVisibility struct {
Type string `json:"type"`
Value string `json:"value"`
}
type SearchResult struct {
Issues Issues `json:"issues"`
Type string `json:"type,omitempty"`
Value string `json:"value,omitempty"`
}
// Get returns a full representation of the issue for the given issue key.
@ -275,35 +296,60 @@ func (s *IssueService) Get(issueID string) (*Issue, *http.Response, error) {
return issue, resp, nil
}
// 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)
// DownloadAttachment returns a http.Response of an attachment for a given attachmentID.
// The attachment is in the http.Response.Body of the response.
// This is an io.ReadCloser.
// The caller should close the resp.Body.
func (s *IssueService) DownloadAttachment(attachmentID string) (*http.Response, error) {
apiEndpoint := fmt.Sprintf("secure/attachment/%s/", attachmentID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
return resp, err
}
return resp, nil
}
// PostAttachment uploads r (io.Reader) as an attachment to a given attachmentID
func (s *IssueService) PostAttachment(attachmentID string, r io.Reader, attachmentName string) (*[]Attachment, *http.Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", attachmentID)
b := new(bytes.Buffer)
writer := multipart.NewWriter(b)
fw, err := writer.CreateFormFile("file", attachmentName)
if err != nil {
return nil, nil, err
}
issue := new(map[string]interface{})
resp, err := s.client.Do(req, issue)
if r != nil {
// Copy the file
if _, err = io.Copy(fw, r); err != nil {
return nil, nil, err
}
}
writer.Close()
req, err := s.client.NewMultiPartRequest("POST", apiEndpoint, b)
if err != nil {
return nil, nil, err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
// PostAttachment response returns a JSON array (as multiple attachments can be posted)
attachment := new([]Attachment)
resp, err := s.client.Do(req, attachment)
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
return attachment, resp, nil
}
// Create creates an issue or a sub-task from a JSON representation.
@ -360,14 +406,50 @@ func (s *IssueService) AddLink(issueLink *IssueLink) (*http.Response, error) {
return resp, err
}
type searchResult struct {
Issues []Issue `json:"issues"`
}
// Search for tickets
// 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) (*SearchResult, error) {
func (s *IssueService) Search(jql string) ([]Issue, error) {
req, err := s.client.NewRequest("GET", "rest/api/2/search?jql="+url.QueryEscape(jql), nil)
if err != nil {
panic(err)
}
resp := new(SearchResult)
resp := new(searchResult)
_, err = s.client.Do(req, resp)
return resp, err
return resp.Issues, err
}
type CustomFields map[string]string
// 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

@ -2,7 +2,10 @@ package jira
import (
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
)
@ -116,3 +119,233 @@ func TestIssueAddLink(t *testing.T) {
t.Errorf("Error given: %s", err)
}
}
func TestIssueFields(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":{"labels":["test"],"watcher":{"self":"http://www.example.com/jira/rest/api/2/issue/EX-1/watchers","isWatching":false,"watchCount":1,"watchers":[{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false}]},"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.Get("10002")
if issue == nil {
t.Error("Expected issue. Issue is nil")
}
if !reflect.DeepEqual(issue.Fields.Labels, []string{"test"}) {
t.Error("Expected labels for the returned issue")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestIssueDownloadAttachment(t *testing.T) {
var testAttachment = "Here is an attachment"
setup()
defer teardown()
testMux.HandleFunc("/secure/attachment/", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, "/secure/attachment/10000/")
w.WriteHeader(http.StatusOK)
w.Write([]byte(testAttachment))
})
resp, err := testClient.Issue.DownloadAttachment("10000")
if resp == nil {
t.Error("Expected response. Response is nil")
}
defer resp.Body.Close()
attachment, err := ioutil.ReadAll(resp.Body)
if err != nil {
t.Error("Expected attachment text", err)
}
if string(attachment) != testAttachment {
t.Errorf("Expecting an attachment: %s", string(attachment))
}
if resp.StatusCode != 200 {
t.Errorf("Expected Status code 200. Given %d", resp.StatusCode)
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestIssueDownloadAttachment_BadStatus(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/secure/attachment/", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, "/secure/attachment/10000/")
w.WriteHeader(http.StatusForbidden)
})
resp, err := testClient.Issue.DownloadAttachment("10000")
if resp == nil {
t.Error("Expected response. Response is nil")
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusForbidden {
t.Errorf("Expected Status code %d. Given %d", http.StatusForbidden, resp.StatusCode)
}
if err == nil {
t.Errorf("Error expected")
}
}
func TestIssuePostAttachment(t *testing.T) {
var testAttachment = "Here is an attachment"
setup()
defer teardown()
testMux.HandleFunc("/rest/api/2/issue/10000/attachments", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, "/rest/api/2/issue/10000/attachments")
status := http.StatusOK
file, _, err := r.FormFile("file")
if err != nil {
status = http.StatusNotAcceptable
}
if file == nil {
status = http.StatusNoContent
} else {
// Read the file into memory
data, err := ioutil.ReadAll(file)
if err != nil {
status = http.StatusInternalServerError
}
if string(data) != testAttachment {
status = http.StatusNotAcceptable
}
w.WriteHeader(status)
fmt.Fprint(w, `[{"self":"http://jira/jira/rest/api/2/attachment/228924","id":"228924","filename":"example.jpg","author":{"self":"http://jira/jira/rest/api/2/user?username=test","name":"test","emailAddress":"test@test.com","avatarUrls":{"16x16":"http://jira/jira/secure/useravatar?size=small&avatarId=10082","48x48":"http://jira/jira/secure/useravatar?avatarId=10082"},"displayName":"Tester","active":true},"created":"2016-05-24T00:25:17.000-0700","size":32280,"mimeType":"image/jpeg","content":"http://jira/jira/secure/attachment/228924/example.jpg","thumbnail":"http://jira/jira/secure/thumbnail/228924/_thumb_228924.png"}]`)
file.Close()
}
})
reader := strings.NewReader(testAttachment)
issue, resp, err := testClient.Issue.PostAttachment("10000", reader, "attachment")
if issue == nil {
t.Error("Expected response. Response is nil")
}
if resp.StatusCode != 200 {
t.Errorf("Expected Status code 200. Given %d", resp.StatusCode)
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestIssuePostAttachment_NoResponse(t *testing.T) {
var testAttachment = "Here is an attachment"
setup()
defer teardown()
testMux.HandleFunc("/rest/api/2/issue/10000/attachments", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, "/rest/api/2/issue/10000/attachments")
w.WriteHeader(http.StatusOK)
})
reader := strings.NewReader(testAttachment)
_, _, err := testClient.Issue.PostAttachment("10000", reader, "attachment")
if err == nil {
t.Errorf("Error expected: %s", err)
}
}
func TestIssuePostAttachment_NoFilename(t *testing.T) {
var testAttachment = "Here is an attachment"
setup()
defer teardown()
testMux.HandleFunc("/rest/api/2/issue/10000/attachments", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, "/rest/api/2/issue/10000/attachments")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `[{"self":"http://jira/jira/rest/api/2/attachment/228924","id":"228924","filename":"example.jpg","author":{"self":"http://jira/jira/rest/api/2/user?username=test","name":"test","emailAddress":"test@test.com","avatarUrls":{"16x16":"http://jira/jira/secure/useravatar?size=small&avatarId=10082","48x48":"http://jira/jira/secure/useravatar?avatarId=10082"},"displayName":"Tester","active":true},"created":"2016-05-24T00:25:17.000-0700","size":32280,"mimeType":"image/jpeg","content":"http://jira/jira/secure/attachment/228924/example.jpg","thumbnail":"http://jira/jira/secure/thumbnail/228924/_thumb_228924.png"}]`)
})
reader := strings.NewReader(testAttachment)
_, _, err := testClient.Issue.PostAttachment("10000", reader, "")
if err != nil {
t.Errorf("Error expected: %s", err)
}
}
func TestIssuePostAttachment_NoAttachment(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/api/2/issue/10000/attachments", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, "/rest/api/2/issue/10000/attachments")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `[{"self":"http://jira/jira/rest/api/2/attachment/228924","id":"228924","filename":"example.jpg","author":{"self":"http://jira/jira/rest/api/2/user?username=test","name":"test","emailAddress":"test@test.com","avatarUrls":{"16x16":"http://jira/jira/secure/useravatar?size=small&avatarId=10082","48x48":"http://jira/jira/secure/useravatar?avatarId=10082"},"displayName":"Tester","active":true},"created":"2016-05-24T00:25:17.000-0700","size":32280,"mimeType":"image/jpeg","content":"http://jira/jira/secure/attachment/228924/example.jpg","thumbnail":"http://jira/jira/secure/thumbnail/228924/_thumb_228924.png"}]`)
})
_, _, err := testClient.Issue.PostAttachment("10000", nil, "attachment")
if err != nil {
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}}}]}`)
})
_ ,err := testClient.Issue.Search("something")
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")
}
}

55
jira.go
View File

@ -5,7 +5,6 @@ import (
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
)
@ -24,6 +23,7 @@ type Client struct {
// Services used for talking to different parts of the JIRA API.
Authentication *AuthenticationService
Issue *IssueService
Project *ProjectService
}
// NewClient returns a new JIRA API client.
@ -49,6 +49,7 @@ func NewClient(httpClient *http.Client, baseURL string) (*Client, error) {
}
c.Authentication = &AuthenticationService{client: c}
c.Issue = &IssueService{client: c}
c.Project = &ProjectService{client: c}
return c, nil
}
@ -83,6 +84,36 @@ 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.Cookies {
req.AddCookie(cookie)
}
}
return req, nil
}
// NewMultiPartRequest creates an API request including a multi-part file.
// A relative URL can be provided in urlStr, in which case it is resolved relative to the baseURL of the Client.
// Relative URLs should always be specified without a preceding slash.
// If specified, the value pointed to by buf is a multipart form.
func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (*http.Request, error) {
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
}
u := c.baseURL.ResolveReference(rel)
req, err := http.NewRequest(method, u.String(), buf)
if err != nil {
return nil, err
}
// Set required headers
req.Header.Set("X-Atlassian-Token", "nocheck")
// Set session cookie if there is one
if c.Authentication.Authenticated() {
req.Header.Set("Cookie", fmt.Sprintf("%s=%s", c.session.Session.Name, c.session.Session.Value))
}
@ -97,8 +128,6 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
return nil, err
}
defer resp.Body.Close()
err = CheckResponse(resp)
if err != nil {
// Even though there was an error, we still return the response
@ -107,6 +136,8 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
}
if v != nil {
// Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to
defer resp.Body.Close()
err = json.NewDecoder(resp.Body).Decode(v)
}
@ -115,17 +146,19 @@ func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
// CheckResponse checks the API response for errors, and returns them if present.
// A response is considered an error if it has a status code outside the 200 range.
// API error responses are expected to have either no response body, or a JSON response body that maps to ErrorResponse.
// Any other response body will be silently ignored.
// The caller is responsible to analyze the response body.
// The body can contain JSON (if the error is intended) or xml (sometimes JIRA just failes).
func CheckResponse(r *http.Response) error {
if c := r.StatusCode; 200 <= c && c <= 299 {
return nil
}
errorResponse := &ErrorResponse{Response: r}
data, err := ioutil.ReadAll(r.Body)
if err == nil && data != nil {
json.Unmarshal(data, errorResponse)
}
return errorResponse
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
}

View File

@ -215,6 +215,32 @@ func TestDo(t *testing.T) {
}
}
func TestDo_HTTPResponse(t *testing.T) {
setup()
defer teardown()
type foo struct {
A string
}
testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if m := "GET"; m != r.Method {
t.Errorf("Request method = %v, want %v", r.Method, m)
}
fmt.Fprint(w, `{"A":"a"}`)
})
req, _ := testClient.NewRequest("GET", "/", nil)
res, _ := testClient.Do(req, nil)
_, err := ioutil.ReadAll(res.Body)
if err != nil {
t.Errorf("Error on parsing HTTP Response = %v", err.Error())
} else if res.StatusCode != 200 {
t.Errorf("Response code = %v, want %v", res.StatusCode, 200)
}
}
func TestDo_HTTPError(t *testing.T) {
setup()
defer teardown()
@ -251,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)
}
}

9872
mocks/all_projects.json Normal file

File diff suppressed because it is too large Load Diff

411
mocks/project.json Normal file
View File

@ -0,0 +1,411 @@
{
"expand": "projectKeys",
"self": "https://issues.apache.org/jira/rest/api/2/project/12310505",
"id": "12310505",
"key": "ABDERA",
"description": "The Abdera project is an implementation of the Atom Syndication Format (RFC4287) and the Atom Publishing Protocol specifications published by the IETF Atompub working group.",
"lead": {
"self": "https://issues.apache.org/jira/rest/api/2/user?username=rooneg",
"key": "rooneg",
"name": "rooneg",
"avatarUrls": {
"48x48": "https://issues.apache.org/jira/secure/useravatar?avatarId=10452",
"24x24": "https://issues.apache.org/jira/secure/useravatar?size=small&avatarId=10452",
"16x16": "https://issues.apache.org/jira/secure/useravatar?size=xsmall&avatarId=10452",
"32x32": "https://issues.apache.org/jira/secure/useravatar?size=medium&avatarId=10452"
},
"displayName": "Garrett Rooney",
"active": true
},
"components": [],
"issueTypes": [
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/1",
"id": "1",
"description": "A problem which impairs or prevents the functions of the product.",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/bug.png",
"name": "Bug",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/2",
"id": "2",
"description": "A new feature of the product, which has yet to be developed.",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/newfeature.png",
"name": "New Feature",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/4",
"id": "4",
"description": "An improvement or enhancement to an existing feature or task.",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/improvement.png",
"name": "Improvement",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/6",
"id": "6",
"description": "A new unit, integration or system test.",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/requirement.png",
"name": "Test",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/5",
"id": "5",
"description": "General wishlist item.",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/improvement.png",
"name": "Wish",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/3",
"id": "3",
"description": "A task that needs to be done.",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/task.png",
"name": "Task",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/7",
"id": "7",
"description": "The sub-task of the issue",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/subtask_alternate.png",
"name": "Sub-task",
"subtask": true
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/8",
"id": "8",
"description": "A request for a new JIRA project to be set up",
"iconUrl": "https://issues.apache.org/jira/images/icons/jiraman18.gif",
"name": "New JIRA Project",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/9",
"id": "9",
"description": "An RTC request",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/newfeature.png",
"name": "RTC",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10",
"id": "10",
"description": "Challenges made against the Sun Compatibility Test Suite",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/task.png",
"name": "TCK Challenge",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/11",
"id": "11",
"description": "A formal question. Initially added for the Legal JIRA.",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/genericissue.png",
"name": "Question",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/12",
"id": "12",
"description": "",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/genericissue.png",
"name": "Temp",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/13",
"id": "13",
"description": "A place to record back and forth on notions not yet formed enough to make a 'New Feature' or 'Task'",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/genericissue.png",
"name": "Brainstorming",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/14",
"id": "14",
"description": "An overarching type made of sub-tasks",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/genericissue.png",
"name": "Umbrella",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/15",
"id": "15",
"description": "Created by JIRA Agile - do not edit or delete. Issue type for a big user story that needs to be broken down.",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/epic.png",
"name": "Epic",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/16",
"id": "16",
"description": "Created by JIRA Agile - do not edit or delete. Issue type for a user story.",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/story.png",
"name": "Story",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/17",
"id": "17",
"description": "A technical task.",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/task_agile.png",
"name": "Technical task",
"subtask": true
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/18",
"id": "18",
"description": "Upgrading a dependency to a newer version",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/improvement.png",
"name": "Dependency upgrade",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/19",
"id": "19",
"description": "A search for a suitable name for an Apache product",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/requirement.png",
"name": "Suitable Name Search",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/20",
"id": "20",
"description": "Documentation or Website",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/documentation.png",
"name": "Documentation",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10000",
"id": "10000",
"description": "Assigned specifically to Contractors by the VP Infra or or other VP/ Board Member.",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/sales.png",
"name": "Planned Work",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10100",
"id": "10100",
"description": "A request for a new Confluence Wiki to be set up",
"iconUrl": "https://issues.apache.org/jira/secure/viewavatar?size=xsmall&avatarId=23211&avatarType=issuetype",
"name": "New Confluence Wiki",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10200",
"id": "10200",
"description": "",
"iconUrl": "https://issues.apache.org/jira/secure/viewavatar?size=xsmall&avatarId=21140&avatarType=issuetype",
"name": "New Git Repo",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10201",
"id": "10201",
"description": "",
"iconUrl": "https://issues.apache.org/jira/secure/viewavatar?size=xsmall&avatarId=21130&avatarType=issuetype",
"name": "Github Integration",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10202",
"id": "10202",
"description": "",
"iconUrl": "https://issues.apache.org/jira/secure/viewavatar?size=xsmall&avatarId=21130&avatarType=issuetype",
"name": "New TLP ",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10204",
"id": "10204",
"description": "",
"iconUrl": "https://issues.apache.org/jira/secure/viewavatar?size=xsmall&avatarId=21130&avatarType=issuetype",
"name": "New TLP - Common Tasks",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10205",
"id": "10205",
"description": "",
"iconUrl": "https://issues.apache.org/jira/secure/viewavatar?size=xsmall&avatarId=21134&avatarType=issuetype",
"name": "SVN->GIT Migration",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10206",
"id": "10206",
"description": "",
"iconUrl": "https://issues.apache.org/jira/secure/viewavatar?size=xsmall&avatarId=21130&avatarType=issuetype",
"name": "Blog - New Blog Request",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10207",
"id": "10207",
"description": "",
"iconUrl": "https://issues.apache.org/jira/secure/viewavatar?size=xsmall&avatarId=21130&avatarType=issuetype",
"name": "Blogs - New Blog User Account Request",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10208",
"id": "10208",
"description": "",
"iconUrl": "https://issues.apache.org/jira/secure/viewavatar?size=xsmall&avatarId=21130&avatarType=issuetype",
"name": "Blogs - Access to Existing Blog",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10209",
"id": "10209",
"description": "",
"iconUrl": "https://issues.apache.org/jira/secure/viewavatar?size=xsmall&avatarId=21130&avatarType=issuetype",
"name": "New Bugzilla Project",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10210",
"id": "10210",
"description": "",
"iconUrl": "https://issues.apache.org/jira/secure/viewavatar?size=xsmall&avatarId=21130&avatarType=issuetype",
"name": "SVN->GIT Mirroring",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10300",
"id": "10300",
"description": "For general IT problems and questions. Created by JIRA Service Desk.",
"iconUrl": "https://issues.apache.org/jira/servicedesk/issue-type-icons?icon=it-help",
"name": "IT Help",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10301",
"id": "10301",
"description": "For new system accounts or passwords. Created by JIRA Service Desk.",
"iconUrl": "https://issues.apache.org/jira/servicedesk/issue-type-icons?icon=access",
"name": "Access",
"subtask": false
},
{
"self": "https://issues.apache.org/jira/rest/api/2/issuetype/10400",
"id": "10400",
"description": "",
"iconUrl": "https://issues.apache.org/jira/images/icons/issuetypes/genericissue.png",
"name": "Request",
"subtask": false
}
],
"url": "http://abdera.apache.org",
"assigneeType": "UNASSIGNED",
"versions": [
{
"self": "https://issues.apache.org/jira/rest/api/2/version/12312506",
"id": "12312506",
"name": "0.2.2",
"archived": false,
"released": true,
"releaseDate": "2007-02-19",
"userReleaseDate": "19/Feb/07",
"projectId": 12310505
},
{
"self": "https://issues.apache.org/jira/rest/api/2/version/12312507",
"id": "12312507",
"name": "0.3.0",
"archived": false,
"released": true,
"releaseDate": "2007-10-05",
"userReleaseDate": "05/Oct/07",
"projectId": 12310505
},
{
"self": "https://issues.apache.org/jira/rest/api/2/version/12312825",
"id": "12312825",
"name": "0.4.0",
"archived": false,
"released": true,
"releaseDate": "2008-04-11",
"userReleaseDate": "11/Apr/08",
"projectId": 12310505
},
{
"self": "https://issues.apache.org/jira/rest/api/2/version/12313105",
"id": "12313105",
"name": "1.0",
"archived": false,
"released": true,
"releaseDate": "2010-05-02",
"userReleaseDate": "02/May/10",
"projectId": 12310505
},
{
"self": "https://issues.apache.org/jira/rest/api/2/version/12314990",
"id": "12314990",
"name": "1.1",
"archived": false,
"released": true,
"releaseDate": "2010-07-11",
"userReleaseDate": "11/Jul/10",
"projectId": 12310505
},
{
"self": "https://issues.apache.org/jira/rest/api/2/version/12315922",
"id": "12315922",
"name": "1.1.1",
"archived": false,
"released": true,
"releaseDate": "2010-12-06",
"userReleaseDate": "06/Dec/10",
"projectId": 12310505
},
{
"self": "https://issues.apache.org/jira/rest/api/2/version/12316041",
"id": "12316041",
"name": "1.1.2",
"archived": false,
"released": true,
"releaseDate": "2011-01-15",
"userReleaseDate": "15/Jan/11",
"projectId": 12310505
},
{
"self": "https://issues.apache.org/jira/rest/api/2/version/12323557",
"id": "12323557",
"name": "1.1.3",
"archived": false,
"released": false,
"projectId": 12310505
},
{
"self": "https://issues.apache.org/jira/rest/api/2/version/12316141",
"id": "12316141",
"name": "1.2",
"archived": false,
"released": false,
"projectId": 12310505
}
],
"name": "Abdera",
"roles": {
"Service Desk Team": "https://issues.apache.org/jira/rest/api/2/project/12310505/role/10251",
"Developers": "https://issues.apache.org/jira/rest/api/2/project/12310505/role/10050",
"Service Desk Customers": "https://issues.apache.org/jira/rest/api/2/project/12310505/role/10250",
"Contributors": "https://issues.apache.org/jira/rest/api/2/project/12310505/role/10010",
"PMC": "https://issues.apache.org/jira/rest/api/2/project/12310505/role/10011",
"Committers": "https://issues.apache.org/jira/rest/api/2/project/12310505/role/10001",
"Administrators": "https://issues.apache.org/jira/rest/api/2/project/12310505/role/10002",
"ASF Members": "https://issues.apache.org/jira/rest/api/2/project/12310505/role/10150",
"Users": "https://issues.apache.org/jira/rest/api/2/project/12310505/role/10040"
},
"avatarUrls": {
"48x48": "https://issues.apache.org/jira/secure/projectavatar?pid=12310505&avatarId=10011",
"24x24": "https://issues.apache.org/jira/secure/projectavatar?size=small&pid=12310505&avatarId=10011",
"16x16": "https://issues.apache.org/jira/secure/projectavatar?size=xsmall&pid=12310505&avatarId=10011",
"32x32": "https://issues.apache.org/jira/secure/projectavatar?size=medium&pid=12310505&avatarId=10011"
}
}

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
}

80
project_test.go Normal file
View File

@ -0,0 +1,80 @@
package jira
import (
"fmt"
"io/ioutil"
"net/http"
"testing"
)
func TestProjectGetAll(t *testing.T) {
setup()
defer teardown()
testAPIEdpoint := "/rest/api/2/project"
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) {
testMethod(t, r, "GET")
testRequestURL(t, r, testAPIEdpoint)
fmt.Fprint(w, string(raw))
})
projects, _, err := testClient.Project.GetList()
if projects == nil {
t.Error("Expected project list. Project list is nil")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestProjectGet(t *testing.T) {
setup()
defer teardown()
testAPIEdpoint := "/rest/api/2/project/12310505"
raw, err := ioutil.ReadFile("./mocks/project.json")
if err != nil {
t.Error(err.Error())
}
testMux.HandleFunc(testAPIEdpoint, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, testAPIEdpoint)
fmt.Fprint(w, string(raw))
})
projects, _, err := testClient.Project.Get("12310505")
if projects == nil {
t.Error("Expected project list. Project list is nil")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestProjectGet_NoProject(t *testing.T) {
setup()
defer teardown()
testAPIEdpoint := "/rest/api/2/project/99999999"
testMux.HandleFunc(testAPIEdpoint, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, testAPIEdpoint)
fmt.Fprint(w, nil)
})
projects, resp, err := testClient.Project.Get("99999999")
if projects != nil {
t.Errorf("Expected nil. Got %+v", projects)
}
if resp.Status == "404" {
t.Errorf("Expected status 404. Got %s", resp.Status)
}
if err == nil {
t.Errorf("Error given: %s", err)
}
}