From 689849ac00493b66132f1b695dc8f1ac1d85405e Mon Sep 17 00:00:00 2001 From: Vladimir Chalupecky Date: Fri, 25 Nov 2022 14:48:08 +0100 Subject: [PATCH 1/2] cloud,onprem: return JiraError from IssueService.Create --- cloud/issue.go | 4 ++-- onpremise/issue.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cloud/issue.go b/cloud/issue.go index e7db3c73..050a8bcb 100644 --- a/cloud/issue.go +++ b/cloud/issue.go @@ -815,8 +815,8 @@ func (s *IssueService) Create(ctx context.Context, issue *Issue) (*Issue, *Respo resp, err := s.client.Do(req, nil) if err != nil { - // incase of error return the resp for further inspection - return nil, resp, err + jerr := NewJiraError(resp, err) + return nil, resp, jerr } defer resp.Body.Close() diff --git a/onpremise/issue.go b/onpremise/issue.go index d1bca7aa..f2e7a6e8 100644 --- a/onpremise/issue.go +++ b/onpremise/issue.go @@ -815,8 +815,8 @@ func (s *IssueService) Create(ctx context.Context, issue *Issue) (*Issue, *Respo resp, err := s.client.Do(req, nil) if err != nil { - // incase of error return the resp for further inspection - return nil, resp, err + jerr := NewJiraError(resp, err) + return nil, resp, jerr } defer resp.Body.Close() From 7d9c08da2165565344113d1fd3aa4c8ee0fa3580 Mon Sep 17 00:00:00 2001 From: Vladimir Chalupecky Date: Fri, 25 Nov 2022 15:01:40 +0100 Subject: [PATCH 2/2] onprem,cloud: add query params to Issue.Create --- cloud/examples/create/main.go | 2 +- cloud/examples/createwithcustomfields/main.go | 4 ++- cloud/issue.go | 31 ++++++++++++++----- cloud/issue_test.go | 10 ++++-- onpremise/examples/create/main.go | 2 +- .../examples/createwithcustomfields/main.go | 4 ++- onpremise/issue.go | 31 ++++++++++++++----- onpremise/issue_test.go | 10 ++++-- 8 files changed, 70 insertions(+), 24 deletions(-) diff --git a/cloud/examples/create/main.go b/cloud/examples/create/main.go index df175331..7f6d7442 100644 --- a/cloud/examples/create/main.go +++ b/cloud/examples/create/main.go @@ -55,7 +55,7 @@ func main() { }, } - issue, _, err := client.Issue.Create(context.Background(), &i) + issue, _, err := client.Issue.Create(context.Background(), &i, nil) if err != nil { panic(err) } diff --git a/cloud/examples/createwithcustomfields/main.go b/cloud/examples/createwithcustomfields/main.go index 4c1b0354..02939064 100644 --- a/cloud/examples/createwithcustomfields/main.go +++ b/cloud/examples/createwithcustomfields/main.go @@ -66,7 +66,9 @@ func main() { }, } - issue, _, err := client.Issue.Create(context.Background(), &i) + issue, _, err := client.Issue.Create(context.Background(), &i, &jira.CreateIssueOptions{ + UpdateHistory: true, + }) if err != nil { panic(err) } diff --git a/cloud/issue.go b/cloud/issue.go index 050a8bcb..1e6a18e9 100644 --- a/cloud/issue.go +++ b/cloud/issue.go @@ -536,6 +536,12 @@ type searchResult struct { Total int `json:"total" structs:"total"` } +// CreateIssueOptions specifies the optional parameters for the Create Issue +// method. +type CreateIssueOptions struct { + UpdateHistory bool `url:"updateHistory,omitempty"` +} + // GetQueryOptions specifies the optional parameters for the Get Issue methods type GetQueryOptions struct { // Fields is the list of fields to return for the issue. By default, all fields are returned. @@ -798,17 +804,26 @@ func WithQueryOptions(options interface{}) func(*http.Request) error { } } -// Create creates an issue or a sub-task from a JSON representation. -// Creating a sub-task is similar to creating a regular issue, with two important differences: -// The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue. +// Create creates an issue or a sub-task. +// +// Creating a sub-task is similar to creating a regular issue, with two +// important differences: the issueType field must correspond to a sub-task +// issue type, and you must provide a parent field in the issue containing the +// ID or key of the parent issue. // -// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues +// Jira API docs: https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issues/#api-rest-api-3-issue-post // // TODO Double check this method if this works as expected, is using the latest API and the response is complete // This double check effort is done for v2 - Remove this two lines if this is completed. -func (s *IssueService) Create(ctx context.Context, issue *Issue) (*Issue, *Response, error) { +func (s *IssueService) Create(ctx context.Context, issue *Issue, opts *CreateIssueOptions) (*Issue, *Response, error) { apiEndpoint := "rest/api/2/issue" - req, err := s.client.NewRequest(ctx, http.MethodPost, apiEndpoint, issue) + + url, err := addOptions(apiEndpoint, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, http.MethodPost, url, issue) if err != nil { return nil, nil, err } @@ -820,13 +835,13 @@ func (s *IssueService) Create(ctx context.Context, issue *Issue) (*Issue, *Respo } defer resp.Body.Close() - responseIssue := new(Issue) + var responseIssue Issue err = json.NewDecoder(resp.Body).Decode(&responseIssue) if err != nil { return nil, resp, err } - return responseIssue, resp, nil + return &responseIssue, resp, nil } // Update updates an issue from a JSON representation, diff --git a/cloud/issue_test.go b/cloud/issue_test.go index a8ad6487..51344dab 100644 --- a/cloud/issue_test.go +++ b/cloud/issue_test.go @@ -63,6 +63,9 @@ func TestIssueService_Create(t *testing.T) { testMux.HandleFunc("/rest/api/2/issue", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) testRequestURL(t, r, "/rest/api/2/issue") + testRequestParams(t, r, map[string]string{ + "updateHistory": "true", + }) w.WriteHeader(http.StatusCreated) fmt.Fprint(w, `{"expand":"renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations","id":"10002","self":"http://www.example.com/jira/rest/api/2/issue/10002","key":"EX-1","fields":{"watcher":{"self":"http://www.example.com/jira/rest/api/2/issue/EX-1/watchers","isWatching":false,"watchCount":1,"watchers":[{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false}]},"attachment":[{"self":"http://www.example.com/jira/rest/api/2.0/attachments/10000","filename":"picture.jpg","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","avatarUrls":{"48x48":"http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred","24x24":"http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred","16x16":"http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred","32x32":"http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred"},"displayName":"Fred F. User","active":false},"created":"2016-03-16T04:22:37.461+0000","size":23123,"mimeType":"image/jpeg","content":"http://www.example.com/jira/attachments/10000","thumbnail":"http://www.example.com/jira/secure/thumbnail/10000"}],"sub-tasks":[{"id":"10000","type":{"id":"10000","name":"","inward":"Parent","outward":"Sub-task"},"outwardIssue":{"id":"10003","key":"EX-2","self":"http://www.example.com/jira/rest/api/2/issue/EX-2","fields":{"status":{"iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png","name":"Open"}}}}],"description":"example bug report","project":{"self":"http://www.example.com/jira/rest/api/2/project/EX","id":"10000","key":"EX","name":"Example","avatarUrls":{"48x48":"http://www.example.com/jira/secure/projectavatar?size=large&pid=10000","24x24":"http://www.example.com/jira/secure/projectavatar?size=small&pid=10000","16x16":"http://www.example.com/jira/secure/projectavatar?size=xsmall&pid=10000","32x32":"http://www.example.com/jira/secure/projectavatar?size=medium&pid=10000"},"projectCategory":{"self":"http://www.example.com/jira/rest/api/2/projectCategory/10000","id":"10000","name":"FIRST","description":"First Project Category"}},"comment":{"comments":[{"self":"http://www.example.com/jira/rest/api/2/issue/10010/comment/10000","id":"10000","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque eget venenatis elit. Duis eu justo eget augue iaculis fermentum. Sed semper quam laoreet nisi egestas at posuere augue semper.","updateAuthor":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"created":"2016-03-16T04:22:37.356+0000","updated":"2016-03-16T04:22:37.356+0000","visibility":{"type":"role","value":"Administrators"}}]},"issuelinks":[{"id":"10001","type":{"id":"10000","name":"Dependent","inward":"depends on","outward":"is depended by"},"outwardIssue":{"id":"10004L","key":"PRJ-2","self":"http://www.example.com/jira/rest/api/2/issue/PRJ-2","fields":{"status":{"iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png","name":"Open"}}}},{"id":"10002","type":{"id":"10000","name":"Dependent","inward":"depends on","outward":"is depended by"},"inwardIssue":{"id":"10004","key":"PRJ-3","self":"http://www.example.com/jira/rest/api/2/issue/PRJ-3","fields":{"status":{"iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png","name":"Open"}}}}],"worklog":{"worklogs":[{"self":"http://www.example.com/jira/rest/api/2/issue/10010/worklog/10000","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"updateAuthor":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"comment":"I did some work here.","updated":"2016-03-16T04:22:37.471+0000","visibility":{"type":"group","value":"jira-developers"},"started":"2016-03-16T04:22:37.471+0000","timeSpent":"3h 20m","timeSpentSeconds":12000,"id":"100028","issueId":"10002"}]},"updated":"2016-04-06T02:36:53.594-0700","duedate":"2018-01-19","timetracking":{"originalEstimate":"10m","remainingEstimate":"3m","timeSpent":"6m","originalEstimateSeconds":600,"remainingEstimateSeconds":200,"timeSpentSeconds":400}},"names":{"watcher":"watcher","attachment":"attachment","sub-tasks":"sub-tasks","description":"description","project":"project","comment":"comment","issuelinks":"issuelinks","worklog":"worklog","updated":"updated","timetracking":"timetracking"},"schema":{}}`) @@ -73,7 +76,7 @@ func TestIssueService_Create(t *testing.T) { Description: "example bug report", }, } - issue, _, err := testClient.Issue.Create(context.Background(), i) + issue, _, err := testClient.Issue.Create(context.Background(), i, &CreateIssueOptions{UpdateHistory: true}) if issue == nil { t.Error("Expected issue. Issue is nil") } @@ -88,6 +91,9 @@ func TestIssueService_CreateThenGet(t *testing.T) { testMux.HandleFunc("/rest/api/2/issue", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) testRequestURL(t, r, "/rest/api/2/issue") + testRequestParams(t, r, map[string]string{ + "updateHistory": "true", + }) w.WriteHeader(http.StatusCreated) io.Copy(w, r.Body) @@ -99,7 +105,7 @@ func TestIssueService_CreateThenGet(t *testing.T) { Created: Time(time.Now()), }, } - issue, _, err := testClient.Issue.Create(context.Background(), i) + issue, _, err := testClient.Issue.Create(context.Background(), i, &CreateIssueOptions{UpdateHistory: true}) if issue == nil { t.Error("Expected issue. Issue is nil") } diff --git a/onpremise/examples/create/main.go b/onpremise/examples/create/main.go index b99b7c8c..e193e381 100644 --- a/onpremise/examples/create/main.go +++ b/onpremise/examples/create/main.go @@ -55,7 +55,7 @@ func main() { }, } - issue, _, err := client.Issue.Create(context.Background(), &i) + issue, _, err := client.Issue.Create(context.Background(), &i, nil) if err != nil { panic(err) } diff --git a/onpremise/examples/createwithcustomfields/main.go b/onpremise/examples/createwithcustomfields/main.go index 2eb954cc..cf650fe7 100644 --- a/onpremise/examples/createwithcustomfields/main.go +++ b/onpremise/examples/createwithcustomfields/main.go @@ -66,7 +66,9 @@ func main() { }, } - issue, _, err := client.Issue.Create(context.Background(), &i) + issue, _, err := client.Issue.Create(context.Background(), &i, &jira.CreateIssueOptions{ + UpdateHistory: true, + }) if err != nil { panic(err) } diff --git a/onpremise/issue.go b/onpremise/issue.go index f2e7a6e8..65373cae 100644 --- a/onpremise/issue.go +++ b/onpremise/issue.go @@ -536,6 +536,12 @@ type searchResult struct { Total int `json:"total" structs:"total"` } +// CreateIssueOptions specifies the optional parameters for the Create Issue +// method. +type CreateIssueOptions struct { + UpdateHistory bool `url:"updateHistory,omitempty"` +} + // GetQueryOptions specifies the optional parameters for the Get Issue methods type GetQueryOptions struct { // Fields is the list of fields to return for the issue. By default, all fields are returned. @@ -798,17 +804,26 @@ func WithQueryOptions(options interface{}) func(*http.Request) error { } } -// Create creates an issue or a sub-task from a JSON representation. -// Creating a sub-task is similar to creating a regular issue, with two important differences: -// The issueType field must correspond to a sub-task issue type and you must provide a parent field in the issue create request containing the id or key of the parent issue. +// Create creates an issue or a sub-task. +// +// Creating a sub-task is similar to creating a regular issue, with two +// important differences: the issueType field must correspond to a sub-task +// issue type, and you must provide a parent field in the issue containing the +// ID or key of the parent issue. // -// Jira API docs: https://docs.atlassian.com/jira/REST/latest/#api/2/issue-createIssues +// Jira API docs: https://docs.atlassian.com/software/jira/docs/api/REST/9.4.0/#api/2/issue-createIssue // // TODO Double check this method if this works as expected, is using the latest API and the response is complete // This double check effort is done for v2 - Remove this two lines if this is completed. -func (s *IssueService) Create(ctx context.Context, issue *Issue) (*Issue, *Response, error) { +func (s *IssueService) Create(ctx context.Context, issue *Issue, opts *CreateIssueOptions) (*Issue, *Response, error) { apiEndpoint := "rest/api/2/issue" - req, err := s.client.NewRequest(ctx, http.MethodPost, apiEndpoint, issue) + + url, err := addOptions(apiEndpoint, opts) + if err != nil { + return nil, nil, err + } + + req, err := s.client.NewRequest(ctx, http.MethodPost, url, issue) if err != nil { return nil, nil, err } @@ -820,13 +835,13 @@ func (s *IssueService) Create(ctx context.Context, issue *Issue) (*Issue, *Respo } defer resp.Body.Close() - responseIssue := new(Issue) + var responseIssue Issue err = json.NewDecoder(resp.Body).Decode(&responseIssue) if err != nil { return nil, resp, err } - return responseIssue, resp, nil + return &responseIssue, resp, nil } // Update updates an issue from a JSON representation, diff --git a/onpremise/issue_test.go b/onpremise/issue_test.go index 36b6aa31..df398f46 100644 --- a/onpremise/issue_test.go +++ b/onpremise/issue_test.go @@ -63,6 +63,9 @@ func TestIssueService_Create(t *testing.T) { testMux.HandleFunc("/rest/api/2/issue", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) testRequestURL(t, r, "/rest/api/2/issue") + testRequestParams(t, r, map[string]string{ + "updateHistory": "true", + }) w.WriteHeader(http.StatusCreated) fmt.Fprint(w, `{"expand":"renderedFields,names,schema,transitions,operations,editmeta,changelog,versionedRepresentations","id":"10002","self":"http://www.example.com/jira/rest/api/2/issue/10002","key":"EX-1","fields":{"watcher":{"self":"http://www.example.com/jira/rest/api/2/issue/EX-1/watchers","isWatching":false,"watchCount":1,"watchers":[{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false}]},"attachment":[{"self":"http://www.example.com/jira/rest/api/2.0/attachments/10000","filename":"picture.jpg","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","avatarUrls":{"48x48":"http://www.example.com/jira/secure/useravatar?size=large&ownerId=fred","24x24":"http://www.example.com/jira/secure/useravatar?size=small&ownerId=fred","16x16":"http://www.example.com/jira/secure/useravatar?size=xsmall&ownerId=fred","32x32":"http://www.example.com/jira/secure/useravatar?size=medium&ownerId=fred"},"displayName":"Fred F. User","active":false},"created":"2016-03-16T04:22:37.461+0000","size":23123,"mimeType":"image/jpeg","content":"http://www.example.com/jira/attachments/10000","thumbnail":"http://www.example.com/jira/secure/thumbnail/10000"}],"sub-tasks":[{"id":"10000","type":{"id":"10000","name":"","inward":"Parent","outward":"Sub-task"},"outwardIssue":{"id":"10003","key":"EX-2","self":"http://www.example.com/jira/rest/api/2/issue/EX-2","fields":{"status":{"iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png","name":"Open"}}}}],"description":"example bug report","project":{"self":"http://www.example.com/jira/rest/api/2/project/EX","id":"10000","key":"EX","name":"Example","avatarUrls":{"48x48":"http://www.example.com/jira/secure/projectavatar?size=large&pid=10000","24x24":"http://www.example.com/jira/secure/projectavatar?size=small&pid=10000","16x16":"http://www.example.com/jira/secure/projectavatar?size=xsmall&pid=10000","32x32":"http://www.example.com/jira/secure/projectavatar?size=medium&pid=10000"},"projectCategory":{"self":"http://www.example.com/jira/rest/api/2/projectCategory/10000","id":"10000","name":"FIRST","description":"First Project Category"}},"comment":{"comments":[{"self":"http://www.example.com/jira/rest/api/2/issue/10010/comment/10000","id":"10000","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"body":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque eget venenatis elit. Duis eu justo eget augue iaculis fermentum. Sed semper quam laoreet nisi egestas at posuere augue semper.","updateAuthor":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"created":"2016-03-16T04:22:37.356+0000","updated":"2016-03-16T04:22:37.356+0000","visibility":{"type":"role","value":"Administrators"}}]},"issuelinks":[{"id":"10001","type":{"id":"10000","name":"Dependent","inward":"depends on","outward":"is depended by"},"outwardIssue":{"id":"10004L","key":"PRJ-2","self":"http://www.example.com/jira/rest/api/2/issue/PRJ-2","fields":{"status":{"iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png","name":"Open"}}}},{"id":"10002","type":{"id":"10000","name":"Dependent","inward":"depends on","outward":"is depended by"},"inwardIssue":{"id":"10004","key":"PRJ-3","self":"http://www.example.com/jira/rest/api/2/issue/PRJ-3","fields":{"status":{"iconUrl":"http://www.example.com/jira//images/icons/statuses/open.png","name":"Open"}}}}],"worklog":{"worklogs":[{"self":"http://www.example.com/jira/rest/api/2/issue/10010/worklog/10000","author":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"updateAuthor":{"self":"http://www.example.com/jira/rest/api/2/user?username=fred","name":"fred","displayName":"Fred F. User","active":false},"comment":"I did some work here.","updated":"2016-03-16T04:22:37.471+0000","visibility":{"type":"group","value":"jira-developers"},"started":"2016-03-16T04:22:37.471+0000","timeSpent":"3h 20m","timeSpentSeconds":12000,"id":"100028","issueId":"10002"}]},"updated":"2016-04-06T02:36:53.594-0700","duedate":"2018-01-19","timetracking":{"originalEstimate":"10m","remainingEstimate":"3m","timeSpent":"6m","originalEstimateSeconds":600,"remainingEstimateSeconds":200,"timeSpentSeconds":400}},"names":{"watcher":"watcher","attachment":"attachment","sub-tasks":"sub-tasks","description":"description","project":"project","comment":"comment","issuelinks":"issuelinks","worklog":"worklog","updated":"updated","timetracking":"timetracking"},"schema":{}}`) @@ -73,7 +76,7 @@ func TestIssueService_Create(t *testing.T) { Description: "example bug report", }, } - issue, _, err := testClient.Issue.Create(context.Background(), i) + issue, _, err := testClient.Issue.Create(context.Background(), i, &CreateIssueOptions{UpdateHistory: true}) if issue == nil { t.Error("Expected issue. Issue is nil") } @@ -88,6 +91,9 @@ func TestIssueService_CreateThenGet(t *testing.T) { testMux.HandleFunc("/rest/api/2/issue", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, http.MethodPost) testRequestURL(t, r, "/rest/api/2/issue") + testRequestParams(t, r, map[string]string{ + "updateHistory": "true", + }) w.WriteHeader(http.StatusCreated) io.Copy(w, r.Body) @@ -99,7 +105,7 @@ func TestIssueService_CreateThenGet(t *testing.T) { Created: Time(time.Now()), }, } - issue, _, err := testClient.Issue.Create(context.Background(), i) + issue, _, err := testClient.Issue.Create(context.Background(), i, &CreateIssueOptions{UpdateHistory: true}) if issue == nil { t.Error("Expected issue. Issue is nil") }