Repository: ctreminiom/go-atlassian
Pull request #115 (2022-05-04 20:20:18)

:sparkles: Mapped the Jira Issue Search endpoints
1. Added the Checks method under the IssueSearch service
2. https://developer.atlassian.com/cloud/jira/platform/rest/v3/api-group-issue-search/#api-rest-api-3-jql-match-post


Patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index b44e4a6..ffba9f5 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -22,7 +22,7 @@ jobs:
         uses: golangci/golangci-lint-action@v2
         with:
           # Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
-          version: v1.36
+          version: v1.38
           # Optional: working directory, useful for monorepos
           # working-directory: somedir
 
@@ -33,4 +33,4 @@ jobs:
           # only-new-issues: true
 
           # Optional: if set to true then the action will use pre-installed Go
-          # skip-go-installation: true
\ No newline at end of file
+          skip-go-installation: true
\ No newline at end of file
diff --git a/jira/mocks/jira-issue-search-check.json b/jira/mocks/jira-issue-search-check.json
new file mode 100644
index 0000000..10c6f7b
--- /dev/null
+++ b/jira/mocks/jira-issue-search-check.json
@@ -0,0 +1,29 @@
+{
+  "matches": [
+    {
+      "matchedIssues": [
+        10000,
+        10004
+      ],
+      "errors": []
+    },
+    {
+      "matchedIssues": [
+        100134,
+        10025,
+        10236
+      ],
+      "errors": []
+    },
+    {
+      "matchedIssues": [],
+      "errors": []
+    },
+    {
+      "matchedIssues": [],
+      "errors": [
+        "Invalid JQL: broken = value"
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/jira/v2/issueSearch.go b/jira/v2/issueSearch.go
index 34b3e97..360938d 100644
--- a/jira/v2/issueSearch.go
+++ b/jira/v2/issueSearch.go
@@ -95,3 +95,31 @@ func (s *IssueSearchService) Post(ctx context.Context, jql string, fields, expan
 
 	return
 }
+
+// Checks checks whether one or more issues would be returned by one or more JQL queries.
+// Docs: https://docs.go-atlassian.io/jira-software-cloud/issues/search#check-issues-against-jql
+func (s *IssueSearchService) Checks(ctx context.Context, payload *models.IssueSearchCheckPayloadScheme) (result *models.IssueMatchesPageScheme,
+	response *ResponseScheme, err error) {
+
+	var endpoint = "rest/api/2/jql/match"
+
+	payloadAsReader, err := transformStructToReader(payload)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	request, err := s.client.newRequest(ctx, http.MethodPost, endpoint, payloadAsReader)
+	if err != nil {
+		return
+	}
+
+	request.Header.Set("Accept", "application/json")
+	request.Header.Set("Content-Type", "application/json")
+
+	response, err = s.client.call(request, &result)
+	if err != nil {
+		return
+	}
+
+	return
+}
diff --git a/jira/v3/issueSearch.go b/jira/v3/issueSearch.go
index 38ddfaa..a50c7ce 100644
--- a/jira/v3/issueSearch.go
+++ b/jira/v3/issueSearch.go
@@ -95,3 +95,31 @@ func (s *IssueSearchService) Post(ctx context.Context, jql string, fields, expan
 
 	return
 }
+
+// Checks checks whether one or more issues would be returned by one or more JQL queries.
+// Docs: https://docs.go-atlassian.io/jira-software-cloud/issues/search#check-issues-against-jql
+func (s *IssueSearchService) Checks(ctx context.Context, payload *models.IssueSearchCheckPayloadScheme) (result *models.IssueMatchesPageScheme,
+	response *ResponseScheme, err error) {
+
+	var endpoint = "rest/api/3/jql/match"
+
+	payloadAsReader, err := transformStructToReader(payload)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	request, err := s.client.newRequest(ctx, http.MethodPost, endpoint, payloadAsReader)
+	if err != nil {
+		return
+	}
+
+	request.Header.Set("Accept", "application/json")
+	request.Header.Set("Content-Type", "application/json")
+
+	response, err = s.client.call(request, &result)
+	if err != nil {
+		return
+	}
+
+	return
+}
diff --git a/pkg/infra/models/jira_issue_search.go b/pkg/infra/models/jira_issue_search.go
new file mode 100644
index 0000000..8a55c1f
--- /dev/null
+++ b/pkg/infra/models/jira_issue_search.go
@@ -0,0 +1,15 @@
+package models
+
+type IssueSearchCheckPayloadScheme struct {
+	IssueIds []int    `json:"issueIds,omitempty"`
+	JQLs     []string `json:"jqls,omitempty"`
+}
+
+type IssueMatchesPageScheme struct {
+	Matches []*IssueMatchesScheme `json:"matches,omitempty"`
+}
+
+type IssueMatchesScheme struct {
+	MatchedIssues []int    `json:"matchedIssues,omitempty"`
+	Errors        []string `json:"errors,omitempty"`
+}


Test patch:
diff --git a/jira/v2/issueSearch_test.go b/jira/v2/issueSearch_test.go
index 7460f98..d3c0b6d 100644
--- a/jira/v2/issueSearch_test.go
+++ b/jira/v2/issueSearch_test.go
@@ -3,6 +3,7 @@ package v2
 import (
 	"context"
 	"fmt"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
 	"github.com/stretchr/testify/assert"
 	"net/http"
 	"net/url"
@@ -446,3 +447,156 @@ func TestIssueSearchService_Post(t *testing.T) {
 	}
 
 }
+
+func TestIssueSearchService_Checks(t *testing.T) {
+
+	testCases := []struct {
+		name               string
+		payload            *models.IssueSearchCheckPayloadScheme
+		mockFile           string
+		wantHTTPMethod     string
+		endpoint           string
+		context            context.Context
+		wantHTTPCodeReturn int
+		wantErr            bool
+		expectedError      string
+	}{
+		{
+			name: "when the parameters are correct",
+			payload: &models.IssueSearchCheckPayloadScheme{
+				IssueIds: []int{1, 2, 3, 4},
+				JQLs:     []string{"project = DUMMY"},
+			},
+			mockFile:           "../mocks/jira-issue-search-check.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/rest/api/2/jql/match",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            false,
+		},
+
+		{
+			name:               "when the payload is not provided",
+			payload:            nil,
+			mockFile:           "../mocks/jira-issue-search-check.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/rest/api/2/jql/match",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "failed to parse the interface pointer, please provide a valid one",
+		},
+
+		{
+			name: "when the context is not provided",
+			payload: &models.IssueSearchCheckPayloadScheme{
+				IssueIds: []int{1, 2, 3, 4},
+				JQLs:     []string{"project = DUMMY"},
+			},
+			mockFile:           "../mocks/jira-issue-search-check.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/rest/api/2/jql/match",
+			context:            nil,
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "request creation failed: net/http: nil Context",
+		},
+
+		{
+			name: "when the response is invalid",
+			payload: &models.IssueSearchCheckPayloadScheme{
+				IssueIds: []int{1, 2, 3, 4},
+				JQLs:     []string{"project = DUMMY"},
+			},
+			mockFile:           "../mocks/jira-issue-search-check.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/rest/api/2/jql/match",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusBadRequest,
+			wantErr:            true,
+			expectedError:      "request failed. Please analyze the request body for more details. Status Code: 400",
+		},
+
+		{
+			name: "when the response body is empty",
+			payload: &models.IssueSearchCheckPayloadScheme{
+				IssueIds: []int{1, 2, 3, 4},
+				JQLs:     []string{"project = DUMMY"},
+			},
+			mockFile:           "../mocks/empty-json.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/rest/api/2/jql/match",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "unexpected end of JSON input",
+		},
+	}
+
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+
+			//Init a new HTTP mock server
+			mockOptions := mockServerOptions{
+				Endpoint:           testCase.endpoint,
+				MockFilePath:       testCase.mockFile,
+				MethodAccepted:     testCase.wantHTTPMethod,
+				ResponseCodeWanted: testCase.wantHTTPCodeReturn,
+			}
+
+			mockServer, err := startMockServer(&mockOptions)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			defer mockServer.Close()
+
+			//Init the library instance
+			mockClient, err := startMockClient(mockServer.URL)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			i := &IssueSearchService{client: mockClient}
+
+			gotResult, gotResponse, err := i.Checks(testCase.context, testCase.payload)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.expectedError)
+
+				if gotResponse != nil {
+					t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code)
+				}
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+
+				apiEndpoint, err := url.Parse(gotResponse.Endpoint)
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				var endpointToAssert string
+
+				if apiEndpoint.Query().Encode() != "" {
+					endpointToAssert = fmt.Sprintf("%v?%v", apiEndpoint.Path, apiEndpoint.Query().Encode())
+				} else {
+					endpointToAssert = apiEndpoint.Path
+				}
+
+				t.Logf("HTTP Endpoint Wanted: %v, HTTP Endpoint Returned: %v", testCase.endpoint, endpointToAssert)
+				assert.Equal(t, testCase.endpoint, endpointToAssert)
+			}
+		})
+
+	}
+
+}
diff --git a/jira/v3/issueSearch_test.go b/jira/v3/issueSearch_test.go
index 5978e0e..963fa82 100644
--- a/jira/v3/issueSearch_test.go
+++ b/jira/v3/issueSearch_test.go
@@ -3,6 +3,7 @@ package v3
 import (
 	"context"
 	"fmt"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
 	"github.com/stretchr/testify/assert"
 	"net/http"
 	"net/url"
@@ -454,3 +455,156 @@ func TestIssueSearchService_Post(t *testing.T) {
 	}
 
 }
