1
0
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:
Andy Grunwald 2016-07-17 10:54:30 +02:00
commit e6dd745ae7
8 changed files with 436 additions and 0 deletions

View File

@ -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/).

View File

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