Repository: ctreminiom/go-atlassian
Pull request #104 (2022-02-02 15:48:11)

:sparkles: Added the Confluence Search service


Patch:
diff --git a/confluence/confluence.go b/confluence/confluence.go
index dcd3a3c..69bcae6 100644
--- a/confluence/confluence.go
+++ b/confluence/confluence.go
@@ -22,6 +22,7 @@ type Client struct {
 	Content *ContentService
 	Space   *SpaceService
 	Label   *LabelService
+	Search  *SearchService
 }
 
 func New(httpClient *http.Client, site string) (client *Client, err error) {
@@ -64,6 +65,7 @@ func New(httpClient *http.Client, site string) (client *Client, err error) {
 
 	client.Space = &SpaceService{client: client}
 	client.Label = &LabelService{client: client}
+	client.Search = &SearchService{client: client}
 	return
 }
 
diff --git a/confluence/mocks/search-content.json b/confluence/mocks/search-content.json
new file mode 100644
index 0000000..a1d8cee
--- /dev/null
+++ b/confluence/mocks/search-content.json
@@ -0,0 +1,340 @@
+{
+  "results": [
+    {
+      "content": {
+        "id": "<string>",
+        "type": "<string>",
+        "status": "<string>",
+        "title": "<string>",
+        "space": {
+          "key": "<string>",
+          "name": "<string>",
+          "type": "<string>",
+          "status": "<string>",
+          "_expandable": {},
+          "_links": {}
+        },
+        "history": {
+          "latest": true
+        },
+        "version": {
+          "when": "<string>",
+          "number": 57,
+          "minorEdit": true
+        },
+        "ancestors": [],
+        "operations": [
+          {
+            "operation": "administer",
+            "targetType": "<string>"
+          }
+        ],
+        "children": {},
+        "childTypes": {},
+        "descendants": {},
+        "container": {},
+        "body": {
+          "view": {
+            "value": "<string>",
+            "representation": "view"
+          },
+          "export_view": {
+            "value": "<string>",
+            "representation": "view"
+          },
+          "styled_view": {
+            "value": "<string>",
+            "representation": "view"
+          },
+          "storage": {
+            "value": "<string>",
+            "representation": "view"
+          },
+          "wiki": {
+            "value": "<string>",
+            "representation": "view"
+          },
+          "editor": {
+            "value": "<string>",
+            "representation": "view"
+          },
+          "editor2": {
+            "value": "<string>",
+            "representation": "view"
+          },
+          "anonymous_export_view": {
+            "value": "<string>",
+            "representation": "view"
+          },
+          "atlas_doc_format": {
+            "value": "<string>",
+            "representation": "view"
+          },
+          "dynamic": {
+            "value": "<string>",
+            "representation": "view"
+          },
+          "_expandable": {
+            "editor": "<string>",
+            "view": "<string>",
+            "export_view": "<string>",
+            "styled_view": "<string>",
+            "storage": "<string>",
+            "editor2": "<string>",
+            "anonymous_export_view": "<string>",
+            "atlas_doc_format": "<string>",
+            "wiki": "<string>",
+            "dynamic": "<string>",
+            "raw": "<string>"
+          }
+        },
+        "restrictions": {
+          "read": {
+            "operation": "administer",
+            "_expandable": {},
+            "_links": {}
+          },
+          "update": {
+            "operation": "administer",
+            "_expandable": {},
+            "_links": {}
+          },
+          "_expandable": {
+            "read": "<string>",
+            "update": "<string>"
+          },
+          "_links": {}
+        },
+        "metadata": {},
+        "macroRenderedOutput": {},
+        "extensions": {},
+        "_expandable": {
+          "childTypes": "<string>",
+          "container": "<string>",
+          "metadata": "<string>",
+          "operations": "<string>",
+          "children": "<string>",
+          "restrictions": "<string>",
+          "history": "<string>",
+          "ancestors": "<string>",
+          "body": "<string>",
+          "version": "<string>",
+          "descendants": "<string>",
+          "space": "<string>",
+          "extensions": "<string>",
+          "schedulePublishDate": "<string>",
+          "macroRenderedOutput": "<string>"
+        },
+        "_links": {}
+      },
+      "user": {
+        "type": "known",
+        "username": "<string>",
+        "userKey": "<string>",
+        "accountId": "<string>",
+        "accountType": "atlassian",
+        "email": "<string>",
+        "publicName": "<string>",
+        "profilePicture": {
+          "path": "<string>",
+          "width": 2154,
+          "height": 2154,
+          "isDefault": true
+        },
+        "displayName": "<string>",
+        "timeZone": "<string>",
+        "isExternalCollaborator": true,
+        "externalCollaborator": true,
+        "operations": [
+          {
+            "operation": "administer",
+            "targetType": "<string>"
+          }
+        ],
+        "details": {},
+        "personalSpace": {
+          "key": "<string>",
+          "name": "<string>",
+          "type": "<string>",
+          "status": "<string>",
+          "_expandable": {},
+          "_links": {}
+        },
+        "_expandable": {
+          "operations": "<string>",
+          "details": "<string>",
+          "personalSpace": "<string>"
+        },
+        "_links": {}
+      },
+      "space": {
+        "id": 2154,
+        "key": "<string>",
+        "name": "<string>",
+        "icon": {
+          "path": "<string>",
+          "width": 2154,
+          "height": 2154,
+          "isDefault": true
+        },
+        "description": {
+          "plain": {
+            "value": "<string>",
+            "representation": "plain",
+            "embeddedContent": [
+              {}
+            ]
+          },
+          "view": {
+            "value": "<string>",
+            "representation": "plain",
+            "embeddedContent": [
+              {}
+            ]
+          },
+          "_expandable": {
+            "view": "<string>",
+            "plain": "<string>"
+          }
+        },
+        "homepage": {
+          "type": "<string>",
+          "status": "<string>"
+        },
+        "type": "<string>",
+        "metadata": {
+          "labels": {
+            "results": [
+              {
+                "prefix": "<string>",
+                "name": "<string>",
+                "id": "<string>",
+                "label": "<string>"
+              }
+            ],
+            "size": 2154
+          },
+          "_expandable": {}
+        },
+        "operations": [
+          {
+            "operation": "administer",
+            "targetType": "<string>"
+          }
+        ],
+        "permissions": [
+          {
+            "operation": {
+              "operation": "administer",
+              "targetType": "<string>"
+            },
+            "anonymousAccess": true,
+            "unlicensedAccess": true
+          }
+        ],
+        "status": "<string>",
+        "settings": {
+          "routeOverrideEnabled": true,
+          "_links": {}
+        },
+        "theme": {
+          "themeKey": "<string>"
+        },
+        "lookAndFeel": {
+          "headings": {
+            "color": "<string>"
+          },
+          "links": {
+            "color": "<string>"
+          },
+          "menus": {
+            "hoverOrFocus": {
+              "backgroundColor": "<string>"
+            },
+            "color": "<string>"
+          },
+          "header": {
+            "backgroundColor": "<string>",
+            "button": {
+              "backgroundColor": "<string>",
+              "color": "<string>"
+            },
+            "primaryNavigation": {
+              "color": "<string>",
+              "hoverOrFocus": {
+                "backgroundColor": "<string>",
+                "color": "<string>"
+              }
+            },
+            "secondaryNavigation": {
+              "color": "<string>",
+              "hoverOrFocus": {
+                "backgroundColor": "<string>",
+                "color": "<string>"
+              }
+            },
+            "search": {
+              "backgroundColor": "<string>",
+              "color": "<string>"
+            }
+          },
+          "content": {},
+          "bordersAndDividers": {
+            "color": "<string>"
+          }
+        },
+        "history": {
+          "createdDate": "<string>",
+          "createdBy": {
+            "type": "known"
+          }
+        },
+        "_expandable": {
+          "settings": "<string>",
+          "metadata": "<string>",
+          "operations": "<string>",
+          "lookAndFeel": "<string>",
+          "permissions": "<string>",
+          "icon": "<string>",
+          "description": "<string>",
+          "theme": "<string>",
+          "history": "<string>",
+          "homepage": "<string>",
+          "identifiers": "<string>"
+        },
+        "_links": {}
+      },
+      "title": "<string>",
+      "excerpt": "<string>",
+      "url": "<string>",
+      "resultParentContainer": {
+        "title": "<string>",
+        "displayUrl": "<string>"
+      },
+      "resultGlobalContainer": {
+        "title": "<string>",
+        "displayUrl": "<string>"
+      },
+      "breadcrumbs": [
+        {
+          "label": "<string>",
+          "url": "<string>",
+          "separator": "<string>"
+        }
+      ],
+      "entityType": "<string>",
+      "iconCssClass": "<string>",
+      "lastModified": "<string>",
+      "friendlyLastModified": "<string>",
+      "score": 2154
+    }
+  ],
+  "start": 2154,
+  "limit": 2154,
+  "size": 2154,
+  "totalSize": 2154,
+  "cqlQuery": "<string>",
+  "searchDuration": 2154,
+  "archivedResultCount": 2154,
+  "_links": {}
+}
\ No newline at end of file
diff --git a/confluence/search.go b/confluence/search.go
new file mode 100644
index 0000000..f7511ed
--- /dev/null
+++ b/confluence/search.go
@@ -0,0 +1,118 @@
+package confluence
+
+import (
+	"context"
+	"fmt"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+)
+
+type SearchService struct{ client *Client }
+
+// Content searches for content using the Confluence Query Language (CQL)
+// Docs: https://docs.go-atlassian.io/confluence-cloud/search#search-content
+func (s *SearchService) Content(ctx context.Context, cql string, options *models.SearchContentOptions) (result *models.SearchPageScheme, response *ResponseScheme, err error) {
+
+	if cql == "" {
+		return nil, nil, models.ErrNoCQLError
+	}
+
+	query := url.Values{}
+	query.Add("cql", cql)
+
+	if options != nil {
+
+		if options.Context != "" {
+			query.Add("cqlcontext", options.Context)
+		}
+
+		if options.Cursor != "" {
+			query.Add("cursor", options.Cursor)
+		}
+
+		if options.Next {
+			query.Add("next", "true")
+		}
+
+		if options.Prev {
+			query.Add("prev", "true")
+		}
+
+		if options.Limit != 0 {
+			query.Add("limit", strconv.Itoa(options.Limit))
+		}
+
+		if options.Start != 0 {
+			query.Add("start", strconv.Itoa(options.Start))
+		}
+
+		if options.IncludeArchivedSpaces {
+			query.Add("includeArchivedSpaces", "true")
+		}
+
+		if options.ExcludeCurrentSpaces {
+			query.Add("excludeCurrentSpaces", "true")
+		}
+
+		if options.Excerpt != "" {
+			query.Add("excerpt", options.Excerpt)
+		}
+
+		if options.SitePermissionTypeFilter != "" {
+			query.Add("sitePermissionTypeFilter", options.SitePermissionTypeFilter)
+		}
+	}
+
+	endpoint := fmt.Sprintf("wiki/rest/api/search?%v", query.Encode())
+
+	request, err := s.client.newRequest(ctx, http.MethodGet, endpoint, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	request.Header.Set("Accept", "application/json")
+
+	response, err = s.client.Call(request, &result)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return
+}
+
+// Users searches for users using user-specific queries from the Confluence Query Language (CQL).
+// Docs: Searches for users using user-specific queries from the Confluence Query Language (CQL).
+func (s *SearchService) Users(ctx context.Context, cql string, start, limit int, expand []string) (result *models.SearchPageScheme, response *ResponseScheme, err error) {
+
+	if cql == "" {
+		return nil, nil, models.ErrNoCQLError
+	}
+
+	query := url.Values{}
+	query.Add("limit", strconv.Itoa(limit))
+	query.Add("start", strconv.Itoa(start))
+	query.Add("cql", cql)
+
+	if len(expand) != 0 {
+		query.Add("expand", strings.Join(expand, ","))
+	}
+
+	endpoint := fmt.Sprintf("wiki/rest/api/search/user?%v", query.Encode())
+
+	request, err := s.client.newRequest(ctx, http.MethodGet, endpoint, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	request.Header.Set("Accept", "application/json")
+
+	response, err = s.client.Call(request, &result)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return
+}
diff --git a/pkg/infra/models/confluence_search.go b/pkg/infra/models/confluence_search.go
new file mode 100644
index 0000000..253be52
--- /dev/null
+++ b/pkg/infra/models/confluence_search.go
@@ -0,0 +1,62 @@
+package models
+
+type SearchContentOptions struct {
+	Context                  string
+	Cursor                   string
+	Next                     bool
+	Prev                     bool
+	Limit                    int
+	Start                    int
+	IncludeArchivedSpaces    bool
+	ExcludeCurrentSpaces     bool
+	SitePermissionTypeFilter string
+	Excerpt                  string
+	Expand                   []string
+}
+
+type SearchPageScheme struct {
+	Results             []*SearchResultScheme  `json:"results,omitempty"`
+	Start               int                    `json:"start,omitempty"`
+	Limit               int                    `json:"limit,omitempty"`
+	Size                int                    `json:"size,omitempty"`
+	TotalSize           int                    `json:"totalSize,omitempty"`
+	CqlQuery            string                 `json:"cqlQuery,omitempty"`
+	SearchDuration      int                    `json:"searchDuration,omitempty"`
+	ArchivedResultCount int                    `json:"archivedResultCount,omitempty"`
+	Links               *SearchPageLinksScheme `json:"_links,omitempty"`
+}
+
+type SearchPageLinksScheme struct {
+	Base    string `json:"base,omitempty"`
+	Context string `json:"context,omitempty"`
+	Next    string `json:"next,omitempty"`
+	Self    string `json:"self,omitempty"`
+}
+
+type SearchResultScheme struct {
+	Content               *ContentScheme            `json:"content,omitempty"`
+	User                  *ContentUserScheme        `json:"user,omitempty"`
+	Space                 *SpaceScheme              `json:"space,omitempty"`
+	Title                 string                    `json:"title,omitempty"`
+	Excerpt               string                    `json:"excerpt,omitempty"`
+	URL                   string                    `json:"url,omitempty"`
+	ResultParentContainer *ContainerSummaryScheme   `json:"resultParentContainer,omitempty"`
+	ResultGlobalContainer *ContainerSummaryScheme   `json:"resultGlobalContainer,omitempty"`
+	Breadcrumbs           []*SearchBreadcrumbScheme `json:"breadcrumbs,omitempty"`
+	EntityType            string                    `json:"entityType,omitempty"`
+	IconCSSClass          string                    `json:"iconCssClass,omitempty"`
+	LastModified          string                    `json:"lastModified,omitempty"`
+	FriendlyLastModified  string                    `json:"friendlyLastModified,omitempty"`
+	Score                 float64                   `json:"score,omitempty"`
+}
+
+type ContainerSummaryScheme struct {
+	Title      string `json:"title,omitempty"`
+	DisplayURL string `json:"displayUrl,omitempty"`
+}
+
+type SearchBreadcrumbScheme struct {
+	Label     string `json:"label,omitempty"`
+	URL       string `json:"url,omitempty"`
+	Separator string `json:"separator,omitempty"`
+}


Test patch:
diff --git a/confluence/search_test.go b/confluence/search_test.go
new file mode 100644
index 0000000..e3aade5
--- /dev/null
+++ b/confluence/search_test.go
@@ -0,0 +1,384 @@
+package confluence
+
+import (
+	"context"
+	"fmt"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"net/url"
+	"testing"
+)
+
+func TestSearchService_Content(t *testing.T) {
+
+	testCases := []struct {
+		name               string
+		cql                string
+		options            *models.SearchContentOptions
+		mockFile           string
+		wantHTTPMethod     string
+		endpoint           string
+		context            context.Context
+		wantHTTPCodeReturn int
+		wantErr            bool
+		expectedError      string
+	}{
+		{
+			name: "when the parameters are correct",
+			cql:  "type=page",
+			options: &models.SearchContentOptions{
+				Context:                  "spaceKey",
+				Cursor:                   "raNDoMsTRiNg",
+				Next:                     true,
+				Prev:                     true,
+				Limit:                    20,
+				Start:                    10,
+				IncludeArchivedSpaces:    true,
+				ExcludeCurrentSpaces:     true,
+				SitePermissionTypeFilter: "externalCollaborator",
+				Excerpt:                  "indexed",
+				Expand:                   []string{"space"},
+			},
+			mockFile:       "./mocks/search-content.json",
+			wantHTTPMethod: http.MethodGet,
+			endpoint: "/wiki/rest/api/search?cql=type%3Dpage&cqlcontext=spaceKey&cursor=raNDoMsTRiNg&excerpt=" +
+				"indexed&excludeCurrentSpaces=true&includeArchivedSpaces=true&limit=20&next=true&prev=true&sitePermission" +
+				"TypeFilter=externalCollaborator&start=10",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+		},
+
+		{
+			name: "when the CQL query is not provided",
+			cql:  "",
+			options: &models.SearchContentOptions{
+				Context:                  "spaceKey",
+				Cursor:                   "raNDoMsTRiNg",
+				Next:                     true,
+				Prev:                     true,
+				Limit:                    20,
+				Start:                    10,
+				IncludeArchivedSpaces:    true,
+				ExcludeCurrentSpaces:     true,
+				SitePermissionTypeFilter: "externalCollaborator",
+				Excerpt:                  "indexed",
+				Expand:                   []string{"space"},
+			},
+			mockFile:       "./mocks/search-content.json",
+			wantHTTPMethod: http.MethodGet,
+			endpoint: "/wiki/rest/api/search?cql=type%3Dpage&cqlcontext=spaceKey&cursor=raNDoMsTRiNg&excerpt=" +
+				"indexed&excludeCurrentSpaces=true&includeArchivedSpaces=true&limit=20&next=true&prev=true&sitePermission" +
+				"TypeFilter=externalCollaborator&start=10",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "confluence: no CQL query set",
+		},
+
+		{
+			name: "when the context is not provided",
+			cql:  "type=page",
+			options: &models.SearchContentOptions{
+				Context:                  "spaceKey",
+				Cursor:                   "raNDoMsTRiNg",
+				Next:                     true,
+				Prev:                     true,
+				Limit:                    20,
+				Start:                    10,
+				IncludeArchivedSpaces:    true,
+				ExcludeCurrentSpaces:     true,
+				SitePermissionTypeFilter: "externalCollaborator",
+				Excerpt:                  "indexed",
+				Expand:                   []string{"space"},
+			},
+			mockFile:       "./mocks/search-content.json",
+			wantHTTPMethod: http.MethodGet,
+			endpoint: "/wiki/rest/api/search?cql=type%3Dpage&cqlcontext=spaceKey&cursor=raNDoMsTRiNg&excerpt=" +
+				"indexed&excludeCurrentSpaces=true&includeArchivedSpaces=true&limit=20&next=true&prev=true&sitePermission" +
+				"TypeFilter=externalCollaborator&start=10",
+			context:            nil,
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "request creation failed: net/http: nil Context",
+		},
+
+		{
+			name: "when the response status is invalid",
+			cql:  "type=page",
+			options: &models.SearchContentOptions{
+				Context:                  "spaceKey",
+				Cursor:                   "raNDoMsTRiNg",
+				Next:                     true,
+				Prev:                     true,
+				Limit:                    20,
+				Start:                    10,
+				IncludeArchivedSpaces:    true,
+				ExcludeCurrentSpaces:     true,
+				SitePermissionTypeFilter: "externalCollaborator",
+				Excerpt:                  "indexed",
+				Expand:                   []string{"space"},
+			},
+			mockFile:       "./mocks/search-content.json",
+			wantHTTPMethod: http.MethodGet,
+			endpoint: "/wiki/rest/api/search?cql=type%3Dpage&cqlcontext=spaceKey&cursor=raNDoMsTRiNg&excerpt=" +
+				"indexed&excludeCurrentSpaces=true&includeArchivedSpaces=true&limit=20&next=true&prev=true&sitePermission" +
+				"TypeFilter=externalCollaborator&start=10",
+			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",
+			cql:  "type=page",
+			options: &models.SearchContentOptions{
+				Context:                  "spaceKey",
+				Cursor:                   "raNDoMsTRiNg",
+				Next:                     true,
+				Prev:                     true,
+				Limit:                    20,
+				Start:                    10,
+				IncludeArchivedSpaces:    true,
+				ExcludeCurrentSpaces:     true,
+				SitePermissionTypeFilter: "externalCollaborator",
+				Excerpt:                  "indexed",
+				Expand:                   []string{"space"},
+			},
+			mockFile:       "./mocks/empty-json.json",
+			wantHTTPMethod: http.MethodGet,
+			endpoint: "/wiki/rest/api/search?cql=type%3Dpage&cqlcontext=spaceKey&cursor=raNDoMsTRiNg&excerpt=" +
+				"indexed&excludeCurrentSpaces=true&includeArchivedSpaces=true&limit=20&next=true&prev=true&sitePermission" +
+				"TypeFilter=externalCollaborator&start=10",
+			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)
+			}
+
+			service := &SearchService{client: mockClient}
+
+			gotResult, gotResponse, err := service.Content(testCase.context, testCase.cql, testCase.options)
+
+			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)
+
+				t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code)
+				assert.Equal(t, gotResponse.Code, testCase.wantHTTPCodeReturn)
+			}
+		})
+	}
+}
+
+func TestSearchService_Users(t *testing.T) {
+
+	testCases := []struct {
+		name               string
+		cql                string
+		start, limit       int
+		expand             []string
+		mockFile           string
+		wantHTTPMethod     string
+		endpoint           string
+		context            context.Context
+		wantHTTPCodeReturn int
+		wantErr            bool
+		expectedError      string
+	}{
+		{
+			name:               "when the parameters are correct",
+			cql:                "type=user",
+			start:              10,
+			limit:              50,
+			expand:             []string{"operations", "personalSpace"},
+			mockFile:           "./mocks/search-content.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/wiki/rest/api/search/user?cql=type%3Duser&expand=operations%2CpersonalSpace&limit=50&start=10",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+		},
+
+		{
+			name:               "when the CQL query is not provided",
+			cql:                "",
+			start:              10,
+			limit:              50,
+			expand:             []string{"operations", "personalSpace"},
+			mockFile:           "./mocks/search-content.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/wiki/rest/api/search/user?cql=type%3Duser&expand=operations%2CpersonalSpace&limit=50&start=10",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "confluence: no CQL query set",
+		},
+
+		{
+			name:               "when the context is not provided",
+			cql:                "type=user",
+			start:              10,
+			limit:              50,
+			expand:             []string{"operations", "personalSpace"},
+			mockFile:           "./mocks/search-content.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/wiki/rest/api/search/user?cql=type%3Duser&expand=operations%2CpersonalSpace&limit=50&start=10",
+			context:            nil,
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "request creation failed: net/http: nil Context",
+		},
+
+		{
+			name:               "when the response status is not provided",
+			cql:                "type=user",
+			start:              10,
+			limit:              50,
+			expand:             []string{"operations", "personalSpace"},
+			mockFile:           "./mocks/search-content.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/wiki/rest/api/search/user?cql=type%3Duser&expand=operations%2CpersonalSpace&limit=50&start=10",
+			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",
+			cql:                "type=user",
+			start:              10,
+			limit:              50,
+			expand:             []string{"operations", "personalSpace"},
+			mockFile:           "./mocks/empty-json.json",
+			wantHTTPMethod:     http.MethodGet,
+			endpoint:           "/wiki/rest/api/search/user?cql=type%3Duser&expand=operations%2CpersonalSpace&limit=50&start=10",
+			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)
+			}
+
+			service := &SearchService{client: mockClient}
+
+			gotResult, gotResponse, err := service.Users(testCase.context, testCase.cql, testCase.start, testCase.limit, testCase.expand)
+
+			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)
+
+				t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code)
+				assert.Equal(t, gotResponse.Code, testCase.wantHTTPCodeReturn)
+			}
+		})
+	}
+}