mirror of
https://github.com/interviewstreet/go-jira.git
synced 2025-02-01 13:07:50 +02:00
Merge branch 'master' into rbriski/jira_error
This commit is contained in:
commit
7df17cc390
@ -6,10 +6,12 @@ go:
|
||||
- 1.4
|
||||
- 1.5
|
||||
- 1.6
|
||||
- 1.7
|
||||
- 1.8
|
||||
- 1.9
|
||||
|
||||
before_install:
|
||||
- go get github.com/mattn/goveralls
|
||||
- go get golang.org/x/tools/cmd/cover
|
||||
- go get -t ./...
|
||||
|
||||
script:
|
||||
- $HOME/gopath/bin/goveralls -service=travis-ci
|
||||
- GOMAXPROCS=4 GORACE="halt_on_error=1" go test -race -v ./...
|
||||
|
114
README.md
114
README.md
@ -3,7 +3,6 @@
|
||||
[![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).
|
||||
|
||||
@ -12,9 +11,9 @@
|
||||
## Features
|
||||
|
||||
* Authentication (HTTP Basic, OAuth, Session Cookie)
|
||||
* Create and receive issues
|
||||
* Create and retrieve issues
|
||||
* Create and retrieve issue transitions (status updates)
|
||||
* Call every API endpoint of the JIRA, even it is not directly implemented in this library
|
||||
* Call every API endpoint of the JIRA, even if 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/).
|
||||
|
||||
@ -28,6 +27,17 @@ It is go gettable
|
||||
|
||||
$ go get github.com/andygrunwald/go-jira
|
||||
|
||||
For stable versions you can use one of our tags with [gopkg.in](http://labix.org/gopkg.in). E.g.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
jira "gopkg.in/andygrunwald/go-jira.v1"
|
||||
)
|
||||
...
|
||||
```
|
||||
|
||||
(optional) to run unit / example tests:
|
||||
|
||||
$ cd $GOPATH/src/github.com/andygrunwald/go-jira
|
||||
@ -58,7 +68,7 @@ import (
|
||||
|
||||
func main() {
|
||||
jiraClient, _ := jira.NewClient(nil, "https://issues.apache.org/jira/")
|
||||
issue, _, _ := jiraClient.Issue.Get("MESOS-3325")
|
||||
issue, _, _ := jiraClient.Issue.Get("MESOS-3325", nil)
|
||||
|
||||
fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
|
||||
fmt.Printf("Type: %s\n", issue.Fields.Type.Name)
|
||||
@ -70,10 +80,41 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
### Authenticate with session cookie
|
||||
### Authenticate with jira
|
||||
|
||||
Some actions require an authenticated user.
|
||||
Here is an example with a session cookie authentification.
|
||||
|
||||
#### Authenticate with basic auth
|
||||
|
||||
Here is an example with basic auth authentication.
|
||||
|
||||
```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)
|
||||
}
|
||||
jiraClient.Authentication.SetBasicAuth("username", "password")
|
||||
|
||||
issue, _, err := jiraClient.Issue.Get("SYS-5156", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
|
||||
}
|
||||
```
|
||||
|
||||
#### Authenticate with session cookie
|
||||
|
||||
Here is an example with session cookie authentication.
|
||||
|
||||
```go
|
||||
package main
|
||||
@ -95,7 +136,64 @@ func main() {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
issue, _, err := jiraClient.Issue.Get("SYS-5156")
|
||||
issue, _, err := jiraClient.Issue.Get("SYS-5156", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
|
||||
}
|
||||
```
|
||||
|
||||
#### Authenticate with OAuth
|
||||
|
||||
If you want to connect via OAuth to your JIRA Cloud instance checkout the [example of using OAuth authentication with JIRA in Go](https://gist.github.com/Lupus/edafe9a7c5c6b13407293d795442fe67) by [@Lupus](https://github.com/Lupus).
|
||||
|
||||
For more details have a look at the [issue #56](https://github.com/andygrunwald/go-jira/issues/56).
|
||||
|
||||
### Create an issue
|
||||
|
||||
Example how to create an issue.
|
||||
|
||||
```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)
|
||||
}
|
||||
|
||||
i := jira.Issue{
|
||||
Fields: &jira.IssueFields{
|
||||
Assignee: &jira.User{
|
||||
Name: "myuser",
|
||||
},
|
||||
Reporter: &jira.User{
|
||||
Name: "youruser",
|
||||
},
|
||||
Description: "Test Issue",
|
||||
Type: jira.IssueType{
|
||||
Name: "Bug",
|
||||
},
|
||||
Project: jira.Project{
|
||||
Key: "PROJ1",
|
||||
},
|
||||
Summary: "Just a demo issue",
|
||||
},
|
||||
}
|
||||
issue, _, err := jiraClient.Issue.Create(&i)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@ -169,4 +267,4 @@ If you are new to pull requests, checkout [Collaborating on projects using issue
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the terms of the [MIT license](http://en.wikipedia.org/wiki/MIT_License).
|
||||
This project is released under the terms of the [MIT license](http://en.wikipedia.org/wiki/MIT_License).
|
||||
|
@ -1,15 +1,33 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
// HTTP Basic Authentication
|
||||
authTypeBasic = 1
|
||||
// HTTP Session Authentication
|
||||
authTypeSession = 2
|
||||
)
|
||||
|
||||
// AuthenticationService handles authentication for the JIRA instance / API.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#authentication
|
||||
type AuthenticationService struct {
|
||||
client *Client
|
||||
|
||||
// Authentication type
|
||||
authType int
|
||||
|
||||
// Basic auth username
|
||||
username string
|
||||
|
||||
// Basic auth password
|
||||
password string
|
||||
}
|
||||
|
||||
// Session represents a Session JSON response by the JIRA API.
|
||||
@ -54,9 +72,9 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
|
||||
session := new(Session)
|
||||
resp, err := s.client.Do(req, session)
|
||||
|
||||
if resp != nil {
|
||||
session.Cookies = resp.Cookies()
|
||||
}
|
||||
if resp != nil {
|
||||
session.Cookies = resp.Cookies()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
|
||||
@ -66,19 +84,97 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
|
||||
}
|
||||
|
||||
s.client.session = session
|
||||
s.authType = authTypeSession
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Authenticated reports if the current Client has an authenticated session with JIRA
|
||||
// SetBasicAuth sets username and password for the basic auth against the JIRA instance.
|
||||
func (s *AuthenticationService) SetBasicAuth(username, password string) {
|
||||
s.username = username
|
||||
s.password = password
|
||||
s.authType = authTypeBasic
|
||||
}
|
||||
|
||||
// Authenticated reports if the current Client has authentication details for JIRA
|
||||
func (s *AuthenticationService) Authenticated() bool {
|
||||
if s != nil {
|
||||
return s.client.session != nil
|
||||
if s.authType == authTypeSession {
|
||||
return s.client.session != nil
|
||||
} else if s.authType == authTypeBasic {
|
||||
return s.username != ""
|
||||
}
|
||||
|
||||
}
|
||||
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
|
||||
// Logout logs out the current user that has been authenticated and the session in the client is destroyed.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
|
||||
func (s *AuthenticationService) Logout() error {
|
||||
if s.authType != authTypeSession || s.client.session == nil {
|
||||
return fmt.Errorf("No user is authenticated yet.")
|
||||
}
|
||||
|
||||
apiEndpoint := "rest/auth/1/session"
|
||||
req, err := s.client.NewRequest("DELETE", apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Creating the request to log the user out failed : %s", err)
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(req, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error sending the logout request: %s", err)
|
||||
}
|
||||
if resp.StatusCode != 204 {
|
||||
return fmt.Errorf("The logout was unsuccessful with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// If logout successful, delete session
|
||||
s.client.session = nil
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// GetCurrentUser gets the details of the current user.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
|
||||
func (s *AuthenticationService) GetCurrentUser() (*Session, error) {
|
||||
if s == nil {
|
||||
return nil, fmt.Errorf("AUthenticaiton Service is not instantiated")
|
||||
}
|
||||
if s.authType != authTypeSession || s.client.session == nil {
|
||||
return nil, fmt.Errorf("No user is authenticated yet")
|
||||
}
|
||||
|
||||
apiEndpoint := "rest/auth/1/session"
|
||||
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not create request for getting user info : %s", err)
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(req, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error sending request to get user info : %s", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("Getting user info failed with status : %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
ret := new(Session)
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't read body from the response : %s", err)
|
||||
}
|
||||
|
||||
err = json.Unmarshal(data, &ret)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not unmarshall received user info : %s", err)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@ -73,6 +74,29 @@ func TestAuthenticationService_AcquireSessionCookie_Success(t *testing.T) {
|
||||
if testClient.Authentication.Authenticated() != true {
|
||||
t.Error("Expected true, but result was false")
|
||||
}
|
||||
|
||||
if testClient.Authentication.authType != authTypeSession {
|
||||
t.Errorf("Expected authType %d. Got %d", authTypeSession, testClient.Authentication.authType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationService_SetBasicAuth(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
testClient.Authentication.SetBasicAuth("test-user", "test-password")
|
||||
|
||||
if testClient.Authentication.username != "test-user" {
|
||||
t.Errorf("Expected username test-user. Got %s", testClient.Authentication.username)
|
||||
}
|
||||
|
||||
if testClient.Authentication.password != "test-password" {
|
||||
t.Errorf("Expected password test-password. Got %s", testClient.Authentication.password)
|
||||
}
|
||||
|
||||
if testClient.Authentication.authType != authTypeBasic {
|
||||
t.Errorf("Expected authType %d. Got %d", authTypeBasic, testClient.Authentication.authType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationService_Authenticated(t *testing.T) {
|
||||
@ -84,3 +108,214 @@ func TestAuthenticationService_Authenticated(t *testing.T) {
|
||||
t.Error("Expected false, but result was true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationService_Authenticated_WithBasicAuth(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
testClient.Authentication.SetBasicAuth("test-user", "test-password")
|
||||
|
||||
// Test before we've attempted to authenticate
|
||||
if testClient.Authentication.Authenticated() != true {
|
||||
t.Error("Expected true, but result was false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationService_Authenticated_WithBasicAuthButNoUsername(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
testClient.Authentication.SetBasicAuth("", "test-password")
|
||||
|
||||
// Test before we've attempted to authenticate
|
||||
if testClient.Authentication.Authenticated() != false {
|
||||
t.Error("Expected false, but result was true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAithenticationService_GetUserInfo_AccessForbidden_Fail(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
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"}}`)
|
||||
}
|
||||
|
||||
if r.Method == "GET" {
|
||||
testMethod(t, r, "GET")
|
||||
testRequestURL(t, r, "/rest/auth/1/session")
|
||||
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
}
|
||||
})
|
||||
|
||||
testClient.Authentication.AcquireSessionCookie("foo", "bar")
|
||||
|
||||
_, err := testClient.Authentication.GetCurrentUser()
|
||||
if err == nil {
|
||||
t.Errorf("Non nil error expect, received nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationService_GetUserInfo_NonOkStatusCode_Fail(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
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"}}`)
|
||||
}
|
||||
|
||||
if r.Method == "GET" {
|
||||
testMethod(t, r, "GET")
|
||||
testRequestURL(t, r, "/rest/auth/1/session")
|
||||
//any status but 200
|
||||
w.WriteHeader(240)
|
||||
}
|
||||
})
|
||||
|
||||
testClient.Authentication.AcquireSessionCookie("foo", "bar")
|
||||
|
||||
_, err := testClient.Authentication.GetCurrentUser()
|
||||
if err == nil {
|
||||
t.Errorf("Non nil error expect, received nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationService_GetUserInfo_FailWithoutLogin(t *testing.T) {
|
||||
// no setup() required here
|
||||
testClient = new(Client)
|
||||
|
||||
_, err := testClient.Authentication.GetCurrentUser()
|
||||
if err == nil {
|
||||
t.Errorf("Expected error, but got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationService_GetUserInfo_Success(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
testUserInfo := new(Session)
|
||||
testUserInfo.Name = "foo"
|
||||
testUserInfo.Self = "https://my.jira.com/rest/api/latest/user?username=foo"
|
||||
testUserInfo.LoginInfo.FailedLoginCount = 12
|
||||
testUserInfo.LoginInfo.LastFailedLoginTime = "2016-09-06T16:41:23.949+0200"
|
||||
testUserInfo.LoginInfo.LoginCount = 357
|
||||
testUserInfo.LoginInfo.PreviousLoginTime = "2016-09-07T11:36:23.476+0200"
|
||||
|
||||
testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
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"}}`)
|
||||
}
|
||||
|
||||
if r.Method == "GET" {
|
||||
testMethod(t, r, "GET")
|
||||
testRequestURL(t, r, "/rest/auth/1/session")
|
||||
fmt.Fprint(w, `{"self":"https://my.jira.com/rest/api/latest/user?username=foo","name":"foo","loginInfo":{"failedLoginCount":12,"loginCount":357,"lastFailedLoginTime":"2016-09-06T16:41:23.949+0200","previousLoginTime":"2016-09-07T11:36:23.476+0200"}}`)
|
||||
}
|
||||
})
|
||||
|
||||
testClient.Authentication.AcquireSessionCookie("foo", "bar")
|
||||
|
||||
userinfo, err := testClient.Authentication.GetCurrentUser()
|
||||
if err != nil {
|
||||
t.Errorf("Nil error expect, received %s", err)
|
||||
}
|
||||
equal := reflect.DeepEqual(*testUserInfo, *userinfo)
|
||||
|
||||
if !equal {
|
||||
t.Error("The user information doesn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationService_Logout_Success(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "POST" {
|
||||
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"}}`)
|
||||
}
|
||||
|
||||
if r.Method == "DELETE" {
|
||||
// return 204
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
})
|
||||
|
||||
testClient.Authentication.AcquireSessionCookie("foo", "bar")
|
||||
|
||||
err := testClient.Authentication.Logout()
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, got %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticationService_Logout_FailWithoutLogin(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == "DELETE" {
|
||||
// 401
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
}
|
||||
})
|
||||
err := testClient.Authentication.Logout()
|
||||
if err == nil {
|
||||
t.Error("Expected not nil, got nil")
|
||||
}
|
||||
}
|
||||
|
38
board.go
38
board.go
@ -14,20 +14,20 @@ type BoardService struct {
|
||||
|
||||
// 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"`
|
||||
MaxResults int `json:"maxResults" structs:"maxResults"`
|
||||
StartAt int `json:"startAt" structs:"startAt"`
|
||||
Total int `json:"total" structs:"total"`
|
||||
IsLast bool `json:"isLast" structs:"isLast"`
|
||||
Values []Board `json:"values" structs:"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"`
|
||||
ID int `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitemtpy"`
|
||||
Type string `json:"type,omitempty" structs:"type,omitempty"`
|
||||
FilterID int `json:"filterId,omitempty" structs:"filterId,omitempty"`
|
||||
}
|
||||
|
||||
// BoardListOptions specifies the optional parameters to the BoardService.GetList
|
||||
@ -46,19 +46,19 @@ type BoardListOptions struct {
|
||||
|
||||
// Wrapper struct for search result
|
||||
type sprintsResult struct {
|
||||
Sprints []Sprint `json:"values"`
|
||||
Sprints []Sprint `json:"values" structs:"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"`
|
||||
ID int `json:"id" structs:"id"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
CompleteDate *time.Time `json:"completeDate" structs:"completeDate"`
|
||||
EndDate *time.Time `json:"endDate" structs:"endDate"`
|
||||
StartDate *time.Time `json:"startDate" structs:"startDate"`
|
||||
OriginBoardID int `json:"originBoardId" structs:"originBoardId"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
State string `json:"state" structs:"state"`
|
||||
}
|
||||
|
||||
// GetAllBoards will returns all boards. This only includes boards that the user has permission to view.
|
||||
|
130
example_test.go
Normal file
130
example_test.go
Normal file
@ -0,0 +1,130 @@
|
||||
package jira_test
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
jira "github.com/andygrunwald/go-jira"
|
||||
)
|
||||
|
||||
func ExampleNewClient() {
|
||||
jiraClient, _ := jira.NewClient(nil, "https://issues.apache.org/jira/")
|
||||
issue, _, _ := jiraClient.Issue.Get("MESOS-3325", nil)
|
||||
|
||||
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)
|
||||
|
||||
// Output:
|
||||
// MESOS-3325: Running mesos-slave@0.23 in a container causes slave to be lost after a restart
|
||||
// Type: Bug
|
||||
// Priority: Critical
|
||||
}
|
||||
|
||||
func ExampleNewClient_ignoreCertificateErrors() {
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
|
||||
jiraClient, _ := jira.NewClient(client, "https://issues.apache.org/jira/")
|
||||
issue, _, _ := jiraClient.Issue.Get("MESOS-3325", nil)
|
||||
|
||||
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)
|
||||
|
||||
// Output:
|
||||
// MESOS-3325: Running mesos-slave@0.23 in a container causes slave to be lost after a restart
|
||||
// Type: Bug
|
||||
// Priority: Critical
|
||||
}
|
||||
|
||||
func ExampleAuthenticationService_SetBasicAuth() {
|
||||
jiraClient, err := jira.NewClient(nil, "https://your.jira-instance.com/")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
jiraClient.Authentication.SetBasicAuth("username", "password")
|
||||
|
||||
issue, _, err := jiraClient.Issue.Get("SYS-5156", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
|
||||
}
|
||||
|
||||
func ExampleAuthenticationService_AcquireSessionCookie() {
|
||||
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", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
|
||||
}
|
||||
|
||||
func ExampleIssueService_Create() {
|
||||
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)
|
||||
}
|
||||
|
||||
i := jira.Issue{
|
||||
Fields: &jira.IssueFields{
|
||||
Assignee: &jira.User{
|
||||
Name: "myuser",
|
||||
},
|
||||
Reporter: &jira.User{
|
||||
Name: "youruser",
|
||||
},
|
||||
Description: "Test Issue",
|
||||
Type: jira.IssueType{
|
||||
Name: "Bug",
|
||||
},
|
||||
Project: jira.Project{
|
||||
Key: "PROJ1",
|
||||
},
|
||||
Summary: "Just a demo issue",
|
||||
},
|
||||
}
|
||||
issue, _, err := jiraClient.Issue.Create(&i)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
fmt.Printf("%s: %+v\n", issue.Key, issue.Fields.Summary)
|
||||
}
|
||||
|
||||
func ExampleClient_Do() {
|
||||
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)
|
||||
}
|
||||
}
|
53
group.go
Normal file
53
group.go
Normal file
@ -0,0 +1,53 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// GroupService handles Groups for the JIRA instance / API.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group
|
||||
type GroupService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// groupMembersResult is only a small wrapper around the Group* methods
|
||||
// to be able to parse the results
|
||||
type groupMembersResult struct {
|
||||
StartAt int `json:"startAt"`
|
||||
MaxResults int `json:"maxResults"`
|
||||
Total int `json:"total"`
|
||||
Members []GroupMember `json:"values"`
|
||||
}
|
||||
|
||||
// GroupMember reflects a single member of a group
|
||||
type GroupMember struct {
|
||||
Self string `json:"self,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
EmailAddress string `json:"emailAddress,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty"`
|
||||
Active bool `json:"active,omitempty"`
|
||||
TimeZone string `json:"timeZone,omitempty"`
|
||||
}
|
||||
|
||||
// Get returns a paginated list of users who are members of the specified group and its subgroups.
|
||||
// Users in the page are ordered by user names.
|
||||
// User of this resource is required to have sysadmin or admin permissions.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/server/#api/2/group-getUsersFromGroup
|
||||
func (s *GroupService) Get(name string) ([]GroupMember, *Response, error) {
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/group/member?groupname=%s", name)
|
||||
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
group := new(groupMembersResult)
|
||||
resp, err := s.client.Do(req, group)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return group.Members, resp, nil
|
||||
}
|
732
issue.go
732
issue.go
@ -2,12 +2,19 @@ package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/structs"
|
||||
"github.com/google/go-querystring/query"
|
||||
"github.com/trivago/tgo/tcontainer"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -24,170 +31,255 @@ type IssueService struct {
|
||||
|
||||
// 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"`
|
||||
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Key string `json:"key,omitempty" structs:"key,omitempty"`
|
||||
Fields *IssueFields `json:"fields,omitempty" structs:"fields,omitempty"`
|
||||
Changelog *Changelog `json:"changelog,omitempty" structs:"changelog,omitempty"`
|
||||
}
|
||||
|
||||
// ChangelogItems reflects one single changelog item of a history item
|
||||
type ChangelogItems struct {
|
||||
Field string `json:"field" structs:"field"`
|
||||
FieldType string `json:"fieldtype" structs:"fieldtype"`
|
||||
From interface{} `json:"from" structs:"from"`
|
||||
FromString string `json:"fromString" structs:"fromString"`
|
||||
To interface{} `json:"to" structs:"to"`
|
||||
ToString string `json:"toString" structs:"toString"`
|
||||
}
|
||||
|
||||
// ChangelogHistory reflects one single changelog history entry
|
||||
type ChangelogHistory struct {
|
||||
Id string `json:"id" structs:"id"`
|
||||
Author User `json:"author" structs:"author"`
|
||||
Created string `json:"created" structs:"created"`
|
||||
Items []ChangelogItems `json:"items" structs:"items"`
|
||||
}
|
||||
|
||||
// Changelog reflects the change log of an issue
|
||||
type Changelog struct {
|
||||
Histories []ChangelogHistory `json:"histories,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"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Filename string `json:"filename,omitempty" structs:"filename,omitempty"`
|
||||
Author *User `json:"author,omitempty" structs:"author,omitempty"`
|
||||
Created string `json:"created,omitempty" structs:"created,omitempty"`
|
||||
Size int `json:"size,omitempty" structs:"size,omitempty"`
|
||||
MimeType string `json:"mimeType,omitempty" structs:"mimeType,omitempty"`
|
||||
Content string `json:"content,omitempty" structs:"content,omitempty"`
|
||||
Thumbnail string `json:"thumbnail,omitempty" structs:"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"`
|
||||
ID int `json:"id" structs:"id"`
|
||||
Key string `json:"key" structs:"key"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
Summary string `json:"summary" structs:"summary"`
|
||||
Done bool `json:"done" structs:"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"`
|
||||
// * "aggregatetimespent": null,
|
||||
// * "workratio": -1,
|
||||
// * "lastViewed": null,
|
||||
// * "aggregatetimeoriginalestimate": null,
|
||||
// * "aggregatetimeestimate": null,
|
||||
// * "environment": null,
|
||||
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
|
||||
Type IssueType `json:"issuetype" structs:"issuetype"`
|
||||
Project Project `json:"project,omitempty" structs:"project,omitempty"`
|
||||
Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"`
|
||||
Priority *Priority `json:"priority,omitempty" structs:"priority,omitempty"`
|
||||
Resolutiondate string `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"`
|
||||
Created string `json:"created,omitempty" structs:"created,omitempty"`
|
||||
Duedate string `json:"duedate,omitempty" structs:"duedate,omitempty"`
|
||||
Watches *Watches `json:"watches,omitempty" structs:"watches,omitempty"`
|
||||
Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"`
|
||||
Updated string `json:"updated,omitempty" structs:"updated,omitempty"`
|
||||
Description string `json:"description,omitempty" structs:"description,omitempty"`
|
||||
Summary string `json:"summary" structs:"summary"`
|
||||
Creator *User `json:"Creator,omitempty" structs:"Creator,omitempty"`
|
||||
Reporter *User `json:"reporter,omitempty" structs:"reporter,omitempty"`
|
||||
Components []*Component `json:"components,omitempty" structs:"components,omitempty"`
|
||||
Status *Status `json:"status,omitempty" structs:"status,omitempty"`
|
||||
Progress *Progress `json:"progress,omitempty" structs:"progress,omitempty"`
|
||||
AggregateProgress *Progress `json:"aggregateprogress,omitempty" structs:"aggregateprogress,omitempty"`
|
||||
TimeTracking *TimeTracking `json:"timetracking,omitempty" structs:"timetracking,omitempty"`
|
||||
TimeSpent int `json:"timespent,omitempty" structs:"timespent,omitempty"`
|
||||
TimeEstimate int `json:"timeestimate,omitempty" structs:"timeestimate,omitempty"`
|
||||
TimeOriginalEstimate int `json:"timeoriginalestimate,omitempty" structs:"timeoriginalestimate,omitempty"`
|
||||
Worklog *Worklog `json:"worklog,omitempty" structs:"worklog,omitempty"`
|
||||
IssueLinks []*IssueLink `json:"issuelinks,omitempty" structs:"issuelinks,omitempty"`
|
||||
Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"`
|
||||
FixVersions []*FixVersion `json:"fixVersions,omitempty" structs:"fixVersions,omitempty"`
|
||||
Labels []string `json:"labels,omitempty" structs:"labels,omitempty"`
|
||||
Subtasks []*Subtasks `json:"subtasks,omitempty" structs:"subtasks,omitempty"`
|
||||
Attachments []*Attachment `json:"attachment,omitempty" structs:"attachment,omitempty"`
|
||||
Epic *Epic `json:"epic,omitempty" structs:"epic,omitempty"`
|
||||
Parent *Parent `json:"parent,omitempty" structs:"parent,omitempty"`
|
||||
Unknowns tcontainer.MarshalMap
|
||||
}
|
||||
|
||||
// MarshalJSON is a custom JSON marshal function for the IssueFields structs.
|
||||
// It handles JIRA custom fields and maps those from / to "Unknowns" key.
|
||||
func (i *IssueFields) MarshalJSON() ([]byte, error) {
|
||||
m := structs.Map(i)
|
||||
unknowns, okay := m["Unknowns"]
|
||||
if okay {
|
||||
// if unknowns present, shift all key value from unknown to a level up
|
||||
for key, value := range unknowns.(tcontainer.MarshalMap) {
|
||||
m[key] = value
|
||||
}
|
||||
delete(m, "Unknowns")
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// UnmarshalJSON is a custom JSON marshal function for the IssueFields structs.
|
||||
// It handles JIRA custom fields and maps those from / to "Unknowns" key.
|
||||
func (i *IssueFields) UnmarshalJSON(data []byte) error {
|
||||
|
||||
// Do the normal unmarshalling first
|
||||
// Details for this way: http://choly.ca/post/go-json-marshalling/
|
||||
type Alias IssueFields
|
||||
aux := &struct {
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(i),
|
||||
}
|
||||
if err := json.Unmarshal(data, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
totalMap := tcontainer.NewMarshalMap()
|
||||
err := json.Unmarshal(data, &totalMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t := reflect.TypeOf(*i)
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
tagDetail := field.Tag.Get("json")
|
||||
if tagDetail == "" {
|
||||
// ignore if there are no tags
|
||||
continue
|
||||
}
|
||||
options := strings.Split(tagDetail, ",")
|
||||
|
||||
if len(options) == 0 {
|
||||
return fmt.Errorf("No tags options found for %s", field.Name)
|
||||
}
|
||||
// the first one is the json tag
|
||||
key := options[0]
|
||||
if _, okay := totalMap.Value(key); okay {
|
||||
delete(totalMap, key)
|
||||
}
|
||||
|
||||
}
|
||||
i = (*IssueFields)(aux.Alias)
|
||||
// all the tags found in the struct were removed. Whatever is left are unknowns to struct
|
||||
i.Unknowns = totalMap
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Description string `json:"description,omitempty" structs:"description,omitempty"`
|
||||
IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||
Subtask bool `json:"subtask,omitempty" structs:"subtask,omitempty"`
|
||||
AvatarID int `json:"avatarId,omitempty" structs:"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"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
Description string `json:"description" structs:"description"`
|
||||
Name string `json:"name" structs:"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"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"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"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
WatchCount int `json:"watchCount,omitempty" structs:"watchCount,omitempty"`
|
||||
IsWatching bool `json:"isWatching,omitempty" structs:"isWatching,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"`
|
||||
Four8X48 string `json:"48x48,omitempty" structs:"48x48,omitempty"`
|
||||
Two4X24 string `json:"24x24,omitempty" structs:"24x24,omitempty"`
|
||||
One6X16 string `json:"16x16,omitempty" structs:"16x16,omitempty"`
|
||||
Three2X32 string `json:"32x32,omitempty" structs:"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"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"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"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
Description string `json:"description" structs:"description"`
|
||||
IconURL string `json:"iconUrl" structs:"iconUrl"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
StatusCategory StatusCategory `json:"statusCategory" structs:"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"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
ID int `json:"id" structs:"id"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
Key string `json:"key" structs:"key"`
|
||||
ColorName string `json:"colorName" structs:"colorName"`
|
||||
}
|
||||
|
||||
// Progress represents the progress of a JIRA issue.
|
||||
type Progress struct {
|
||||
Progress int `json:"progress"`
|
||||
Total int `json:"total"`
|
||||
Progress int `json:"progress" structs:"progress"`
|
||||
Total int `json:"total" structs:"total"`
|
||||
}
|
||||
|
||||
// Parent represents the parent of a JIRA issue, to be used with subtask issue types.
|
||||
type Parent struct {
|
||||
ID string `json:"id,omitempty" structs:"id"`
|
||||
Key string `json:"key,omitempty" structs:"key"`
|
||||
}
|
||||
|
||||
// Time represents the Time definition of JIRA as a time.Time of go
|
||||
@ -195,29 +287,35 @@ type Time time.Time
|
||||
|
||||
// Wrapper struct for search result
|
||||
type transitionResult struct {
|
||||
Transitions []Transition `json:"transitions"`
|
||||
Transitions []Transition `json:"transitions" structs:"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"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
Fields map[string]TransitionField `json:"fields" structs:"fields"`
|
||||
}
|
||||
|
||||
// TransitionField represents the value of one Transistion
|
||||
// TransitionField represents the value of one Transition
|
||||
type TransitionField struct {
|
||||
Required bool `json:"required"`
|
||||
Required bool `json:"required" structs:"required"`
|
||||
}
|
||||
|
||||
// CreateTransitionPayload is used for creating new issue transitions
|
||||
type CreateTransitionPayload struct {
|
||||
Transition TransitionPayload `json:"transition"`
|
||||
Transition TransitionPayload `json:"transition" structs:"transition"`
|
||||
}
|
||||
|
||||
// TransitionPayload represents the request payload of Transistion calls like DoTransition
|
||||
// TransitionPayload represents the request payload of Transition calls like DoTransition
|
||||
type TransitionPayload struct {
|
||||
ID string `json:"id"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
}
|
||||
|
||||
// Option represents an option value in a SelectList or MultiSelect
|
||||
// custom issue field
|
||||
type Option struct {
|
||||
Value string `json:"value" structs:"value"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON will transform the JIRA time into a time.Time
|
||||
@ -235,90 +333,100 @@ func (t *Time) UnmarshalJSON(b []byte) error {
|
||||
// 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"`
|
||||
StartAt int `json:"startAt" structs:"startAt"`
|
||||
MaxResults int `json:"maxResults" structs:"maxResults"`
|
||||
Total int `json:"total" structs:"total"`
|
||||
Worklogs []WorklogRecord `json:"worklogs" structs:"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"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
Author User `json:"author" structs:"author"`
|
||||
UpdateAuthor User `json:"updateAuthor" structs:"updateAuthor"`
|
||||
Comment string `json:"comment" structs:"comment"`
|
||||
Created Time `json:"created" structs:"created"`
|
||||
Updated Time `json:"updated" structs:"updated"`
|
||||
Started Time `json:"started" structs:"started"`
|
||||
TimeSpent string `json:"timeSpent" structs:"timeSpent"`
|
||||
TimeSpentSeconds int `json:"timeSpentSeconds" structs:"timeSpentSeconds"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
IssueID string `json:"issueId" structs:"issueId"`
|
||||
}
|
||||
|
||||
// TimeTracking represents the timetracking fields of a JIRA issue.
|
||||
type TimeTracking struct {
|
||||
OriginalEstimate string `json:"originalEstimate,omitempty" structs:"originalEstimate,omitempty"`
|
||||
RemainingEstimate string `json:"remainingEstimate,omitempty" structs:"remainingEstimate,omitempty"`
|
||||
TimeSpent string `json:"timeSpent,omitempty" structs:"timeSpent,omitempty"`
|
||||
OriginalEstimateSeconds int `json:"originalEstimateSeconds,omitempty" structs:"originalEstimateSeconds,omitempty"`
|
||||
RemainingEstimateSeconds int `json:"remainingEstimateSeconds,omitempty" structs:"remainingEstimateSeconds,omitempty"`
|
||||
TimeSpentSeconds int `json:"timeSpentSeconds,omitempty" structs:"timeSpentSeconds,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
Key string `json:"key" structs:"key"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
Fields IssueFields `json:"fields" structs:"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"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Type IssueLinkType `json:"type" structs:"type"`
|
||||
OutwardIssue *Issue `json:"outwardIssue" structs:"outwardIssue"`
|
||||
InwardIssue *Issue `json:"inwardIssue" structs:"inwardIssue"`
|
||||
Comment *Comment `json:"comment,omitempty" structs:"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"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
Inward string `json:"inward" structs:"inward"`
|
||||
Outward string `json:"outward" structs:"outward"`
|
||||
}
|
||||
|
||||
// Comments represents a list of Comment.
|
||||
type Comments struct {
|
||||
Comments []*Comment `json:"comments,omitempty"`
|
||||
Comments []*Comment `json:"comments,omitempty" structs:"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"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||
Author User `json:"author,omitempty" structs:"author,omitempty"`
|
||||
Body string `json:"body,omitempty" structs:"body,omitempty"`
|
||||
UpdateAuthor User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"`
|
||||
Updated string `json:"updated,omitempty" structs:"updated,omitempty"`
|
||||
Created string `json:"created,omitempty" structs:"created,omitempty"`
|
||||
Visibility CommentVisibility `json:"visibility,omitempty" structs:"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"`
|
||||
Archived *bool `json:"archived,omitempty" structs:"archived,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||
ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"`
|
||||
ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"`
|
||||
Released *bool `json:"released,omitempty" structs:"released,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"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"`
|
||||
Type string `json:"type,omitempty" structs:"type,omitempty"`
|
||||
Value string `json:"value,omitempty" structs:"value,omitempty"`
|
||||
}
|
||||
|
||||
// SearchOptions specifies the optional parameters to various List methods that
|
||||
@ -332,15 +440,30 @@ type SearchOptions struct {
|
||||
StartAt int `url:"startAt,omitempty"`
|
||||
// MaxResults: The maximum number of projects to return per page. Default: 50.
|
||||
MaxResults int `url:"maxResults,omitempty"`
|
||||
// Expand: Expand specific sections in the returned issues
|
||||
Expand string `url:"expand,omitempty"`
|
||||
Fields []string
|
||||
}
|
||||
|
||||
// searchResult is only a small wrapper arround the Search (with JQL) method
|
||||
// searchResult is only a small wrapper around 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"`
|
||||
Issues []Issue `json:"issues" structs:"issues"`
|
||||
StartAt int `json:"startAt" structs:"startAt"`
|
||||
MaxResults int `json:"maxResults" structs:"maxResults"`
|
||||
Total int `json:"total" structs:"total"`
|
||||
}
|
||||
|
||||
// GetQueryOptions specifies the optional parameters for the Get Issue methods
|
||||
type GetQueryOptions struct {
|
||||
// Fields is the list of fields to return for the issue. By default, all fields are returned.
|
||||
Fields string `url:"fields,omitempty"`
|
||||
Expand string `url:"expand,omitempty"`
|
||||
// Properties is the list of properties to return for the issue. By default no properties are returned.
|
||||
Properties string `url:"properties,omitempty"`
|
||||
// FieldsByKeys if true then fields in issues will be referenced by keys instead of ids
|
||||
FieldsByKeys bool `url:"fieldsByKeys,omitempty"`
|
||||
UpdateHistory bool `url:"updateHistory,omitempty"`
|
||||
}
|
||||
|
||||
// CustomFields represents custom fields of JIRA
|
||||
@ -352,14 +475,24 @@ type CustomFields map[string]string
|
||||
// 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.
|
||||
//
|
||||
// The given options will be appended to the query string
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-getIssue
|
||||
func (s *IssueService) Get(issueID string) (*Issue, *Response, error) {
|
||||
func (s *IssueService) Get(issueID string, options *GetQueryOptions) (*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
|
||||
}
|
||||
|
||||
if options != nil {
|
||||
q, err := query.Values(options)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
req.URL.RawQuery = q.Encode()
|
||||
}
|
||||
|
||||
issue := new(Issue)
|
||||
resp, err := s.client.Do(req, issue)
|
||||
if err != nil {
|
||||
@ -390,9 +523,9 @@ func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error
|
||||
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)
|
||||
// PostAttachment uploads r (io.Reader) as an attachment to a given issueID
|
||||
func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) {
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", issueID)
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
writer := multipart.NewWriter(b)
|
||||
@ -428,6 +561,23 @@ func (s *IssueService) PostAttachment(attachmentID string, r io.Reader, attachme
|
||||
return attachment, resp, nil
|
||||
}
|
||||
|
||||
// GetWorklogs gets all the worklogs for an issue.
|
||||
// This method is especially important if you need to read all the worklogs, not just the first page.
|
||||
//
|
||||
// https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/worklog-getIssueWorklog
|
||||
func (s *IssueService) GetWorklogs(issueID string) (*Worklog, *Response, error) {
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/worklog", issueID)
|
||||
|
||||
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
v := new(Worklog)
|
||||
resp, err := s.client.Do(req, v)
|
||||
return v, resp, err
|
||||
}
|
||||
|
||||
// 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.
|
||||
@ -439,15 +589,63 @@ func (s *IssueService) Create(issue *Issue) (*Issue, *Response, error) {
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
resp, err := s.client.Do(req, nil)
|
||||
if err != nil {
|
||||
// incase of error return the resp for further inspection
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
responseIssue := new(Issue)
|
||||
resp, err := s.client.Do(req, responseIssue)
|
||||
defer resp.Body.Close()
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, resp, fmt.Errorf("Could not read the returned data")
|
||||
}
|
||||
err = json.Unmarshal(data, responseIssue)
|
||||
if err != nil {
|
||||
return nil, resp, fmt.Errorf("Could not unmarshall the data into struct")
|
||||
}
|
||||
return responseIssue, resp, nil
|
||||
}
|
||||
|
||||
// Update updates an issue from a JSON representation. The issue is found by key.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue-editIssue
|
||||
func (s *IssueService) Update(issue *Issue) (*Issue, *Response, error) {
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", issue.Key)
|
||||
req, err := s.client.NewRequest("PUT", apiEndpoint, issue)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
resp, err := s.client.Do(req, nil)
|
||||
if err != nil {
|
||||
jerr := NewJiraError(resp, err)
|
||||
return nil, resp, jerr
|
||||
}
|
||||
|
||||
return responseIssue, resp, nil
|
||||
// This is just to follow the rest of the API's convention of returning an issue.
|
||||
// Returning the same pointer here is pointless, so we return a copy instead.
|
||||
ret := *issue
|
||||
return &ret, resp, nil
|
||||
}
|
||||
|
||||
// Update updates an issue from a JSON representation. The issue is found by key.
|
||||
//
|
||||
// https://docs.atlassian.com/jira/REST/7.4.0/#api/2/issue-editIssue
|
||||
func (s *IssueService) UpdateIssue(jiraId string, data map[string]interface{}) (*Response, error) {
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%v", jiraId)
|
||||
req, err := s.client.NewRequest("PUT", apiEndpoint, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := s.client.Do(req, nil)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// This is just to follow the rest of the API's convention of returning an issue.
|
||||
// Returning the same pointer here is pointless, so we return a copy instead.
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// AddComment adds a new comment to issueID.
|
||||
@ -470,6 +668,30 @@ func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *
|
||||
return responseComment, resp, nil
|
||||
}
|
||||
|
||||
// UpdateComment updates the body of a comment, identified by comment.ID, on the issueID.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/issue/{issueIdOrKey}/comment-updateComment
|
||||
func (s *IssueService) UpdateComment(issueID string, comment *Comment) (*Comment, *Response, error) {
|
||||
reqBody := struct {
|
||||
Body string `json:"body"`
|
||||
}{
|
||||
Body: comment.Body,
|
||||
}
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/comment/%s", issueID, comment.ID)
|
||||
req, err := s.client.NewRequest("POST", apiEndpoint, reqBody)
|
||||
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
|
||||
@ -496,8 +718,8 @@ func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Res
|
||||
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)
|
||||
u = fmt.Sprintf("rest/api/2/search?jql=%s&startAt=%d&maxResults=%d&expand=%s&fields=%s", url.QueryEscape(jql),
|
||||
options.StartAt, options.MaxResults, options.Expand, strings.Join(options.Fields, ","))
|
||||
}
|
||||
|
||||
req, err := s.client.NewRequest("GET", u, nil)
|
||||
@ -513,6 +735,46 @@ func (s *IssueService) Search(jql string, options *SearchOptions) ([]Issue, *Res
|
||||
return v.Issues, resp, err
|
||||
}
|
||||
|
||||
// SearchPages will get issues from all pages in a search
|
||||
//
|
||||
// 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) SearchPages(jql string, options *SearchOptions, f func(Issue) error) error {
|
||||
if options == nil {
|
||||
options = &SearchOptions{
|
||||
StartAt: 0,
|
||||
MaxResults: 50,
|
||||
}
|
||||
}
|
||||
|
||||
if options.MaxResults == 0 {
|
||||
options.MaxResults = 50
|
||||
}
|
||||
|
||||
issues, resp, err := s.Search(jql, options)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for {
|
||||
for _, issue := range issues {
|
||||
err = f(issue)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if resp.StartAt+resp.MaxResults >= resp.Total {
|
||||
return nil
|
||||
}
|
||||
|
||||
options.StartAt += resp.MaxResults
|
||||
issues, resp, err = s.Search(jql, options)
|
||||
if err != nil {
|
||||
return 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)
|
||||
@ -538,6 +800,11 @@ func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response,
|
||||
if rec, ok := f.(map[string]interface{}); ok {
|
||||
for key, val := range rec {
|
||||
if strings.Contains(key, "customfield") {
|
||||
if valMap, ok := val.(map[string]interface{}); ok {
|
||||
if v, ok := valMap["value"]; ok {
|
||||
val = v
|
||||
}
|
||||
}
|
||||
cf[key] = fmt.Sprint(val)
|
||||
}
|
||||
}
|
||||
@ -569,13 +836,21 @@ func (s *IssueService) GetTransitions(id string) ([]Transition, *Response, error
|
||||
//
|
||||
// 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,
|
||||
},
|
||||
}
|
||||
return s.DoTransitionWithPayload(ticketID, payload)
|
||||
}
|
||||
|
||||
// DoTransitionWithPayload performs a transition on an issue using any payload.
|
||||
// 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) DoTransitionWithPayload(ticketID, payload interface{}) (*Response, error) {
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions", ticketID)
|
||||
|
||||
req, err := s.client.NewRequest("POST", apiEndpoint, payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -588,3 +863,100 @@ func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, e
|
||||
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// InitIssueWithMetaAndFields returns Issue with with values from fieldsConfig properly set.
|
||||
// * metaProject should contain metaInformation about the project where the issue should be created.
|
||||
// * metaIssuetype is the MetaInformation about the Issuetype that needs to be created.
|
||||
// * fieldsConfig is a key->value pair where key represents the name of the field as seen in the UI
|
||||
// And value is the string value for that particular key.
|
||||
// Note: This method doesn't verify that the fieldsConfig is complete with mandatory fields. The fieldsConfig is
|
||||
// supposed to be already verified with MetaIssueType.CheckCompleteAndAvailable. It will however return
|
||||
// error if the key is not found.
|
||||
// All values will be packed into Unknowns. This is much convenient. If the struct fields needs to be
|
||||
// configured as well, marshalling and unmarshalling will set the proper fields.
|
||||
func InitIssueWithMetaAndFields(metaProject *MetaProject, metaIssuetype *MetaIssueType, fieldsConfig map[string]string) (*Issue, error) {
|
||||
issue := new(Issue)
|
||||
issueFields := new(IssueFields)
|
||||
issueFields.Unknowns = tcontainer.NewMarshalMap()
|
||||
|
||||
// map the field names the User presented to jira's internal key
|
||||
allFields, _ := metaIssuetype.GetAllFields()
|
||||
for key, value := range fieldsConfig {
|
||||
jiraKey, found := allFields[key]
|
||||
if !found {
|
||||
return nil, fmt.Errorf("Key %s is not found in the list of fields.", key)
|
||||
}
|
||||
|
||||
valueType, err := metaIssuetype.Fields.String(jiraKey + "/schema/type")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch valueType {
|
||||
case "array":
|
||||
elemType, err := metaIssuetype.Fields.String(jiraKey + "/schema/items")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch elemType {
|
||||
case "component":
|
||||
issueFields.Unknowns[jiraKey] = []Component{{Name: value}}
|
||||
case "option":
|
||||
issueFields.Unknowns[jiraKey] = []map[string]string{{"value": value}}
|
||||
default:
|
||||
issueFields.Unknowns[jiraKey] = []string{value}
|
||||
}
|
||||
case "string":
|
||||
issueFields.Unknowns[jiraKey] = value
|
||||
case "date":
|
||||
issueFields.Unknowns[jiraKey] = value
|
||||
case "datetime":
|
||||
issueFields.Unknowns[jiraKey] = value
|
||||
case "any":
|
||||
// Treat any as string
|
||||
issueFields.Unknowns[jiraKey] = value
|
||||
case "project":
|
||||
issueFields.Unknowns[jiraKey] = Project{
|
||||
Name: metaProject.Name,
|
||||
ID: metaProject.Id,
|
||||
}
|
||||
case "priority":
|
||||
issueFields.Unknowns[jiraKey] = Priority{Name: value}
|
||||
case "user":
|
||||
issueFields.Unknowns[jiraKey] = User{
|
||||
Name: value,
|
||||
}
|
||||
case "issuetype":
|
||||
issueFields.Unknowns[jiraKey] = IssueType{
|
||||
Name: value,
|
||||
}
|
||||
case "option":
|
||||
issueFields.Unknowns[jiraKey] = Option{
|
||||
Value: value,
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("Unknown issue type encountered: %s for %s", valueType, key)
|
||||
}
|
||||
}
|
||||
|
||||
issue.Fields = issueFields
|
||||
|
||||
return issue, nil
|
||||
}
|
||||
|
||||
// Delete will delete a specified issue.
|
||||
func (s *IssueService) Delete(issueID string) (*Response, error) {
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
|
||||
|
||||
// to enable deletion of subtasks; without this, the request will fail if the issue has subtasks
|
||||
deletePayload := make(map[string]interface{})
|
||||
deletePayload["deleteSubtasks"] = "true"
|
||||
content, _ := json.Marshal(deletePayload)
|
||||
|
||||
req, err := s.client.NewRequest("DELETE", apiEndpoint, content)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(req, nil)
|
||||
return resp, err
|
||||
}
|
||||
|
757
issue_test.go
757
issue_test.go
@ -8,6 +8,8 @@ import (
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/trivago/tgo/tcontainer"
|
||||
)
|
||||
|
||||
func TestIssueService_Get_Success(t *testing.T) {
|
||||
@ -20,7 +22,29 @@ func TestIssueService_Get_Success(t *testing.T) {
|
||||
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")
|
||||
issue, _, err := testClient.Issue.Get("10002", nil)
|
||||
if issue == nil {
|
||||
t.Error("Expected issue. Issue is nil")
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Error given: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Get_WithQuerySuccess(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?expand=foo")
|
||||
|
||||
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":{}}`)
|
||||
})
|
||||
|
||||
opt := &GetQueryOptions{
|
||||
Expand: "foo",
|
||||
}
|
||||
issue, _, err := testClient.Issue.Get("10002", opt)
|
||||
if issue == nil {
|
||||
t.Error("Expected issue. Issue is nil")
|
||||
}
|
||||
@ -54,6 +78,54 @@ func TestIssueService_Create(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_Update(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testMux.HandleFunc("/rest/api/2/issue/PROJ-9001", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "PUT")
|
||||
testRequestURL(t, r, "/rest/api/2/issue/PROJ-9001")
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
|
||||
i := &Issue{
|
||||
Key: "PROJ-9001",
|
||||
Fields: &IssueFields{
|
||||
Description: "example bug report",
|
||||
},
|
||||
}
|
||||
issue, _, err := testClient.Issue.Update(i)
|
||||
if issue == nil {
|
||||
t.Error("Expected issue. Issue is nil")
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Error given: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_UpdateIssue(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testMux.HandleFunc("/rest/api/2/issue/PROJ-9001", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "PUT")
|
||||
testRequestURL(t, r, "/rest/api/2/issue/PROJ-9001")
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
})
|
||||
jId := "PROJ-9001"
|
||||
i := make(map[string]interface{})
|
||||
fields := make(map[string]interface{})
|
||||
i["fields"] = fields
|
||||
resp, err := testClient.Issue.UpdateIssue(jId, i)
|
||||
if resp == nil {
|
||||
t.Error("Expected resp. resp is nil")
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Error given: %s", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIssueService_AddComment(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
@ -81,6 +153,34 @@ func TestIssueService_AddComment(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_UpdateComment(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testMux.HandleFunc("/rest/api/2/issue/10000/comment/10001", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "POST")
|
||||
testRequestURL(t, r, "/rest/api/2/issue/10000/comment/10001")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
fmt.Fprint(w, `{"self":"http://www.example.com/jira/rest/api/2/issue/10010/comment/10001","id":"10001","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{
|
||||
ID: "10001",
|
||||
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.UpdateComment("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()
|
||||
@ -131,7 +231,7 @@ func TestIssueService_Get_Fields(t *testing.T) {
|
||||
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")
|
||||
issue, _, err := testClient.Issue.Get("10002", nil)
|
||||
if issue == nil {
|
||||
t.Error("Expected issue. Issue is nil")
|
||||
}
|
||||
@ -322,12 +422,12 @@ func TestIssueService_Search(t *testing.T) {
|
||||
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")
|
||||
testRequestURL(t, r, "/rest/api/2/search?jql=something&startAt=1&maxResults=40&expand=foo")
|
||||
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}
|
||||
opt := &SearchOptions{StartAt: 1, MaxResults: 40, Expand: "foo"}
|
||||
_, resp, err := testClient.Issue.Search("something", opt)
|
||||
|
||||
if resp == nil {
|
||||
@ -378,6 +478,44 @@ func TestIssueService_Search_WithoutPaging(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_SearchPages(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
if r.URL.String() == "/rest/api/2/search?jql=something&startAt=1&maxResults=2&expand=foo&fields=" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 2,"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}}}]}`)
|
||||
return
|
||||
} else if r.URL.String() == "/rest/api/2/search?jql=something&startAt=3&maxResults=2&expand=foo&fields=" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, `{"expand": "schema,names","startAt": 3,"maxResults": 2,"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}}}]}`)
|
||||
return
|
||||
} else if r.URL.String() == "/rest/api/2/search?jql=something&startAt=5&maxResults=2&expand=foo&fields=" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, `{"expand": "schema,names","startAt": 5,"maxResults": 2,"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}}]}`)
|
||||
return
|
||||
}
|
||||
|
||||
t.Errorf("Unexpected URL: %v", r.URL)
|
||||
})
|
||||
|
||||
opt := &SearchOptions{StartAt: 1, MaxResults: 2, Expand: "foo"}
|
||||
issues := make([]Issue, 0)
|
||||
err := testClient.Issue.SearchPages("something", opt, func(issue Issue) error {
|
||||
issues = append(issues, issue)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error given: %s", err)
|
||||
}
|
||||
|
||||
if len(issues) != 5 {
|
||||
t.Errorf("Expected 5 issues, %v given", len(issues))
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_GetCustomFields(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
@ -400,6 +538,28 @@ func TestIssueService_GetCustomFields(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_GetComplexCustomFields(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":{"self":"http://www.example.com/jira/rest/api/2/customFieldOption/123","value":"test","id":"123"},"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()
|
||||
@ -465,3 +625,592 @@ func TestIssueService_DoTransition(t *testing.T) {
|
||||
t.Errorf("Got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_DoTransitionWithPayload(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
testAPIEndpoint := "/rest/api/2/issue/123/transitions"
|
||||
|
||||
transitionID := "22"
|
||||
|
||||
customPayload := map[string]interface{}{
|
||||
"update": map[string]interface{}{
|
||||
"comment": []map[string]interface{}{
|
||||
{
|
||||
"add": map[string]string{
|
||||
"body": "Hello World",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"transition": TransitionPayload{
|
||||
ID: transitionID,
|
||||
},
|
||||
}
|
||||
|
||||
testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "POST")
|
||||
testRequestURL(t, r, testAPIEndpoint)
|
||||
|
||||
decoder := json.NewDecoder(r.Body)
|
||||
payload := map[string]interface{}{}
|
||||
err := decoder.Decode(&payload)
|
||||
if err != nil {
|
||||
t.Errorf("Got error: %v", err)
|
||||
}
|
||||
|
||||
contains := func(key string) bool {
|
||||
_, ok := payload[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
if !contains("update") || !contains("transition") {
|
||||
t.Fatalf("Excpected update, transition to be in payload, got %s instead", payload)
|
||||
}
|
||||
|
||||
transition, ok := payload["transition"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("Excpected transition to be in payload, got %s instead", payload["transition"])
|
||||
}
|
||||
|
||||
if transition["id"].(string) != transitionID {
|
||||
t.Errorf("Expected %s to be in payload, got %s instead", transitionID, transition["id"])
|
||||
}
|
||||
})
|
||||
_, err := testClient.Issue.DoTransitionWithPayload("123", customPayload)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueFields_TestMarshalJSON_PopulateUnknownsSuccess(t *testing.T) {
|
||||
data := `{
|
||||
"customfield_123":"test",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
}`
|
||||
|
||||
i := new(IssueFields)
|
||||
err := json.Unmarshal([]byte(data), i)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, received %s", err)
|
||||
}
|
||||
|
||||
if len(i.Unknowns) != 1 {
|
||||
t.Errorf("Expected 1 unknown field to be present, received %d", len(i.Unknowns))
|
||||
}
|
||||
if i.Description != "example bug report" {
|
||||
t.Errorf("Expected description to be \"%s\", received \"%s\"", "example bug report", i.Description)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIssueFields_MarshalJSON_OmitsEmptyFields(t *testing.T) {
|
||||
i := &IssueFields{
|
||||
Description: "blahblah",
|
||||
Type: IssueType{
|
||||
Name: "Story",
|
||||
},
|
||||
Labels: []string{"aws-docker"},
|
||||
}
|
||||
|
||||
rawdata, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil err, received %s", err)
|
||||
}
|
||||
|
||||
// convert json to map and see if unset keys are there
|
||||
issuef := tcontainer.NewMarshalMap()
|
||||
err = json.Unmarshal(rawdata, &issuef)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil err, received %s", err)
|
||||
}
|
||||
|
||||
_, err = issuef.Int("issuetype/avatarId")
|
||||
if err == nil {
|
||||
t.Error("Expected non nil error, received nil")
|
||||
}
|
||||
|
||||
// verify that the field that should be there, is.
|
||||
name, err := issuef.String("issuetype/name")
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil err, received %s", err)
|
||||
}
|
||||
|
||||
if name != "Story" {
|
||||
t.Errorf("Expected Story, received %s", name)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIssueFields_MarshalJSON_Success(t *testing.T) {
|
||||
i := &IssueFields{
|
||||
Description: "example bug report",
|
||||
Unknowns: tcontainer.MarshalMap{
|
||||
"customfield_123": "test",
|
||||
},
|
||||
Project: Project{
|
||||
Self: "http://www.example.com/jira/rest/api/2/project/EX",
|
||||
ID: "10000",
|
||||
Key: "EX",
|
||||
},
|
||||
}
|
||||
|
||||
bytes, err := json.Marshal(i)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil err, received %s", err)
|
||||
}
|
||||
|
||||
received := new(IssueFields)
|
||||
// the order of json might be different. so unmarshal it again and compare objects
|
||||
err = json.Unmarshal(bytes, received)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil err, received %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(i, received) {
|
||||
t.Errorf("Received object different from expected. Expected %+v, received %+v", i, received)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitIssueWithMetaAndFields_Success(t *testing.T) {
|
||||
metaProject := MetaProject{
|
||||
Name: "Engineering - Dept",
|
||||
Id: "ENG",
|
||||
}
|
||||
|
||||
fields := tcontainer.NewMarshalMap()
|
||||
fields["summary"] = map[string]interface{}{
|
||||
"name": "Summary",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "string",
|
||||
},
|
||||
}
|
||||
|
||||
metaIssueType := MetaIssueType{
|
||||
Fields: fields,
|
||||
}
|
||||
expectedSummary := "Issue Summary"
|
||||
fieldConfig := map[string]string{
|
||||
"Summary": "Issue Summary",
|
||||
}
|
||||
|
||||
issue, err := InitIssueWithMetaAndFields(&metaProject, &metaIssueType, fieldConfig)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, received %s", err)
|
||||
}
|
||||
|
||||
gotSummary, found := issue.Fields.Unknowns["summary"]
|
||||
if !found {
|
||||
t.Errorf("Expected summary to be set in issue. Not set.")
|
||||
}
|
||||
|
||||
if gotSummary != expectedSummary {
|
||||
t.Errorf("Expected %s received %s", expectedSummary, gotSummary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitIssueWithMetaAndFields_ArrayValueType(t *testing.T) {
|
||||
metaProject := MetaProject{
|
||||
Name: "Engineering - Dept",
|
||||
Id: "ENG",
|
||||
}
|
||||
|
||||
fields := tcontainer.NewMarshalMap()
|
||||
fields["component"] = map[string]interface{}{
|
||||
"name": "Component/s",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "array",
|
||||
"items": "component",
|
||||
},
|
||||
}
|
||||
|
||||
metaIssueType := MetaIssueType{
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
expectedComponent := "Jira automation"
|
||||
fieldConfig := map[string]string{
|
||||
"Component/s": expectedComponent,
|
||||
}
|
||||
|
||||
issue, err := InitIssueWithMetaAndFields(&metaProject, &metaIssueType, fieldConfig)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, received %s", err)
|
||||
}
|
||||
|
||||
c, isArray := issue.Fields.Unknowns["component"].([]Component)
|
||||
if isArray == false {
|
||||
t.Error("Expected array, non array object received")
|
||||
}
|
||||
|
||||
if len(c) != 1 {
|
||||
t.Errorf("Expected received array to be of length 1. Got %d", len(c))
|
||||
}
|
||||
|
||||
gotComponent := c[0].Name
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected err to be nil, received %s", err)
|
||||
}
|
||||
|
||||
if gotComponent != expectedComponent {
|
||||
t.Errorf("Expected %s received %s", expectedComponent, gotComponent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitIssueWithMetaAndFields_DateValueType(t *testing.T) {
|
||||
metaProject := MetaProject{
|
||||
Name: "Engineering - Dept",
|
||||
Id: "ENG",
|
||||
}
|
||||
|
||||
fields := tcontainer.NewMarshalMap()
|
||||
fields["created"] = map[string]interface{}{
|
||||
"name": "Created",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "date",
|
||||
},
|
||||
}
|
||||
|
||||
metaIssueType := MetaIssueType{
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
expectedCreated := "19 oct 2012"
|
||||
fieldConfig := map[string]string{
|
||||
"Created": expectedCreated,
|
||||
}
|
||||
|
||||
issue, err := InitIssueWithMetaAndFields(&metaProject, &metaIssueType, fieldConfig)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, received %s", err)
|
||||
}
|
||||
|
||||
gotCreated, err := issue.Fields.Unknowns.String("created")
|
||||
if err != nil {
|
||||
t.Errorf("Expected err to be nil, received %s", err)
|
||||
}
|
||||
|
||||
if gotCreated != expectedCreated {
|
||||
t.Errorf("Expected %s received %s", expectedCreated, gotCreated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitIssueWithMetaAndFields_UserValueType(t *testing.T) {
|
||||
metaProject := MetaProject{
|
||||
Name: "Engineering - Dept",
|
||||
Id: "ENG",
|
||||
}
|
||||
|
||||
fields := tcontainer.NewMarshalMap()
|
||||
fields["assignee"] = map[string]interface{}{
|
||||
"name": "Assignee",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "user",
|
||||
},
|
||||
}
|
||||
|
||||
metaIssueType := MetaIssueType{
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
expectedAssignee := "jdoe"
|
||||
fieldConfig := map[string]string{
|
||||
"Assignee": expectedAssignee,
|
||||
}
|
||||
|
||||
issue, err := InitIssueWithMetaAndFields(&metaProject, &metaIssueType, fieldConfig)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, received %s", err)
|
||||
}
|
||||
|
||||
a, _ := issue.Fields.Unknowns.Value("assignee")
|
||||
gotAssignee := a.(User).Name
|
||||
|
||||
if gotAssignee != expectedAssignee {
|
||||
t.Errorf("Expected %s received %s", expectedAssignee, gotAssignee)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitIssueWithMetaAndFields_ProjectValueType(t *testing.T) {
|
||||
metaProject := MetaProject{
|
||||
Name: "Engineering - Dept",
|
||||
Id: "ENG",
|
||||
}
|
||||
|
||||
fields := tcontainer.NewMarshalMap()
|
||||
fields["project"] = map[string]interface{}{
|
||||
"name": "Project",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "project",
|
||||
},
|
||||
}
|
||||
|
||||
metaIssueType := MetaIssueType{
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
setProject := "somewhere"
|
||||
fieldConfig := map[string]string{
|
||||
"Project": setProject,
|
||||
}
|
||||
|
||||
issue, err := InitIssueWithMetaAndFields(&metaProject, &metaIssueType, fieldConfig)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, received %s", err)
|
||||
}
|
||||
|
||||
a, _ := issue.Fields.Unknowns.Value("project")
|
||||
gotProject := a.(Project).Name
|
||||
|
||||
if gotProject != metaProject.Name {
|
||||
t.Errorf("Expected %s received %s", metaProject.Name, gotProject)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitIssueWithMetaAndFields_PriorityValueType(t *testing.T) {
|
||||
metaProject := MetaProject{
|
||||
Name: "Engineering - Dept",
|
||||
Id: "ENG",
|
||||
}
|
||||
|
||||
fields := tcontainer.NewMarshalMap()
|
||||
fields["priority"] = map[string]interface{}{
|
||||
"name": "Priority",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "priority",
|
||||
},
|
||||
}
|
||||
|
||||
metaIssueType := MetaIssueType{
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
expectedPriority := "Normal"
|
||||
fieldConfig := map[string]string{
|
||||
"Priority": expectedPriority,
|
||||
}
|
||||
|
||||
issue, err := InitIssueWithMetaAndFields(&metaProject, &metaIssueType, fieldConfig)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, received %s", err)
|
||||
}
|
||||
|
||||
a, _ := issue.Fields.Unknowns.Value("priority")
|
||||
gotPriority := a.(Priority).Name
|
||||
|
||||
if gotPriority != expectedPriority {
|
||||
t.Errorf("Expected %s received %s", expectedPriority, gotPriority)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitIssueWithMetaAndFields_SelectList(t *testing.T) {
|
||||
metaProject := MetaProject{
|
||||
Name: "Engineering - Dept",
|
||||
Id: "ENG",
|
||||
}
|
||||
|
||||
fields := tcontainer.NewMarshalMap()
|
||||
fields["someitem"] = map[string]interface{}{
|
||||
"name": "A Select Item",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "option",
|
||||
},
|
||||
}
|
||||
|
||||
metaIssueType := MetaIssueType{
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
expectedVal := "Value"
|
||||
fieldConfig := map[string]string{
|
||||
"A Select Item": expectedVal,
|
||||
}
|
||||
|
||||
issue, err := InitIssueWithMetaAndFields(&metaProject, &metaIssueType, fieldConfig)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, received %s", err)
|
||||
}
|
||||
|
||||
a, _ := issue.Fields.Unknowns.Value("someitem")
|
||||
gotVal := a.(Option).Value
|
||||
|
||||
if gotVal != expectedVal {
|
||||
t.Errorf("Expected %s received %s", expectedVal, gotVal)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitIssueWithMetaAndFields_IssuetypeValueType(t *testing.T) {
|
||||
metaProject := MetaProject{
|
||||
Name: "Engineering - Dept",
|
||||
Id: "ENG",
|
||||
}
|
||||
|
||||
fields := tcontainer.NewMarshalMap()
|
||||
fields["issuetype"] = map[string]interface{}{
|
||||
"name": "Issue type",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "issuetype",
|
||||
},
|
||||
}
|
||||
|
||||
metaIssueType := MetaIssueType{
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
expectedIssuetype := "Bug"
|
||||
fieldConfig := map[string]string{
|
||||
"Issue type": expectedIssuetype,
|
||||
}
|
||||
|
||||
issue, err := InitIssueWithMetaAndFields(&metaProject, &metaIssueType, fieldConfig)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, received %s", err)
|
||||
}
|
||||
|
||||
a, _ := issue.Fields.Unknowns.Value("issuetype")
|
||||
gotIssuetype := a.(IssueType).Name
|
||||
|
||||
if gotIssuetype != expectedIssuetype {
|
||||
t.Errorf("Expected %s received %s", expectedIssuetype, gotIssuetype)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitIssueWithmetaAndFields_FailureWithUnknownValueType(t *testing.T) {
|
||||
metaProject := MetaProject{
|
||||
Name: "Engineering - Dept",
|
||||
Id: "ENG",
|
||||
}
|
||||
|
||||
fields := tcontainer.NewMarshalMap()
|
||||
fields["issuetype"] = map[string]interface{}{
|
||||
"name": "Issue type",
|
||||
"schema": map[string]interface{}{
|
||||
"type": "randomType",
|
||||
},
|
||||
}
|
||||
|
||||
metaIssueType := MetaIssueType{
|
||||
Fields: fields,
|
||||
}
|
||||
|
||||
fieldConfig := map[string]string{
|
||||
"Issue tyoe": "sometype",
|
||||
}
|
||||
_, err := InitIssueWithMetaAndFields(&metaProject, &metaIssueType, fieldConfig)
|
||||
if err == nil {
|
||||
t.Error("Expected non nil error, received nil")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestIssueService_Delete(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testMux.HandleFunc("/rest/api/2/issue/10002", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "DELETE")
|
||||
testRequestURL(t, r, "/rest/api/2/issue/10002")
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
fmt.Fprint(w, `{}`)
|
||||
})
|
||||
|
||||
resp, err := testClient.Issue.Delete("10002")
|
||||
if resp.StatusCode != 204 {
|
||||
t.Error("Expected issue not deleted.")
|
||||
}
|
||||
if err != nil {
|
||||
t.Errorf("Error given: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueService_GetWorklogs(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testMux.HandleFunc("/rest/api/2/issue/10002/worklog", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
testRequestURL(t, r, "/rest/api/2/issue/10002/worklog")
|
||||
|
||||
fmt.Fprint(w, `{"startAt": 1,"maxResults": 40,"total": 1,"worklogs": [{"id": "3","self": "http://kelpie9:8081/rest/api/2/issue/10002/worklog/3","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},"created":"2016-03-16T04:22:37.356+0000","updated":"2016-03-16T04:22:37.356+0000","comment":"","started":"2016-03-16T04:22:37.356+0000","timeSpent": "1h","timeSpentSeconds": 3600,"issueId":"10002"}]}`)
|
||||
})
|
||||
|
||||
worklog, _, err := testClient.Issue.GetWorklogs("10002")
|
||||
if worklog == nil {
|
||||
t.Error("Expected worklog. Worklog is nil")
|
||||
}
|
||||
|
||||
if len(worklog.Worklogs) != 1 {
|
||||
t.Error("Expected 1 worklog")
|
||||
}
|
||||
|
||||
if worklog.Worklogs[0].Author.Name != "fred" {
|
||||
t.Error("Expected worklog author to be fred")
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Error given: %s", err)
|
||||
}
|
||||
}
|
||||
|
75
jira.go
75
jira.go
@ -29,6 +29,8 @@ type Client struct {
|
||||
Project *ProjectService
|
||||
Board *BoardService
|
||||
Sprint *SprintService
|
||||
User *UserService
|
||||
Group *GroupService
|
||||
}
|
||||
|
||||
// NewClient returns a new JIRA API client.
|
||||
@ -57,10 +59,49 @@ func NewClient(httpClient *http.Client, baseURL string) (*Client, error) {
|
||||
c.Project = &ProjectService{client: c}
|
||||
c.Board = &BoardService{client: c}
|
||||
c.Sprint = &SprintService{client: c}
|
||||
c.User = &UserService{client: c}
|
||||
c.Group = &GroupService{client: c}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// NewRawRequest 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.
|
||||
// Allows using an optional native io.Reader for sourcing the request body.
|
||||
func (c *Client) NewRawRequest(method, urlStr string, body io.Reader) (*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(), body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Set authentication information
|
||||
if c.Authentication.authType == authTypeSession {
|
||||
// Set session cookie if there is one
|
||||
if c.session != nil {
|
||||
for _, cookie := range c.session.Cookies {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
}
|
||||
} else if c.Authentication.authType == authTypeBasic {
|
||||
// Set basic auth information
|
||||
if c.Authentication.username != "" {
|
||||
req.SetBasicAuth(c.Authentication.username, c.Authentication.password)
|
||||
}
|
||||
}
|
||||
|
||||
return req, 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.
|
||||
@ -76,7 +117,7 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
|
||||
var buf io.ReadWriter
|
||||
if body != nil {
|
||||
buf = new(bytes.Buffer)
|
||||
err := json.NewEncoder(buf).Encode(body)
|
||||
err = json.NewEncoder(buf).Encode(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -89,10 +130,18 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ
|
||||
|
||||
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)
|
||||
// Set authentication information
|
||||
if c.Authentication.authType == authTypeSession {
|
||||
// Set session cookie if there is one
|
||||
if c.session != nil {
|
||||
for _, cookie := range c.session.Cookies {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
}
|
||||
} else if c.Authentication.authType == authTypeBasic {
|
||||
// Set basic auth information
|
||||
if c.Authentication.username != "" {
|
||||
req.SetBasicAuth(c.Authentication.username, c.Authentication.password)
|
||||
}
|
||||
}
|
||||
|
||||
@ -141,10 +190,18 @@ func (c *Client) NewMultiPartRequest(method, urlStr string, buf *bytes.Buffer) (
|
||||
// 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)
|
||||
// Set authentication information
|
||||
if c.Authentication.authType == authTypeSession {
|
||||
// Set session cookie if there is one
|
||||
if c.session != nil {
|
||||
for _, cookie := range c.session.Cookies {
|
||||
req.AddCookie(cookie)
|
||||
}
|
||||
}
|
||||
} else if c.Authentication.authType == authTypeBasic {
|
||||
// Set basic auth information
|
||||
if c.Authentication.username != "" {
|
||||
req.SetBasicAuth(c.Authentication.username, c.Authentication.password)
|
||||
}
|
||||
}
|
||||
|
||||
|
106
jira_test.go
106
jira_test.go
@ -2,7 +2,6 @@ package jira
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@ -107,6 +106,12 @@ func TestNewClient_WithServices(t *testing.T) {
|
||||
if c.Sprint == nil {
|
||||
t.Error("No SprintService provided")
|
||||
}
|
||||
if c.User == nil {
|
||||
t.Error("No UserService provided")
|
||||
}
|
||||
if c.Group == nil {
|
||||
t.Error("No GroupService provided")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckResponse(t *testing.T) {
|
||||
@ -127,7 +132,7 @@ func TestCheckResponse(t *testing.T) {
|
||||
func TestClient_NewRequest(t *testing.T) {
|
||||
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||
if err != nil {
|
||||
t.Errorf("An error occured. Expected nil. Got %+v.", err)
|
||||
t.Errorf("An error occurred. Expected nil. Got %+v.", err)
|
||||
}
|
||||
|
||||
inURL, outURL := "rest/api/2/issue/", testJIRAInstanceURL+"rest/api/2/issue/"
|
||||
@ -146,22 +151,27 @@ func TestClient_NewRequest(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_NewRequest_InvalidJSON(t *testing.T) {
|
||||
func TestClient_NewRawRequest(t *testing.T) {
|
||||
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||
if err != nil {
|
||||
t.Errorf("An error occured. Expected nil. Got %+v.", err)
|
||||
t.Errorf("An error occurred. Expected nil. Got %+v.", err)
|
||||
}
|
||||
|
||||
type T struct {
|
||||
A map[int]interface{}
|
||||
}
|
||||
_, err = c.NewRequest("GET", "/", &T{})
|
||||
inURL, outURL := "rest/api/2/issue/", testJIRAInstanceURL+"rest/api/2/issue/"
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error to be returned.")
|
||||
outBody := `{"key":"MESOS"}` + "\n"
|
||||
inBody := outBody
|
||||
req, _ := c.NewRawRequest("GET", inURL, strings.NewReader(outBody))
|
||||
|
||||
// Test that relative URL was expanded
|
||||
if got, want := req.URL.String(), outURL; got != want {
|
||||
t.Errorf("NewRawRequest(%q) URL is %v, want %v", inURL, got, want)
|
||||
}
|
||||
if err, ok := err.(*json.UnsupportedTypeError); !ok {
|
||||
t.Errorf("Expected a JSON error; got %+v.", err)
|
||||
|
||||
// Test that body was JSON encoded
|
||||
body, _ := ioutil.ReadAll(req.Body)
|
||||
if got, want := string(body), outBody; got != want {
|
||||
t.Errorf("NewRawRequest(%v) Body is %v, want %v", inBody, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
@ -177,7 +187,7 @@ func testURLParseError(t *testing.T, err error) {
|
||||
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)
|
||||
t.Errorf("An error occurred. Expected nil. Got %+v.", err)
|
||||
}
|
||||
_, err = c.NewRequest("GET", ":", nil)
|
||||
testURLParseError(t, err)
|
||||
@ -186,31 +196,54 @@ func TestClient_NewRequest_BadURL(t *testing.T) {
|
||||
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)
|
||||
t.Errorf("An error occurred. Expected nil. Got %+v.", err)
|
||||
}
|
||||
|
||||
cookie := &http.Cookie{Name: "testcookie", Value: "testvalue"}
|
||||
c.session = &Session{Cookies: []*http.Cookie{cookie}}
|
||||
c.Authentication.authType = authTypeSession
|
||||
|
||||
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)
|
||||
t.Errorf("An error occurred. 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()))
|
||||
t.Errorf("An error occurred. 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())
|
||||
t.Errorf("An error occurred. Unexpected cookie. Expected %s, actual %s.", v.String(), c.session.Cookies[i].String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_NewRequest_BasicAuth(t *testing.T) {
|
||||
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||
if err != nil {
|
||||
t.Errorf("An error occurred. Expected nil. Got %+v.", err)
|
||||
}
|
||||
|
||||
c.Authentication.SetBasicAuth("test-user", "test-password")
|
||||
|
||||
inURL := "rest/api/2/issue/"
|
||||
inBody := &Issue{Key: "MESOS"}
|
||||
req, err := c.NewRequest("GET", inURL, inBody)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("An error occurred. Expected nil. Got %+v.", err)
|
||||
}
|
||||
|
||||
username, password, ok := req.BasicAuth()
|
||||
if !ok || username != "test-user" || password != "test-password" {
|
||||
t.Errorf("An error occurred. Expected basic auth username %s and password %s. Got username %s and password %s.", "test-user", "test-password", username, password)
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@ -218,7 +251,7 @@ func TestClient_NewRequest_SessionCookies(t *testing.T) {
|
||||
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)
|
||||
t.Errorf("An error occurred. Expected nil. Got %+v.", err)
|
||||
}
|
||||
req, err := c.NewRequest("GET", "/", nil)
|
||||
if err != nil {
|
||||
@ -232,32 +265,59 @@ func TestClient_NewRequest_EmptyBody(t *testing.T) {
|
||||
func TestClient_NewMultiPartRequest(t *testing.T) {
|
||||
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||
if err != nil {
|
||||
t.Errorf("An error occured. Expected nil. Got %+v.", err)
|
||||
t.Errorf("An error occurred. Expected nil. Got %+v.", err)
|
||||
}
|
||||
|
||||
cookie := &http.Cookie{Name: "testcookie", Value: "testvalue"}
|
||||
c.session = &Session{Cookies: []*http.Cookie{cookie}}
|
||||
c.Authentication.authType = authTypeSession
|
||||
|
||||
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)
|
||||
t.Errorf("An error occurred. 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()))
|
||||
t.Errorf("An error occurred. 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())
|
||||
t.Errorf("An error occurred. 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"))
|
||||
t.Errorf("An error occurred. Unexpected X-Atlassian-Token header value. Expected nocheck, actual %s.", req.Header.Get("X-Atlassian-Token"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestClient_NewMultiPartRequest_BasicAuth(t *testing.T) {
|
||||
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||
if err != nil {
|
||||
t.Errorf("An error occurred. Expected nil. Got %+v.", err)
|
||||
}
|
||||
|
||||
c.Authentication.SetBasicAuth("test-user", "test-password")
|
||||
|
||||
inURL := "rest/api/2/issue/"
|
||||
inBuf := bytes.NewBufferString("teststring")
|
||||
req, err := c.NewMultiPartRequest("GET", inURL, inBuf)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("An error occurred. Expected nil. Got %+v.", err)
|
||||
}
|
||||
|
||||
username, password, ok := req.BasicAuth()
|
||||
if !ok || username != "test-user" || password != "test-password" {
|
||||
t.Errorf("An error occurred. Expected basic auth username %s and password %s. Got username %s and password %s.", "test-user", "test-password", username, password)
|
||||
}
|
||||
|
||||
if req.Header.Get("X-Atlassian-Token") != "nocheck" {
|
||||
t.Errorf("An error occurred. Unexpected X-Atlassian-Token header value. Expected nocheck, actual %s.", req.Header.Get("X-Atlassian-Token"))
|
||||
}
|
||||
}
|
||||
|
||||
|
182
metaissue.go
Normal file
182
metaissue.go
Normal file
@ -0,0 +1,182 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/trivago/tgo/tcontainer"
|
||||
)
|
||||
|
||||
// CreateMetaInfo contains information about fields and their attributed to create a ticket.
|
||||
type CreateMetaInfo struct {
|
||||
Expand string `json:"expand,omitempty"`
|
||||
Projects []*MetaProject `json:"projects,omitempty"`
|
||||
}
|
||||
|
||||
// MetaProject is the meta information about a project returned from createmeta api
|
||||
type MetaProject struct {
|
||||
Expand string `json:"expand,omitempty"`
|
||||
Self string `json:"self, omitempty"`
|
||||
Id string `json:"id,omitempty"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
// omitted avatarUrls
|
||||
IssueTypes []*MetaIssueType `json:"issuetypes,omitempty"`
|
||||
}
|
||||
|
||||
// MetaIssueType represents the different issue types a project has.
|
||||
//
|
||||
// Note: Fields is interface because this is an object which can
|
||||
// have arbitraty keys related to customfields. It is not possible to
|
||||
// expect these for a general way. This will be returning a map.
|
||||
// Further processing must be done depending on what is required.
|
||||
type MetaIssueType 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"`
|
||||
Subtasks bool `json:"subtask,omitempty"`
|
||||
Expand string `json:"expand,omitempty"`
|
||||
Fields tcontainer.MarshalMap `json:"fields,omitempty"`
|
||||
}
|
||||
|
||||
// GetCreateMeta makes the api call to get the meta information required to create a ticket
|
||||
func (s *IssueService) GetCreateMeta(projectkey string) (*CreateMetaInfo, *Response, error) {
|
||||
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/createmeta?projectKeys=%s&expand=projects.issuetypes.fields", projectkey)
|
||||
|
||||
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
meta := new(CreateMetaInfo)
|
||||
resp, err := s.client.Do(req, meta)
|
||||
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
return meta, resp, nil
|
||||
}
|
||||
|
||||
// GetProjectWithName returns a project with "name" from the meta information received. If not found, this returns nil.
|
||||
// The comparison of the name is case insensitive.
|
||||
func (m *CreateMetaInfo) GetProjectWithName(name string) *MetaProject {
|
||||
for _, m := range m.Projects {
|
||||
if strings.ToLower(m.Name) == strings.ToLower(name) {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProjectWithKey returns a project with "name" from the meta information received. If not found, this returns nil.
|
||||
// The comparison of the name is case insensitive.
|
||||
func (m *CreateMetaInfo) GetProjectWithKey(key string) *MetaProject {
|
||||
for _, m := range m.Projects {
|
||||
if strings.ToLower(m.Key) == strings.ToLower(key) {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetIssueTypeWithName returns an IssueType with name from a given MetaProject. If not found, this returns nil.
|
||||
// The comparison of the name is case insensitive
|
||||
func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueType {
|
||||
for _, m := range p.IssueTypes {
|
||||
if strings.ToLower(m.Name) == strings.ToLower(name) {
|
||||
return m
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetMandatoryFields returns a map of all the required fields from the MetaIssueTypes.
|
||||
// if a field returned by the api was:
|
||||
// "customfield_10806": {
|
||||
// "required": true,
|
||||
// "schema": {
|
||||
// "type": "any",
|
||||
// "custom": "com.pyxis.greenhopper.jira:gh-epic-link",
|
||||
// "customId": 10806
|
||||
// },
|
||||
// "name": "Epic Link",
|
||||
// "hasDefaultValue": false,
|
||||
// "operations": [
|
||||
// "set"
|
||||
// ]
|
||||
// }
|
||||
// the returned map would have "Epic Link" as the key and "customfield_10806" as value.
|
||||
// This choice has been made so that the it is easier to generate the create api request later.
|
||||
func (t *MetaIssueType) GetMandatoryFields() (map[string]string, error) {
|
||||
ret := make(map[string]string)
|
||||
for key := range t.Fields {
|
||||
required, err := t.Fields.Bool(key + "/required")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if required {
|
||||
name, err := t.Fields.String(key + "/name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[name] = key
|
||||
}
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// GetAllFields returns a map of all the fields for an IssueType. This includes all required and not required.
|
||||
// The key of the returned map is what you see in the form and the value is how it is representated in the jira schema.
|
||||
func (t *MetaIssueType) GetAllFields() (map[string]string, error) {
|
||||
ret := make(map[string]string)
|
||||
for key := range t.Fields {
|
||||
|
||||
name, err := t.Fields.String(key + "/name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[name] = key
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// CheckCompleteAndAvailable checks if the given fields satisfies the mandatory field required to create a issue for the given type
|
||||
// And also if the given fields are available.
|
||||
func (t *MetaIssueType) CheckCompleteAndAvailable(config map[string]string) (bool, error) {
|
||||
mandatory, err := t.GetMandatoryFields()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
all, err := t.GetAllFields()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// check templateconfig against mandatory fields
|
||||
for key := range mandatory {
|
||||
if _, okay := config[key]; !okay {
|
||||
var requiredFields []string
|
||||
for name := range mandatory {
|
||||
requiredFields = append(requiredFields, name)
|
||||
}
|
||||
return false, fmt.Errorf("Required field not found in provided jira.fields. Required are: %#v", requiredFields)
|
||||
}
|
||||
}
|
||||
|
||||
// check templateConfig against all fields to verify they are available
|
||||
for key := range config {
|
||||
if _, okay := all[key]; !okay {
|
||||
var availableFields []string
|
||||
for name := range all {
|
||||
availableFields = append(availableFields, name)
|
||||
}
|
||||
return false, fmt.Errorf("Fields in jira.fields are not available in jira. Available are: %#v", availableFields)
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
629
metaissue_test.go
Normal file
629
metaissue_test.go
Normal file
@ -0,0 +1,629 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIssueService_GetCreateMeta_Success(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
|
||||
testAPIEndpoint := "/rest/api/2/issue/createmeta"
|
||||
|
||||
testMux.HandleFunc(testAPIEndpoint, func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
testRequestURL(t, r, testAPIEndpoint)
|
||||
|
||||
fmt.Fprint(w, `{
|
||||
"expand": "projects",
|
||||
"projects": [{
|
||||
"expand": "issuetypes",
|
||||
"self": "https://my.jira.com/rest/api/2/project/11300",
|
||||
"id": "11300",
|
||||
"key": "SPN",
|
||||
"name": "Super Project Name",
|
||||
"avatarUrls": {
|
||||
"48x48": "https://my.jira.com/secure/projectavatar?pid=11300&avatarId=14405",
|
||||
"24x24": "https://my.jira.com/secure/projectavatar?size=small&pid=11300&avatarId=14405",
|
||||
"16x16": "https://my.jira.com/secure/projectavatar?size=xsmall&pid=11300&avatarId=14405",
|
||||
"32x32": "https://my.jira.com/secure/projectavatar?size=medium&pid=11300&avatarId=14405"
|
||||
},
|
||||
"issuetypes": [{
|
||||
"self": "https://my.jira.com/rest/api/2/issuetype/6",
|
||||
"id": "6",
|
||||
"description": "An issue which ideally should be able to be completed in one step",
|
||||
"iconUrl": "https://my.jira.com/secure/viewavatar?size=xsmall&avatarId=14006&avatarType=issuetype",
|
||||
"name": "Request",
|
||||
"subtask": false,
|
||||
"expand": "fields",
|
||||
"fields": {
|
||||
"summary": {
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"system": "summary"
|
||||
},
|
||||
"name": "Summary",
|
||||
"hasDefaultValue": false,
|
||||
"operations": [
|
||||
"set"
|
||||
]
|
||||
},
|
||||
"issuetype": {
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "issuetype",
|
||||
"system": "issuetype"
|
||||
},
|
||||
"name": "Issue Type",
|
||||
"hasDefaultValue": false,
|
||||
"operations": [
|
||||
|
||||
],
|
||||
"allowedValues": [{
|
||||
"self": "https://my.jira.com/rest/api/2/issuetype/6",
|
||||
"id": "6",
|
||||
"description": "An issue which ideally should be able to be completed in one step",
|
||||
"iconUrl": "https://my.jira.com/secure/viewavatar?size=xsmall&avatarId=14006&avatarType=issuetype",
|
||||
"name": "Request",
|
||||
"subtask": false,
|
||||
"avatarId": 14006
|
||||
}]
|
||||
},
|
||||
"components": {
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": "component",
|
||||
"system": "components"
|
||||
},
|
||||
"name": "Component/s",
|
||||
"hasDefaultValue": false,
|
||||
"operations": [
|
||||
"add",
|
||||
"set",
|
||||
"remove"
|
||||
],
|
||||
"allowedValues": [{
|
||||
"self": "https://my.jira.com/rest/api/2/component/14144",
|
||||
"id": "14144",
|
||||
"name": "Build automation",
|
||||
"description": "Jenkins, webhooks, etc."
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14149",
|
||||
"id": "14149",
|
||||
"name": "Caches and noSQL",
|
||||
"description": "Cassandra, Memcached, Redis, Twemproxy, Xcache"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14152",
|
||||
"id": "14152",
|
||||
"name": "Cloud services",
|
||||
"description": "AWS and similar services"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14147",
|
||||
"id": "14147",
|
||||
"name": "Code quality tools",
|
||||
"description": "Code sniffer, Sonar"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14156",
|
||||
"id": "14156",
|
||||
"name": "Configuration management and provisioning",
|
||||
"description": "Apache/PHP modules, Consul, Salt"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/13606",
|
||||
"id": "13606",
|
||||
"name": "Cronjobs",
|
||||
"description": "Cronjobs in general"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14150",
|
||||
"id": "14150",
|
||||
"name": "Data pipelines and queues",
|
||||
"description": "Kafka, RabbitMq"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14159",
|
||||
"id": "14159",
|
||||
"name": "Database",
|
||||
"description": "MySQL related problems"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14314",
|
||||
"id": "14314",
|
||||
"name": "Documentation"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14151",
|
||||
"id": "14151",
|
||||
"name": "Git",
|
||||
"description": "Bitbucket, GitHub, GitLab, Git in general"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14155",
|
||||
"id": "14155",
|
||||
"name": "HTTP services",
|
||||
"description": "CDN, HaProxy, HTTP, Varnish"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14154",
|
||||
"id": "14154",
|
||||
"name": "Job and service scheduling",
|
||||
"description": "Chronos, Docker, Marathon, Mesos"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14158",
|
||||
"id": "14158",
|
||||
"name": "Legacy",
|
||||
"description": "Everything related to legacy"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14157",
|
||||
"id": "14157",
|
||||
"name": "Monitoring",
|
||||
"description": "Collectd, Nagios, Monitoring in general"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14148",
|
||||
"id": "14148",
|
||||
"name": "Other services"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/13602",
|
||||
"id": "13602",
|
||||
"name": "Package management",
|
||||
"description": "Composer, Medusa, Satis"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14145",
|
||||
"id": "14145",
|
||||
"name": "Release",
|
||||
"description": "Directory config, release queries, rewrite rules"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14146",
|
||||
"id": "14146",
|
||||
"name": "Staging systems and VMs",
|
||||
"description": "Stage, QA machines, KVMs,Vagrant"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14153",
|
||||
"id": "14153",
|
||||
"name": "Blog"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14143",
|
||||
"id": "14143",
|
||||
"name": "Test automation",
|
||||
"description": "Testing infrastructure in general"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/component/14221",
|
||||
"id": "14221",
|
||||
"name": "Internal Infrastructure"
|
||||
}]
|
||||
},
|
||||
"attachment": {
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": "attachment",
|
||||
"system": "attachment"
|
||||
},
|
||||
"name": "Attachment",
|
||||
"hasDefaultValue": false,
|
||||
"operations": [
|
||||
|
||||
]
|
||||
},
|
||||
"duedate": {
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "date",
|
||||
"system": "duedate"
|
||||
},
|
||||
"name": "Due Date",
|
||||
"hasDefaultValue": false,
|
||||
"operations": [
|
||||
"set"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"system": "description"
|
||||
},
|
||||
"name": "Description",
|
||||
"hasDefaultValue": false,
|
||||
"operations": [
|
||||
"set"
|
||||
]
|
||||
},
|
||||
"customfield_10806": {
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "any",
|
||||
"custom": "com.pyxis.greenhopper.jira:gh-epic-link",
|
||||
"customId": 10806
|
||||
},
|
||||
"name": "Epic Link",
|
||||
"hasDefaultValue": false,
|
||||
"operations": [
|
||||
"set"
|
||||
]
|
||||
},
|
||||
"project": {
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "project",
|
||||
"system": "project"
|
||||
},
|
||||
"name": "Project",
|
||||
"hasDefaultValue": false,
|
||||
"operations": [
|
||||
"set"
|
||||
],
|
||||
"allowedValues": [{
|
||||
"self": "https://my.jira.com/rest/api/2/project/11300",
|
||||
"id": "11300",
|
||||
"key": "SPN",
|
||||
"name": "Super Project Name",
|
||||
"avatarUrls": {
|
||||
"48x48": "https://my.jira.com/secure/projectavatar?pid=11300&avatarId=14405",
|
||||
"24x24": "https://my.jira.com/secure/projectavatar?size=small&pid=11300&avatarId=14405",
|
||||
"16x16": "https://my.jira.com/secure/projectavatar?size=xsmall&pid=11300&avatarId=14405",
|
||||
"32x32": "https://my.jira.com/secure/projectavatar?size=medium&pid=11300&avatarId=14405"
|
||||
},
|
||||
"projectCategory": {
|
||||
"self": "https://my.jira.com/rest/api/2/projectCategory/10100",
|
||||
"id": "10100",
|
||||
"description": "",
|
||||
"name": "Product & Development"
|
||||
}
|
||||
}]
|
||||
},
|
||||
"assignee": {
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "user",
|
||||
"system": "assignee"
|
||||
},
|
||||
"name": "Assignee",
|
||||
"autoCompleteUrl": "https://my.jira.com/rest/api/latest/user/assignable/search?issueKey=null&username=",
|
||||
"hasDefaultValue": true,
|
||||
"operations": [
|
||||
"set"
|
||||
]
|
||||
},
|
||||
"priority": {
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "priority",
|
||||
"system": "priority"
|
||||
},
|
||||
"name": "Priority",
|
||||
"hasDefaultValue": true,
|
||||
"operations": [
|
||||
"set"
|
||||
],
|
||||
"allowedValues": [{
|
||||
"self": "https://my.jira.com/rest/api/2/priority/1",
|
||||
"iconUrl": "https://my.jira.com/images/icons/priorities/blocker.svg",
|
||||
"name": "Immediate",
|
||||
"id": "1"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/priority/2",
|
||||
"iconUrl": "https://my.jira.com/images/icons/priorities/critical.svg",
|
||||
"name": "Urgent",
|
||||
"id": "2"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/priority/3",
|
||||
"iconUrl": "https://my.jira.com/images/icons/priorities/major.svg",
|
||||
"name": "High",
|
||||
"id": "3"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/priority/6",
|
||||
"iconUrl": "https://my.jira.com/images/icons/priorities/moderate.svg",
|
||||
"name": "Moderate",
|
||||
"id": "6"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/priority/4",
|
||||
"iconUrl": "https://my.jira.com/images/icons/priorities/minor.svg",
|
||||
"name": "Normal",
|
||||
"id": "4"
|
||||
}, {
|
||||
"self": "https://my.jira.com/rest/api/2/priority/5",
|
||||
"iconUrl": "https://my.jira.com/images/icons/priorities/trivial.svg",
|
||||
"name": "Low",
|
||||
"id": "5"
|
||||
}]
|
||||
},
|
||||
"labels": {
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": "string",
|
||||
"system": "labels"
|
||||
},
|
||||
"name": "Labels",
|
||||
"autoCompleteUrl": "https://my.jira.com/rest/api/1.0/labels/suggest?query=",
|
||||
"hasDefaultValue": false,
|
||||
"operations": [
|
||||
"add",
|
||||
"set",
|
||||
"remove"
|
||||
]
|
||||
}
|
||||
}
|
||||
}]
|
||||
}]
|
||||
}`)
|
||||
})
|
||||
|
||||
issue, _, err := testClient.Issue.GetCreateMeta("SPN")
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error but got %s", err)
|
||||
}
|
||||
|
||||
if len(issue.Projects) != 1 {
|
||||
t.Errorf("Expected 1 project, got %d", len(issue.Projects))
|
||||
}
|
||||
for _, project := range issue.Projects {
|
||||
if len(project.IssueTypes) != 1 {
|
||||
t.Errorf("Expected 1 issueTypes, got %d", len(project.IssueTypes))
|
||||
}
|
||||
for _, issueTypes := range project.IssueTypes {
|
||||
requiredFields := 0
|
||||
fields := issueTypes.Fields
|
||||
for _, value := range fields {
|
||||
for key, value := range value.(map[string]interface{}) {
|
||||
if key == "required" && value == true {
|
||||
requiredFields = requiredFields + 1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if requiredFields != 5 {
|
||||
t.Errorf("Expected 5 required fields from Create Meta information, got %d", requiredFields)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMetaIssueType_GetMandatoryFields(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["summary"] = map[string]interface{}{
|
||||
"required": true,
|
||||
"name": "Summary",
|
||||
}
|
||||
|
||||
data["components"] = map[string]interface{}{
|
||||
"required": true,
|
||||
"name": "Components",
|
||||
}
|
||||
|
||||
data["epicLink"] = map[string]interface{}{
|
||||
"required": false,
|
||||
"name": "Epic Link",
|
||||
}
|
||||
|
||||
m := new(MetaIssueType)
|
||||
m.Fields = data
|
||||
|
||||
mandatory, err := m.GetMandatoryFields()
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error, received %s", err)
|
||||
}
|
||||
|
||||
if len(mandatory) != 2 {
|
||||
t.Errorf("Expected 2 received %+v", mandatory)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaIssueType_GetMandatoryFields_NonExistentRequiredKey_Fail(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["summary"] = map[string]interface{}{
|
||||
"name": "Summary",
|
||||
}
|
||||
|
||||
m := new(MetaIssueType)
|
||||
m.Fields = data
|
||||
|
||||
_, err := m.GetMandatoryFields()
|
||||
if err == nil {
|
||||
t.Error("Expected non nil errpr, received nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaIssueType_GetMandatoryFields_NonExistentNameKey_Fail(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["summary"] = map[string]interface{}{
|
||||
"required": true,
|
||||
}
|
||||
|
||||
m := new(MetaIssueType)
|
||||
m.Fields = data
|
||||
|
||||
_, err := m.GetMandatoryFields()
|
||||
if err == nil {
|
||||
t.Error("Expected non nil errpr, received nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaIssueType_GetAllFields(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["summary"] = map[string]interface{}{
|
||||
"required": true,
|
||||
"name": "Summary",
|
||||
}
|
||||
|
||||
data["components"] = map[string]interface{}{
|
||||
"required": true,
|
||||
"name": "Components",
|
||||
}
|
||||
|
||||
data["epicLink"] = map[string]interface{}{
|
||||
"required": false,
|
||||
"name": "Epic Link",
|
||||
}
|
||||
|
||||
m := new(MetaIssueType)
|
||||
m.Fields = data
|
||||
|
||||
mandatory, err := m.GetAllFields()
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil err, received %s", err)
|
||||
}
|
||||
|
||||
if len(mandatory) != 3 {
|
||||
t.Errorf("Expected 3 received %+v", mandatory)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaIssueType_GetAllFields_NonExistingNameKey_Fail(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["summary"] = map[string]interface{}{
|
||||
"required": true,
|
||||
}
|
||||
|
||||
m := new(MetaIssueType)
|
||||
m.Fields = data
|
||||
|
||||
_, err := m.GetAllFields()
|
||||
if err == nil {
|
||||
t.Error("Expected non nil error, received nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaIssueType_CheckCompleteAndAvailable_MandatoryMissing(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["summary"] = map[string]interface{}{
|
||||
"required": true,
|
||||
"name": "Summary",
|
||||
}
|
||||
|
||||
data["someKey"] = map[string]interface{}{
|
||||
"required": false,
|
||||
"name": "SomeKey",
|
||||
}
|
||||
|
||||
config := map[string]string{
|
||||
"SomeKey": "somevalue",
|
||||
}
|
||||
|
||||
m := new(MetaIssueType)
|
||||
m.Fields = data
|
||||
|
||||
ok, err := m.CheckCompleteAndAvailable(config)
|
||||
if err == nil {
|
||||
t.Error("Expected non nil error. Received nil")
|
||||
}
|
||||
|
||||
if ok != false {
|
||||
t.Error("Expected false, got true")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMetaIssueType_CheckCompleteAndAvailable_NotAvailable(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["summary"] = map[string]interface{}{
|
||||
"required": true,
|
||||
"name": "Summary",
|
||||
}
|
||||
|
||||
config := map[string]string{
|
||||
"Summary": "Issue Summary",
|
||||
"SomeKey": "somevalue",
|
||||
}
|
||||
|
||||
m := new(MetaIssueType)
|
||||
m.Fields = data
|
||||
|
||||
ok, err := m.CheckCompleteAndAvailable(config)
|
||||
if err == nil {
|
||||
t.Error("Expected non nil error. Received nil")
|
||||
}
|
||||
|
||||
if ok != false {
|
||||
t.Error("Expected false, got true")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestMetaIssueType_CheckCompleteAndAvailable_Success(t *testing.T) {
|
||||
data := make(map[string]interface{})
|
||||
|
||||
data["summary"] = map[string]interface{}{
|
||||
"required": true,
|
||||
"name": "Summary",
|
||||
}
|
||||
|
||||
data["someKey"] = map[string]interface{}{
|
||||
"required": false,
|
||||
"name": "SomeKey",
|
||||
}
|
||||
|
||||
config := map[string]string{
|
||||
"SomeKey": "somevalue",
|
||||
"Summary": "Issue summary",
|
||||
}
|
||||
|
||||
m := new(MetaIssueType)
|
||||
m.Fields = data
|
||||
|
||||
ok, err := m.CheckCompleteAndAvailable(config)
|
||||
if err != nil {
|
||||
t.Errorf("Expected nil error. Received %s", err)
|
||||
}
|
||||
|
||||
if ok != true {
|
||||
t.Error("Expected true, got false")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestCreateMetaInfo_GetProjectWithName_Success(t *testing.T) {
|
||||
metainfo := new(CreateMetaInfo)
|
||||
metainfo.Projects = append(metainfo.Projects, &MetaProject{
|
||||
Name: "SPN",
|
||||
})
|
||||
|
||||
project := metainfo.GetProjectWithName("SPN")
|
||||
if project == nil {
|
||||
t.Errorf("Expected non nil value, received nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMetaProject_GetIssueTypeWithName_CaseMismatch_Success(t *testing.T) {
|
||||
m := new(MetaProject)
|
||||
m.IssueTypes = append(m.IssueTypes, &MetaIssueType{
|
||||
Name: "Bug",
|
||||
})
|
||||
|
||||
issuetype := m.GetIssueTypeWithName("BUG")
|
||||
|
||||
if issuetype == nil {
|
||||
t.Errorf("Expected non nil value, received nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateMetaInfo_GetProjectWithKey_Success(t *testing.T) {
|
||||
metainfo := new(CreateMetaInfo)
|
||||
metainfo.Projects = append(metainfo.Projects, &MetaProject{
|
||||
Key: "SPNKEY",
|
||||
})
|
||||
|
||||
project := metainfo.GetProjectWithKey("SPNKEY")
|
||||
if project == nil {
|
||||
t.Errorf("Expected non nil value, received nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateMetaInfo_GetProjectWithKey_NilForNonExistent(t *testing.T) {
|
||||
metainfo := new(CreateMetaInfo)
|
||||
metainfo.Projects = append(metainfo.Projects, &MetaProject{
|
||||
Key: "SPNKEY",
|
||||
})
|
||||
|
||||
project := metainfo.GetProjectWithKey("SPN")
|
||||
if project != nil {
|
||||
t.Errorf("Expected nil, received value")
|
||||
}
|
||||
}
|
100
project.go
100
project.go
@ -13,72 +13,72 @@ type ProjectService struct {
|
||||
|
||||
// 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"`
|
||||
Expand string `json:"expand" structs:"expand"`
|
||||
Self string `json:"self" structs:"self"`
|
||||
ID string `json:"id" structs:"id"`
|
||||
Key string `json:"key" structs:"key"`
|
||||
Name string `json:"name" structs:"name"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls" structs:"avatarUrls"`
|
||||
ProjectTypeKey string `json:"projectTypeKey" structs:"projectTypeKey"`
|
||||
ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectsCategory,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"`
|
||||
Self string `json:"self" structs:"self,omitempty"`
|
||||
ID string `json:"id" structs:"id,omitempty"`
|
||||
Name string `json:"name" structs:"name,omitempty"`
|
||||
Description string `json:"description" structs:"description,omitempty"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||
Key string `json:"key,omitempty" structs:"key,omitempty"`
|
||||
Description string `json:"description,omitempty" structs:"description,omitempty"`
|
||||
Lead User `json:"lead,omitempty" structs:"lead,omitempty"`
|
||||
Components []ProjectComponent `json:"components,omitempty" structs:"components,omitempty"`
|
||||
IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"`
|
||||
URL string `json:"url,omitempty" structs:"url,omitempty"`
|
||||
Email string `json:"email,omitempty" structs:"email,omitempty"`
|
||||
AssigneeType string `json:"assigneeType,omitempty" structs:"assigneeType,omitempty"`
|
||||
Versions []Version `json:"versions,omitempty" structs:"versions,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||
Roles struct {
|
||||
Developers string `json:"Developers,omitempty"`
|
||||
} `json:"roles,omitempty"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"`
|
||||
ProjectCategory ProjectCategory `json:"projectCategory,omitempty"`
|
||||
Developers string `json:"Developers,omitempty" structs:"Developers,omitempty"`
|
||||
} `json:"roles,omitempty" structs:"roles,omitempty"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"`
|
||||
ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"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
|
||||
Self string `json:"self" structs:"self,omitempty"`
|
||||
ID string `json:"id" structs:"id,omitempty"`
|
||||
Name string `json:"name" structs:"name,omitempty"`
|
||||
Archived bool `json:"archived" structs:"archived,omitempty"`
|
||||
Released bool `json:"released" structs:"released,omitempty"`
|
||||
ReleaseDate string `json:"releaseDate" structs:"releaseDate,omitempty"`
|
||||
UserReleaseDate string `json:"userReleaseDate" structs:"userReleaseDate,omitempty"`
|
||||
ProjectID int `json:"projectId" structs:"projectId,omitempty"` // 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"`
|
||||
Self string `json:"self" structs:"self,omitempty"`
|
||||
ID string `json:"id" structs:"id,omitempty"`
|
||||
Name string `json:"name" structs:"name,omitempty"`
|
||||
Description string `json:"description" structs:"description,omitempty"`
|
||||
Lead User `json:"lead,omitempty" structs:"lead,omitempty"`
|
||||
AssigneeType string `json:"assigneeType" structs:"assigneeType,omitempty"`
|
||||
Assignee User `json:"assignee" structs:"assignee,omitempty"`
|
||||
RealAssigneeType string `json:"realAssigneeType" structs:"realAssigneeType,omitempty"`
|
||||
RealAssignee User `json:"realAssignee" structs:"realAssignee,omitempty"`
|
||||
IsAssigneeTypeValid bool `json:"isAssigneeTypeValid" structs:"isAssigneeTypeValid,omitempty"`
|
||||
Project string `json:"project" structs:"project,omitempty"`
|
||||
ProjectID int `json:"projectId" structs:"projectId,omitempty"`
|
||||
}
|
||||
|
||||
// GetList gets all projects form JIRA
|
||||
@ -107,7 +107,7 @@ func (s *ProjectService) GetList() (*ProjectList, *Response, error) {
|
||||
//
|
||||
// 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)
|
||||
apiEndpoint := fmt.Sprintf("rest/api/2/project/%s", projectID)
|
||||
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
74
user.go
Normal file
74
user.go
Normal file
@ -0,0 +1,74 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// UserService handles users for the JIRA instance / API.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user
|
||||
type UserService struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
// User represents a JIRA user.
|
||||
type User struct {
|
||||
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||
Password string `json:"-"`
|
||||
Key string `json:"key,omitempty" structs:"key,omitempty"`
|
||||
EmailAddress string `json:"emailAddress,omitempty" structs:"emailAddress,omitempty"`
|
||||
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"`
|
||||
DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"`
|
||||
Active bool `json:"active,omitempty" structs:"active,omitempty"`
|
||||
TimeZone string `json:"timeZone,omitempty" structs:"timeZone,omitempty"`
|
||||
ApplicationKeys []string `json:"applicationKeys,omitempty" structs:"applicationKeys,omitempty"`
|
||||
}
|
||||
|
||||
// Get gets user info from JIRA
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-getUser
|
||||
func (s *UserService) Get(username string) (*User, *Response, error) {
|
||||
apiEndpoint := fmt.Sprintf("/rest/api/2/user?username=%s", username)
|
||||
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
user := new(User)
|
||||
resp, err := s.client.Do(req, user)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
return user, resp, nil
|
||||
}
|
||||
|
||||
// Create creates an user in JIRA.
|
||||
//
|
||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/cloud/#api/2/user-createUser
|
||||
func (s *UserService) Create(user *User) (*User, *Response, error) {
|
||||
apiEndpoint := "/rest/api/2/user"
|
||||
req, err := s.client.NewRequest("POST", apiEndpoint, user)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
resp, err := s.client.Do(req, nil)
|
||||
if err != nil {
|
||||
return nil, resp, err
|
||||
}
|
||||
|
||||
responseUser := new(User)
|
||||
defer resp.Body.Close()
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, resp, fmt.Errorf("Could not read the returned data")
|
||||
}
|
||||
err = json.Unmarshal(data, responseUser)
|
||||
if err != nil {
|
||||
return nil, resp, fmt.Errorf("Could not unmarshall the data into struct")
|
||||
}
|
||||
return responseUser, resp, nil
|
||||
}
|
57
user_test.go
Normal file
57
user_test.go
Normal file
@ -0,0 +1,57 @@
|
||||
package jira
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUserService_Get_Success(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testMux.HandleFunc("/rest/api/2/user", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "GET")
|
||||
testRequestURL(t, r, "/rest/api/2/user?username=fred")
|
||||
|
||||
fmt.Fprint(w, `{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","key":"fred",
|
||||
"name":"fred","emailAddress":"fred@example.com","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":true,"timeZone":"Australia/Sydney","groups":{"size":3,"items":[
|
||||
{"name":"jira-user","self":"http://www.example.com/jira/rest/api/2/group?groupname=jira-user"},{"name":"jira-admin",
|
||||
"self":"http://www.example.com/jira/rest/api/2/group?groupname=jira-admin"},{"name":"important","self":"http://www.example.com/jira/rest/api/2/group?groupname=important"
|
||||
}]},"applicationRoles":{"size":1,"items":[]},"expand":"groups,applicationRoles"}`)
|
||||
})
|
||||
|
||||
if user, _, err := testClient.User.Get("fred"); err != nil {
|
||||
t.Errorf("Error given: %s", err)
|
||||
} else if user == nil {
|
||||
t.Error("Expected user. User is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserService_Create(t *testing.T) {
|
||||
setup()
|
||||
defer teardown()
|
||||
testMux.HandleFunc("/rest/api/2/user", func(w http.ResponseWriter, r *http.Request) {
|
||||
testMethod(t, r, "POST")
|
||||
testRequestURL(t, r, "/rest/api/2/user")
|
||||
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
fmt.Fprint(w, `{"name":"charlie","password":"abracadabra","emailAddress":"charlie@atlassian.com",
|
||||
"displayName":"Charlie of Atlassian","applicationKeys":["jira-core"]}`)
|
||||
})
|
||||
|
||||
u := &User{
|
||||
Name: "charlie",
|
||||
Password: "abracadabra",
|
||||
EmailAddress: "charlie@atlassian.com",
|
||||
DisplayName: "Charlie of Atlassian",
|
||||
ApplicationKeys: []string{"jira-core"},
|
||||
}
|
||||
|
||||
if user, _, err := testClient.User.Create(u); err != nil {
|
||||
t.Errorf("Error given: %s", err)
|
||||
} else if user == nil {
|
||||
t.Error("Expected user. User is nil")
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user