1
0
mirror of https://github.com/interviewstreet/go-jira.git synced 2025-01-07 23:01:48 +02:00

Merge branch 'master' of https://github.com/bidesh/go-jira into bidesh-master

* 'master' of https://github.com/bidesh/go-jira:
  Adds test for authentication on expected json. Adds test to metaissue
  Removes check for statuscode as jiraclient already does it. Adds test for nonok status code returned
  Adds test for GetProjectWithName and GetIssueTypeWithName
  Omit more empty attributes when converting from struct to map
  Adds unknown map for arbitrary fields in IssueFields. Adds Custom Marshall,Unmarshall. Adds structs tag where necessary
  Adds metaissue support.
  Completes the APi for session. Adds logout and GetCurrentUser
This commit is contained in:
Andy Grunwald 2016-10-03 13:15:05 +02:00
commit 8d3b47871f
8 changed files with 1339 additions and 227 deletions

View File

@ -1,7 +1,9 @@
package jira
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
@ -54,9 +56,9 @@ func (s *AuthenticationService) AcquireSessionCookie(username, password string)
session := new(Session)
resp, err := s.client.Do(req, session)
if resp != nil {
session.Cookies = resp.Cookies()
}
if resp != nil {
session.Cookies = resp.Cookies()
}
if err != nil {
return false, fmt.Errorf("Auth at JIRA instance failed (HTTP(S) request). %s", err)
@ -78,7 +80,76 @@ func (s *AuthenticationService) Authenticated() bool {
return false
}
// TODO Missing API Call GET (Returns information about the currently authenticated user's session)
// See https://docs.atlassian.com/jira/REST/latest/#auth/1/session
// TODO Missing API Call DELETE (Logs the current user out of JIRA, destroying the existing session, if any.)
// See https://docs.atlassian.com/jira/REST/latest/#auth/1/session
// Logout logs out the current user that has been authenticated and the session in the client is destroyed.
//
// JIRA API docs: https://docs.atlassian.com/jira/REST/latest/#auth/1/session
func (s *AuthenticationService) Logout() error {
if s == nil {
return fmt.Errorf("Authenticaiton Service is not instantiated")
}
if 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)
}
//var dump interface{}
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 successfull, 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.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 recieved user info : %s", err)
}
return ret, nil
}

View File

@ -5,6 +5,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"reflect"
"testing"
)
@ -84,3 +85,195 @@ func TestAuthenticationService_Authenticated(t *testing.T) {
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, recieved 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, recieved 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://tasks.trivago.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://tasks.trivago.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, recieved %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")
}
}

View File

