Skip to content

Commit

Permalink
Merge branch 'andygrunwald:master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
CondensedTea authored Feb 19, 2022
2 parents d1a0acc + 8413325 commit aa37f2b
Show file tree
Hide file tree
Showing 13 changed files with 297 additions and 21 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ package main

import (
"fmt"
"github.com/andygrunwald/go-jira"
jira "github.com/andygrunwald/go-jira"
)

func main() {
Expand Down
2 changes: 1 addition & 1 deletion authentication_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ func TestAuthenticationService_Authenticated_WithBasicAuthButNoUsername(t *testi
}
}

func TestAithenticationService_GetUserInfo_AccessForbidden_Fail(t *testing.T) {
func TestAuthenticationService_GetUserInfo_AccessForbidden_Fail(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/auth/1/session", func(w http.ResponseWriter, r *http.Request) {
Expand Down
2 changes: 2 additions & 0 deletions board.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ type BoardConfigurationColumnConfig struct {
type BoardConfigurationColumn struct {
Name string `json:"name"`
Status []BoardConfigurationColumnStatus `json:"statuses"`
Min int `json:"min,omitempty"`
Max int `json:"max,omitempty"`
}

// BoardConfigurationColumnStatus represents a status in the column configuration
Expand Down
15 changes: 15 additions & 0 deletions board_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,19 @@ func TestBoardService_GetBoardConfigoration(t *testing.T) {
t.Errorf("Expected 6 columns. go %d", len(boardConfiguration.ColumnConfig.Columns))
}

backlogColumn := boardConfiguration.ColumnConfig.Columns[0]
if backlogColumn.Min != 5 {
t.Errorf("Expected a min of 5 issues in backlog. Got %d", backlogColumn.Min)
}
if backlogColumn.Max != 30 {
t.Errorf("Expected a max of 30 issues in backlog. Got %d", backlogColumn.Max)
}

inProgressColumn := boardConfiguration.ColumnConfig.Columns[2]
if inProgressColumn.Min != 0 {
t.Errorf("Expected a min of 0 issues in progress. Got %d", inProgressColumn.Min)
}
if inProgressColumn.Max != 0 {
t.Errorf("Expected a max of 0 issues in progress. Got %d", inProgressColumn.Max)
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ go 1.15

require (
github.com/fatih/structs v1.1.0
github.com/golang-jwt/jwt/v4 v4.1.0
github.com/google/go-cmp v0.5.6
github.com/golang-jwt/jwt/v4 v4.3.0
github.com/google/go-cmp v0.5.7
github.com/google/go-querystring v1.1.0
github.com/pkg/errors v0.9.1
github.com/trivago/tgo v1.0.7
Expand Down
8 changes: 4 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/golang-jwt/jwt/v4 v4.1.0 h1:XUgk2Ex5veyVFVeLm0xhusUTQybEbexJXrvPNOKkSY0=
github.com/golang-jwt/jwt/v4 v4.1.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/golang-jwt/jwt/v4 v4.3.0 h1:kHL1vqdqWNfATmA0FNMdmZNMyZI1U6O31X4rlIPoBog=
github.com/golang-jwt/jwt/v4 v4.3.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
Expand Down
28 changes: 26 additions & 2 deletions issue.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,8 +283,8 @@ type Progress struct {

// Parent represents the parent of a Jira issue, to be used with subtask issue types.
type Parent struct {
ID string `json:"id,omitempty" structs:"id"`
Key string `json:"key,omitempty" structs:"key"`
ID string `json:"id,omitempty" structs:"id,omitempty"`
Key string `json:"key,omitempty" structs:"key,omitempty"`
}

// Time represents the Time definition of Jira as a time.Time of go
Expand Down Expand Up @@ -1547,3 +1547,27 @@ func (s *IssueService) AddRemoteLinkWithContext(ctx context.Context, issueID str
func (s *IssueService) AddRemoteLink(issueID string, remotelink *RemoteLink) (*RemoteLink, *Response, error) {
return s.AddRemoteLinkWithContext(context.Background(), issueID, remotelink)
}

// UpdateRemoteLinkWithContext updates a remote issue link by linkID.
//
// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v2/api-group-issue-remote-links/#api-rest-api-2-issue-issueidorkey-remotelink-linkid-put
func (s *IssueService) UpdateRemoteLinkWithContext(ctx context.Context, issueID string, linkID int, remotelink *RemoteLink) (*Response, error) {
apiEndpoint := fmt.Sprintf("rest/api/2/issue/%s/remotelink/%d", issueID, linkID)
req, err := s.client.NewRequestWithContext(ctx, "PUT", apiEndpoint, remotelink)
if err != nil {
return nil, err
}

resp, err := s.client.Do(req, nil)
if err != nil {
jerr := NewJiraError(resp, err)
return resp, jerr
}

return resp, nil
}

// UpdateRemoteLink wraps UpdateRemoteLinkWithContext using the background context.
func (s *IssueService) UpdateRemoteLink(issueID string, linkID int, remotelink *RemoteLink) (*Response, error) {
return s.UpdateRemoteLinkWithContext(context.Background(), issueID, linkID, remotelink)
}
48 changes: 47 additions & 1 deletion issue_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,7 @@ func TestIssueFields_MarshalJSON_OmitsEmptyFields(t *testing.T) {
Name: "Story",
},
Labels: []string{"aws-docker"},
Parent: &Parent{Key: "FOO-300"},
}

rawdata, err := json.Marshal(i)
Expand All @@ -1050,6 +1051,12 @@ func TestIssueFields_MarshalJSON_OmitsEmptyFields(t *testing.T) {
t.Error("Expected non nil error, received nil")
}

// verify Parent nil values are being omitted
_, err = issuef.String("parent/id")
if err == nil {
t.Error("Expected non nil err, received nil")
}

// verify that the field that should be there, is.
name, err := issuef.String("issuetype/name")
if err != nil {
Expand All @@ -1059,7 +1066,6 @@ func TestIssueFields_MarshalJSON_OmitsEmptyFields(t *testing.T) {
if name != "Story" {
t.Errorf("Expected Story, received %s", name)
}

}

func TestIssueFields_MarshalJSON_Success(t *testing.T) {
Expand Down Expand Up @@ -1855,6 +1861,46 @@ func TestIssueService_AddRemoteLink(t *testing.T) {
}
}

func TestIssueService_UpdateRemoteLink(t *testing.T) {
setup()
defer teardown()
testMux.HandleFunc("/rest/api/2/issue/100/remotelink/200", func(w http.ResponseWriter, r *http.Request) {
testMethod(t, r, "PUT")
testRequestURL(t, r, "/rest/api/2/issue/100/remotelink/200")

w.WriteHeader(http.StatusNoContent)
})
r := &RemoteLink{
Application: &RemoteLinkApplication{
Name: "My Acme Tracker",
Type: "com.acme.tracker",
},
GlobalID: "system=http://www.mycompany.com/support&id=1",
Relationship: "causes",
Object: &RemoteLinkObject{
Summary: "Customer support issue",
Icon: &RemoteLinkIcon{
Url16x16: "http://www.mycompany.com/support/ticket.png",
Title: "Support Ticket",
},
Title: "TSTSUP-111",
URL: "http://www.mycompany.com/support?id=1",
Status: &RemoteLinkStatus{
Icon: &RemoteLinkIcon{
Url16x16: "http://www.mycompany.com/support/resolved.png",
Title: "Case Closed",
Link: "http://www.mycompany.com/support?id=1&details=closed",
},
Resolved: true,
},
},
}
_, err := testClient.Issue.UpdateRemoteLink("100", 200, r)
if err != nil {
t.Errorf("Error given: %s", err)
}
}

func TestTime_MarshalJSON(t *testing.T) {
timeFormatParseFrom := "2006-01-02T15:04:05.999Z"
testCases := []struct {
Expand Down
71 changes: 71 additions & 0 deletions jira.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,77 @@ func (t *BasicAuthTransport) transport() http.RoundTripper {
return http.DefaultTransport
}

// BearerAuthTransport is a http.RoundTripper that authenticates all requests
// using Jira's bearer (oauth 2.0 (3lo)) based authentication.
type BearerAuthTransport struct {
Token string

// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http.RoundTripper
}

// RoundTrip implements the RoundTripper interface. We just add the
// bearer token and return the RoundTripper for this transport type.
func (t *BearerAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req2 := cloneRequest(req) // per RoundTripper contract

req2.Header.Set("Authorization", fmt.Sprintf("Bearer %s", t.Token))
return t.transport().RoundTrip(req2)
}

// Client returns an *http.Client that makes requests that are authenticated
// using HTTP Basic Authentication. This is a nice little bit of sugar
// so we can just get the client instead of creating the client in the calling code.
// If it's necessary to send more information on client init, the calling code can
// always skip this and set the transport itself.
func (t *BearerAuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}

func (t *BearerAuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}

// PATAuthTransport is an http.RoundTripper that authenticates all requests
// using the Personal Access Token specified.
// See here for more info: https://confluence.atlassian.com/enterprise/using-personal-access-tokens-1026032365.html
type PATAuthTransport struct {
// Token is the key that was provided by Jira when creating the Personal Access Token.
Token string

// Transport is the underlying HTTP transport to use when making requests.
// It will default to http.DefaultTransport if nil.
Transport http.RoundTripper
}

// RoundTrip implements the RoundTripper interface. We just add the
// basic auth and return the RoundTripper for this transport type.
func (t *PATAuthTransport) RoundTrip(req *http.Request) (*http.Response, error) {
req2 := cloneRequest(req) // per RoundTripper contract
req2.Header.Set("Authorization", "Bearer "+t.Token)
return t.transport().RoundTrip(req2)
}

// Client returns an *http.Client that makes requests that are authenticated
// using HTTP Basic Authentication. This is a nice little bit of sugar
// so we can just get the client instead of creating the client in the calling code.
// If it's necessary to send more information on client init, the calling code can
// always skip this and set the transport itself.
func (t *PATAuthTransport) Client() *http.Client {
return &http.Client{Transport: t}
}

func (t *PATAuthTransport) transport() http.RoundTripper {
if t.Transport != nil {
return t.Transport
}
return http.DefaultTransport
}

// CookieAuthTransport is an http.RoundTripper that authenticates all requests
// using Jira's cookie-based authentication.
//
Expand Down
23 changes: 23 additions & 0 deletions jira_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -631,3 +631,26 @@ func TestJWTAuthTransport_HeaderContainsJWT(t *testing.T) {
jwtClient, _ := NewClient(jwtTransport.Client(), testServer.URL)
jwtClient.Issue.Get("TEST-1", nil)
}

func TestPATAuthTransport_HeaderContainsAuth(t *testing.T) {
setup()
defer teardown()

token := "shhh, it's a token"

patTransport := &PATAuthTransport{
Token: token,
}

testMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
val := r.Header.Get("Authorization")
expected := "Bearer " + token
if val != expected {
t.Errorf("request does not contain bearer token in the Authorization header.")
}
})

client, _ := NewClient(patTransport.Client(), testServer.URL)
client.User.GetSelf()

}
4 changes: 3 additions & 1 deletion mocks/board_configuration.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
"id": "10005",
"self": "https://test.jira.org/rest/api/2/status/10005"
}
]
],
"min": 5,
"max": 30
},
{
"name": "Selected for development",
Expand Down
18 changes: 9 additions & 9 deletions servicedesk.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ type ServiceDeskOrganizationDTO struct {
// all organizations associated with a service desk.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-get
func (s *ServiceDeskService) GetOrganizationsWithContext(ctx context.Context, serviceDeskID int, start int, limit int, accountID string) (*PagedDTO, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%d/organization?start=%d&limit=%d", serviceDeskID, start, limit)
func (s *ServiceDeskService) GetOrganizationsWithContext(ctx context.Context, serviceDeskID interface{}, start int, limit int, accountID string) (*PagedDTO, *Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/organization?start=%d&limit=%d", serviceDeskID, start, limit)
if accountID != "" {
apiEndPoint += fmt.Sprintf("&accountId=%s", accountID)
}
Expand All @@ -43,7 +43,7 @@ func (s *ServiceDeskService) GetOrganizationsWithContext(ctx context.Context, se
}

// GetOrganizations wraps GetOrganizationsWithContext using the background context.
func (s *ServiceDeskService) GetOrganizations(serviceDeskID int, start int, limit int, accountID string) (*PagedDTO, *Response, error) {
func (s *ServiceDeskService) GetOrganizations(serviceDeskID interface{}, start int, limit int, accountID string) (*PagedDTO, *Response, error) {
return s.GetOrganizationsWithContext(context.Background(), serviceDeskID, start, limit, accountID)
}

Expand All @@ -53,8 +53,8 @@ func (s *ServiceDeskService) GetOrganizations(serviceDeskID int, start int, limi
// and the resource returns a 204 success code.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-post
func (s *ServiceDeskService) AddOrganizationWithContext(ctx context.Context, serviceDeskID int, organizationID int) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%d/organization", serviceDeskID)
func (s *ServiceDeskService) AddOrganizationWithContext(ctx context.Context, serviceDeskID interface{}, organizationID int) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/organization", serviceDeskID)

organization := ServiceDeskOrganizationDTO{
OrganizationID: organizationID,
Expand All @@ -76,7 +76,7 @@ func (s *ServiceDeskService) AddOrganizationWithContext(ctx context.Context, ser
}

// AddOrganization wraps AddOrganizationWithContext using the background context.
func (s *ServiceDeskService) AddOrganization(serviceDeskID int, organizationID int) (*Response, error) {
func (s *ServiceDeskService) AddOrganization(serviceDeskID interface{}, organizationID int) (*Response, error) {
return s.AddOrganizationWithContext(context.Background(), serviceDeskID, organizationID)
}

Expand All @@ -86,8 +86,8 @@ func (s *ServiceDeskService) AddOrganization(serviceDeskID int, organizationID i
// no change is made and the resource returns a 204 success code.
//
// https://developer.atlassian.com/cloud/jira/service-desk/rest/api-group-organization/#api-rest-servicedeskapi-servicedesk-servicedeskid-organization-delete
func (s *ServiceDeskService) RemoveOrganizationWithContext(ctx context.Context, serviceDeskID int, organizationID int) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%d/organization", serviceDeskID)
func (s *ServiceDeskService) RemoveOrganizationWithContext(ctx context.Context, serviceDeskID interface{}, organizationID int) (*Response, error) {
apiEndPoint := fmt.Sprintf("rest/servicedeskapi/servicedesk/%v/organization", serviceDeskID)

organization := ServiceDeskOrganizationDTO{
OrganizationID: organizationID,
Expand All @@ -109,6 +109,6 @@ func (s *ServiceDeskService) RemoveOrganizationWithContext(ctx context.Context,
}

// RemoveOrganization wraps RemoveOrganizationWithContext using the background context.
func (s *ServiceDeskService) RemoveOrganization(serviceDeskID int, organizationID int) (*Response, error) {
func (s *ServiceDeskService) RemoveOrganization(serviceDeskID interface{}, organizationID int) (*Response, error) {
return s.RemoveOrganizationWithContext(context.Background(), serviceDeskID, organizationID)
}
Loading

0 comments on commit aa37f2b

Please sign in to comment.