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) + } +}