mirror of https://github.com/matrix-org/go-neb.git
Kegan Dougal
8 years ago
23 changed files with 13303 additions and 0 deletions
-
6vendor/manifest
-
22vendor/src/github.com/andygrunwald/go-jira/LICENSE
-
172vendor/src/github.com/andygrunwald/go-jira/README.md
-
81vendor/src/github.com/andygrunwald/go-jira/authentication.go
-
86vendor/src/github.com/andygrunwald/go-jira/authentication_test.go
-
155vendor/src/github.com/andygrunwald/go-jira/board.go
-
186vendor/src/github.com/andygrunwald/go-jira/board_test.go
-
BINvendor/src/github.com/andygrunwald/go-jira/img/go-jira-compressed.png
-
574vendor/src/github.com/andygrunwald/go-jira/issue.go
-
467vendor/src/github.com/andygrunwald/go-jira/issue_test.go
-
224vendor/src/github.com/andygrunwald/go-jira/jira.go
-
390vendor/src/github.com/andygrunwald/go-jira/jira_test.go
-
43vendor/src/github.com/andygrunwald/go-jira/mocks/all_boards.json
-
25vendor/src/github.com/andygrunwald/go-jira/mocks/all_boards_filtered.json
-
9872vendor/src/github.com/andygrunwald/go-jira/mocks/all_projects.json
-
115vendor/src/github.com/andygrunwald/go-jira/mocks/issues_in_sprint.json
-
411vendor/src/github.com/andygrunwald/go-jira/mocks/project.json
-
46vendor/src/github.com/andygrunwald/go-jira/mocks/sprints.json
-
101vendor/src/github.com/andygrunwald/go-jira/mocks/transitions.json
-
120vendor/src/github.com/andygrunwald/go-jira/project.go
-
80vendor/src/github.com/andygrunwald/go-jira/project_test.go
-
60vendor/src/github.com/andygrunwald/go-jira/sprint.go
-
67vendor/src/github.com/andygrunwald/go-jira/sprint_test.go
@ -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. |
|||
|
@ -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). |
@ -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
|
@ -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") |
|||
} |
|||
} |
@ -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 |
|||
} |
@ -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)) |
|||
} |
|||
} |
After Width: 793 | Height: 288 | Size: 9.7 KiB |
@ -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 |
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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 |
|||
} |
@ -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") |
|||
} |
|||
} |
@ -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" |
|||
} |
|||
] |
|||
} |
@ -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
File diff suppressed because it is too large
View File
@ -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" |
|||
} |
|||
} |
|||
} |
|||
} |
|||
] |
|||
} |
@ -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" |
|||
} |
|||
} |
@ -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" |
|||
} |
|||
] |
|||
} |
@ -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" |
|||
] |
|||
} |
|||
} |
|||
} |
|||
] |
|||
} |
@ -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 |
|||
} |
@ -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) |
|||
} |
|||
} |
@ -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 |
|||
} |
@ -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)) |
|||
} |
|||
|
|||
} |
Write
Preview
Loading…
Cancel
Save
Reference in new issue