1
0
mirror of https://github.com/interviewstreet/go-jira.git synced 2024-11-24 08:22:42 +02:00
# Conflicts:
#	issue.go
This commit is contained in:
Evgen Kostenko 2016-06-01 16:08:30 +03:00
commit c75a2ca567
7 changed files with 308 additions and 124 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

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

View File

@ -1,7 +1,10 @@
package jira
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
)
@ -26,6 +29,19 @@ type Issue struct {
Fields *IssueFields `json:"fields,omitempty"`
}
// Attachment represents a JIRA attachment
type Attachment struct {
Self string `json:"self,omitempty"`
ID string `json:"id,omitempty"`
Filename string `json:"filename,omitempty"`
Author *Assignee `json:"author,omitempty"`
Created string `json:"created,omitempty"`
Size int `json:"size,omitempty"`
MimeType string `json:"mimeType,omitempty"`
Content string `json:"content,omitempty"`
Thumbnail string `json:"thumbnail,omitempty"`
}
// IssueFields represents single fields of a JIRA issue.
// Every JIRA issue has several fields attached.
type IssueFields struct {
@ -38,7 +54,6 @@ type IssueFields struct {
// * "aggregatetimeoriginalestimate": null,
// * "timeoriginalestimate": null,
// * "timetracking": {},
// * "attachment": [],
// * "aggregatetimeestimate": null,
// * "environment": null,
// * "duedate": null,
@ -65,6 +80,7 @@ type IssueFields struct {
FixVersions []*FixVersion `json:"fixVersions,omitempty"`
Labels []string `json:"labels,omitempty"`
Subtasks []*Subtasks `json:"subtasks,omitempty"`
Attachments []*Attachment `json:"attachment,omitempty"`
}
// IssueType represents a type of a JIRA issue.
@ -267,14 +283,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 Assignee `json:"author,omitempty"`
Body string `json:"body,omitempty"`
UpdateAuthor Assignee `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.
@ -292,8 +308,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 string `json:"type,omitempty"`
Value string `json:"value,omitempty"`
}
// Get returns a full representation of the issue for the given issue key.
@ -318,6 +334,62 @@ func (s *IssueService) Get(issueID string) (*Issue, *http.Response, error) {
return issue, resp, nil
}
// 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
}
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
}
return attachment, resp, nil
}
// Create creates an issue or a sub-task from a JSON representation.
// Creating a sub-task is similar to creating a regular issue, with two important differences:
// The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue.

View File

@ -2,8 +2,10 @@ package jira
import (
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
)
@ -135,6 +137,174 @@ func TestIssueFields(t *testing.T) {
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)
}

41
jira.go
View File

@ -4,7 +4,6 @@ import (
"bytes"
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
)
@ -92,6 +91,34 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
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))
}
return req, nil
}
// Do sends an API request and returns the API response.
// The API response is JSON decoded and stored in the value pointed to by v, or returned as an error if an API error has occurred.
func (c *Client) Do(req *http.Request, v interface{}) (*http.Response, error) {
@ -118,17 +145,13 @@ 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
}