+
+func TestIssueSearchService_Checks(t *testing.T) {
+
+	testCases := []struct {
+		name               string
+		payload            *models.IssueSearchCheckPayloadScheme
+		mockFile           string
+		wantHTTPMethod     string
+		endpoint           string
+		context            context.Context
+		wantHTTPCodeReturn int
+		wantErr            bool
+		expectedError      string
+	}{
+		{
+			name: "when the parameters are correct",
+			payload: &models.IssueSearchCheckPayloadScheme{
+				IssueIds: []int{1, 2, 3, 4},
+				JQLs:     []string{"project = DUMMY"},
+			},
+			mockFile:           "../mocks/jira-issue-search-check.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/rest/api/3/jql/match",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            false,
+		},
+
+		{
+			name:               "when the payload is not provided",
+			payload:            nil,
+			mockFile:           "../mocks/jira-issue-search-check.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/rest/api/3/jql/match",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "failed to parse the interface pointer, please provide a valid one",
+		},
+
+		{
+			name: "when the context is not provided",
+			payload: &models.IssueSearchCheckPayloadScheme{
+				IssueIds: []int{1, 2, 3, 4},
+				JQLs:     []string{"project = DUMMY"},
+			},
+			mockFile:           "../mocks/jira-issue-search-check.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/rest/api/3/jql/match",
+			context:            nil,
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "request creation failed: net/http: nil Context",
+		},
+
+		{
+			name: "when the response is invalid",
+			payload: &models.IssueSearchCheckPayloadScheme{
+				IssueIds: []int{1, 2, 3, 4},
+				JQLs:     []string{"project = DUMMY"},
+			},
+			mockFile:           "../mocks/jira-issue-search-check.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/rest/api/3/jql/match",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusBadRequest,
+			wantErr:            true,
+			expectedError:      "request failed. Please analyze the request body for more details. Status Code: 400",
+		},
+
+		{
+			name: "when the response body is empty",
+			payload: &models.IssueSearchCheckPayloadScheme{
+				IssueIds: []int{1, 2, 3, 4},
+				JQLs:     []string{"project = DUMMY"},
+			},
+			mockFile:           "../mocks/empty-json.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/rest/api/3/jql/match",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "unexpected end of JSON input",
+		},
+	}
+
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+
+			//Init a new HTTP mock server
+			mockOptions := mockServerOptions{
+				Endpoint:           testCase.endpoint,
+				MockFilePath:       testCase.mockFile,
+				MethodAccepted:     testCase.wantHTTPMethod,
+				ResponseCodeWanted: testCase.wantHTTPCodeReturn,
+			}
+
+			mockServer, err := startMockServer(&mockOptions)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			defer mockServer.Close()
+
+			//Init the library instance
+			mockClient, err := startMockClient(mockServer.URL)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			i := &IssueSearchService{client: mockClient}
+
+			gotResult, gotResponse, err := i.Checks(testCase.context, testCase.payload)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.expectedError)
+
+				if gotResponse != nil {
+					t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code)
+				}
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+
+				apiEndpoint, err := url.Parse(gotResponse.Endpoint)
+				if err != nil {
+					t.Fatal(err)
+				}
+
+				var endpointToAssert string
+
+				if apiEndpoint.Query().Encode() != "" {
+					endpointToAssert = fmt.Sprintf("%v?%v", apiEndpoint.Path, apiEndpoint.Query().Encode())
+				} else {
+					endpointToAssert = apiEndpoint.Path
+				}
+
+				t.Logf("HTTP Endpoint Wanted: %v, HTTP Endpoint Returned: %v", testCase.endpoint, endpointToAssert)
+				assert.Equal(t, testCase.endpoint, endpointToAssert)
+			}
+		})
+
+	}
+
+}