mirror of
https://github.com/interviewstreet/go-jira.git
synced 2025-04-17 11:56:28 +02:00
Merge branch 'master' of https://github.com/nebril/go-jira into nebril-master
* 'master' of https://github.com/nebril/go-jira: Added basic Sprint API handling Removed strings.Compare Add Transition API handling
This commit is contained in:
commit
e6dd745ae7
@ -13,6 +13,7 @@
|
||||
|
||||
* 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/).
|
||||
|
4
jira.go
4
jira.go
@ -28,6 +28,8 @@ type Client struct {
|
||||
Issue *IssueService
|
||||
Project *ProjectService
|
||||
Board *BoardService
|
||||
Transition *TransitionService
|
||||
Sprint *SprintService
|
||||
}
|
||||
|
||||
// NewClient returns a new JIRA API client.
|
||||
@ -55,6 +57,8 @@ func NewClient(httpClient *http.Client, baseURL string) (*Client, error) {
|
||||
c.Issue = &IssueService{client: c}
|
||||
c.Project = &ProjectService{client: c}
|
||||
c.Board = &BoardService{client: c}
|
||||
c.Transition = &TransitionService{client: c}
|
||||
c.Sprint = &SprintService{client: c}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
46
mocks/sprints.json
Normal file
46
mocks/sprints.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"isLast": true,
|
||||
"maxResults": 50,
|
||||
"startAt": 0,
|
||||
"values": [
|
||||
{
|
||||
"completeDate": "2016-04-28T05:08:48.543-07:00",
|
||||
"endDate": "2016-04-27T08:29:00.000-07:00",
|
||||
"id": 740,
|
||||
"name": "Iteration-10",
|
||||
"originBoardId": 734,
|
||||
"self": "https://jira.com/rest/agile/1.0/sprint/740",
|
||||
"startDate": "2016-04-11T07:29:03.294-07:00",
|
||||
"state": "closed"
|
||||
},
|
||||
{
|
||||
"completeDate": "2016-05-30T02:45:44.991-07:00",
|
||||
"endDate": "2016-05-26T14:56:00.000-07:00",
|
||||
"id": 776,
|
||||
"name": "Iteration-12-1",
|
||||
"originBoardId": 734,
|
||||
"self": "https://jira.com/rest/agile/1.0/sprint/776",
|
||||
"startDate": "2016-05-19T13:56:00.000-07:00",
|
||||
"state": "closed"
|
||||
},
|
||||
{
|
||||
"completeDate": "2016-06-08T07:54:13.723-07:00",
|
||||
"endDate": "2016-06-08T01:06:00.000-07:00",
|
||||
"id": 807,
|
||||
"name": "Iteration-12-2",
|
||||
"originBoardId": 734,
|
||||
"self": "https://jira.com/rest/agile/1.0/sprint/807",
|
||||
"startDate": "2016-06-01T00:06:00.000-07:00",
|
||||
"state": "closed"
|
||||
},
|
||||
{
|
||||
"endDate": "2016-06-28T14:24:00.000-07:00",
|
||||
"id": 832,
|
||||
"name": "Iteration-13-2",
|
||||
"originBoardId": 734,
|
||||
"self": "https://jira.com/rest/agile/1.0/sprint/832",
|
||||
"startDate": "2016-06-20T13:24:39.161-07:00",
|
||||
"state": "active"
|
||||
}
|
||||
]
|
||||
}
|
101
mocks/transitions.json
Normal file
101
mocks/transitions.json
Normal file
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
63
sprint.go
Normal file
63
sprint.go
Normal file
@ -0,0 +1,63 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SprintService handles sprints in JIRA Agile API.
|
||||
type SprintService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
// Wrapper struct for moving issues to sprint
|
||||
type IssuesWrapper struct {
|
||||
Issues []string `json:"issues"`
|
||||
}
|
||||
|
||||
// GetList gets sprints for given board
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira-software/REST/cloud/#agile/1.0/board/{boardId}/sprint
|
||||
func (s *SprintService) GetList(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
|
||||
}
|
||||
|
||||
func (s *SprintService) AddIssuesToSprint(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
|
||||
}
|
71
sprint_test.go
Normal file
71
sprint_test.go
Normal file
@ -0,0 +1,71 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSprintGetList(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.Sprint.GetList("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))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMoveIssueToSprint(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.Error("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.AddIssuesToSprint(123, issuesToMove)
|
||||
|
||||
if err != nil {
|
||||
t.Error("Got error: %v", err)
|
||||
}
|
||||
}
|
75
transition.go
Normal file
75
transition.go
Normal file
@ -0,0 +1,75 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// TransitionService handles transitions for JIRA issue.
|
||||
type TransitionService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
||||
type TransitionField struct {
|
||||
Required bool `json:"required"`
|
||||
}
|
||||
|
||||
// CreatePayload is used for creating new issue transitions
|
||||
type CreateTransitionPayload struct {
|
||||
Transition TransitionPayload `json:"transition"`
|
||||
}
|
||||
|
||||
type TransitionPayload struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// GetList gets transitions available for given issue
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getTransitions
|
||||
func (s *TransitionService) GetList(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
|
||||
}
|
||||
|
||||
// Basic transition creation. This simply creates transition with given ID for issue
|
||||
// with given ID. It doesn't yet support anything else.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition
|
||||
func (s *TransitionService) Create(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
|
||||
}
|
75
transition_test.go
Normal file
75
transition_test.go
Normal file
@ -0,0 +1,75 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTransitionGetList(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.Transition.GetList("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 TestTransitionCreate(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.Error("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.Transition.Create("123", transitionID)
|
||||
|
||||
if err != nil {
|
||||
t.Error("Got error: %v", err)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user