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 package jira
import ( import (
"encoding/json"
"fmt" "fmt"
"io/ioutil"
"net/http" "net/http"
) )
@ -54,9 +56,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)
@ -78,7 +80,76 @@ func (s *AuthenticationService) Authenticated() bool {
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 == 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" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"reflect"
"testing" "testing"
) )
@ -84,3 +85,195 @@ func TestAuthenticationService_Authenticated(t *testing.T) {
t.Error("Expected false, but result was true") 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 // 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.

369
issue.go
View File

@ -2,10 +2,14 @@ package jira
import ( import (
"bytes" "bytes"
"encoding/json"
"fmt" "fmt"
"github.com/fatih/structs"
"github.com/trivago/tgo/tcontainer"
"io" "io"
"mime/multipart" "mime/multipart"
"net/url" "net/url"
"reflect"
"strings" "strings"
"time" "time"
) )
@ -24,35 +28,35 @@ 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"`
} }
// 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.
@ -70,124 +74,185 @@ type IssueFields struct {
// * "aggregatetimeestimate": null, // * "aggregatetimeestimate": null,
// * "environment": null, // * "environment": null,
// * "duedate": null, // * "duedate": null,
Type IssueType `json:"issuetype"` Type IssueType `json:"issuetype" structs:"issuetype"`
Project Project `json:"project,omitempty"` Project Project `json:"project,omitempty" structs:"project,omitempty"`
Resolution *Resolution `json:"resolution,omitempty"` Resolution *Resolution `json:"resolution,omitempty" structs:"resolution,omitempty"`
Priority *Priority `json:"priority,omitempty"` Priority *Priority `json:"priority,omitempty" structs:"priority,omitempty"`
Resolutiondate string `json:"resolutiondate,omitempty"` Resolutiondate string `json:"resolutiondate,omitempty" structs:"resolutiondate,omitempty"`
Created string `json:"created,omitempty"` Created string `json:"created,omitempty" structs:"created,omitempty"`
Watches *Watches `json:"watches,omitempty"` Watches *Watches `json:"watches,omitempty" structs:"watches,omitempty"`
Assignee *User `json:"assignee,omitempty"` Assignee *User `json:"assignee,omitempty" structs:"assignee,omitempty"`
Updated string `json:"updated,omitempty"` Updated string `json:"updated,omitempty" structs:"updated,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty" structs:"description,omitempty"`
Summary string `json:"summary"` Summary string `json:"summary" structs:"summary"`
Creator *User `json:"Creator,omitempty"` Creator *User `json:"Creator,omitempty" structs:"Creator,omitempty"`
Reporter *User `json:"reporter,omitempty"` Reporter *User `json:"reporter,omitempty" structs:"reporter,omitempty"`
Components []*Component `json:"components,omitempty"` Components []*Component `json:"components,omitempty" structs:"components,omitempty"`
Status *Status `json:"status,omitempty"` Status *Status `json:"status,omitempty" structs:"status,omitempty"`
Progress *Progress `json:"progress,omitempty"` Progress *Progress `json:"progress,omitempty" structs:"progress,omitempty"`
AggregateProgress *Progress `json:"aggregateprogress,omitempty"` AggregateProgress *Progress `json:"aggregateprogress,omitempty" structs:"aggregateprogress,omitempty"`
Worklog *Worklog `json:"worklog,omitempty"` Worklog *Worklog `json:"worklog,omitempty" structs:"worklog,omitempty"`
IssueLinks []*IssueLink `json:"issuelinks,omitempty"` IssueLinks []*IssueLink `json:"issuelinks,omitempty" structs:"issuelinks,omitempty"`
Comments *Comments `json:"comment,omitempty"` Comments *Comments `json:"comment,omitempty" structs:"comment,omitempty"`
FixVersions []*FixVersion `json:"fixVersions,omitempty"` FixVersions []*FixVersion `json:"fixVersions,omitempty" structs:"fixVersions,omitempty"`
Labels []string `json:"labels,omitempty"` Labels []string `json:"labels,omitempty" structs:"labels,omitempty"`
Subtasks []*Subtasks `json:"subtasks,omitempty"` Subtasks []*Subtasks `json:"subtasks,omitempty" structs:"subtasks,omitempty"`
Attachments []*Attachment `json:"attachment,omitempty"` Attachments []*Attachment `json:"attachment,omitempty" structs:"attachment,omitempty"`
Epic *Epic `json:"epic,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. // 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" struct:"id,omitempty"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty" struct:"description,omitempty"`
IconURL string `json:"iconUrl,omitempty"` IconURL string `json:"iconUrl,omitempty" struct:"iconUrl,omitempty"`
Name string `json:"name,omitempty"` Name string `json:"name,omitempty" struct:"name,omitempty"`
Subtask bool `json:"subtask,omitempty"` Subtask bool `json:"subtask,omitempty" struct:"subtask,omitempty"`
AvatarID int `json:"avatarId,omitempty"` AvatarID int `json:"avatarId,omitempty" struct:"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. // User represents a user who is this JIRA issue assigned to.
type User struct { type User struct {
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"`
Key string `json:"key,omitempty"` Key string `json:"key,omitempty" structs:"key,omitempty"`
EmailAddress string `json:"emailAddress,omitempty"` EmailAddress string `json:"emailAddress,omitempty" structs:"emailAddress,omitempty"`
AvatarUrls AvatarUrls `json:"avatarUrls,omitempty"` AvatarUrls AvatarUrls `json:"avatarUrls,omitempty" structs:"avatarUrls,omitempty"`
DisplayName string `json:"displayName,omitempty"` DisplayName string `json:"displayName,omitempty" structs:"displayName,omitempty"`
Active bool `json:"active,omitempty"` Active bool `json:"active,omitempty" structs:"active,omitempty"`
TimeZone string `json:"timeZone,omitempty"` TimeZone string `json:"timeZone,omitempty" structs:"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"`
} }
// 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 +260,29 @@ 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 Transistion
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 Transistion calls like DoTransition
type TransitionPayload struct { type TransitionPayload struct {
ID string `json:"id"` ID string `json:"id" structs:"id"`
} }
// UnmarshalJSON will transform the JIRA time into a time.Time // 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 // 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"`
} }
// 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
@ -337,10 +402,10 @@ type SearchOptions struct {
// searchResult is only a small wrapper arround the Search (with JQL) method // searchResult is only a small wrapper arround 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"`
} }
// CustomFields represents custom fields of JIRA // CustomFields represents custom fields of JIRA

View File

@ -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) {
@ -487,3 +489,128 @@ func TestIssueService_DoTransition(t *testing.T) {
t.Errorf("Got error: %v", err) 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 // 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