Browse Source

Add go-jira dependency

pull/17/head
Kegan Dougal 8 years ago
parent
commit
4bd5ca3003
  1. 6
      vendor/manifest
  2. 22
      vendor/src/github.com/andygrunwald/go-jira/LICENSE
  3. 172
      vendor/src/github.com/andygrunwald/go-jira/README.md
  4. 81
      vendor/src/github.com/andygrunwald/go-jira/authentication.go
  5. 86
      vendor/src/github.com/andygrunwald/go-jira/authentication_test.go
  6. 155
      vendor/src/github.com/andygrunwald/go-jira/board.go
  7. 186
      vendor/src/github.com/andygrunwald/go-jira/board_test.go
  8. BIN
      vendor/src/github.com/andygrunwald/go-jira/img/go-jira-compressed.png
  9. 574
      vendor/src/github.com/andygrunwald/go-jira/issue.go
  10. 467
      vendor/src/github.com/andygrunwald/go-jira/issue_test.go
  11. 224
      vendor/src/github.com/andygrunwald/go-jira/jira.go
  12. 390
      vendor/src/github.com/andygrunwald/go-jira/jira_test.go
  13. 43
      vendor/src/github.com/andygrunwald/go-jira/mocks/all_boards.json
  14. 25
      vendor/src/github.com/andygrunwald/go-jira/mocks/all_boards_filtered.json
  15. 9872
      vendor/src/github.com/andygrunwald/go-jira/mocks/all_projects.json
  16. 115
      vendor/src/github.com/andygrunwald/go-jira/mocks/issues_in_sprint.json
  17. 411
      vendor/src/github.com/andygrunwald/go-jira/mocks/project.json
  18. 46
      vendor/src/github.com/andygrunwald/go-jira/mocks/sprints.json
  19. 101
      vendor/src/github.com/andygrunwald/go-jira/mocks/transitions.json
  20. 120
      vendor/src/github.com/andygrunwald/go-jira/project.go
  21. 80
      vendor/src/github.com/andygrunwald/go-jira/project_test.go
  22. 60
      vendor/src/github.com/andygrunwald/go-jira/sprint.go
  23. 67
      vendor/src/github.com/andygrunwald/go-jira/sprint_test.go

6
vendor/manifest

