diff --git a/issue.go b/issue.go index 0aa03b70..3d7a76cb 100644 --- a/issue.go +++ b/issue.go @@ -11,7 +11,6 @@ import ( "net/http" "net/url" "reflect" - "strconv" "strings" "time" @@ -517,14 +516,23 @@ type CommentVisibility struct { // Default Pagination options type SearchOptions struct { // StartAt: The starting index of the returned projects. Base index: 0. - StartAt int `url:"startAt,omitempty"` + StartAt int `url:"startAt,omitempty" json:"startAt,omitempty"` // MaxResults: The maximum number of projects to return per page. Default: 50. - MaxResults int `url:"maxResults,omitempty"` + MaxResults int `url:"maxResults,omitempty" json:"maxResults,omitempty"` // Expand: Expand specific sections in the returned issues - Expand string `url:"expand,omitempty"` - Fields []string + Expand string `url:"expand,omitempty" json:"-"` + Fields []string `url:"fields,comma,omitempty" json:"fields,omitempty"` // ValidateQuery: The validateQuery param offers control over whether to validate and how strictly to treat the validation. Default: strict. - ValidateQuery string `url:"validateQuery,omitempty"` + ValidateQuery string `url:"validateQuery,omitempty" json:"validateQuery,omitempty"` +} + +// searchRequest is for the Search (with JQL) method to encode its inputs into a request whether it's POST or GET. +type searchRequest struct { + // JQL is the query that the user passed. + JQL string `url:"jql,omitempty" json:"jql,omitempty"` + // ExpandArray is the array form of SearchOptions.Expand used for the POST/JSON version of the search request. + ExpandArray []string `url:"-" json:"expand,omitempty"` + *SearchOptions `url:",omitempty" json:",omitempty"` } // searchResult is only a small wrapper around the Search (with JQL) method @@ -1091,32 +1099,29 @@ func (s *IssueService) SearchWithContext(ctx context.Context, jql string, option u := url.URL{ Path: "rest/api/2/search", } - uv := url.Values{} - if jql != "" { - uv.Add("jql", jql) + r := searchRequest{ + JQL: jql, + SearchOptions: options, } - - if options != nil { - if options.StartAt != 0 { - uv.Add("startAt", strconv.Itoa(options.StartAt)) - } - if options.MaxResults != 0 { - uv.Add("maxResults", strconv.Itoa(options.MaxResults)) - } - if options.Expand != "" { - uv.Add("expand", options.Expand) - } - if strings.Join(options.Fields, ",") != "" { - uv.Add("fields", strings.Join(options.Fields, ",")) - } - if options.ValidateQuery != "" { - uv.Add("validateQuery", options.ValidateQuery) - } + if options != nil && options.Expand != "" { + r.ExpandArray = strings.Split(options.Expand, ",") } + uv, err := query.Values(r) + if err != nil { + return []Issue{}, nil, err + } u.RawQuery = uv.Encode() - req, err := s.client.NewRequestWithContext(ctx, "GET", u.String(), nil) + var req *http.Request + if len(u.String()) > 2000 { + // If the JQL is too long, switch to the post method instead. + u.RawQuery = "" + req, err = s.client.NewRequestWithContext(ctx, "POST", u.String(), r) + } else { + req, err = s.client.NewRequestWithContext(ctx, "GET", u.String(), nil) + } + if err != nil { return []Issue{}, nil, err } diff --git a/issue_test.go b/issue_test.go index ceeff9a8..dc6d82b8 100644 --- a/issue_test.go +++ b/issue_test.go @@ -647,6 +647,45 @@ func TestIssueService_Search(t *testing.T) { } } +func TestIssueService_Search_Long_Query(t *testing.T) { + setup() + defer teardown() + testMux.HandleFunc("/rest/api/2/search", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "POST") + testRequestURL(t, r, "/rest/api/2/search") + w.WriteHeader(http.StatusOK) + fmt.Fprint(w, `{"expand": "schema,names","startAt": 1,"maxResults": 40,"total": 0,"issues": []}`) + }) + + opt := &SearchOptions{StartAt: 1, MaxResults: 40, Expand: "foo"} + + // LONGKEY- is 8 characters; 250 x 8 is 2000, which is the limit for URL length. + keys := make([]string, 250) + for n := range keys { + keys[n] = fmt.Sprintf("LONGKEY-%d", n+1) + } + + query := fmt.Sprintf("type = Bug and Key IN (%s)", strings.Join(keys, ",")) + _, resp, err := testClient.Issue.Search(query, opt) + + if resp == nil { + t.Errorf("Response given: %+v", resp) + } + if err != nil { + t.Errorf("Error given: %s", err) + } + + if resp.StartAt != 1 { + t.Errorf("StartAt should populate with 1, %v given", resp.StartAt) + } + if resp.MaxResults != 40 { + t.Errorf("MaxResults should populate with 40, %v given", resp.MaxResults) + } + if resp.Total != 0 { + t.Errorf("Total should populate with 0, %v given", resp.Total) + } +} + func TestIssueService_SearchEmptyJQL(t *testing.T) { setup() defer teardown()