From 1fd364aea27cd55d877bb8c073417204a0ab0f3e Mon Sep 17 00:00:00 2001 From: Maciej Kwiek Date: Mon, 20 Jun 2016 17:14:55 +0200 Subject: [PATCH] Add Transition API handling Added TransitionService with 2 methods: * GetList for retrieving possible transitions for an issue * Create for creating transition and changing issue status in the process --- README.md | 1 + jira.go | 2 + mocks/transitions.json | 101 +++++++++++++++++++++++++++++++++++++++++ transition.go | 75 ++++++++++++++++++++++++++++++ transition_test.go | 76 +++++++++++++++++++++++++++++++ 5 files changed, 255 insertions(+) create mode 100644 mocks/transitions.json create mode 100644 transition.go create mode 100644 transition_test.go diff --git a/README.md b/README.md index c6c0a77..f9a92c9 100644 --- a/README.md +++ b/README.md @@ -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/). diff --git a/jira.go b/jira.go index 6222a37..34dcc3d 100644 --- a/jira.go +++ b/jira.go @@ -28,6 +28,7 @@ type Client struct { Issue *IssueService Project *ProjectService Board *BoardService + Transition *TransitionService } // NewClient returns a new JIRA API client. @@ -55,6 +56,7 @@ 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} return c, nil } diff --git a/mocks/transitions.json b/mocks/transitions.json new file mode 100644 index 0000000..d21f409 --- /dev/null +++ b/mocks/transitions.json @@ -0,0 +1,101 @@ +{ + "expand": "transitions", + "transitions": [ + { + "id": "2", + "name": "Close Issue", + "to": { + "self": "http://localhost:8090/jira/rest/api/2.0/status/10000", + "description": "The issue is currently being worked on.", + "iconUrl": "http://localhost:8090/jira/images/icons/progress.gif", + "name": "In Progress", + "id": "10000", + "statusCategory": { + "self": "http://localhost:8090/jira/rest/api/2.0/statuscategory/1", + "id": 1, + "key": "in-flight", + "colorName": "yellow", + "name": "In Progress" + } + }, + "fields": { + "summary": { + "required": false, + "schema": { + "type": "array", + "items": "option", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:multiselect", + "customId": 10001 + }, + "name": "My Multi Select", + "hasDefaultValue": false, + "operations": [ + "set", + "add" + ], + "allowedValues": [ + "red", + "blue" + ] + } + } + }, + { + "id": "711", + "name": "QA Review", + "to": { + "self": "http://localhost:8090/jira/rest/api/2.0/status/5", + "description": "The issue is closed.", + "iconUrl": "http://localhost:8090/jira/images/icons/closed.gif", + "name": "Closed", + "id": "5", + "statusCategory": { + "self": "http://localhost:8090/jira/rest/api/2.0/statuscategory/9", + "id": 9, + "key": "completed", + "colorName": "green" + } + }, + "fields": { + "summary": { + "required": false, + "schema": { + "type": "array", + "items": "option", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:multiselect", + "customId": 10001 + }, + "name": "My Multi Select", + "hasDefaultValue": false, + "operations": [ + "set", + "add" + ], + "allowedValues": [ + "red", + "blue" + ] + }, + "colour": { + "required": false, + "schema": { + "type": "array", + "items": "option", + "custom": "com.atlassian.jira.plugin.system.customfieldtypes:multiselect", + "customId": 10001 + }, + "name": "My Multi Select", + "hasDefaultValue": false, + "operations": [ + "set", + "add" + ], + "allowedValues": [ + "red", + "blue" + ] + } + } + } + ] +} diff --git a/transition.go b/transition.go new file mode 100644 index 0000000..c128825 --- /dev/null +++ b/transition.go @@ -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 +} diff --git a/transition_test.go b/transition_test.go new file mode 100644 index 0000000..b062274 --- /dev/null +++ b/transition_test.go @@ -0,0 +1,76 @@ +package jira + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + "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 strings.Compare(payload.Transition.ID, transitionID) != 0 { + 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) + } +}