@ -7,6 +7,12 @@
"revision": "a283a10442df8dc09befd873fab202bf8a253d6a",
"branch": "master"
},
{
"importpath": "github.com/andygrunwald/go-jira",
"repository": "https://github.com/andygrunwald/go-jira",
"revision": "ae45380959ecd26b9c0adcae4c194b7e2253e214",
"branch": "master"
},
{
"importpath": "github.com/google/go-github/github",
"repository": "https://github.com/google/go-github",

22
vendor/src/github.com/andygrunwald/go-jira/LICENSE

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2015 Andy Grunwald
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

172
vendor/src/github.com/andygrunwald/go-jira/README.md

@ -0,0 +1,172 @@
# go-jira
[![GoDoc](https://godoc.org/github.com/andygrunwald/go-jira?status.svg)](https://godoc.org/github.com/andygrunwald/go-jira)
[![Build Status](https://travis-ci.org/andygrunwald/go-jira.svg?branch=master)](https://travis-ci.org/andygrunwald/go-jira)
[![Go Report Card](https://goreportcard.com/badge/github.com/andygrunwald/go-jira)](https://goreportcard.com/report/github.com/andygrunwald/go-jira)
[![Coverage Status](https://coveralls.io/repos/github/andygrunwald/go-jira/badge.svg?branch=master)](https://coveralls.io/github/andygrunwald/go-jira?branch=master)
[Go](https://golang.org/) client library for [Atlassian JIRA](https://www.atlassian.com/software/jira).
![Go client library for Atlassian JIRA](./img/go-jira-compressed.png "Go client library for Atlassian JIRA.")
## Features
* Authentication (HTTP Basic, OAuth, Session Cookie)
* Create and receive issues
* Create and retrieve issue transitions (status updates)
* Call every API endpoint of the JIRA, even it is not directly implemented in this library
This package is not JIRA API complete (yet), but you can call every API endpoint you want. See [Call a not implemented API endpoint](#call-a-not-implemented-api-endpoint) how to do this. For all possible API endpoints of JRIA have a look at [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/).
## Compatible JIRA versions
This package was tested against JIRA v6.3.4 and v7.1.2.
## Installation
It is go gettable
$ go get github.com/andygrunwald/go-jira
(optional) to run unit / example tests:
$ cd $GOPATH/src/github.com/andygrunwald/go-jira
$ go test -v ./...
## API
Please have a look at the [GoDoc documentation](https://godoc.org/github.com/andygrunwald/go-jira) for a detailed API description.
The [latest JIRA REST API documentation](https://docs.atlassian.com/jira/REST/latest/) was the base document for this package.
## Examples
Further a few examples how the API can be used.
A few more examples are available in the [GoDoc examples section](https://godoc.org/github.com/andygrunwald/go-jira#pkg-examples).
### Get a single issue
Lets retrieve [MESOS-3325](https://issues.apache.org/jira/browse/MESOS-3325) from the [Apache Mesos](http://mesos.apache.org/) project.
```go
package main
import (
"fmt"
"github.com/andygrunwald/go-jira"
)
func main() {
jiraClient, _ := jira.NewClient(nil, "https://issues.apache.org/jira/")
issue, _, _ := jiraClient.Issue.Get("MESOS-3325")
fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
fmt.Printf("Type: %s\n", issue.Fields.Type.Name)
fmt.Printf("Priority: %s\n", issue.Fields.Priority.Name)
// MESOS-3325: Running mesos-slave@0.23 in a container causes slave to be lost after a restart
// Type: Bug
// Priority: Critical
}
```
### Authenticate with session cookie
Some actions require an authenticated user.
Here is an example with a session cookie authentification.
```go
package main
import (
"fmt"
"github.com/andygrunwald/go-jira"
)
func main() {
jiraClient, err := jira.NewClient(nil, "https://your.jira-instance.com/")
if err != nil {
panic(err)
}
res, err := jiraClient.Authentication.AcquireSessionCookie("username", "password")
if err != nil || res == false {
fmt.Printf("Result: %v\n", res)
panic(err)
}
issue, _, err := jiraClient.Issue.Get("SYS-5156")
if err != nil {
panic(err)
}
fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
}
```
### 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
package main
import (
"fmt"
"github.com/andygrunwald/go-jira"
)
func main() {
jiraClient, _ := jira.NewClient(nil, "https://jira.atlassian.com/")
req, _ := jiraClient.NewRequest("GET", "/rest/api/2/project", nil)
projects := new([]jira.Project)
_, err := jiraClient.Do(req, projects)
if err != nil {
panic(err)
}
for _, project := range *projects {
fmt.Printf("%s: %s\n", project.Key, project.Name)
}
// ...
// BAM: Bamboo
// BAMJ: Bamboo JIRA Plugin
// CLOV: Clover
// CONF: Confluence
// ...
}
```
## Implementations
* [andygrunwald/jitic](https://github.com/andygrunwald/jitic) - The JIRA Ticket Checker
## Code structure
The code structure of this package was inspired by [google/go-github](https://github.com/google/go-github).
There is one main part (the client).
Based on this main client the other endpoints, like Issues or Authentication are extracted in services. E.g. `IssueService` or `AuthenticationService`.
These services own a responsibility of the single endpoints / usecases of JIRA.
## Contribution
Contribution, in any kind of way, is highly welcome!
It doesn't matter if you are not able to write code.
Creating issues or holding talks and help other people to use [go-jira](https://github.com/andygrunwald/go-jira) is contribution, too!
A few examples:
* Correct typos in the README / documentation
* Reporting bugs
* Implement a new feature or endpoint
* Sharing the love if [go-jira](https://github.com/andygrunwald/go-jira) and help people to get use to it
If you are new to pull requests, checkout [Collaborating on projects using issues and pull requests / Creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
## License
This project is released under the terms of the [MIT license](http://en.wikipedia.org/wiki/MIT_License).

81
vendor/src/github.com/andygrunwald/go-jira/authentication.go

@ -0,0 +1,81 @@
package jira
import (
"fmt"
"net/http"
)
// AuthenticationService handles authentication for the JIRA instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#authentication
type AuthenticationService struct {
client *Client
}
// Session represents a Session JSON response by the JIRA API.
type Session struct {
Self string `json:"self,omitempty"`
Name string `json:"name,omitempty"`
Session struct {
Name string `json:"name"`
Value string `json:"value"`
} `json:"session,omitempty"`
LoginInfo struct {
FailedLoginCount int `json:"failedLoginCount"`
LoginCount int `json:"loginCount"`
LastFailedLoginTime string `json:"lastFailedLoginTime"`
PreviousLoginTime string `json:"previousLoginTime"`
} `json:"loginInfo"`
Cookies []*http.Cookie
}
// AcquireSessionCookie creates a new session for a user in JIRA.
// Once a session has been successfully created it can be used to access any of JIRA's remote APIs and also the web UI by passing the appropriate HTTP Cookie header.
// The header will by automatically applied to every API request.
// Note that it is generally preferrable to use HTTP BASIC authentication with the REST API.
// However, this resource may be used to mimic the behaviour of JIRA's log-in page (e.g. to display log-in errors to a user).
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
func (s *AuthenticationService) AcquireSessionCookie(username, password string) (bool, error) {
apiEndpoint := "rest/auth/1/session"
body := struct {
Username string `json:"username"`
Password string `json:"password"`
}{
username,
password,
}
req, err := s.client.NewRequest("POST", apiEndpoint, body)
if err != nil {
return false, err
}
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)
}
if resp != nil && resp.StatusCode != 200 {
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). Status code: %d", resp.StatusCode)
}
s.client.session = session
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.)
// See https://docs.atlassian.com/jira/REST/latest/#auth/1/session

86
vendor/src/github.com/andygrunwald/go-jira/authentication_test.go

@ -0,0 +1,86 @@
package jira
import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"testing"
)
func TestAuthenticationService_AcquireSessionCookie_Failure(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, "/rest/auth/1/session")
b, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Errorf("Error in read body: %s", err)
}
if bytes.Index(b, []byte(`"username":"foo"`)) < 0 {
t.Error("No username found")
}
if bytes.Index(b, []byte(`"password":"bar"`)) < 0 {
t.Error("No password found")
}
// Emulate error
w.WriteHeader(http.StatusInternalServerError)
})
res, err := testClient.Authentication.AcquireSessionCookie("foo", "bar")
if err == nil {
t.Errorf("Expected error, but no error given")
}
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 TestAuthenticationService_AcquireSessionCookie_Success(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, "/rest/auth/1/session")
b, err := ioutil.ReadAll(r.Body)
if err != nil {
t.Errorf("Error in read body: %s", err)
}
if bytes.Index(b, []byte(`"username":"foo"`)) < 0 {
t.Error("No username found")
}
if bytes.Index(b, []byte(`"password":"bar"`)) < 0 {
t.Error("No password found")
}
fmt.Fprint(w, `{"session":{"name":"JSESSIONID","value":"12345678901234567890"},"loginInfo":{"failedLoginCount":10,"loginCount":127,"lastFailedLoginTime":"2016-03-16T04:22:35.386+0000","previousLoginTime":"2016-03-16T04:22:35.386+0000"}}`)
})
res, err := testClient.Authentication.AcquireSessionCookie("foo", "bar")
if err != nil {
t.Errorf("No error expected. Got %s", err)
}
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 TestAuthenticationService_Authenticated(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")
}
}

155
vendor/src/github.com/andygrunwald/go-jira/board.go

@ -0,0 +1,155 @@
package jira
import (
"fmt"
"time"
)
// BoardService handles Agile Boards for the JIRA instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/server/
type BoardService struct {
client *Client
}
// BoardsList reflects a list of agile boards
type BoardsList struct {
MaxResults int `json:"maxResults"`
StartAt int `json:"startAt"`
Total int `json:"total"`
IsLast bool `json:"isLast"`
Values []Board `json:"values"`
}
// Board represents a JIRA agile board
type Board struct {
ID int `json:"id,omitempty"`
Self string `json:"self,omitempty"`
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
FilterID int `json:"filterId,omitempty"`
}
// BoardListOptions specifies the optional parameters to the BoardService.GetList
type BoardListOptions struct {
// BoardType filters results to boards of the specified type.
// Valid values: scrum, kanban.
BoardType string `url:"boardType,omitempty"`
// Name filters results to boards that match or partially match the specified name.
Name string `url:"name,omitempty"`
// ProjectKeyOrID filters results to boards that are relevant to a project.
// Relevance meaning that the JQL filter defined in board contains a reference to a project.
ProjectKeyOrID string `url:"projectKeyOrId,omitempty"`
SearchOptions
}
// Wrapper struct for search result
type sprintsResult struct {
Sprints []Sprint `json:"values"`
}
// Sprint represents a sprint on JIRA agile board
type Sprint struct {
ID int `json:"id"`
Name string `json:"name"`
CompleteDate *time.Time `json:"completeDate"`
EndDate *time.Time `json:"endDate"`
StartDate *time.Time `json:"startDate"`
OriginBoardID int `json:"originBoardId"`
Self string `json:"self"`
State string `json:"state"`
}
// GetAllBoards will returns all boards. This only includes boards that the user has permission to view.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getAllBoards
func (s *BoardService) GetAllBoards(opt *BoardListOptions) (*BoardsList, *Response, error) {
apiEndpoint := "rest/agile/1.0/board"
url, err := addOptions(apiEndpoint, opt)
req, err := s.client.NewRequest("GET", url, nil)
if err != nil {
return nil, nil, err
}
boards := new(BoardsList)
resp, err := s.client.Do(req, boards)
if err != nil {
return nil, resp, err
}
return boards, resp, err
}
// GetBoard will returns the board for the given boardID.
// This board will only be returned if the user has permission to view it.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-getBoard
func (s *BoardService) GetBoard(boardID int) (*Board, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
board := new(Board)
resp, err := s.client.Do(req, board)
if err != nil {
return nil, resp, err
}
return board, resp, nil
}
// CreateBoard creates a new board. Board name, type and filter Id is required.
// name - Must be less than 255 characters.
// type - Valid values: scrum, kanban
// filterId - Id of a filter that the user has permissions to view.
// Note, if the user does not have the 'Create shared objects' permission and tries to create a shared board, a private
// board will be created instead (remember that board sharing depends on the filter sharing).
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-createBoard
func (s *BoardService) CreateBoard(board *Board) (*Board, *Response, error) {
apiEndpoint := "rest/agile/1.0/board"
req, err := s.client.NewRequest("POST", apiEndpoint, board)
if err != nil {
return nil, nil, err
}
responseBoard := new(Board)
resp, err := s.client.Do(req, responseBoard)
if err != nil {
return nil, resp, err
}
return responseBoard, resp, nil
}
// DeleteBoard will delete an agile board.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board-deleteBoard
func (s *BoardService) DeleteBoard(boardID int) (*Board, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%v", boardID)
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
resp, err := s.client.Do(req, nil)
return nil, resp, err
}
// GetAllSprints will returns all sprints from a board, for a given board Id.
// This only includes sprints that the user has permission to view.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
func (s *BoardService) GetAllSprints(boardID string) ([]Sprint, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/board/%s/sprint", boardID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
result := new(sprintsResult)
resp, err := s.client.Do(req, result)
return result.Sprints, resp, err
}

186
vendor/src/github.com/andygrunwald/go-jira/board_test.go

@ -0,0 +1,186 @@
package jira
import (
"fmt"
"io/ioutil"
"net/http"
"testing"
)
func TestBoardService_GetAllBoards(t *testing.T) {
setup()
defer teardown()
testAPIEdpoint := "/rest/agile/1.0/board"
raw, err := ioutil.ReadFile("./mocks/all_boards.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.Board.GetAllBoards(nil)
if projects == nil {
t.Error("Expected boards list. Boards list is nil")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
// Test with params
func TestBoardService_GetAllBoards_WithFilter(t *testing.T) {
setup()
defer teardown()
testAPIEdpoint := "/rest/agile/1.0/board"
raw, err := ioutil.ReadFile("./mocks/all_boards_filtered.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))
})
boardsListOptions := &BoardListOptions{
BoardType: "scrum",
Name: "Test",
ProjectKeyOrID: "TE",
}
boardsListOptions.StartAt = 1
boardsListOptions.MaxResults = 10
projects, _, err := testClient.Board.GetAllBoards(boardsListOptions)
if projects == nil {
t.Error("Expected boards list. Boards list is nil")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestBoardService_GetBoard(t *testing.T) {
setup()
defer teardown()
testAPIEdpoint := "/rest/agile/1.0/board/1"
testMux.HandleFunc(testAPIEdpoint, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, testAPIEdpoint)
fmt.Fprint(w, `{"id":4,"self":"https://test.jira.org/rest/agile/1.0/board/1","name":"Test Weekly","type":"scrum"}`)
})
board, _, err := testClient.Board.GetBoard(1)
if board == nil {
t.Error("Expected board list. Board list is nil")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestBoardService_GetBoard_WrongID(t *testing.T) {
setup()
defer teardown()
testAPIEndpoint := "/rest/api/2/board/99999999"
testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, testAPIEndpoint)
fmt.Fprint(w, nil)
})
board, resp, err := testClient.Board.GetBoard(99999999)
if board != nil {
t.Errorf("Expected nil. Got %s", err)
}
if resp.Status == "404" {
t.Errorf("Expected status 404. Got %s", resp.Status)
}
if err == nil {
t.Errorf("Error given: %s", err)
}
}
func TestBoardService_CreateBoard(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/agile/1.0/board", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, "/rest/agile/1.0/board")
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, `{"id":17,"self":"https://test.jira.org/rest/agile/1.0/board/17","name":"Test","type":"kanban"}`)
})
b := &Board{
Name: "Test",
Type: "kanban",
FilterID: 17,
}
issue, _, err := testClient.Board.CreateBoard(b)
if issue == nil {
t.Error("Expected board. Board is nil")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestBoardService_DeleteBoard(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/agile/1.0/board/1", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "DELETE")
testRequestURL(t, r, "/rest/agile/1.0/board/1")
w.WriteHeader(http.StatusNoContent)
fmt.Fprint(w, `{}`)
})
_, resp, err := testClient.Board.DeleteBoard(1)
if resp.StatusCode != 204 {
t.Error("Expected board not deleted.")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestBoardService_GetAllSprints(t *testing.T) {
setup()
defer teardown()
testAPIEndpoint := "/rest/agile/1.0/board/123/sprint"
raw, err := ioutil.ReadFile("./mocks/sprints.json")
if err != nil {
t.Error(err.Error())
}
testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, testAPIEndpoint)
fmt.Fprint(w, string(raw))
})
sprints, _, err := testClient.Board.GetAllSprints("123")
if err != nil {
t.Errorf("Got error: %v", err)
}
if sprints == nil {
t.Error("Expected sprint list. Got nil.")
}
if len(sprints) != 4 {
t.Errorf("Expected 4 transitions. Got %d", len(sprints))
}
}

BIN
vendor/src/github.com/andygrunwald/go-jira/img/go-jira-compressed.png

After

Width: 793  |  Height: 288  |  Size: 9.7 KiB

574
vendor/src/github.com/andygrunwald/go-jira/issue.go

@ -0,0 +1,574 @@
package jira
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/url"
"strings"
"time"
)
const (
// AssigneeAutomatic represents the value of the "Assignee: Automatic" of JIRA
AssigneeAutomatic = "-1"
)
// IssueService handles Issues for the JIRA instance / API.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue
type IssueService struct {
client *Client
}
// Issue represents a JIRA issue.
type Issue struct {
Expand string `json:"expand,omitempty"`
ID string `json:"id,omitempty"`
Self string `json:"self,omitempty"`
Key string `json:"key,omitempty"`
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 *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"`
}
// Epic represents the epic to which an issue is associated
// Not that this struct does not process the returned "color" value
type Epic struct {
ID int `json:"id"`
Key string `json:"key"`
Self string `json:"self"`
Name string `json:"name"`
Summary string `json:"summary"`
Done bool `json:"done"`
}
// IssueFields represents single fields of a JIRA issue.
// Every JIRA issue has several fields attached.
type IssueFields struct {
// TODO Missing fields
// * "timespent": null,
// * "aggregatetimespent": null,
// * "workratio": -1,
// * "lastViewed": null,
// * "timeestimate": null,
// * "aggregatetimeoriginalestimate": null,
// * "timeoriginalestimate": null,
// * "timetracking": {},
// * "aggregatetimeestimate": null,
// * "environment": null,
// * "duedate": null,
Type IssueType `json:"issuetype"`
Project Project `json:"project,omitempty"`
Resolution *Resolution `json:"resolution,omitempty"`
Priority *Priority `json:"priority,omitempty"`
Resolutiondate string `json:"resolutiondate,omitempty"`
Created string `json:"created,omitempty"`
Watches *Watches `json:"watches,omitempty"`
Assignee *User `json:"assignee,omitempty"`
Updated string `json:"updated,omitempty"`
Description string `json:"description,omitempty"`
Summary string `json:"summary"`
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"`
Worklog *Worklog `json:"worklog,omitempty"`
IssueLinks []*IssueLink `json:"issuelinks,omitempty"`
Comments *Comments `json:"comment,omitempty"`
FixVersions []*FixVersion `json:"fixVersions,omitempty"`
Labels []string `json:"labels,omitempty"`
Subtasks []*Subtasks `json:"subtasks,omitempty"`
Attachments []*Attachment `json:"attachment,omitempty"`
Epic *Epic `json:"epic,omitempty"`
}
// IssueType represents a type of a JIRA issue.
// Typical types are "Request", "Bug", "Story", ...
type IssueType struct {
Self string `json:"self,omitempty"`
ID string `json:"id,omitempty"`
Description string `json:"description,omitempty"`
IconURL string `json:"iconUrl,omitempty"`
Name string `json:"name,omitempty"`
Subtask bool `json:"subtask,omitempty"`
AvatarID int `json:"avatarId,omitempty"`
}
// Resolution represents a resolution of a JIRA issue.
// Typical types are "Fixed", "Suspended", "Won't Fix", ...
type Resolution struct {
Self string `json:"self"`
ID string `json:"id"`
Description string `json:"description"`
Name string `json:"name"`
}
// Priority represents a priority of a JIRA issue.
// Typical types are "Normal", "Moderate", "Urgent", ...
type Priority struct {
Self string `json:"self,omitempty"`
IconURL string `json:"iconUrl,omitempty"`
Name string `json:"name,omitempty"`
ID string `json:"id,omitempty"`
}
// Watches represents a type of how many user are "observing" a JIRA issue to track the status / updates.
type Watches struct {
Self string `json:"self,omitempty"`
WatchCount int `json:"watchCount,omitempty"`
IsWatching bool `json:"isWatching,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.
// Components can be user defined in every JIRA instance.
type Component struct {
Self string `json:"self,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
}
// Status represents the current status of a JIRA issue.
// Typical status are "Open", "In Progress", "Closed", ...
// Status can be user defined in every JIRA instance.
type Status struct {
Self string `json:"self"`
Description string `json:"description"`
IconURL string `json:"iconUrl"`
Name string `json:"name"`
ID string `json:"id"`
StatusCategory StatusCategory `json:"statusCategory"`
}
// StatusCategory represents the category a status belongs to.
// Those categories can be user defined in every JIRA instance.
type StatusCategory struct {
Self string `json:"self"`
ID int `json:"id"`
Name string `json:"name"`
Key string `json:"key"`
ColorName string `json:"colorName"`
}
// Progress represents the progress of a JIRA issue.
type Progress struct {
Progress int `json:"progress"`
Total int `json:"total"`
}
// Time represents the Time definition of JIRA as a time.Time of go
type Time time.Time
// Wrapper struct for search result
type transitionResult struct {
Transitions []Transition `json:"transitions"`
}
// Transition represents an issue transition in JIRA
type Transition struct {
ID string `json:"id"`
Name string `json:"name"`
Fields map[string]TransitionField `json:"fields"`
}
// TransitionField represents the value of one Transistion
type TransitionField struct {
Required bool `json:"required"`
}
// CreateTransitionPayload is used for creating new issue transitions
type CreateTransitionPayload struct {
Transition TransitionPayload `json:"transition"`
}
// TransitionPayload represents the request payload of Transistion calls like DoTransition
type TransitionPayload struct {
ID string `json:"id"`
}
// UnmarshalJSON will transform the JIRA time into a time.Time
// during the transformation of the JIRA JSON response
func (t *Time) UnmarshalJSON(b []byte) error {
ti, err := time.Parse("\"2006-01-02T15:04:05.999-0700\"", string(b))
if err != nil {
return err
}
*t = Time(ti)
return nil
}
// Worklog represents the work log of a JIRA issue.
// One Worklog contains zero or n WorklogRecords
// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
type Worklog struct {
StartAt int `json:"startAt"`
MaxResults int `json:"maxResults"`
Total int `json:"total"`
Worklogs []WorklogRecord `json:"worklogs"`
}
// WorklogRecord represents one entry of a Worklog
type WorklogRecord struct {
Self string `json:"self"`
Author User `json:"author"`
UpdateAuthor User `json:"updateAuthor"`
Comment string `json:"comment"`
Created Time `json:"created"`
Updated Time `json:"updated"`
Started Time `json:"started"`
TimeSpent string `json:"timeSpent"`
TimeSpentSeconds int `json:"timeSpentSeconds"`
ID string `json:"id"`
IssueID string `json:"issueId"`
}
// Subtasks represents all issues of a parent issue.
type Subtasks struct {
ID string `json:"id"`
Key string `json:"key"`
Self string `json:"self"`
Fields IssueFields `json:"fields"`
}
// IssueLink represents a link between two issues in JIRA.
type IssueLink struct {
ID string `json:"id,omitempty"`
Self string `json:"self,omitempty"`
Type IssueLinkType `json:"type"`
OutwardIssue *Issue `json:"outwardIssue"`
InwardIssue *Issue `json:"inwardIssue"`
Comment *Comment `json:"comment,omitempty"`
}
// IssueLinkType represents a type of a link between to issues in JIRA.
// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc.
type IssueLinkType struct {
ID string `json:"id,omitempty"`
Self string `json:"self,omitempty"`
Name string `json:"name"`
Inward string `json:"inward"`
Outward string `json:"outward"`
}
// Comments represents a list of Comment.
type Comments struct {
Comments []*Comment `json:"comments,omitempty"`
}
// Comment represents a comment by a person to an issue in JIRA.
type Comment struct {
ID string `json:"id,omitempty"`
Self string `json:"self,omitempty"`
Name string `json:"name,omitempty"`
Author 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.
type FixVersion struct {
Archived *bool `json:"archived,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
ProjectID int `json:"projectId,omitempty"`
ReleaseDate string `json:"releaseDate,omitempty"`
Released *bool `json:"released,omitempty"`
Self string `json:"self,omitempty"`
UserReleaseDate string `json:"userReleaseDate,omitempty"`
}
// CommentVisibility represents he visibility of a comment.
// E.g. Type could be "role" and Value "Administrators"
type CommentVisibility struct {
Type string `json:"type,omitempty"`
Value string `json:"value,omitempty"`
}
// SearchOptions specifies the optional parameters to various List methods that
// support pagination.
// Pagination is used for the JIRA REST APIs to conserve server resources and limit
// response size for resources that return potentially large collection of items.
// A request to a pages API will result in a values array wrapped in a JSON object with some paging metadata
// Default Pagination options
type SearchOptions struct {
// StartAt: The starting index of the returned projects. Base index: 0.
StartAt int `url:"startAt,omitempty"`
// MaxResults: The maximum number of projects to return per page. Default: 50.
MaxResults int `url:"maxResults,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"`
StartAt int `json:"startAt"`
MaxResults int `json:"maxResults"`
Total int `json:"total"`
}
// CustomFields represents custom fields of JIRA
// This can heavily differ between JIRA instances
type CustomFields map[string]string
// Get returns a full representation of the issue for the given issue key.
// JIRA will attempt to identify the issue by the issueIdOrKey path parameter.
// This can be an issue id, or an issue key.
// If the issue cannot be found via an exact match, JIRA will also look for the issue in a case-insensitive way, or by looking to see if the issue was moved.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue
func (s *IssueService) Get(issueID string) (*Issue, *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(Issue)
resp, err := s.client.Do(req, issue)
if err != nil {
return nil, resp, err
}
return issue, resp, nil
}
// DownloadAttachment returns a Response of an attachment for a given attachmentID.
// The attachment is in the Response.Body of the response.
// This is an io.ReadCloser.
// The caller should close the resp.Body.
func (s *IssueService) DownloadAttachment(attachmentID string) (*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, *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.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues
func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) {
apiEndpoint := "rest/api/2/issue/"
req, err := s.client.NewRequest("POST", apiEndpoint, issue)
if err != nil {
return nil, nil, err
}
responseIssue := new(Issue)
resp, err := s.client.Do(req, responseIssue)
if err != nil {
return nil, resp, err
}
return responseIssue, resp, nil
}
// AddComment adds a new comment to issueID.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-addComment
func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment", issueID)
req, err := s.client.NewRequest("POST", apiEndpoint, comment)
if err != nil {
return nil, nil, err
}
responseComment := new(Comment)
resp, err := s.client.Do(req, responseComment)
if err != nil {
return nil, resp, err
}
return responseComment, resp, nil
}
// AddLink adds a link between two issues.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issueLink
func (s *IssueService) AddLink(issueLink *IssueLink) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issueLink")
req, err := s.client.NewRequest("POST", apiEndpoint, issueLink)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
// Search will search for tickets according to the jql
//
// JIRA API docs: https://developer.atlassian.com/jiradev/jira-apis/jira-rest-apis/jira-rest-api-tutorials/jira-rest-api-example-query-issues
func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Response, error) {
var u string
if options == nil {
u = fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql))
} else {
u = fmt.Sprintf("rest/api/2/search?jql=%s&startAt=%d&maxResults=%d", url.QueryEscape(jql),
options.StartAt, options.MaxResults)
}
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, *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
}
// GetTransitions gets a list of the transitions possible for this issue by the current user,
// along with fields that are required and their types.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getTransitions
func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions?expand=transitions.fields", id)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
result := new(transitionResult)
resp, err := s.client.Do(req, result)
return result.Transitions, resp, err
}
// DoTransition performs a transition on an issue.
// When performing the transition you can update or set other issue fields.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition
func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions", ticketID)
payload := CreateTransitionPayload{
Transition: TransitionPayload{
ID: transitionID,
},
}
req, err := s.client.NewRequest("POST", apiEndpoint, payload)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
if err != nil {
return nil, err
}
return resp, nil
}

467
vendor/src/github.com/andygrunwald/go-jira/issue_test.go

@ -0,0 +1,467 @@
package jira
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"testing"
)
func TestIssueService_Get_Success(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":{"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 err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestIssueService_Create(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/api/2/issue/", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, "/rest/api/2/issue/")
w.WriteHeader(http.StatusCreated)
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":{"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":{}}`)
})
i := &Issue{
Fields: &IssueFields{
Description: "example bug report",
},
}
issue, _, err := testClient.Issue.Create(i)
if issue == nil {
t.Error("Expected issue. Issue is nil")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestIssueService_AddComment(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/api/2/issue/10000/comment", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, "/rest/api/2/issue/10000/comment")
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, `{"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"}}`)
})
c := &Comment{
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.",
Visibility: CommentVisibility{
Type: "role",
Value: "Administrators",
},
}
comment, _, err := testClient.Issue.AddComment("10000", c)
if comment == nil {
t.Error("Expected Comment. Comment is nil")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestIssueService_AddLink(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/api/2/issueLink", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, "/rest/api/2/issueLink")
w.WriteHeader(http.StatusOK)
})
il := &IssueLink{
Type: IssueLinkType{
Name: "Duplicate",
},
InwardIssue: &Issue{
Key: "HSP-1",
},
OutwardIssue: &Issue{
Key: "MKY-1",
},
Comment: &Comment{
Body: "Linked related issue!",
Visibility: CommentVisibility{
Type: "group",
Value: "jira-software-users",
},
},
}
resp, err := testClient.Issue.AddLink(il)
if resp == 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 TestIssueService_Get_Fields(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}]},"epic": {"id": 19415,"key": "EPIC-77","self": "https://example.atlassian.net/rest/agile/1.0/epic/19415","name": "Epic Name","summary": "Do it","color": {"key": "color_11"},"done": 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 len(issue.Fields.Comments.Comments) != 1 {
t.Errorf("Expected one comment, %v found", len(issue.Fields.Comments.Comments))
}
if issue.Fields.Epic == nil {
t.Error("Epic expected but not found")
}
if err != nil {
t.Errorf("Error given: %s", err)
}
}
func TestIssueService_DownloadAttachment(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 TestIssueService_DownloadAttachment_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 TestIssueService_PostAttachment(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 TestIssueService_PostAttachment_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 TestIssueService_PostAttachment_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 TestIssueService_PostAttachment_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 TestIssueService_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&startAt=1&maxResults=40")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 40,"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}}}]}`)
})
opt := &SearchOptions{StartAt: 1, MaxResults: 40}
_, resp, err := testClient.Issue.Search("something", opt)
if resp == nil {
t.Errorf("Response given: %+v", resp)
}
if err != nil {
t.Errorf("Error given: %s", err)
}
if resp.StartAt != 1 {
t.Errorf("StartAt should populate with 1, %v given", resp.StartAt)
}
if resp.MaxResults != 40 {
t.Errorf("StartAt should populate with 40, %v given", resp.MaxResults)
}
if resp.Total != 6 {
t.Errorf("StartAt should populate with 6, %v given", resp.Total)
}
}
func TestIssueService_Search_WithoutPaging(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", nil)
if resp == nil {
t.Errorf("Response given: %+v", resp)
}
if err != nil {
t.Errorf("Error given: %s", err)
}
if resp.StartAt != 0 {
t.Errorf("StartAt should populate with 0, %v given", resp.StartAt)
}
if resp.MaxResults != 50 {
t.Errorf("StartAt should populate with 50, %v given", resp.MaxResults)
}
if resp.Total != 6 {
t.Errorf("StartAt should populate with 6, %v given", resp.Total)
}
}
func TestIssueService_GetCustomFields(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")
}
}
func TestIssueService_GetTransitions(t *testing.T) {
setup()
defer teardown()
testAPIEndpoint := "/rest/api/2/issue/123/transitions"
raw, err := ioutil.ReadFile("./mocks/transitions.json")
if err != nil {
t.Error(err.Error())
}
testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "GET")
testRequestURL(t, r, testAPIEndpoint)
fmt.Fprint(w, string(raw))
})
transitions, _, err := testClient.Issue.GetTransitions("123")
if err != nil {
t.Errorf("Got error: %v", err)
}
if transitions == nil {
t.Error("Expected transition list. Got nil.")
}
if len(transitions) != 2 {
t.Errorf("Expected 2 transitions. Got %d", len(transitions))
}
if transitions[0].Fields["summary"].Required != false {
t.Errorf("First transition summary field should not be required")
}
}
func TestIssueService_DoTransition(t *testing.T) {
setup()
defer teardown()
testAPIEndpoint := "/rest/api/2/issue/123/transitions"
transitionID := "22"
testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, testAPIEndpoint)
decoder := json.NewDecoder(r.Body)
var payload CreateTransitionPayload
err := decoder.Decode(&payload)
if err != nil {
t.Errorf("Got error: %v", err)
}
if payload.Transition.ID != transitionID {
t.Errorf("Expected %s to be in payload, got %s instead", transitionID, payload.Transition.ID)
}
})
_, err := testClient.Issue.DoTransition("123", transitionID)
if err != nil {
t.Errorf("Got error: %v", err)
}
}

224
vendor/src/github.com/andygrunwald/go-jira/jira.go

@ -0,0 +1,224 @@
package jira
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"reflect"
"github.com/google/go-querystring/query"
)
// A Client manages communication with the JIRA API.
type Client struct {
// HTTP client used to communicate with the API.
client *http.Client
// Base URL for API requests.
baseURL *url.URL
// Session storage if the user authentificate with a Session cookie
session *Session
// Services used for talking to different parts of the JIRA API.
Authentication *AuthenticationService
Issue *IssueService
Project *ProjectService
Board *BoardService
Sprint *SprintService
}
// NewClient returns a new JIRA API client.
// If a nil httpClient is provided, http.DefaultClient will be used.
// To use API methods which require authentication you can follow the preferred solution and
// provide an http.Client that will perform the authentication for you with OAuth and HTTP Basic (such as that provided by the golang.org/x/oauth2 library).
// As an alternative you can use Session Cookie based authentication provided by this package as well.
// See https://docs.atlassian.com/jira/REST/latest/#authentication
// baseURL is the HTTP endpoint of your JIRA instance and should always be specified with a trailing slash.
func NewClient(httpClient *http.Client, baseURL string) (*Client, error) {
if httpClient == nil {
httpClient = http.DefaultClient
}
parsedBaseURL, err := url.Parse(baseURL)
if err != nil {
return nil, err
}
c := &Client{
client: httpClient,
baseURL: parsedBaseURL,
}
c.Authentication = &AuthenticationService{client: c}
c.Issue = &IssueService{client: c}
c.Project = &ProjectService{client: c}
c.Board = &BoardService{client: c}
c.Sprint = &SprintService{client: c}
return c, nil
}
// NewRequest creates an API request.
// 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 body is JSON encoded and included as the request body.
func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {
rel, err := url.Parse(urlStr)
if err != nil {
return nil, err
}
u := c.baseURL.ResolveReference(rel)
var buf io.ReadWriter
if body != nil {
buf = new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(body)
if err != nil {
return nil, err
}
}
req, err := http.NewRequest(method, u.String(), buf)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
// Set session cookie if there is one
if c.session != nil {
for _, cookie := range c.session.Cookies {
req.AddCookie(cookie)
}
}
return req, nil
}
// addOptions adds the parameters in opt as URL query parameters to s. opt
// must be a struct whose fields may contain "url" tags.
func addOptions(s string, opt interface{}) (string, error) {
v := reflect.ValueOf(opt)
if v.Kind() == reflect.Ptr && v.IsNil() {
return s, nil
}
u, err := url.Parse(s)
if err != nil {
return s, err
}
qs, err := query.Values(opt)
if err != nil {
return s, err
}
u.RawQuery = qs.Encode()
return u.String(), 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.session != nil {
for _, cookie := range c.session.Cookies {
req.AddCookie(cookie)
}
}
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{}) (*Response, error) {
httpResp, err := c.client.Do(req)
if err != nil {
return nil, err
}
err = CheckResponse(httpResp)
if err != nil {
// Even though there was an error, we still return the response
// in case the caller wants to inspect it further
return newResponse(httpResp, nil), err
}
if v != nil {
// Open a NewDecoder and defer closing the reader only if there is a provided interface to decode to
defer httpResp.Body.Close()
err = json.NewDecoder(httpResp.Body).Decode(v)
}
resp := newResponse(httpResp, v)
return resp, err
}
// 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.
// 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
}
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
}
// Response represents JIRA API response. It wraps http.Response returned from
// API and provides information about paging.
type Response struct {
*http.Response
StartAt int
MaxResults int
Total int
}
func newResponse(r *http.Response, v interface{}) *Response {
resp := &Response{Response: r}
resp.populatePageValues(v)
return resp
}
// Sets paging values if response json was parsed to searchResult type
// (can be extended with other types if they also need paging info)
func (r *Response) populatePageValues(v interface{}) {
switch value := v.(type) {
case *searchResult:
r.StartAt = value.StartAt
r.MaxResults = value.MaxResults
r.Total = value.Total
}
return
}

390
vendor/src/github.com/andygrunwald/go-jira/jira_test.go

@ -0,0 +1,390 @@
package jira
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"strings"
"testing"
"time"
)
const (
testJIRAInstanceURL = "https://issues.apache.org/jira/"
)
var (
// testMux is the HTTP request multiplexer used with the test server.
testMux *http.ServeMux
// testClient is the JIRA client being tested.
testClient *Client
// testServer is a test HTTP server used to provide mock API responses.
testServer *httptest.Server
)
type testValues map[string]string
// setup sets up a test HTTP server along with a jira.Client that is configured to talk to that test server.
// Tests should register handlers on mux which provide mock responses for the API method being tested.
func setup() {
// Test server
testMux = http.NewServeMux()
testServer = httptest.NewServer(testMux)
// jira client configured to use test server
testClient, _ = NewClient(nil, testServer.URL)
}
// teardown closes the test HTTP server.
func teardown() {
testServer.Close()
}
func testMethod(t *testing.T, r *http.Request, want string) {
if got := r.Method; got != want {
t.Errorf("Request method: %v, want %v", got, want)
}
}
func testRequestURL(t *testing.T, r *http.Request, want string) {
if got := r.URL.String(); !strings.HasPrefix(got, want) {
t.Errorf("Request URL: %v, want %v", got, want)
}
}
func TestNewClient_WrongUrl(t *testing.T) {
c, err := NewClient(nil, "://issues.apache.org/jira/")
if err == nil {
t.Error("Expected an error. Got none")
}
if c != nil {
t.Errorf("Expected no client. Got %+v", c)
}
}
func TestNewClient_WithHttpClient(t *testing.T) {
httpClient := http.DefaultClient
httpClient.Timeout = 10 * time.Minute
c, err := NewClient(httpClient, testJIRAInstanceURL)
if err != nil {
t.Errorf("Got an error: %s", err)
}
if c == nil {
t.Error("Expected a client. Got none")
}
if !reflect.DeepEqual(c.client, httpClient) {
t.Errorf("HTTP clients are not equal. Injected %+v, got %+v", httpClient, c.client)
}
}
func TestNewClient_WithServices(t *testing.T) {
c, err := NewClient(nil, testJIRAInstanceURL)
if err != nil {
t.Errorf("Got an error: %s", err)
}
if c.Authentication == nil {
t.Error("No AuthenticationService provided")
}
if c.Issue == nil {
t.Error("No IssueService provided")
}
if c.Project == nil {
t.Error("No ProjectService provided")
}
if c.Board == nil {
t.Error("No BoardService provided")
}
if c.Sprint == nil {
t.Error("No SprintService provided")
}
}
func TestCheckResponse(t *testing.T) {
codes := []int{
http.StatusOK, http.StatusPartialContent, 299,
}
for _, c := range codes {
r := &http.Response{
StatusCode: c,
}
if err := CheckResponse(r); err != nil {
t.Errorf("CheckResponse throws an error: %s", err)
}
}
}
func TestClient_NewRequest(t *testing.T) {
c, err := NewClient(nil, testJIRAInstanceURL)
if err != nil {
t.Errorf("An error occured. Expected nil. Got %+v.", err)
}
inURL, outURL := "rest/api/2/issue/", testJIRAInstanceURL+"rest/api/2/issue/"
inBody, outBody := &Issue{Key: "MESOS"}, `{"key":"MESOS"}`+"\n"
req, _ := c.NewRequest("GET", inURL, inBody)
// Test that relative URL was expanded
if got, want := req.URL.String(), outURL; got != want {
t.Errorf("NewRequest(%q) URL is %v, want %v", inURL, got, want)
}
// Test that body was JSON encoded
body, _ := ioutil.ReadAll(req.Body)
if got, want := string(body), outBody; got != want {
t.Errorf("NewRequest(%v) Body is %v, want %v", inBody, got, want)
}
}
func TestClient_NewRequest_InvalidJSON(t *testing.T) {
c, err := NewClient(nil, testJIRAInstanceURL)
if err != nil {
t.Errorf("An error occured. Expected nil. Got %+v.", err)
}
type T struct {
A map[int]interface{}
}
_, err = c.NewRequest("GET", "/", &T{})
if err == nil {
t.Error("Expected error to be returned.")
}
if err, ok := err.(*json.UnsupportedTypeError); !ok {
t.Errorf("Expected a JSON error; got %+v.", err)
}
}
func testURLParseError(t *testing.T, err error) {
if err == nil {
t.Errorf("Expected error to be returned")
}
if err, ok := err.(*url.Error); !ok || err.Op != "parse" {
t.Errorf("Expected URL parse error, got %+v", err)
}
}
func TestClient_NewRequest_BadURL(t *testing.T) {
c, err := NewClient(nil, testJIRAInstanceURL)
if err != nil {
t.Errorf("An error occured. Expected nil. Got %+v.", err)
}
_, err = c.NewRequest("GET", ":", nil)
testURLParseError(t, err)
}
func TestClient_NewRequest_SessionCookies(t *testing.T) {
c, err := NewClient(nil, testJIRAInstanceURL)
if err != nil {
t.Errorf("An error occured. Expected nil. Got %+v.", err)
}
cookie := &http.Cookie{Name: "testcookie", Value: "testvalue"}
c.session = &Session{Cookies: []*http.Cookie{cookie}}
inURL := "rest/api/2/issue/"
inBody := &Issue{Key: "MESOS"}
req, err := c.NewRequest("GET", inURL, inBody)
if err != nil {
t.Errorf("An error occured. Expected nil. Got %+v.", err)
}
if len(req.Cookies()) != len(c.session.Cookies) {
t.Errorf("An error occured. Expected %d cookie(s). Got %d.", len(c.session.Cookies), len(req.Cookies()))
}
for i, v := range req.Cookies() {
if v.String() != c.session.Cookies[i].String() {
t.Errorf("An error occured. Unexpected cookie. Expected %s, actual %s.", v.String(), c.session.Cookies[i].String())
}
}
}
// If a nil body is passed to gerrit.NewRequest, make sure that nil is also passed to http.NewRequest.
// In most cases, passing an io.Reader that returns no content is fine,
// since there is no difference between an HTTP request body that is an empty string versus one that is not set at all.
// However in certain cases, intermediate systems may treat these differently resulting in subtle errors.
func TestClient_NewRequest_EmptyBody(t *testing.T) {
c, err := NewClient(nil, testJIRAInstanceURL)
if err != nil {
t.Errorf("An error occured. Expected nil. Got %+v.", err)
}
req, err := c.NewRequest("GET", "/", nil)
if err != nil {
t.Fatalf("NewRequest returned unexpected error: %v", err)
}
if req.Body != nil {
t.Fatalf("constructed request contains a non-nil Body")
}
}
func TestClient_NewMultiPartRequest(t *testing.T) {
c, err := NewClient(nil, testJIRAInstanceURL)
if err != nil {
t.Errorf("An error occured. Expected nil. Got %+v.", err)
}
cookie := &http.Cookie{Name: "testcookie", Value: "testvalue"}
c.session = &Session{Cookies: []*http.Cookie{cookie}}
inURL := "rest/api/2/issue/"
inBuf := bytes.NewBufferString("teststring")
req, err := c.NewMultiPartRequest("GET", inURL, inBuf)
if err != nil {
t.Errorf("An error occured. Expected nil. Got %+v.", err)
}
if len(req.Cookies()) != len(c.session.Cookies) {
t.Errorf("An error occured. Expected %d cookie(s). Got %d.", len(c.session.Cookies), len(req.Cookies()))
}
for i, v := range req.Cookies() {
if v.String() != c.session.Cookies[i].String() {
t.Errorf("An error occured. Unexpected cookie. Expected %s, actual %s.", v.String(), c.session.Cookies[i].String())
}
}
if req.Header.Get("X-Atlassian-Token") != "nocheck" {
t.Errorf("An error occured. Unexpected X-Atlassian-Token header value. Expected nocheck, actual %s.", req.Header.Get("X-Atlassian-Token"))
}
}
func TestClient_Do(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)
body := new(foo)
testClient.Do(req, body)
want := &foo{"a"}
if !reflect.DeepEqual(body, want) {
t.Errorf("Response body = %v, want %v", body, want)
}
}
func TestClient_Do_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 TestClient_Do_HTTPError(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Bad Request", 400)
})
req, _ := testClient.NewRequest("GET", "/", nil)
_, err := testClient.Do(req, nil)
if err == nil {
t.Error("Expected HTTP 400 error.")
}
}
// Test handling of an error caused by the internal http client's Do() function.
// A redirect loop is pretty unlikely to occur within the Gerrit API, but does allow us to exercise the right code path.
func TestClient_Do_RedirectLoop(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusFound)
})
req, _ := testClient.NewRequest("GET", "/", nil)
_, err := testClient.Do(req, nil)
if err == nil {
t.Error("Expected error to be returned.")
}
if err, ok := err.(*url.Error); !ok {
t.Errorf("Expected a URL error; got %+v.", err)
}
}
func TestClient_GetBaseURL_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)
}
}
func TestClient_Do_PagingInfoEmptyByDefault(t *testing.T) {
c, _ := NewClient(nil, testJIRAInstanceURL)
req, _ := c.NewRequest("GET", "/", nil)
type foo struct {
A string
}
body := new(foo)
resp, _ := c.Do(req, body)
if resp.StartAt != 0 {
t.Errorf("StartAt not equal to 0")
}
if resp.MaxResults != 0 {
t.Errorf("StartAt not equal to 0")
}
if resp.Total != 0 {
t.Errorf("StartAt not equal to 0")
}
}

43
vendor/src/github.com/andygrunwald/go-jira/mocks/all_boards.json

@ -0,0 +1,43 @@
{
"maxResults": 50,
"startAt": 0,
"isLast": true,
"values": [
{
"id": 4,
"self": "https://test.jira.org/rest/agile/1.0/board/4",
"name": "Test Weekly",
"type": "scrum"
},
{
"id": 5,
"self": "https://test.jira.org/rest/agile/1.0/board/5",
"name": "Test Production Support",
"type": "kanban"
},
{
"id": 6,
"self": "https://test.jira.org/rest/agile/1.0/board/6",
"name": "Test To Give",
"type": "kanban"
},
{
"id": 7,
"self": "https://test.jira.org/rest/agile/1.0/board/7",
"name": "Test Journey App",
"type": "kanban"
},
{
"id": 9,
"self": "https://test.jira.org/rest/agile/1.0/board/9",
"name": "Testix",
"type": "scrum"
},
{
"id": 1,
"self": "https://test.jira.org/rest/agile/1.0/board/1",
"name": "Test Mobile",
"type": "scrum"
}
]
}

25
vendor/src/github.com/andygrunwald/go-jira/mocks/all_boards_filtered.json

@ -0,0 +1,25 @@
{
"maxResults": 10,
"startAt": 1,
"isLast": true,
"values": [
{
"id": 4,
"self": "https://test.jira.org/rest/agile/1.0/board/4",
"name": "Test Weekly",
"type": "scrum"
},
{
"id": 9,
"self": "https://test.jira.org/rest/agile/1.0/board/9",
"name": "Testix",
"type": "scrum"
},
{
"id": 1,
"self": "https://test.jira.org/rest/agile/1.0/board/1",
"name": "Test Mobile",
"type": "scrum"
}
]
}

9872
vendor/src/github.com/andygrunwald/go-jira/mocks/all_projects.json
File diff suppressed because it is too large
View File

115
vendor/src/github.com/andygrunwald/go-jira/mocks/issues_in_sprint.json

@ -0,0 +1,115 @@
{
"expand": "schema,names",
"startAt": 0,
"maxResults": 50,
"total": 10,
"issues": [
{
"expand": "operations,versionedRepresentations,editmeta,changelog,renderedFields",
"id": "12338",
"self": "https://example.atlassian.net/rest/agile/1.0/issue/12338",
"key": "AR-86",
"fields": {
"issuetype": {
"self": "https://example.atlassian.net/rest/api/2/issuetype/3",
"id": "3",
"description": "A task that needs to be done.",
"iconUrl": "https://example.atlassian.net/secure/viewavatar?size=xsmall&avatarId=10418&avatarType=issuetype",
"name": "Task",
"subtask": false,
"avatarId": 10418
},
"timespent": null,
"project": {
"self": "https://example.atlassian.net/rest/api/2/project/10302",
"id": "10302",
"key": "AR",
"name": "Team Argon",
"avatarUrls": {
"48x48": "https://example.atlassian.net/secure/projectavatar?pid=10302&avatarId=10610",
"24x24": "https://example.atlassian.net/secure/projectavatar?size=small&pid=10302&avatarId=10610",
"16x16": "https://example.atlassian.net/secure/projectavatar?size=xsmall&pid=10302&avatarId=10610",
"32x32": "https://example.atlassian.net/secure/projectavatar?size=medium&pid=10302&avatarId=10610"
}
},
"fixVersions": [],
"customfield_11200": "0|0zzzzd:vi",
"aggregatetimespent": null,
"resolution": {
"self": "https://example.atlassian.net/rest/api/2/resolution/6",
"id": "6",
"description": "",
"name": "Done"
},
"customfield_11401": null,
"customfield_11400": null,
"customfield_10105": 13.0,
"customfield_10700": "AR-37",
"resolutiondate": "2015-12-07T14:19:13.000-0800",
"workratio": -1,
"lastViewed": null,
"watches": {
"self": "https://example.atlassian.net/rest/api/2/issue/AR-86/watchers",
"watchCount": 2,
"isWatching": true
},
"created": "2015-12-02T07:39:15.000-0800",
"epic": {
"id": 11900,
"key": "AR-37",
"self": "https://example.atlassian.net/rest/agile/1.0/epic/11900",
"name": "Moderation: Design",
"summary": "Moderation design",
"color": {
"key": "color_8"
},
"done": true
},
"priority": {
"self": "https://example.atlassian.net/rest/api/2/priority/3",
"iconUrl": "https://example.atlassian.net/images/icons/priorities/major.svg",
"name": "Major",
"id": "3"
},
"customfield_10102": null,
"customfield_10103": null,
"labels": [],
"customfield_11700": null,
"timeestimate": null,
"aggregatetimeoriginalestimate": null,
"versions": [],
"issuelinks": [],
"assignee": {
"self": "https://example.atlassian.net/rest/api/2/user?username=mister.morris",
"name": "mister.morris",
"key": "mister.morris",
"emailAddress": "mister.morris@uservoice.com",
"avatarUrls": {
"48x48": "https://example.atlassian.net/secure/useravatar?ownerId=mister.morris&avatarId=10604",
"24x24": "https://example.atlassian.net/secure/useravatar?size=small&ownerId=mister.morris&avatarId=10604",
"16x16": "https://example.atlassian.net/secure/useravatar?size=xsmall&ownerId=mister.morris&avatarId=10604",
"32x32": "https://example.atlassian.net/secure/useravatar?size=medium&ownerId=mister.morris&avatarId=10604"
},
"displayName": "mister Morris",
"active": true,
"timeZone": "America/New_York"
},
"updated": "2016-02-01T08:17:04.000-0800",
"status": {
"self": "https://example.atlassian.net/rest/api/2/status/10000",
"description": "Ready to move to dev team for grooming",
"iconUrl": "https://example.atlassian.net/images/icons/statuses/closed.png",
"name": "Ready",
"id": "10000",
"statusCategory": {
"self": "https://example.atlassian.net/rest/api/2/statuscategory/2",
"id": 2,
"key": "new",
"colorName": "blue-gray",
"name": "To Do"
}
}
}
}
]
}

411
vendor/src/github.com/andygrunwald/go-jira/mocks/project.json

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

46
vendor/src/github.com/andygrunwald/go-jira/mocks/sprints.json

@ -0,0 +1,46 @@
{
"isLast": true,
"maxResults": 50,
"startAt": 0,
"values": [
{
"completeDate": "2016-04-28T05:08:48.543-07:00",
"endDate": "2016-04-27T08:29:00.000-07:00",
"id": 740,
"name": "Iteration-10",
"originBoardId": 734,
"self": "https://jira.com/rest/agile/1.0/sprint/740",
"startDate": "2016-04-11T07:29:03.294-07:00",
"state": "closed"
},
{
"completeDate": "2016-05-30T02:45:44.991-07:00",
"endDate": "2016-05-26T14:56:00.000-07:00",
"id": 776,
"name": "Iteration-12-1",
"originBoardId": 734,
"self": "https://jira.com/rest/agile/1.0/sprint/776",
"startDate": "2016-05-19T13:56:00.000-07:00",
"state": "closed"
},
{
"completeDate": "2016-06-08T07:54:13.723-07:00",
"endDate": "2016-06-08T01:06:00.000-07:00",
"id": 807,
"name": "Iteration-12-2",
"originBoardId": 734,
"self": "https://jira.com/rest/agile/1.0/sprint/807",
"startDate": "2016-06-01T00:06:00.000-07:00",
"state": "closed"
},
{
"endDate": "2016-06-28T14:24:00.000-07:00",
"id": 832,
"name": "Iteration-13-2",
"originBoardId": 734,
"self": "https://jira.com/rest/agile/1.0/sprint/832",
"startDate": "2016-06-20T13:24:39.161-07:00",
"state": "active"
}
]
}

101
vendor/src/github.com/andygrunwald/go-jira/mocks/transitions.json

@ -0,0 +1,101 @@
{
"expand": "transitions",
"transitions": [
{
"id": "2",
"name": "Close Issue",
"to": {
"self": "http://localhost:8090/jira/rest/api/2.0/status/10000",
"description": "The issue is currently being worked on.",
"iconUrl": "http://localhost:8090/jira/images/icons/progress.gif",
"name": "In Progress",
"id": "10000",
"statusCategory": {
"self": "http://localhost:8090/jira/rest/api/2.0/statuscategory/1",
"id": 1,
"key": "in-flight",
"colorName": "yellow",
"name": "In Progress"
}
},
"fields": {
"summary": {
"required": false,
"schema": {
"type": "array",
"items": "option",
"custom": "com.atlassian.jira.plugin.system.customfieldtypes:multiselect",
"customId": 10001
},
"name": "My Multi Select",
"hasDefaultValue": false,
"operations": [
"set",
"add"
],
"allowedValues": [
"red",
"blue"
]
}
}
},
{
"id": "711",
"name": "QA Review",
"to": {
"self": "http://localhost:8090/jira/rest/api/2.0/status/5",
"description": "The issue is closed.",
"iconUrl": "http://localhost:8090/jira/images/icons/closed.gif",
"name": "Closed",
"id": "5",
"statusCategory": {
"self": "http://localhost:8090/jira/rest/api/2.0/statuscategory/9",
"id": 9,
"key": "completed",
"colorName": "green"
}
},
"fields": {
"summary": {
"required": false,
"schema": {
"type": "array",
"items": "option",
"custom": "com.atlassian.jira.plugin.system.customfieldtypes:multiselect",
"customId": 10001
},
"name": "My Multi Select",
"hasDefaultValue": false,
"operations": [
"set",
"add"
],
"allowedValues": [
"red",
"blue"
]
},
"colour": {
"required": false,
"schema": {
"type": "array",
"items": "option",
"custom": "com.atlassian.jira.plugin.system.customfieldtypes:multiselect",
"customId": 10001
},
"name": "My Multi Select",
"hasDefaultValue": false,
"operations": [
"set",
"add"
],
"allowedValues": [
"red",
"blue"
]
}
}
}
]
}

120
vendor/src/github.com/andygrunwald/go-jira/project.go

@ -0,0 +1,120 @@
package jira
import (
"fmt"
)
// 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 []Version `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"`
}
// Version represents a single release version of a project
type Version struct {
Self string `json:"self"`
ID string `json:"id"`
Name string `json:"name"`
Archived bool `json:"archived"`
Released bool `json:"released"`
ReleaseDate string `json:"releaseDate"`
UserReleaseDate string `json:"userReleaseDate"`
ProjectID int `json:"projectId"` // Unlike other IDs, this is returned as a number
}
// 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, *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, *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
vendor/src/github.com/andygrunwald/go-jira/project_test.go

@ -0,0 +1,80 @@
package jira
import (
"fmt"
"io/ioutil"
"net/http"
"testing"
)
func TestProjectService_GetList(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 TestProjectService_Get(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 TestProjectService_Get_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)
}
}

60
vendor/src/github.com/andygrunwald/go-jira/sprint.go

@ -0,0 +1,60 @@
package jira
import (
"fmt"
)
// SprintService handles sprints in JIRA Agile API.
// See https://docs.atlassian.com/jira-software/REST/cloud/
type SprintService struct {
client *Client
}
// IssuesWrapper represents a wrapper struct for moving issues to sprint
type IssuesWrapper struct {
Issues []string `json:"issues"`
}
// IssuesInSprintResult represents a wrapper struct for search result
type IssuesInSprintResult struct {
Issues []Issue `json:"issues"`
}
// MoveIssuesToSprint moves issues to a sprint, for a given sprint Id.
// Issues can only be moved to open or active sprints.
// The maximum number of issues that can be moved in one operation is 50.
//
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-moveIssuesToSprint
func (s *SprintService) MoveIssuesToSprint(sprintID int, issueIDs []string) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID)
payload := IssuesWrapper{Issues: issueIDs}
req, err := s.client.NewRequest("POST", apiEndpoint, payload)
if err != nil {
return nil, err
}
resp, err := s.client.Do(req, nil)
return resp, err
}
// GetIssuesForSprint returns all issues in a sprint, for a given sprint Id.
// This only includes issues that the user has permission to view.
// By default, the returned issues are ordered by rank.
//
// JIRA API Docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/sprint-getIssuesForSprint
func (s *SprintService) GetIssuesForSprint(sprintID int) ([]Issue, *Response, error) {
apiEndpoint := fmt.Sprintf("rest/agile/1.0/sprint/%d/issue", sprintID)
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
if err != nil {
return nil, nil, err
}
result := new(IssuesInSprintResult)
resp, err := s.client.Do(req, result)
return result.Issues, resp, err
}

67
vendor/src/github.com/andygrunwald/go-jira/sprint_test.go

@ -0,0 +1,67 @@
package jira
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"testing"
)
func TestSprintService_MoveIssuesToSprint(t *testing.T) {
setup()
defer teardown()
testAPIEndpoint := "/rest/agile/1.0/sprint/123/issue"
issuesToMove := []string{"KEY-1", "KEY-2"}
testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "POST")
testRequestURL(t, r, testAPIEndpoint)
decoder := json.NewDecoder(r.Body)
var payload IssuesWrapper
err := decoder.Decode(&payload)
if err != nil {
t.Errorf("Got error: %v", err)
}
if payload.Issues[0] != issuesToMove[0] {
t.Errorf("Expected %s to be in payload, got %s instead", issuesToMove[0], payload.Issues[0])
}
})
_, err := testClient.Sprint.MoveIssuesToSprint(123, issuesToMove)
if err != nil {
t.Errorf("Got error: %v", err)
}
}
func TestSprintService_GetIssuesForSprint(t *testing.T) {
setup()
defer teardown()
testAPIEdpoint := "/rest/agile/1.0/sprint/123/issue"
raw, err := ioutil.ReadFile("./mocks/issues_in_sprint.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))
})
issues, _, err := testClient.Sprint.GetIssuesForSprint(123)
if err != nil {
t.Errorf("Error given: %v", err)
}
if issues == nil {
t.Error("Expected issues in sprint list. Issues list is nil")
}
if len(issues) != 1 {
t.Errorf("Expect there to be 1 issue in the sprint, found %v", len(issues))
}
}
Loading…
Cancel
Save