@ -14,20 +14,20 @@ type BoardService struct {
// BoardsList reflects a list of agile boards
type BoardsList struct {
MaxResults int `json:"maxResults"`
StartAt int `json:"startAt"`
Total int `json:"total"`
IsLast bool `json:"isLast"`
Values []Board `json:"values"`
MaxResults int `json:"maxResults" structs:"maxResults"`
StartAt int `json:"startAt" structs:"startAt"`
Total int `json:"total" structs:"total"`
IsLast bool `json:"isLast" structs:"isLast"`
Values []Board `json:"values" structs:"values"`
}
// Board represents a JIRA agile board
type Board struct {
ID int `json:"id,omitempty"`
Self string `json:"self,omitempty"`
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
FilterID int `json:"filterId,omitempty"`
ID int `json:"id,omitempty" structs:"id,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitemtpy"`
Type string `json:"type,omitempty" structs:"type,omitempty"`
FilterID int `json:"filterId,omitempty" structs:"filterId,omitempty"`
}
// BoardListOptions specifies the optional parameters to the BoardService.GetList
@ -46,19 +46,19 @@ type BoardListOptions struct {
// Wrapper struct for search result
type sprintsResult struct {
Sprints []Sprint `json:"values"`
Sprints []Sprint `json:"values" structs:"values"`
}
// Sprint represents a sprint on JIRA agile board
type Sprint struct {
ID int `json:"id"`
Name string `json:"name"`
CompleteDate *time.Time `json:"completeDate"`
EndDate *time.Time `json:"endDate"`
StartDate *time.Time `json:"startDate"`
OriginBoardID int `json:"originBoardId"`
Self string `json:"self"`
State string `json:"state"`
ID int `json:"id" structs:"id"`
Name string `json:"name" structs:"name"`
CompleteDate *time.Time `json:"completeDate" structs:"completeDate"`
EndDate *time.Time `json:"endDate" structs:"endDate"`
StartDate *time.Time `json:"startDate" structs:"startDate"`
OriginBoardID int `json:"originBoardId" structs:"originBoardId"`
Self string `json:"self" structs:"self"`
State string `json:"state" structs:"state"`
}
// GetAllBoards will returns all boards. This only includes boards that the user has permission to view.

369
issue.go
View File

@ -2,10 +2,14 @@ package jira
import (
"bytes"
"encoding/json"
"fmt"
"github.com/fatih/structs"
"github.com/trivago/tgo/tcontainer"
"io"
"mime/multipart"
"net/url"
"reflect"
"strings"
"time"
)
@ -24,35 +28,35 @@ type IssueService struct {
// Issue represents a JIRA issue.
type Issue struct {
Expand string `json:"expand,omitempty"`
ID string `json:"id,omitempty"`
Self string `json:"self,omitempty"`
Key string `json:"key,omitempty"`
Fields *IssueFields `json:"fields,omitempty"`
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
Key string `json:"key,omitempty" structs:"key,omitempty"`
Fields *IssueFields `json:"fields,omitempty" structs:"fields,omitempty"`
}
// Attachment represents a JIRA attachment
type Attachment struct {
Self string `json:"self,omitempty"`
ID string `json:"id,omitempty"`
Filename string `json:"filename,omitempty"`
Author *User `json:"author,omitempty"`
Created string `json:"created,omitempty"`
Size int `json:"size,omitempty"`
MimeType string `json:"mimeType,omitempty"`
Content string `json:"content,omitempty"`
Thumbnail string `json:"thumbnail,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
Filename string `json:"filename,omitempty" structs:"filename,omitempty"`
Author *User `json:"author,omitempty" structs:"author,omitempty"`
Created string `json:"created,omitempty" structs:"created,omitempty"`
Size int `json:"size,omitempty" structs:"size,omitempty"`
MimeType string `json:"mimeType,omitempty" structs:"mimeType,omitempty"`
Content string `json:"content,omitempty" structs:"content,omitempty"`
Thumbnail string `json:"thumbnail,omitempty" structs:"thumbnail,omitempty"`
}
// Epic represents the epic to which an issue is associated
// Not that this struct does not process the returned "color" value
type Epic struct {
ID int `json:"id"`
Key string `json:"key"`
Self string `json:"self"`
Name string `json:"name"`
Summary string `json:"summary"`
Done bool `json:"done"`
ID int `json:"id" structs:"id"`
Key string `json:"key" structs:"key"`
Self string `json:"self" structs:"self"`
Name string `json:"name" structs:"name"`
Summary string `json:"summary" structs:"summary"`
Done bool `json:"done" structs:"done"`
}
// IssueFields represents single fields of a JIRA issue.
@ -70,124 +74,185 @@ type IssueFields struct {
// * "aggregatetimeestimate": null,
// * "environment": null,
// * "duedate": null,
Type IssueType `json:"issuetype"`
Project Project `json:"project,omitempty"`
Resolution *Resolution `json:"resolution,omitempty"`
Priority *Priority `json:"priority,omitempty"`
Resolutiondate string `json:"resolutiondate,omitempty"`
Created string `json:"created,omitempty"`
Watches *Watches `json:"watches,omitempty"`
Assignee *User `json:"assignee,omitempty"`
Updated string `json:"updated,omitempty"`
Description string `json:"description,omitempty"`
Summary string `json:"summary"`
Creator *User `json:"Creator,omitempty"`
Reporter *User `json:"reporter,omitempty"`
Components []*Component `json:"components,omitempty"`
Status *Status `json:"status,omitempty"`
Progress *Progress `json:"progress,omitempty"`
AggregateProgress *Progress `json:"aggregateprogress,omitempty"`
Worklog *Worklog `json:"worklog,omitempty"`
IssueLinks []*IssueLink `json:"issuelinks,omitempty"`
Comments *Comments `json:"comment,omitempty"`
FixVersions []*FixVersion `json:"fixVersions,omitempty"`
Labels []string `json:"labels,omitempty"`
Subtasks []*Subtasks `json:"subtasks,omitempty"`
Attachments []*Attachment `json:"attachment,omitempty"`
Epic *Epic `json:"epic,omitempty"`
Type IssueType `json:"issuetype" structs:"issuetype"`
Project Project `json:"project,omitempty" structs:"project,omitempty"`
Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"`
Priority *Priority `json:"priority,omitempty" structs:"priority,omitempty"`
Resolutiondate string `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"`
Created string `json:"created,omitempty" structs:"created,omitempty"`
Watches *Watches `json:"watches,omitempty" structs:"watches,omitempty"`
Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"`
Updated string `json:"updated,omitempty" structs:"updated,omitempty"`
Description string `json:"description,omitempty" structs:"description,omitempty"`
Summary string `json:"summary" structs:"summary"`
Creator *User `json:"Creator,omitempty" structs:"Creator,omitempty"`
Reporter *User `json:"reporter,omitempty" structs:"reporter,omitempty"`
Components []*Component `json:"components,omitempty" structs:"components,omitempty"`
Status *Status `json:"status,omitempty" structs:"status,omitempty"`
Progress *Progress `json:"progress,omitempty" structs:"progress,omitempty"`
AggregateProgress *Progress `json:"aggregateprogress,omitempty" structs:"aggregateprogress,omitempty"`
Worklog *Worklog `json:"worklog,omitempty" structs:"worklog,omitempty"`
IssueLinks []*IssueLink `json:"issuelinks,omitempty" structs:"issuelinks,omitempty"`
Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"`
FixVersions []*FixVersion `json:"fixVersions,omitempty" structs:"fixVersions,omitempty"`
Labels []string `json:"labels,omitempty" structs:"labels,omitempty"`
Subtasks []*Subtasks `json:"subtasks,omitempty" structs:"subtasks,omitempty"`
Attachments []*Attachment `json:"attachment,omitempty" structs:"attachment,omitempty"`
Epic *Epic `json:"epic,omitempty" structs:"epic,omitempty"`
Unknowns tcontainer.MarshalMap
}
func (i *IssueFields) MarshalJSON() ([]byte, error) {
m := structs.Map(i)
unknowns, okay := m["Unknowns"]
if okay {
// if unknowns present, shift all key value from unkown to a level up
for key, value := range unknowns.(tcontainer.MarshalMap) {
m[key] = value
}
delete(m, "Unknowns")
}
return json.Marshal(m)
}
func (i *IssueFields) UnmarshalJSON(data []byte) error {
// Do the normal unmarshalling first
// Details for this way: http://choly.ca/post/go-json-marshalling/
type Alias IssueFields
aux := &struct {
*Alias
}{
Alias: (*Alias)(i),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
totalMap := tcontainer.NewMarshalMap()
err := json.Unmarshal(data, &totalMap)
if err != nil {
return err
}
t := reflect.TypeOf(*i)
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tagDetail := field.Tag.Get("json")
if tagDetail == "" {
// ignore if there are no tags
continue
}
options := strings.Split(tagDetail, ",")
if len(options) == 0 {
return fmt.Errorf("No tags options found for %s", field.Name)
}
// the first one is the json tag
key := options[0]
if _, okay := totalMap.Value(key); okay {
delete(totalMap, key)
}
}
i = (*IssueFields)(aux.Alias)
// all the tags found in the struct were removed. Whatever is left are unknowns to struct
i.Unknowns = totalMap
return nil
}
// IssueType represents a type of a JIRA issue.
// Typical types are "Request", "Bug", "Story", ...
type IssueType struct {
Self string `json:"self,omitempty"`
ID string `json:"id,omitempty"`
Description string `json:"description,omitempty"`
IconURL string `json:"iconUrl,omitempty"`
Name string `json:"name,omitempty"`
Subtask bool `json:"subtask,omitempty"`
AvatarID int `json:"avatarId,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
ID string `json:"id,omitempty" struct:"id,omitempty"`
Description string `json:"description,omitempty" struct:"description,omitempty"`
IconURL string `json:"iconUrl,omitempty" struct:"iconUrl,omitempty"`
Name string `json:"name,omitempty" struct:"name,omitempty"`
Subtask bool `json:"subtask,omitempty" struct:"subtask,omitempty"`
AvatarID int `json:"avatarId,omitempty" struct:"avatarId,omitempty"`
}
// Resolution represents a resolution of a JIRA issue.
// Typical types are "Fixed", "Suspended", "Won't Fix", ...
type Resolution struct {
Self string `json:"self"`
ID string `json:"id"`
Description string `json:"description"`
Name string `json:"name"`
Self string `json:"self" structs:"self"`
ID string `json:"id" structs:"id"`
Description string `json:"description" structs:"description"`
Name string `json:"name" structs:"name"`
}
// Priority represents a priority of a JIRA issue.
// Typical types are "Normal", "Moderate", "Urgent", ...
type Priority struct {
Self string `json:"self,omitempty"`
IconURL string `json:"iconUrl,omitempty"`
Name string `json:"name,omitempty"`
ID string `json:"id,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
IconURL string `json:"iconUrl,omitempty" structs:"iconUrl,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
}
// Watches represents a type of how many user are "observing" a JIRA issue to track the status / updates.
type Watches struct {
Self string `json:"self,omitempty"`
WatchCount int `json:"watchCount,omitempty"`
IsWatching bool `json:"isWatching,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
WatchCount int `json:"watchCount,omitempty" structs:"watchCount,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"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
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"`
}
// AvatarUrls represents different dimensions of avatars / images
type AvatarUrls struct {
Four8X48 string `json:"48x48,omitempty"`
Two4X24 string `json:"24x24,omitempty"`
One6X16 string `json:"16x16,omitempty"`
Three2X32 string `json:"32x32,omitempty"`
Four8X48 string `json:"48x48,omitempty" structs:"48x48,omitempty"`
Two4X24 string `json:"24x24,omitempty" structs:"24x24,omitempty"`
One6X16 string `json:"16x16,omitempty" structs:"16x16,omitempty"`
Three2X32 string `json:"32x32,omitempty" structs:"32x32,omitempty"`
}
// Component represents a "component" of a JIRA issue.
// Components can be user defined in every JIRA instance.
type Component struct {
Self string `json:"self,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
}
// Status represents the current status of a JIRA issue.
// Typical status are "Open", "In Progress", "Closed", ...
// Status can be user defined in every JIRA instance.
type Status struct {
Self string `json:"self"`
Description string `json:"description"`
IconURL string `json:"iconUrl"`
Name string `json:"name"`
ID string `json:"id"`
StatusCategory StatusCategory `json:"statusCategory"`
Self string `json:"self" structs:"self"`
Description string `json:"description" structs:"description"`
IconURL string `json:"iconUrl" structs:"iconUrl"`
Name string `json:"name" structs:"name"`
ID string `json:"id" structs:"id"`
StatusCategory StatusCategory `json:"statusCategory" structs:"statusCategory"`
}
// StatusCategory represents the category a status belongs to.
// Those categories can be user defined in every JIRA instance.
type StatusCategory struct {
Self string `json:"self"`
ID int `json:"id"`
Name string `json:"name"`
Key string `json:"key"`
ColorName string `json:"colorName"`
Self string `json:"self" structs:"self"`
ID int `json:"id" structs:"id"`
Name string `json:"name" structs:"name"`
Key string `json:"key" structs:"key"`
ColorName string `json:"colorName" structs:"colorName"`
}
// Progress represents the progress of a JIRA issue.
type Progress struct {
Progress int `json:"progress"`
Total int `json:"total"`
Progress int `json:"progress" structs:"progress"`
Total int `json:"total" structs:"total"`
}
// Time represents the Time definition of JIRA as a time.Time of go
@ -195,29 +260,29 @@ type Time time.Time
// Wrapper struct for search result
type transitionResult struct {
Transitions []Transition `json:"transitions"`
Transitions []Transition `json:"transitions" structs:"transitions"`
}
// Transition represents an issue transition in JIRA
type Transition struct {
ID string `json:"id"`
Name string `json:"name"`
Fields map[string]TransitionField `json:"fields"`
ID string `json:"id" structs:"id"`
Name string `json:"name" structs:"name"`
Fields map[string]TransitionField `json:"fields" structs:"fields"`
}
// TransitionField represents the value of one Transistion
type TransitionField struct {
Required bool `json:"required"`
Required bool `json:"required" structs:"required"`
}
// CreateTransitionPayload is used for creating new issue transitions
type CreateTransitionPayload struct {
Transition TransitionPayload `json:"transition"`
Transition TransitionPayload `json:"transition" structs:"transition"`
}
// TransitionPayload represents the request payload of Transistion calls like DoTransition
type TransitionPayload struct {
ID string `json:"id"`
ID string `json:"id" structs:"id"`
}
// UnmarshalJSON will transform the JIRA time into a time.Time
@ -235,90 +300,90 @@ func (t *Time) UnmarshalJSON(b []byte) error {
// One Worklog contains zero or n WorklogRecords
// JIRA Wiki: https://confluence.atlassian.com/jira/logging-work-on-an-issue-185729605.html
type Worklog struct {
StartAt int `json:"startAt"`
MaxResults int `json:"maxResults"`
Total int `json:"total"`
Worklogs []WorklogRecord `json:"worklogs"`
StartAt int `json:"startAt" structs:"startAt"`
MaxResults int `json:"maxResults" structs:"maxResults"`
Total int `json:"total" structs:"total"`
Worklogs []WorklogRecord `json:"worklogs" structs:"worklogs"`
}
// WorklogRecord represents one entry of a Worklog
type WorklogRecord struct {
Self string `json:"self"`
Author User `json:"author"`
UpdateAuthor User `json:"updateAuthor"`
Comment string `json:"comment"`
Created Time `json:"created"`
Updated Time `json:"updated"`
Started Time `json:"started"`
TimeSpent string `json:"timeSpent"`
TimeSpentSeconds int `json:"timeSpentSeconds"`
ID string `json:"id"`
IssueID string `json:"issueId"`
Self string `json:"self" structs:"self"`
Author User `json:"author" structs:"author"`
UpdateAuthor User `json:"updateAuthor" structs:"updateAuthor"`
Comment string `json:"comment" structs:"comment"`
Created Time `json:"created" structs:"created"`
Updated Time `json:"updated" structs:"updated"`
Started Time `json:"started" structs:"started"`
TimeSpent string `json:"timeSpent" structs:"timeSpent"`
TimeSpentSeconds int `json:"timeSpentSeconds" structs:"timeSpentSeconds"`
ID string `json:"id" structs:"id"`
IssueID string `json:"issueId" structs:"issueId"`
}
// Subtasks represents all issues of a parent issue.
type Subtasks struct {
ID string `json:"id"`
Key string `json:"key"`
Self string `json:"self"`
Fields IssueFields `json:"fields"`
ID string `json:"id" structs:"id"`
Key string `json:"key" structs:"key"`
Self string `json:"self" structs:"self"`
Fields IssueFields `json:"fields" structs:"fields"`
}
// IssueLink represents a link between two issues in JIRA.
type IssueLink struct {
ID string `json:"id,omitempty"`
Self string `json:"self,omitempty"`
Type IssueLinkType `json:"type"`
OutwardIssue *Issue `json:"outwardIssue"`
InwardIssue *Issue `json:"inwardIssue"`
Comment *Comment `json:"comment,omitempty"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
Type IssueLinkType `json:"type" structs:"type"`
OutwardIssue *Issue `json:"outwardIssue" structs:"outwardIssue"`
InwardIssue *Issue `json:"inwardIssue" structs:"inwardIssue"`
Comment *Comment `json:"comment,omitempty" structs:"comment,omitempty"`
}
// IssueLinkType represents a type of a link between to issues in JIRA.
// Typical issue link types are "Related to", "Duplicate", "Is blocked by", etc.
type IssueLinkType struct {
ID string `json:"id,omitempty"`
Self string `json:"self,omitempty"`
Name string `json:"name"`
Inward string `json:"inward"`
Outward string `json:"outward"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
Name string `json:"name" structs:"name"`
Inward string `json:"inward" structs:"inward"`
Outward string `json:"outward" structs:"outward"`
}
// Comments represents a list of Comment.
type Comments struct {
Comments []*Comment `json:"comments,omitempty"`
Comments []*Comment `json:"comments,omitempty" structs:"comments,omitempty"`
}
// Comment represents a comment by a person to an issue in JIRA.
type Comment struct {
ID string `json:"id,omitempty"`
Self string `json:"self,omitempty"`
Name string `json:"name,omitempty"`
Author User `json:"author,omitempty"`
Body string `json:"body,omitempty"`
UpdateAuthor User `json:"updateAuthor,omitempty"`
Updated string `json:"updated,omitempty"`
Created string `json:"created,omitempty"`
Visibility CommentVisibility `json:"visibility,omitempty"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
Name string `json:"name,omitempty" structs:name,omitempty"`
Author User `json:"author,omitempty" structs:"author,omitempty"`
Body string `json:"body,omitempty" structs:"body,omitempty"`
UpdateAuthor User `json:"updateAuthor,omitempty" structs:"updateAuthor,omitempty"`
Updated string `json:"updated,omitempty" structs:"updated,omitempty"`
Created string `json:"created,omitempty" structs:"created,omitempty"`
Visibility CommentVisibility `json:"visibility,omitempty" structs:"visibility,omitempty"`
}
// FixVersion represents a software release in which an issue is fixed.
type FixVersion struct {
Archived *bool `json:"archived,omitempty"`
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
ProjectID int `json:"projectId,omitempty"`
ReleaseDate string `json:"releaseDate,omitempty"`
Released *bool `json:"released,omitempty"`
Self string `json:"self,omitempty"`
UserReleaseDate string `json:"userReleaseDate,omitempty"`
Archived *bool `json:"archived,omitempty" structs:"archived,omitempty"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
ProjectID int `json:"projectId,omitempty" structs:"projectId,omitempty"`
ReleaseDate string `json:"releaseDate,omitempty" structs:"releaseDate,omitempty"`
Released *bool `json:"released,omitempty" structs:"released,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
UserReleaseDate string `json:"userReleaseDate,omitempty" structs:"userReleaseDate,omitempty"`
}
// CommentVisibility represents he visibility of a comment.
// E.g. Type could be "role" and Value "Administrators"
type CommentVisibility struct {
Type string `json:"type,omitempty"`
Value string `json:"value,omitempty"`
Type string `json:"type,omitempty" structs:"type,omitempty"`
Value string `json:"value,omitempty" structs:"value,omitempty"`
}
// SearchOptions specifies the optional parameters to various List methods that
@ -337,10 +402,10 @@ type SearchOptions struct {
// searchResult is only a small wrapper arround the Search (with JQL) method
// to be able to parse the results
type searchResult struct {
Issues []Issue `json:"issues"`
StartAt int `json:"startAt"`
MaxResults int `json:"maxResults"`
Total int `json:"total"`
Issues []Issue `json:"issues" structs:"issues"`
StartAt int `json:"startAt" structs:"startAt"`
MaxResults int `json:"maxResults" structs:"maxResults"`
Total int `json:"total" structs:"total"`
}
// CustomFields represents custom fields of JIRA

View File

@ -8,6 +8,8 @@ import (
"reflect"
"strings"
"testing"
"github.com/trivago/tgo/tcontainer"
)
func TestIssueService_Get_Success(t *testing.T) {
@ -487,3 +489,128 @@ func TestIssueService_DoTransition(t *testing.T) {
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, recieved %s", err)
}
if len(i.Unknowns) != 1 {
t.Errorf("Expected 1 unknown field to be present, recieved %d", len(i.Unknowns))
}
if i.Description != "example bug report" {
t.Errorf("Expected description to be \"%s\", recieved \"%s\"", "example bug report", i.Description)
}
}
func TestIssueFields_MarshalJSON_Success(t *testing.T) {
/*
{
"customfield_123":"test",
"description":"example bug report",
"project":{
"self":"http://www.example.com/jira/rest/api/2/project/EX",
"id":"10000",
"key":"EX"
}
}
*/
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, recieved %s", err)
}
recieved := new(IssueFields)
// the order of json might be different. so unmarshal it again and comapre objects
err = json.Unmarshal(bytes, recieved)
if err != nil {
t.Errorf("Expected nil err, recieved %s", err)
}
if !reflect.DeepEqual(i, recieved) {
t.Errorf("Recieved object different from expected")
}
}

133
metaissue.go Normal file
View File

@ -0,0 +1,133 @@
package jira
import (
"fmt"
"github.com/trivago/tgo/tcontainer"
"strings"
)
// CreateMeta 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 []*MetaIssueTypes `json:"issuetypes,omitempty"`
}
// metaIssueTypes 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 MetaIssueTypes struct {
Self string `json:"expand,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
}
fmt.Println(req.URL)
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 recieved. If not found, this returns nil.
// The comparision 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
}
// GetIssueWithName returns an IssueType with name from a given MetaProject. If not found, this returns nil.
// The comparision of the name is case insensitive
func (p *MetaProject) GetIssueTypeWithName(name string) *MetaIssueTypes {
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 frield 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 *MetaIssueTypes) 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 *MetaIssueTypes) 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
}

523
metaissue_test.go Normal file
View File

@ -0,0 +1,523 @@
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://tasks.trivago.com/rest/api/2/project/11300",
"id": "11300",
"key": "SOP",
"name": "DSE - Software Operations",
"avatarUrls": {
"48x48": "https://tasks.trivago.com/secure/projectavatar?pid=11300&avatarId=14405",
"24x24": "https://tasks.trivago.com/secure/projectavatar?size=small&pid=11300&avatarId=14405",
"16x16": "https://tasks.trivago.com/secure/projectavatar?size=xsmall&pid=11300&avatarId=14405",
"32x32": "https://tasks.trivago.com/secure/projectavatar?size=medium&pid=11300&avatarId=14405"
},
"issuetypes": [{
"self": "https://tasks.trivago.com/rest/api/2/issuetype/6",
"id": "6",
"description": "An issue which ideally should be able to be completed in one step",
"iconUrl": "https://tasks.trivago.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://tasks.trivago.com/rest/api/2/issuetype/6",
"id": "6",
"description": "An issue which ideally should be able to be completed in one step",
"iconUrl": "https://tasks.trivago.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://tasks.trivago.com/rest/api/2/component/14144",
"id": "14144",
"name": "Build automation",
"description": "Jenkins, webhooks, etc."
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14149",
"id": "14149",
"name": "Caches and noSQL",
"description": "Cassandra, Memcached, Redis, Twemproxy, Xcache"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14152",
"id": "14152",
"name": "Cloud services",
"description": "AWS and similiar services"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14147",
"id": "14147",
"name": "Code quality tools",
"description": "Code sniffer, Arqtig, Sonar"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14156",
"id": "14156",
"name": "Configuration management and provisioning",
"description": "Apache/PHP modules, Consul, Salt"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/13606",
"id": "13606",
"name": "Cronjobs",
"description": "Cronjobs in general"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14150",
"id": "14150",
"name": "Data pipelines and queues",
"description": "Gollum, Kafka, RabbitMq"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14159",
"id": "14159",
"name": "Database",
"description": "MySQL related problems"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14314",
"id": "14314",
"name": "Documentation"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14151",
"id": "14151",
"name": "Git",
"description": "Bitbucket, GitHub, GitLab, Git in general"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14155",
"id": "14155",
"name": "HTTP services",
"description": "CDN, HaProxy, HTTP, Varnish"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14154",
"id": "14154",
"name": "Job and service scheduling",
"description": "Chronos, Docker, Marathon, Mesos"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14158",
"id": "14158",
"name": "Legacy",
"description": "Everything related to legacy"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14157",
"id": "14157",
"name": "Monitoring",
"description": "Collectd, Nagios, Monitoring in general"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14148",
"id": "14148",
"name": "Other services"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/13602",
"id": "13602",
"name": "Package management",
"description": "Composer, Medusa, Satis"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14145",
"id": "14145",
"name": "Release",
"description": "Directory config, release queries, rewrite rules"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14146",
"id": "14146",
"name": "Staging systems and VMs",
"description": "Stage, QA machines, KVMs,Vagrant"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14153",
"id": "14153",
"name": "Techblog"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14143",
"id": "14143",
"name": "Test automation",
"description": "Testing infrastructure in general"
}, {
"self": "https://tasks.trivago.com/rest/api/2/component/14221",
"id": "14221",
"name": "Zup"
}]
},
"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://tasks.trivago.com/rest/api/2/project/11300",
"id": "11300",
"key": "SOP",
"name": "DSE - Software Operations",
"avatarUrls": {
"48x48": "https://tasks.trivago.com/secure/projectavatar?pid=11300&avatarId=14405",
"24x24": "https://tasks.trivago.com/secure/projectavatar?size=small&pid=11300&avatarId=14405",
"16x16": "https://tasks.trivago.com/secure/projectavatar?size=xsmall&pid=11300&avatarId=14405",
"32x32": "https://tasks.trivago.com/secure/projectavatar?size=medium&pid=11300&avatarId=14405"
},
"projectCategory": {
"self": "https://tasks.trivago.com/rest/api/2/projectCategory/10100",
"id": "10100",
"description": "",
"name": "Product & Development"
}
}]
},
"assignee": {
"required": true,
"schema": {
"type": "user",
"system": "assignee"
},
"name": "Assignee",
"autoCompleteUrl": "https://tasks.trivago.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://tasks.trivago.com/rest/api/2/priority/1",
"iconUrl": "https://tasks.trivago.com/images/icons/priorities/blocker.svg",
"name": "Immediate",
"id": "1"
}, {
"self": "https://tasks.trivago.com/rest/api/2/priority/2",
"iconUrl": "https://tasks.trivago.com/images/icons/priorities/critical.svg",
"name": "Urgent",
"id": "2"
}, {
"self": "https://tasks.trivago.com/rest/api/2/priority/3",
"iconUrl": "https://tasks.trivago.com/images/icons/priorities/major.svg",
"name": "High",
"id": "3"
}, {
"self": "https://tasks.trivago.com/rest/api/2/priority/6",
"iconUrl": "https://tasks.trivago.com/images/icons/priorities/moderate.svg",
"name": "Moderate",
"id": "6"
}, {
"self": "https://tasks.trivago.com/rest/api/2/priority/4",
"iconUrl": "https://tasks.trivago.com/images/icons/priorities/minor.svg",
"name": "Normal",
"id": "4"
}, {
"self": "https://tasks.trivago.com/rest/api/2/priority/5",
"iconUrl": "https://tasks.trivago.com/images/icons/priorities/trivial.svg",
"name": "Low",
"id": "5"
}]
},
"labels": {
"required": false,
"schema": {
"type": "array",
"items": "string",
"system": "labels"
},
"name": "Labels",
"autoCompleteUrl": "https://tasks.trivago.com/rest/api/1.0/labels/suggest?query=",
"hasDefaultValue": false,
"operations": [
"add",
"set",
"remove"
]
}
}
}]
}]
}`)
})
issue, _, err := testClient.Issue.GetCreateMeta("SOP")
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 TestMetaIssueTypes_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(MetaIssueTypes)
m.Fields = data
mandatory, err := m.GetMandatoryFields()
if err != nil {
t.Errorf("Expected nil error, recieved %s", err)
}
if len(mandatory) != 2 {
t.Errorf("Expected 2 recieved %d", mandatory)
}
}
func TestMetaIssueTypes_GetMandatoryFields_NonExistentRequiredKey_Fail(t *testing.T) {
data := make(map[string]interface{})
data["summary"] = map[string]interface{}{
"name": "Summary",
}
m := new(MetaIssueTypes)
m.Fields = data
_, err := m.GetMandatoryFields()
if err == nil {
t.Error("Expected non nil errpr, recieved nil")
}
}
func TestMetaIssueTypes_GetMandatoryFields_NonExistentNameKey_Fail(t *testing.T) {
data := make(map[string]interface{})
data["summary"] = map[string]interface{}{
"required": true,
}
m := new(MetaIssueTypes)
m.Fields = data
_, err := m.GetMandatoryFields()
if err == nil {
t.Error("Expected non nil errpr, recieved nil")
}
}
func TestMetaIssueTypes_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(MetaIssueTypes)
m.Fields = data
mandatory, err := m.GetAllFields()
if err != nil {
t.Errorf("Expected nil err, recieved %s", err)
}
if len(mandatory) != 3 {
t.Errorf("Expected 3 recieved %d", mandatory)
}
}
func TestMetaIssueTypes_GetAllFields_NonExistingNameKey_Fail(t *testing.T) {
data := make(map[string]interface{})
data["summary"] = map[string]interface{}{
"required": true,
}
m := new(MetaIssueTypes)
m.Fields = data
_, err := m.GetAllFields()
if err == nil {
t.Error("Expected non nil error, recieved nil")
}
}
func TestCreateMetaInfo_GetProjectName_Success(t *testing.T) {
metainfo := new(CreateMetaInfo)
metainfo.Projects = append(metainfo.Projects, &MetaProject{
Name: "SOP",
})
project := metainfo.GetProjectWithName("SOP")
if project == nil {
t.Errorf("Expected non nil value, recieved nil")
return
}
}
func TestMetaProject_GetIssueTypeWithName_CaseMismatch_Success(t *testing.T) {
m := new(MetaProject)
m.IssueTypes = append(m.IssueTypes, &MetaIssueTypes{
Name: "Bug",
})
issuetype := m.GetIssueTypeWithName("BUG")
if issuetype == nil {
t.Errorf("Expected non nil value, recieved nil")
return
}
}

