diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index b44e4a66..ffba9f58 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 00000000..10c6f7b9 --- /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 34b3e977..360938d0 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/v2/issueSearch_test.go b/jira/v2/issueSearch_test.go index 7460f986..d3c0b6d0 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.go b/jira/v3/issueSearch.go index 38ddfaa6..a50c7ce4 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/jira/v3/issueSearch_test.go b/jira/v3/issueSearch_test.go index 5978e0ee..963fa82b 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) + } + }) + + } + +} diff --git a/pkg/infra/models/jira_issue_search.go b/pkg/infra/models/jira_issue_search.go new file mode 100644 index 00000000..8a55c1f7 --- /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"` +}