mirror of
https://github.com/interviewstreet/go-jira.git
synced 2025-02-03 13:11:49 +02:00
Merge branch 'master' into rbriski/jira_error
This commit is contained in:
commit
7df17cc390
@ -6,10 +6,12 @@ go:
|
|||||||
- 1.4
|
- 1.4
|
||||||
- 1.5
|
- 1.5
|
||||||
- 1.6
|
- 1.6
|
||||||
|
- 1.7
|
||||||
|
- 1.8
|
||||||
|
- 1.9
|
||||||
|
|
||||||
before_install:
|
before_install:
|
||||||
- go get github.com/mattn/goveralls
|
- go get -t ./...
|
||||||
- go get golang.org/x/tools/cmd/cover
|
|
||||||
|
|
||||||
script:
|
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)
|
[![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)
|
[![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)
|
[![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).
|
[Go](https://golang.org/) client library for [Atlassian JIRA](https://www.atlassian.com/software/jira).
|
||||||
|
|
||||||
@ -12,9 +11,9 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Authentication (HTTP Basic, OAuth, Session Cookie)
|
* Authentication (HTTP Basic, OAuth, Session Cookie)
|
||||||
* Create and receive issues
|
* Create and retrieve issues
|
||||||
* Create and retrieve issue transitions (status updates)
|
* 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/).
|
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
|
$ 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:
|
(optional) to run unit / example tests:
|
||||||
|
|
||||||
$ cd $GOPATH/src/github.com/andygrunwald/go-jira
|
$ cd $GOPATH/src/github.com/andygrunwald/go-jira
|
||||||
@ -58,7 +68,7 @@ import (
|
|||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
jiraClient, _ := jira.NewClient(nil, "https://issues.apache.org/jira/")
|
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("%s: %+v\n", issue.Key, issue.Fields.Summary)
|
||||||
fmt.Printf("Type: %s\n", issue.Fields.Type.Name)
|
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.
|
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
|
```go
|
||||||
package main
|
package main
|
||||||
@ -95,7 +136,64 @@ func main() {
|
|||||||
panic(err)
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -169,4 +267,4 @@ If you are new to pull requests, checkout [Collaborating on projects using issue
|
|||||||
|
|
||||||
## License
|
## 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
|
package jira
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// HTTP Basic Authentication
|
||||||
|
authTypeBasic = 1
|
||||||
|
// HTTP Session Authentication
|
||||||
|
authTypeSession = 2
|
||||||
|
)
|
||||||
|
|
||||||
// AuthenticationService handles authentication for the JIRA instance / API.
|
// AuthenticationService handles authentication for the JIRA instance / API.
|
||||||
//
|
//
|
||||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#authentication
|
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#authentication
|
||||||
type AuthenticationService struct {
|
type AuthenticationService struct {
|
||||||
client *Client
|
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.
|
// Session represents a Session JSON response by the JIRA API.
|
||||||
@ -54,9 +72,9 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
|
|||||||
session := new(Session)
|
session := new(Session)
|
||||||
resp, err := s.client.Do(req, session)
|
resp, err := s.client.Do(req, session)
|
||||||
|
|
||||||
if resp != nil {
|
if resp != nil {
|
||||||
session.Cookies = resp.Cookies()
|
session.Cookies = resp.Cookies()
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
|
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.client.session = session
|
||||||
|
s.authType = authTypeSession
|
||||||
|
|
||||||
return true, nil
|
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 {
|
func (s *AuthenticationService) Authenticated() bool {
|
||||||
if s != nil {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Missing API Call GET (Returns information about the currently authenticated user's session)
|
// Logout logs out the current user that has been authenticated and the session in the client is destroyed.
|
||||||
// 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.)
|
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
|
||||||
// See 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"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -73,6 +74,29 @@ func TestAuthenticationService_AcquireSessionCookie_Success(t *testing.T) {
|
|||||||
if testClient.Authentication.Authenticated() != true {
|
if testClient.Authentication.Authenticated() != true {
|
||||||
t.Error("Expected true, but result was false")
|
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) {
|
func TestAuthenticationService_Authenticated(t *testing.T) {
|
||||||
@ -84,3 +108,214 @@ func TestAuthenticationService_Authenticated(t *testing.T) {
|
|||||||
t.Error("Expected false, but result was true")
|
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
|
// BoardsList reflects a list of agile boards
|
||||||
type BoardsList struct {
|
type BoardsList struct {
|
||||||
MaxResults int `json:"maxResults"`
|
MaxResults int `json:"maxResults" structs:"maxResults"`
|
||||||
StartAt int `json:"startAt"`
|
StartAt int `json:"startAt" structs:"startAt"`
|
||||||
Total int `json:"total"`
|
Total int `json:"total" structs:"total"`
|
||||||
IsLast bool `json:"isLast"`
|
IsLast bool `json:"isLast" structs:"isLast"`
|
||||||
Values []Board `json:"values"`
|
Values []Board `json:"values" structs:"values"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Board represents a JIRA agile board
|
// Board represents a JIRA agile board
|
||||||
type Board struct {
|
type Board struct {
|
||||||
ID int `json:"id,omitempty"`
|
ID int `json:"id,omitempty" structs:"id,omitempty"`
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty" structs:"name,omitemtpy"`
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty" structs:"type,omitempty"`
|
||||||
FilterID int `json:"filterId,omitempty"`
|
FilterID int `json:"filterId,omitempty" structs:"filterId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// BoardListOptions specifies the optional parameters to the BoardService.GetList
|
// BoardListOptions specifies the optional parameters to the BoardService.GetList
|
||||||
@ -46,19 +46,19 @@ type BoardListOptions struct {
|
|||||||
|
|
||||||
// Wrapper struct for search result
|
// Wrapper struct for search result
|
||||||
type sprintsResult struct {
|
type sprintsResult struct {
|
||||||
Sprints []Sprint `json:"values"`
|
Sprints []Sprint `json:"values" structs:"values"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sprint represents a sprint on JIRA agile board
|
// Sprint represents a sprint on JIRA agile board
|
||||||
type Sprint struct {
|
type Sprint struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id" structs:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" structs:"name"`
|
||||||
CompleteDate *time.Time `json:"completeDate"`
|
CompleteDate *time.Time `json:"completeDate" structs:"completeDate"`
|
||||||
EndDate *time.Time `json:"endDate"`
|
EndDate *time.Time `json:"endDate" structs:"endDate"`
|
||||||
StartDate *time.Time `json:"startDate"`
|
StartDate *time.Time `json:"startDate" structs:"startDate"`
|
||||||
OriginBoardID int `json:"originBoardId"`
|
OriginBoardID int `json:"originBoardId" structs:"originBoardId"`
|
||||||
Self string `json:"self"`
|
Self string `json:"self" structs:"self"`
|
||||||
State string `json:"state"`
|
State string `json:"state" structs:"state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetAllBoards will returns all boards. This only includes boards that the user has permission to view.
|
// 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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/fatih/structs"
|
||||||
|
"github.com/google/go-querystring/query"
|
||||||
|
"github.com/trivago/tgo/tcontainer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -24,170 +31,255 @@ type IssueService struct {
|
|||||||
|
|
||||||
// Issue represents a JIRA issue.
|
// Issue represents a JIRA issue.
|
||||||
type Issue struct {
|
type Issue struct {
|
||||||
Expand string `json:"expand,omitempty"`
|
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
Key string `json:"key,omitempty"`
|
Key string `json:"key,omitempty" structs:"key,omitempty"`
|
||||||
Fields *IssueFields `json:"fields,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
|
// Attachment represents a JIRA attachment
|
||||||
type Attachment struct {
|
type Attachment struct {
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||||
Filename string `json:"filename,omitempty"`
|
Filename string `json:"filename,omitempty" structs:"filename,omitempty"`
|
||||||
Author *User `json:"author,omitempty"`
|
Author *User `json:"author,omitempty" structs:"author,omitempty"`
|
||||||
Created string `json:"created,omitempty"`
|
Created string `json:"created,omitempty" structs:"created,omitempty"`
|
||||||
Size int `json:"size,omitempty"`
|
Size int `json:"size,omitempty" structs:"size,omitempty"`
|
||||||
MimeType string `json:"mimeType,omitempty"`
|
MimeType string `json:"mimeType,omitempty" structs:"mimeType,omitempty"`
|
||||||
Content string `json:"content,omitempty"`
|
Content string `json:"content,omitempty" structs:"content,omitempty"`
|
||||||
Thumbnail string `json:"thumbnail,omitempty"`
|
Thumbnail string `json:"thumbnail,omitempty" structs:"thumbnail,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Epic represents the epic to which an issue is associated
|
// Epic represents the epic to which an issue is associated
|
||||||
// Not that this struct does not process the returned "color" value
|
// Not that this struct does not process the returned "color" value
|
||||||
type Epic struct {
|
type Epic struct {
|
||||||
ID int `json:"id"`
|
ID int `json:"id" structs:"id"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key" structs:"key"`
|
||||||
Self string `json:"self"`
|
Self string `json:"self" structs:"self"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" structs:"name"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary" structs:"summary"`
|
||||||
Done bool `json:"done"`
|
Done bool `json:"done" structs:"done"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssueFields represents single fields of a JIRA issue.
|
// IssueFields represents single fields of a JIRA issue.
|
||||||
// Every JIRA issue has several fields attached.
|
// Every JIRA issue has several fields attached.
|
||||||
type IssueFields struct {
|
type IssueFields struct {
|
||||||
// TODO Missing fields
|
// TODO Missing fields
|
||||||
// * "timespent": null,
|
// * "aggregatetimespent": null,
|
||||||
// * "aggregatetimespent": null,
|
// * "workratio": -1,
|
||||||
// * "workratio": -1,
|
// * "lastViewed": null,
|
||||||
// * "lastViewed": null,
|
// * "aggregatetimeoriginalestimate": null,
|
||||||
// * "timeestimate": null,
|
// * "aggregatetimeestimate": null,
|
||||||
// * "aggregatetimeoriginalestimate": null,
|
// * "environment": null,
|
||||||
// * "timeoriginalestimate": null,
|
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
|
||||||
// * "timetracking": {},
|
Type IssueType `json:"issuetype" structs:"issuetype"`
|
||||||
// * "aggregatetimeestimate": null,
|
Project Project `json:"project,omitempty" structs:"project,omitempty"`
|
||||||
// * "environment": null,
|
Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"`
|
||||||
// * "duedate": null,
|
Priority *Priority `json:"priority,omitempty" structs:"priority,omitempty"`
|
||||||
Type IssueType `json:"issuetype"`
|
Resolutiondate string `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"`
|
||||||
Project Project `json:"project,omitempty"`
|
Created string `json:"created,omitempty" structs:"created,omitempty"`
|
||||||
Resolution *Resolution `json:"resolution,omitempty"`
|
Duedate string `json:"duedate,omitempty" structs:"duedate,omitempty"`
|
||||||
Priority *Priority `json:"priority,omitempty"`
|
Watches *Watches `json:"watches,omitempty" structs:"watches,omitempty"`
|
||||||
Resolutiondate string `json:"resolutiondate,omitempty"`
|
Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"`
|
||||||
Created string `json:"created,omitempty"`
|
Updated string `json:"updated,omitempty" structs:"updated,omitempty"`
|
||||||
Watches *Watches `json:"watches,omitempty"`
|
Description string `json:"description,omitempty" structs:"description,omitempty"`
|
||||||
Assignee *User `json:"assignee,omitempty"`
|
Summary string `json:"summary" structs:"summary"`
|
||||||
Updated string `json:"updated,omitempty"`
|
Creator *User `json:"Creator,omitempty" structs:"Creator,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Reporter *User `json:"reporter,omitempty" structs:"reporter,omitempty"`
|
||||||
Summary string `json:"summary"`
|
Components []*Component `json:"components,omitempty" structs:"components,omitempty"`
|
||||||
Creator *User `json:"Creator,omitempty"`
|
Status *Status `json:"status,omitempty" structs:"status,omitempty"`
|
||||||
Reporter *User `json:"reporter,omitempty"`
|
Progress *Progress `json:"progress,omitempty" structs:"progress,omitempty"`
|
||||||
Components []*Component `json:"components,omitempty"`
|
AggregateProgress *Progress `json:"aggregateprogress,omitempty" structs:"aggregateprogress,omitempty"`
|
||||||
Status *Status `json:"status,omitempty"`
|
TimeTracking *TimeTracking `json:"timetracking,omitempty" structs:"timetracking,omitempty"`
|
||||||
Progress *Progress `json:"progress,omitempty"`
|
TimeSpent int `json:"timespent,omitempty" structs:"timespent,omitempty"`
|
||||||
AggregateProgress *Progress `json:"aggregateprogress,omitempty"`
|
TimeEstimate int `json:"timeestimate,omitempty" structs:"timeestimate,omitempty"`
|
||||||
Worklog *Worklog `json:"worklog,omitempty"`
|
TimeOriginalEstimate int `json:"timeoriginalestimate,omitempty" structs:"timeoriginalestimate,omitempty"`
|
||||||
IssueLinks []*IssueLink `json:"issuelinks,omitempty"`
|
Worklog *Worklog `json:"worklog,omitempty" structs:"worklog,omitempty"`
|
||||||
Comments *Comments `json:"comment,omitempty"`
|
IssueLinks []*IssueLink `json:"issuelinks,omitempty" structs:"issuelinks,omitempty"`
|
||||||
FixVersions []*FixVersion `json:"fixVersions,omitempty"`
|
Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"`
|
||||||
Labels []string `json:"labels,omitempty"`
|
FixVersions []*FixVersion `json:"fixVersions,omitempty" structs:"fixVersions,omitempty"`
|
||||||
Subtasks []*Subtasks `json:"subtasks,omitempty"`
|
Labels []string `json:"labels,omitempty" structs:"labels,omitempty"`
|
||||||
Attachments []*Attachment `json:"attachment,omitempty"`
|
Subtasks []*Subtasks `json:"subtasks,omitempty" structs:"subtasks,omitempty"`
|
||||||
Epic *Epic `json:"epic,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.
|
// IssueType represents a type of a JIRA issue.
|
||||||
// Typical types are "Request", "Bug", "Story", ...
|
// Typical types are "Request", "Bug", "Story", ...
|
||||||
type IssueType struct {
|
type IssueType struct {
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty" structs:"description,omitempty"`
|
||||||
IconURL string `json:"iconUrl,omitempty"`
|
IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||||
Subtask bool `json:"subtask,omitempty"`
|
Subtask bool `json:"subtask,omitempty" structs:"subtask,omitempty"`
|
||||||
AvatarID int `json:"avatarId,omitempty"`
|
AvatarID int `json:"avatarId,omitempty" structs:"avatarId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolution represents a resolution of a JIRA issue.
|
// Resolution represents a resolution of a JIRA issue.
|
||||||
// Typical types are "Fixed", "Suspended", "Won't Fix", ...
|
// Typical types are "Fixed", "Suspended", "Won't Fix", ...
|
||||||
type Resolution struct {
|
type Resolution struct {
|
||||||
Self string `json:"self"`
|
Self string `json:"self" structs:"self"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id" structs:"id"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description" structs:"description"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" structs:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Priority represents a priority of a JIRA issue.
|
// Priority represents a priority of a JIRA issue.
|
||||||
// Typical types are "Normal", "Moderate", "Urgent", ...
|
// Typical types are "Normal", "Moderate", "Urgent", ...
|
||||||
type Priority struct {
|
type Priority struct {
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
IconURL string `json:"iconUrl,omitempty"`
|
IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||||
ID string `json:"id,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.
|
// Watches represents a type of how many user are "observing" a JIRA issue to track the status / updates.
|
||||||
type Watches struct {
|
type Watches struct {
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
WatchCount int `json:"watchCount,omitempty"`
|
WatchCount int `json:"watchCount,omitempty" structs:"watchCount,omitempty"`
|
||||||
IsWatching bool `json:"isWatching,omitempty"`
|
IsWatching bool `json:"isWatching,omitempty" structs:"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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// AvatarUrls represents different dimensions of avatars / images
|
// AvatarUrls represents different dimensions of avatars / images
|
||||||
type AvatarUrls struct {
|
type AvatarUrls struct {
|
||||||
Four8X48 string `json:"48x48,omitempty"`
|
Four8X48 string `json:"48x48,omitempty" structs:"48x48,omitempty"`
|
||||||
Two4X24 string `json:"24x24,omitempty"`
|
Two4X24 string `json:"24x24,omitempty" structs:"24x24,omitempty"`
|
||||||
One6X16 string `json:"16x16,omitempty"`
|
One6X16 string `json:"16x16,omitempty" structs:"16x16,omitempty"`
|
||||||
Three2X32 string `json:"32x32,omitempty"`
|
Three2X32 string `json:"32x32,omitempty" structs:"32x32,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Component represents a "component" of a JIRA issue.
|
// Component represents a "component" of a JIRA issue.
|
||||||
// Components can be user defined in every JIRA instance.
|
// Components can be user defined in every JIRA instance.
|
||||||
type Component struct {
|
type Component struct {
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status represents the current status of a JIRA issue.
|
// Status represents the current status of a JIRA issue.
|
||||||
// Typical status are "Open", "In Progress", "Closed", ...
|
// Typical status are "Open", "In Progress", "Closed", ...
|
||||||
// Status can be user defined in every JIRA instance.
|
// Status can be user defined in every JIRA instance.
|
||||||
type Status struct {
|
type Status struct {
|
||||||
Self string `json:"self"`
|
Self string `json:"self" structs:"self"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description" structs:"description"`
|
||||||
IconURL string `json:"iconUrl"`
|
IconURL string `json:"iconUrl" structs:"iconUrl"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" structs:"name"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id" structs:"id"`
|
||||||
StatusCategory StatusCategory `json:"statusCategory"`
|
StatusCategory StatusCategory `json:"statusCategory" structs:"statusCategory"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// StatusCategory represents the category a status belongs to.
|
// StatusCategory represents the category a status belongs to.
|
||||||
// Those categories can be user defined in every JIRA instance.
|
// Those categories can be user defined in every JIRA instance.
|
||||||
type StatusCategory struct {
|
type StatusCategory struct {
|
||||||
Self string `json:"self"`
|
Self string `json:"self" structs:"self"`
|
||||||
ID int `json:"id"`
|
ID int `json:"id" structs:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" structs:"name"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key" structs:"key"`
|
||||||
ColorName string `json:"colorName"`
|
ColorName string `json:"colorName" structs:"colorName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Progress represents the progress of a JIRA issue.
|
// Progress represents the progress of a JIRA issue.
|
||||||
type Progress struct {
|
type Progress struct {
|
||||||
Progress int `json:"progress"`
|
Progress int `json:"progress" structs:"progress"`
|
||||||
Total int `json:"total"`
|
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
|
// 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
|
// Wrapper struct for search result
|
||||||
type transitionResult struct {
|
type transitionResult struct {
|
||||||
Transitions []Transition `json:"transitions"`
|
Transitions []Transition `json:"transitions" structs:"transitions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Transition represents an issue transition in JIRA
|
// Transition represents an issue transition in JIRA
|
||||||
type Transition struct {
|
type Transition struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id" structs:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" structs:"name"`
|
||||||
Fields map[string]TransitionField `json:"fields"`
|
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 {
|
type TransitionField struct {
|
||||||
Required bool `json:"required"`
|
Required bool `json:"required" structs:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateTransitionPayload is used for creating new issue transitions
|
// CreateTransitionPayload is used for creating new issue transitions
|
||||||
type CreateTransitionPayload struct {
|
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 {
|
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
|
// 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
|
// One Worklog contains zero or n WorklogRecords
|
||||||
// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
|
// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
|
||||||
type Worklog struct {
|
type Worklog struct {
|
||||||
StartAt int `json:"startAt"`
|
StartAt int `json:"startAt" structs:"startAt"`
|
||||||
MaxResults int `json:"maxResults"`
|
MaxResults int `json:"maxResults" structs:"maxResults"`
|
||||||
Total int `json:"total"`
|
Total int `json:"total" structs:"total"`
|
||||||
Worklogs []WorklogRecord `json:"worklogs"`
|
Worklogs []WorklogRecord `json:"worklogs" structs:"worklogs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// WorklogRecord represents one entry of a Worklog
|
// WorklogRecord represents one entry of a Worklog
|
||||||
type WorklogRecord struct {
|
type WorklogRecord struct {
|
||||||
Self string `json:"self"`
|
Self string `json:"self" structs:"self"`
|
||||||
Author User `json:"author"`
|
Author User `json:"author" structs:"author"`
|
||||||
UpdateAuthor User `json:"updateAuthor"`
|
UpdateAuthor User `json:"updateAuthor" structs:"updateAuthor"`
|
||||||
Comment string `json:"comment"`
|
Comment string `json:"comment" structs:"comment"`
|
||||||
Created Time `json:"created"`
|
Created Time `json:"created" structs:"created"`
|
||||||
Updated Time `json:"updated"`
|
Updated Time `json:"updated" structs:"updated"`
|
||||||
Started Time `json:"started"`
|
Started Time `json:"started" structs:"started"`
|
||||||
TimeSpent string `json:"timeSpent"`
|
TimeSpent string `json:"timeSpent" structs:"timeSpent"`
|
||||||
TimeSpentSeconds int `json:"timeSpentSeconds"`
|
TimeSpentSeconds int `json:"timeSpentSeconds" structs:"timeSpentSeconds"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id" structs:"id"`
|
||||||
IssueID string `json:"issueId"`
|
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.
|
// Subtasks represents all issues of a parent issue.
|
||||||
type Subtasks struct {
|
type Subtasks struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id" structs:"id"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key" structs:"key"`
|
||||||
Self string `json:"self"`
|
Self string `json:"self" structs:"self"`
|
||||||
Fields IssueFields `json:"fields"`
|
Fields IssueFields `json:"fields" structs:"fields"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssueLink represents a link between two issues in JIRA.
|
// IssueLink represents a link between two issues in JIRA.
|
||||||
type IssueLink struct {
|
type IssueLink struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
Type IssueLinkType `json:"type"`
|
Type IssueLinkType `json:"type" structs:"type"`
|
||||||
OutwardIssue *Issue `json:"outwardIssue"`
|
OutwardIssue *Issue `json:"outwardIssue" structs:"outwardIssue"`
|
||||||
InwardIssue *Issue `json:"inwardIssue"`
|
InwardIssue *Issue `json:"inwardIssue" structs:"inwardIssue"`
|
||||||
Comment *Comment `json:"comment,omitempty"`
|
Comment *Comment `json:"comment,omitempty" structs:"comment,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// IssueLinkType represents a type of a link between to issues in JIRA.
|
// IssueLinkType represents a type of a link between to issues in JIRA.
|
||||||
// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc.
|
// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc.
|
||||||
type IssueLinkType struct {
|
type IssueLinkType struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" structs:"name"`
|
||||||
Inward string `json:"inward"`
|
Inward string `json:"inward" structs:"inward"`
|
||||||
Outward string `json:"outward"`
|
Outward string `json:"outward" structs:"outward"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Comments represents a list of Comment.
|
// Comments represents a list of Comment.
|
||||||
type Comments struct {
|
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.
|
// Comment represents a comment by a person to an issue in JIRA.
|
||||||
type Comment struct {
|
type Comment struct {
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||||
Author User `json:"author,omitempty"`
|
Author User `json:"author,omitempty" structs:"author,omitempty"`
|
||||||
Body string `json:"body,omitempty"`
|
Body string `json:"body,omitempty" structs:"body,omitempty"`
|
||||||
UpdateAuthor User `json:"updateAuthor,omitempty"`
|
UpdateAuthor User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"`
|
||||||
Updated string `json:"updated,omitempty"`
|
Updated string `json:"updated,omitempty" structs:"updated,omitempty"`
|
||||||
Created string `json:"created,omitempty"`
|
Created string `json:"created,omitempty" structs:"created,omitempty"`
|
||||||
Visibility CommentVisibility `json:"visibility,omitempty"`
|
Visibility CommentVisibility `json:"visibility,omitempty" structs:"visibility,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// FixVersion represents a software release in which an issue is fixed.
|
// FixVersion represents a software release in which an issue is fixed.
|
||||||
type FixVersion struct {
|
type FixVersion struct {
|
||||||
Archived *bool `json:"archived,omitempty"`
|
Archived *bool `json:"archived,omitempty" structs:"archived,omitempty"`
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||||
ProjectID int `json:"projectId,omitempty"`
|
ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"`
|
||||||
ReleaseDate string `json:"releaseDate,omitempty"`
|
ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"`
|
||||||
Released *bool `json:"released,omitempty"`
|
Released *bool `json:"released,omitempty" structs:"released,omitempty"`
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
UserReleaseDate string `json:"userReleaseDate,omitempty"`
|
UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// CommentVisibility represents he visibility of a comment.
|
// CommentVisibility represents he visibility of a comment.
|
||||||
// E.g. Type could be "role" and Value "Administrators"
|
// E.g. Type could be "role" and Value "Administrators"
|
||||||
type CommentVisibility struct {
|
type CommentVisibility struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty" structs:"type,omitempty"`
|
||||||
Value string `json:"value,omitempty"`
|
Value string `json:"value,omitempty" structs:"value,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SearchOptions specifies the optional parameters to various List methods that
|
// SearchOptions specifies the optional parameters to various List methods that
|
||||||
@ -332,15 +440,30 @@ type SearchOptions struct {
|
|||||||
StartAt int `url:"startAt,omitempty"`
|
StartAt int `url:"startAt,omitempty"`
|
||||||
// MaxResults: The maximum number of projects to return per page. Default: 50.
|
// MaxResults: The maximum number of projects to return per page. Default: 50.
|
||||||
MaxResults int `url:"maxResults,omitempty"`
|
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
|
// to be able to parse the results
|
||||||
type searchResult struct {
|
type searchResult struct {
|
||||||
Issues []Issue `json:"issues"`
|
Issues []Issue `json:"issues" structs:"issues"`
|
||||||
StartAt int `json:"startAt"`
|
StartAt int `json:"startAt" structs:"startAt"`
|
||||||
MaxResults int `json:"maxResults"`
|
MaxResults int `json:"maxResults" structs:"maxResults"`
|
||||||
Total int `json:"total"`
|
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
|
// 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.
|
// 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.
|
// 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
|
// 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)
|
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
|
||||||
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
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)
|
issue := new(Issue)
|
||||||
resp, err := s.client.Do(req, issue)
|
resp, err := s.client.Do(req, issue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -390,9 +523,9 @@ func (s *IssueService) DownloadAttachment(attachmentID string) (*Response, error
|
|||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostAttachment uploads r (io.Reader) as an attachment to a given attachmentID
|
// PostAttachment uploads r (io.Reader) as an attachment to a given issueID
|
||||||
func (s *IssueService) PostAttachment(attachmentID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) {
|
func (s *IssueService) PostAttachment(issueID string, r io.Reader, attachmentName string) (*[]Attachment, *Response, error) {
|
||||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", attachmentID)
|
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/attachments", issueID)
|
||||||
|
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
writer := multipart.NewWriter(b)
|
writer := multipart.NewWriter(b)
|
||||||
@ -428,6 +561,23 @@ func (s *IssueService) PostAttachment(attachmentID string, r io.Reader, attachme
|
|||||||
return attachment, resp, nil
|
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.
|
// 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:
|
// 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.
|
// 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 {
|
if err != nil {
|
||||||
return nil, nil, err
|
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)
|
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 {
|
if err != nil {
|
||||||
jerr := NewJiraError(resp, err)
|
jerr := NewJiraError(resp, err)
|
||||||
return nil, resp, jerr
|
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.
|
// AddComment adds a new comment to issueID.
|
||||||
@ -470,6 +668,30 @@ func (s *IssueService) AddComment(issueID string, comment *Comment) (*Comment, *
|
|||||||
return responseComment, resp, nil
|
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.
|
// AddLink adds a link between two issues.
|
||||||
//
|
//
|
||||||
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issueLink
|
// 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 {
|
if options == nil {
|
||||||
u = fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql))
|
u = fmt.Sprintf("rest/api/2/search?jql=%s", url.QueryEscape(jql))
|
||||||
} else {
|
} else {
|
||||||
u = fmt.Sprintf("rest/api/2/search?jql=%s&startAt=%d&maxResults=%d", url.QueryEscape(jql),
|
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.StartAt, options.MaxResults, options.Expand, strings.Join(options.Fields, ","))
|
||||||
}
|
}
|
||||||
|
|
||||||
req, err := s.client.NewRequest("GET", u, nil)
|
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
|
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
|
// GetCustomFields returns a map of customfield_* keys with string values
|
||||||
func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, error) {
|
func (s *IssueService) GetCustomFields(issueID string) (CustomFields, *Response, error) {
|
||||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s", issueID)
|
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 {
|
if rec, ok := f.(map[string]interface{}); ok {
|
||||||
for key, val := range rec {
|
for key, val := range rec {
|
||||||
if strings.Contains(key, "customfield") {
|
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)
|
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
|
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-doTransition
|
||||||
func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, error) {
|
func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, error) {
|
||||||
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/transitions", ticketID)
|
|
||||||
|
|
||||||
payload := CreateTransitionPayload{
|
payload := CreateTransitionPayload{
|
||||||
Transition: TransitionPayload{
|
Transition: TransitionPayload{
|
||||||
ID: transitionID,
|
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)
|
req, err := s.client.NewRequest("POST", apiEndpoint, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -588,3 +863,100 @@ func (s *IssueService) DoTransition(ticketID, transitionID string) (*Response, e
|
|||||||
|
|
||||||
return resp, err
|
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"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/trivago/tgo/tcontainer"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestIssueService_Get_Success(t *testing.T) {
|
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":{}}`)
|
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 {
|
if issue == nil {
|
||||||
t.Error("Expected issue. Issue is 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) {
|
func TestIssueService_AddComment(t *testing.T) {
|
||||||
setup()
|
setup()
|
||||||
defer teardown()
|
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) {
|
func TestIssueService_AddLink(t *testing.T) {
|
||||||
setup()
|
setup()
|
||||||
defer teardown()
|
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":{}}`)
|
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 {
|
if issue == nil {
|
||||||
t.Error("Expected issue. Issue is nil")
|
t.Error("Expected issue. Issue is nil")
|
||||||
}
|
}
|
||||||
@ -322,12 +422,12 @@ func TestIssueService_Search(t *testing.T) {
|
|||||||
defer teardown()
|
defer teardown()
|
||||||
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
|
testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) {
|
||||||
testMethod(t, r, "GET")
|
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)
|
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}}}]}`)
|
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)
|
_, resp, err := testClient.Issue.Search("something", opt)
|
||||||
|
|
||||||
if resp == nil {
|
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) {
|
func TestIssueService_GetCustomFields(t *testing.T) {
|
||||||
setup()
|
setup()
|
||||||
defer teardown()
|
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) {
|
func TestIssueService_GetTransitions(t *testing.T) {
|
||||||
setup()
|
setup()
|
||||||
defer teardown()
|
defer teardown()
|
||||||
@ -465,3 +625,592 @@ func TestIssueService_DoTransition(t *testing.T) {
|
|||||||
t.Errorf("Got error: %v", err)
|
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
|
Project *ProjectService
|
||||||
Board *BoardService
|
Board *BoardService
|
||||||
Sprint *SprintService
|
Sprint *SprintService
|
||||||
|
User *UserService
|
||||||
|
Group *GroupService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewClient returns a new JIRA API client.
|
// 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.Project = &ProjectService{client: c}
|
||||||
c.Board = &BoardService{client: c}
|
c.Board = &BoardService{client: c}
|
||||||
c.Sprint = &SprintService{client: c}
|
c.Sprint = &SprintService{client: c}
|
||||||
|
c.User = &UserService{client: c}
|
||||||
|
c.Group = &GroupService{client: c}
|
||||||
|
|
||||||
return c, nil
|
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.
|
// 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.
|
// 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.
|
// 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
|
var buf io.ReadWriter
|
||||||
if body != nil {
|
if body != nil {
|
||||||
buf = new(bytes.Buffer)
|
buf = new(bytes.Buffer)
|
||||||
err := json.NewEncoder(buf).Encode(body)
|
err = json.NewEncoder(buf).Encode(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
// Set session cookie if there is one
|
// Set authentication information
|
||||||
if c.session != nil {
|
if c.Authentication.authType == authTypeSession {
|
||||||
for _, cookie := range c.session.Cookies {
|
// Set session cookie if there is one
|
||||||
req.AddCookie(cookie)
|
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
|
// Set required headers
|
||||||
req.Header.Set("X-Atlassian-Token", "nocheck")
|
req.Header.Set("X-Atlassian-Token", "nocheck")
|
||||||
|
|
||||||
// Set session cookie if there is one
|
// Set authentication information
|
||||||
if c.session != nil {
|
if c.Authentication.authType == authTypeSession {
|
||||||
for _, cookie := range c.session.Cookies {
|
// Set session cookie if there is one
|
||||||
req.AddCookie(cookie)
|
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 (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
@ -107,6 +106,12 @@ func TestNewClient_WithServices(t *testing.T) {
|
|||||||
if c.Sprint == nil {
|
if c.Sprint == nil {
|
||||||
t.Error("No SprintService provided")
|
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) {
|
func TestCheckResponse(t *testing.T) {
|
||||||
@ -127,7 +132,7 @@ func TestCheckResponse(t *testing.T) {
|
|||||||
func TestClient_NewRequest(t *testing.T) {
|
func TestClient_NewRequest(t *testing.T) {
|
||||||
c, err := NewClient(nil, testJIRAInstanceURL)
|
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||||
if err != nil {
|
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/"
|
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)
|
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||||
if err != nil {
|
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 {
|
inURL, outURL := "rest/api/2/issue/", testJIRAInstanceURL+"rest/api/2/issue/"
|
||||||
A map[int]interface{}
|
|
||||||
}
|
|
||||||
_, err = c.NewRequest("GET", "/", &T{})
|
|
||||||
|
|
||||||
if err == nil {
|
outBody := `{"key":"MESOS"}` + "\n"
|
||||||
t.Error("Expected error to be returned.")
|
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) {
|
func TestClient_NewRequest_BadURL(t *testing.T) {
|
||||||
c, err := NewClient(nil, testJIRAInstanceURL)
|
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||||
if err != nil {
|
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)
|
_, err = c.NewRequest("GET", ":", nil)
|
||||||
testURLParseError(t, err)
|
testURLParseError(t, err)
|
||||||
@ -186,31 +196,54 @@ func TestClient_NewRequest_BadURL(t *testing.T) {
|
|||||||
func TestClient_NewRequest_SessionCookies(t *testing.T) {
|
func TestClient_NewRequest_SessionCookies(t *testing.T) {
|
||||||
c, err := NewClient(nil, testJIRAInstanceURL)
|
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||||
if err != nil {
|
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"}
|
cookie := &http.Cookie{Name: "testcookie", Value: "testvalue"}
|
||||||
c.session = &Session{Cookies: []*http.Cookie{cookie}}
|
c.session = &Session{Cookies: []*http.Cookie{cookie}}
|
||||||
|
c.Authentication.authType = authTypeSession
|
||||||
|
|
||||||
inURL := "rest/api/2/issue/"
|
inURL := "rest/api/2/issue/"
|
||||||
inBody := &Issue{Key: "MESOS"}
|
inBody := &Issue{Key: "MESOS"}
|
||||||
req, err := c.NewRequest("GET", inURL, inBody)
|
req, err := c.NewRequest("GET", inURL, inBody)
|
||||||
|
|
||||||
if err != nil {
|
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) {
|
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() {
|
for i, v := range req.Cookies() {
|
||||||
if v.String() != c.session.Cookies[i].String() {
|
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.
|
// 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,
|
// 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.
|
// 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) {
|
func TestClient_NewRequest_EmptyBody(t *testing.T) {
|
||||||
c, err := NewClient(nil, testJIRAInstanceURL)
|
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||||
if err != nil {
|
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)
|
req, err := c.NewRequest("GET", "/", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -232,32 +265,59 @@ func TestClient_NewRequest_EmptyBody(t *testing.T) {
|
|||||||
func TestClient_NewMultiPartRequest(t *testing.T) {
|
func TestClient_NewMultiPartRequest(t *testing.T) {
|
||||||
c, err := NewClient(nil, testJIRAInstanceURL)
|
c, err := NewClient(nil, testJIRAInstanceURL)
|
||||||
if err != nil {
|
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"}
|
cookie := &http.Cookie{Name: "testcookie", Value: "testvalue"}
|
||||||
c.session = &Session{Cookies: []*http.Cookie{cookie}}
|
c.session = &Session{Cookies: []*http.Cookie{cookie}}
|
||||||
|
c.Authentication.authType = authTypeSession
|
||||||
|
|
||||||
inURL := "rest/api/2/issue/"
|
inURL := "rest/api/2/issue/"
|
||||||
inBuf := bytes.NewBufferString("teststring")
|
inBuf := bytes.NewBufferString("teststring")
|
||||||
req, err := c.NewMultiPartRequest("GET", inURL, inBuf)
|
req, err := c.NewMultiPartRequest("GET", inURL, inBuf)
|
||||||
|
|
||||||
if err != nil {
|
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) {
|
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() {
|
for i, v := range req.Cookies() {
|
||||||
if v.String() != c.session.Cookies[i].String() {
|
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" {
|
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
|
// ProjectList represent a list of Projects
|
||||||
type ProjectList []struct {
|
type ProjectList []struct {
|
||||||
Expand string `json:"expand"`
|
Expand string `json:"expand" structs:"expand"`
|
||||||
Self string `json:"self"`
|
Self string `json:"self" structs:"self"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id" structs:"id"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key" structs:"key"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" structs:"name"`
|
||||||
AvatarUrls AvatarUrls `json:"avatarUrls"`
|
AvatarUrls AvatarUrls `json:"avatarUrls" structs:"avatarUrls"`
|
||||||
ProjectTypeKey string `json:"projectTypeKey"`
|
ProjectTypeKey string `json:"projectTypeKey" structs:"projectTypeKey"`
|
||||||
ProjectCategory ProjectCategory `json:"projectCategory,omitempty"`
|
ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectsCategory,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectCategory represents a single project category
|
// ProjectCategory represents a single project category
|
||||||
type ProjectCategory struct {
|
type ProjectCategory struct {
|
||||||
Self string `json:"self"`
|
Self string `json:"self" structs:"self,omitempty"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id" structs:"id,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" structs:"name,omitempty"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description" structs:"description,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Project represents a JIRA Project.
|
// Project represents a JIRA Project.
|
||||||
type Project struct {
|
type Project struct {
|
||||||
Expand string `json:"expand,omitempty"`
|
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
|
||||||
Self string `json:"self,omitempty"`
|
Self string `json:"self,omitempty" structs:"self,omitempty"`
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty" structs:"id,omitempty"`
|
||||||
Key string `json:"key,omitempty"`
|
Key string `json:"key,omitempty" structs:"key,omitempty"`
|
||||||
Description string `json:"description,omitempty"`
|
Description string `json:"description,omitempty" structs:"description,omitempty"`
|
||||||
Lead User `json:"lead,omitempty"`
|
Lead User `json:"lead,omitempty" structs:"lead,omitempty"`
|
||||||
Components []ProjectComponent `json:"components,omitempty"`
|
Components []ProjectComponent `json:"components,omitempty" structs:"components,omitempty"`
|
||||||
IssueTypes []IssueType `json:"issueTypes,omitempty"`
|
IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"`
|
||||||
URL string `json:"url,omitempty"`
|
URL string `json:"url,omitempty" structs:"url,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty" structs:"email,omitempty"`
|
||||||
AssigneeType string `json:"assigneeType,omitempty"`
|
AssigneeType string `json:"assigneeType,omitempty" structs:"assigneeType,omitempty"`
|
||||||
Versions []Version `json:"versions,omitempty"`
|
Versions []Version `json:"versions,omitempty" structs:"versions,omitempty"`
|
||||||
Name string `json:"name,omitempty"`
|
Name string `json:"name,omitempty" structs:"name,omitempty"`
|
||||||
Roles struct {
|
Roles struct {
|
||||||
Developers string `json:"Developers,omitempty"`
|
Developers string `json:"Developers,omitempty" structs:"Developers,omitempty"`
|
||||||
} `json:"roles,omitempty"`
|
} `json:"roles,omitempty" structs:"roles,omitempty"`
|
||||||
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"`
|
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"`
|
||||||
ProjectCategory ProjectCategory `json:"projectCategory,omitempty"`
|
ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectCategory,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version represents a single release version of a project
|
// Version represents a single release version of a project
|
||||||
type Version struct {
|
type Version struct {
|
||||||
Self string `json:"self"`
|
Self string `json:"self" structs:"self,omitempty"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id" structs:"id,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" structs:"name,omitempty"`
|
||||||
Archived bool `json:"archived"`
|
Archived bool `json:"archived" structs:"archived,omitempty"`
|
||||||
Released bool `json:"released"`
|
Released bool `json:"released" structs:"released,omitempty"`
|
||||||
ReleaseDate string `json:"releaseDate"`
|
ReleaseDate string `json:"releaseDate" structs:"releaseDate,omitempty"`
|
||||||
UserReleaseDate string `json:"userReleaseDate"`
|
UserReleaseDate string `json:"userReleaseDate" structs:"userReleaseDate,omitempty"`
|
||||||
ProjectID int `json:"projectId"` // Unlike other IDs, this is returned as a number
|
ProjectID int `json:"projectId" structs:"projectId,omitempty"` // Unlike other IDs, this is returned as a number
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProjectComponent represents a single component of a project
|
// ProjectComponent represents a single component of a project
|
||||||
type ProjectComponent struct {
|
type ProjectComponent struct {
|
||||||
Self string `json:"self"`
|
Self string `json:"self" structs:"self,omitempty"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id" structs:"id,omitempty"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name" structs:"name,omitempty"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description" structs:"description,omitempty"`
|
||||||
Lead User `json:"lead"`
|
Lead User `json:"lead,omitempty" structs:"lead,omitempty"`
|
||||||
AssigneeType string `json:"assigneeType"`
|
AssigneeType string `json:"assigneeType" structs:"assigneeType,omitempty"`
|
||||||
Assignee User `json:"assignee"`
|
Assignee User `json:"assignee" structs:"assignee,omitempty"`
|
||||||
RealAssigneeType string `json:"realAssigneeType"`
|
RealAssigneeType string `json:"realAssigneeType" structs:"realAssigneeType,omitempty"`
|
||||||
RealAssignee User `json:"realAssignee"`
|
RealAssignee User `json:"realAssignee" structs:"realAssignee,omitempty"`
|
||||||
IsAssigneeTypeValid bool `json:"isAssigneeTypeValid"`
|
IsAssigneeTypeValid bool `json:"isAssigneeTypeValid" structs:"isAssigneeTypeValid,omitempty"`
|
||||||
Project string `json:"project"`
|
Project string `json:"project" structs:"project,omitempty"`
|
||||||
ProjectID int `json:"projectId"`
|
ProjectID int `json:"projectId" structs:"projectId,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetList gets all projects form JIRA
|
// 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
|
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/project-getProject
|
||||||
func (s *ProjectService) Get(projectID string) (*Project, *Response, error) {
|
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)
|
req, err := s.client.NewRequest("GET", apiEndpoint, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
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