View File

@ -13,72 +13,72 @@ type ProjectService struct {
// ProjectList represent a list of Projects
type ProjectList []struct {
Expand string `json:"expand"`
Self string `json:"self"`
ID string `json:"id"`
Key string `json:"key"`
Name string `json:"name"`
AvatarUrls AvatarUrls `json:"avatarUrls"`
ProjectTypeKey string `json:"projectTypeKey"`
ProjectCategory ProjectCategory `json:"projectCategory,omitempty"`
Expand string `json:"expand" structs:"expand"`
Self string `json:"self" structs:"self"`
ID string `json:"id" structs:"id"`
Key string `json:"key" structs:"key"`
Name string `json:"name" structs:"name"`
AvatarUrls AvatarUrls `json:"avatarUrls" structs:"avatarUrls"`
ProjectTypeKey string `json:"projectTypeKey" structs:"projectTypeKey"`
ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectsCategory,omitempty"`
}
// ProjectCategory represents a single project category
type ProjectCategory struct {
Self string `json:"self"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Self string `json:"self" structs:"self,omitempty"`
ID string `json:"id" structs:"id,omitempty"`
Name string `json:"name" structs:"name,omitempty"`
Description string `json:"description" structs:"description,omitempty"`
}
// Project represents a JIRA Project.
type Project struct {
Expand string `json:"expand,omitempty"`
Self string `json:"self,omitempty"`
ID string `json:"id,omitempty"`
Key string `json:"key,omitempty"`
Description string `json:"description,omitempty"`
Lead User `json:"lead,omitempty"`
Components []ProjectComponent `json:"components,omitempty"`
IssueTypes []IssueType `json:"issueTypes,omitempty"`
URL string `json:"url,omitempty"`
Email string `json:"email,omitempty"`
AssigneeType string `json:"assigneeType,omitempty"`
Versions []Version `json:"versions,omitempty"`
Name string `json:"name,omitempty"`
Expand string `json:"expand,omitempty" structs:"expand,omitempty"`
Self string `json:"self,omitempty" structs:"self,omitempty"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
Key string `json:"key,omitempty" structs:"key,omitempty"`
Description string `json:"description,omitempty" structs:"description,omitempty"`
Lead User `json:"lead,omitempty" structs:"lead,omitempty"`
Components []ProjectComponent `json:"components,omitempty" structs:"components,omitempty"`
IssueTypes []IssueType `json:"issueTypes,omitempty" structs:"issueTypes,omitempty"`
URL string `json:"url,omitempty" structs:"url,omitempty"`
Email string `json:"email,omitempty" structs:"email,omitempty"`
AssigneeType string `json:"assigneeType,omitempty" structs:"assigneeType,omitempty"`
Versions []Version `json:"versions,omitempty" structs:"versions,omitempty"`
Name string `json:"name,omitempty" structs:"name,omitempty"`
Roles struct {
Developers string `json:"Developers,omitempty"`
} `json:"roles,omitempty"`
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"`
ProjectCategory ProjectCategory `json:"projectCategory,omitempty"`
Developers string `json:"Developers,omitempty" structs:"Developers,omitempty"`
} `json:"roles,omitempty" structs:"roles,omitempty"`
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"`
ProjectCategory ProjectCategory `json:"projectCategory,omitempty" structs:"projectCategory,omitempty"`
}
// Version represents a single release version of a project
type Version struct {
Self string `json:"self"`
ID string `json:"id"`
Name string `json:"name"`
Archived bool `json:"archived"`
Released bool `json:"released"`
ReleaseDate string `json:"releaseDate"`
UserReleaseDate string `json:"userReleaseDate"`
ProjectID int `json:"projectId"` // Unlike other IDs, this is returned as a number
Self string `json:"self" structs:"self,omitempty"`
ID string `json:"id" structs:"id,omitempty"`
Name string `json:"name" structs:"name,omitempty"`
Archived bool `json:"archived" structs:"archived,omitempty"`
Released bool `json:"released" structs:"released,omitempty"`
ReleaseDate string `json:"releaseDate" structs:"releaseDate,omitempty"`
UserReleaseDate string `json:"userReleaseDate" structs:"userReleaseDate,omitempty"`
ProjectID int `json:"projectId" structs:"projectId,omitempty"` // Unlike other IDs, this is returned as a number
}
// ProjectComponent represents a single component of a project
type ProjectComponent struct {
Self string `json:"self"`
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Lead User `json:"lead"`
AssigneeType string `json:"assigneeType"`
Assignee User `json:"assignee"`
RealAssigneeType string `json:"realAssigneeType"`
RealAssignee User `json:"realAssignee"`
IsAssigneeTypeValid bool `json:"isAssigneeTypeValid"`
Project string `json:"project"`
ProjectID int `json:"projectId"`
Self string `json:"self" structs:"self,omitempty"`
ID string `json:"id" structs:"id,omitempty"`
Name string `json:"name" structs:"name,omitempty"`
Description string `json:"description" structs:"description,omitempty"`
Lead User `json:"lead,omitempty" structs:"lead,omitempty"`
AssigneeType string `json:"assigneeType" structs:"assigneeType,omitempty"`
Assignee User `json:"assignee" structs:"assignee,omitempty"`
RealAssigneeType string `json:"realAssigneeType" structs:"realAssigneeType,omitempty"`
RealAssignee User `json:"realAssignee" structs:"realAssignee,omitempty"`
IsAssigneeTypeValid bool `json:"isAssigneeTypeValid" structs:"isAssigneeTypeValid,omitempty"`
Project string `json:"project" structs:"project,omitempty"`
ProjectID int `json:"projectId" structs:"projectId,omitempty"`
}
// GetList gets all projects form JIRA