Repository: ctreminiom/go-atlassian
Pull request #183 (2023-04-01 07:55:30)

:newspaper: Implemented the first Confluence Cloud V2 services
1. Created a new interface called PageConnector.
2. This interface will interacts with the Confluence Cloud V2 space endpoints.
3. Mapped the page structures on the infra package
4. Created the documentation links and associated them with the interface comments.


Patch:
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index abee207..02ebba3 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -34,7 +34,7 @@ jobs:
 
           # Optional: golangci-lint command line arguments.
           # args: --issues-exit-code=0
-          args: --timeout=5m
+          args: --timeout=10m
 
           # Optional: show only new issues if it's a pull request. The default value is `false`.
           # only-new-issues: true
\ No newline at end of file
diff --git a/README.md b/README.md
index dbfa1b7..52c20be 100644
--- a/README.md
+++ b/README.md
@@ -1,30 +1,39 @@
-<p align="center"><img width="600" src="https://user-images.githubusercontent.com/16035390/131232958-022b0382-e6bc-42db-97b6-82fbd190e19a.png"></p>
-
-<p align="center">
-    <a href="https://github.com/ctreminiom/go-atlassian/releases/latest"><img src="https://img.shields.io/github/v/release/ctreminiom/go-atlassian"></a>
-    <a href="https://pkg.go.dev/github.com/ctreminiom/go-atlassian"><img src="https://pkg.go.dev/badge/github.com/ctreminiom/go-atlassian?utm_source=godoc"></a>
-    <a href="https://goreportcard.com/report/github.com/ctreminiom/go-atlassian"><img src="https://goreportcard.com/badge/ctreminiom/go-atlassian"></a>
-    <a href="https://app.fossa.com/projects/git%2Bgithub.com%2Fctreminiom%2Fgo-atlassian?ref=badge_shield" alt="FOSSA Status"><img src="https://app.fossa.com/api/projects/git%2Bgithub.com%2Fctreminiom%2Fgo-atlassian.svg?type=shield"/></a>
-    <a href="https://codecov.io/gh/ctreminiom/go-atlassian"><img src="https://codecov.io/gh/ctreminiom/go-atlassian/branch/main/graph/badge.svg?token=G0KPNMTIRV"></a>
-    <a href="https://www.codacy.com/gh/ctreminiom/go-atlassian/dashboard?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=ctreminiom/go-atlassian&amp;utm_campaign=Badge_Grade"><img src="https://app.codacy.com/project/badge/Grade/fe5c1b3c9fd64f84989ae51c42803456"/></a>
-    <a href="https://github.com/ctreminiom/go-atlassian/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg"></a>
-    <a href="https://github.com/ctreminiom/go-atlassian/actions?query=workflow%3ATesting"><img src="https://img.shields.io/github/workflow/status/ctreminiom/go-atlassian/Testing?label=%F0%9F%A7%AA%20tests&style=flat&color=75C46B"></a>
-    <a href="https://docs.go-atlassian.io/"><img src="https://img.shields.io/badge/%F0%9F%92%A1%20go-documentation-00ACD7.svg?style=flat"></a>
-    <a href="https://bestpractices.coreinfrastructure.org/projects/4861"><img src="https://bestpractices.coreinfrastructure.org/projects/4861/badge"></a> 
-    <a href="https://github.com/avelino/awesome-go#third-party-apis"><img src="https://awesome.re/mentioned-badge-flat.svg"></a> 
-</p>
-
-Communicate with the [Atlassian API's](https://developer.atlassian.com/cloud/) quickly and easily
-with the **go-atlassian** module. With the **go-atlassian** client, you can retrieve and manipulate
-the extensive Atlassian Cloud API's like Jira, Confluence, Jira Agile, Jira Service Management, Atlassian Admin and much more!.
-
-If you find an endpoint not supported, please submit a pull request or raise a feature issue - it's always greatly appreciated.
-
-## Installation
+![Go Atlassian Framework](https://user-images.githubusercontent.com/16035390/131232958-022b0382-e6bc-42db-97b6-82fbd190e19a.png)
+
+[![Releases](https://img.shields.io/github/v/release/ctreminiom/go-atlassian)](https://github.com/ctreminiom/go-atlassian/releases/latest)
+[![Testing](https://github.com/ctreminiom/go-atlassian/actions/workflows/test.yml/badge.svg)](https://github.com/ctreminiom/go-atlassian/actions/workflows/test.yml)
+[![codecov](https://codecov.io/gh/ctreminiom/go-atlassian/branch/main/graph/badge.svg?token=G0KPNMTIRV)](https://codecov.io/gh/ctreminiom/go-atlassian)
+[![Go Reference](https://pkg.go.dev/badge/github.com/ctreminiom/go-atlassian.svg)](https://pkg.go.dev/github.com/ctreminiom/go-atlassian)
+[![Go Report Card](https://goreportcard.com/badge/ctreminiom/go-atlassian)](https://goreportcard.com/report/github.com/ctreminiom/go-atlassian)
+[![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fctreminiom%2Fgo-atlassian.svg?type=shield)](https://app.fossa.com/projects/git%2Bgithub.com%2Fctreminiom%2Fgo-atlassian?ref=badge_shield)
+[![Codacy Badge](https://app.codacy.com/project/badge/Grade/fe5c1b3c9fd64f84989ae51c42803456)](https://app.codacy.com/gh/ctreminiom/go-atlassian/dashboard?utm_source=gh&utm_medium=referral&utm_content=&utm_campaign=Badge_grade)
+![GitHub commits since latest release (by date)](https://img.shields.io/github/commits-since/ctreminiom/go-atlassian/v1.4.4)
+![GitHub](https://img.shields.io/github/license/ctreminiom/go-atlassian)
+[![Mentioned in Awesome Go-Atlassian](https://awesome.re/mentioned-badge-flat.svg)](https://github.com/avelino/awesome-go#third-party-apis)
+[![OpenSSF Best Practices](https://bestpractices.coreinfrastructure.org/projects/4861/badge)](https://bestpractices.coreinfrastructure.org/projects/4861)
+[![Documentation](https://img.shields.io/badge/%F0%9F%92%A1%20go-documentation-00ACD7.svg?style=flat)](https://docs.go-atlassian.io/)
+[![Dependency Review](https://github.com/ctreminiom/go-atlassian/actions/workflows/dependency-review.yml/badge.svg)](https://github.com/ctreminiom/go-atlassian/actions/workflows/dependency-review.yml)
+[![Analysis](https://github.com/ctreminiom/go-atlassian/actions/workflows/codeql-analysis.yml/badge.svg)](https://github.com/ctreminiom/go-atlassian/actions/workflows/codeql-analysis.yml)
+
+
+**go-atlassian** is a Go library that provides a simple and convenient way to interact with various Atlassian products' REST APIs. [Atlassian](https://developer.atlassian.com/cloud/) is a leading provider of software and tools for software development, 
+project management, and collaboration. Some of the products that **go-atlassian** supports include Jira, Confluence, Jira Service Management, and more.
+
+The **go-atlassian** library is designed to simplify the process of building Go applications that interact with Atlassian products. It provides a set of functions and data structures that can be used to easily send HTTP requests to the Atlassian APIs, parse the responses, and work with the data returned.&#x20;
+
+### 🚀Features
+
+- Easy-to-use functions and data structures that abstract away much of the complexity of working with the APIs.
+- Comprehensive support for various Atlassian products' APIs.
+- Support for common operations like creating, updating, and deleting entities in Atlassian products.
+- Active development and maintenance by the community, with regular updates and bug fixes.
+- Comprehensive [documentation](https://docs.go-atlassian.io/jira-software-cloud/introduction) and examples to help developers get started with using the library.
+
+### 📁 Installation
 
 If you do not have [Go](https://golang.org/) installed yet, you can find installation instructions
 [here](https://golang.org/doc/install). Please note that the package requires Go version
-1.13 or later for module support.
+1.17 or later for module support.
 
 To pull the most recent version of **go-atlassian**, use `go get`.
 
@@ -32,6 +41,7 @@ To pull the most recent version of **go-atlassian**, use `go get`.
 go get github.com/ctreminiom/go-atlassian
 ```
 
+### 📪 Packages
 Then import the package into your project as you normally would. You can import the following packages
 
 |Package|import path  |
@@ -41,30 +51,27 @@ Then import the package into your project as you normally would. You can import
 |Jira Agile|`github.com/ctreminiom/go-atlassian/jira/agile`|
 |Jira ITSM|`github.com/ctreminiom/go-atlassian/jira/sm`|
 |Confluence|`github.com/ctreminiom/go-atlassian/confluence`|
+|Confluence V2|`github.com/ctreminiom/go-atlassian/confluence/v2`|
 |Cloud Admin|`github.com/ctreminiom/go-atlassian/admin`|
 
-Now you're ready to Go.
+Now you're ready to Go 😁.
 
-### 🧳 Creating A Client
+### 🔨 Usage
 
-Before using the **go-atlassian** package, you need to have an Atlassian API key. If you do
-not have a key yet, you can sign up [here](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/).
+Before using the **go-atlassian** package, you need to have an Atlassian API key. If you do not have a key yet, you can sign up [here](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/).
 
-Create a client with your instance host and access token to start communicating with the Atlassian API's.
+Create a client with your instance host and access token to start communicating with the Atlassian API's. In this example, we're going to instance a new Confluence Cloud client.
 
 ```go
 instance, err := confluence.New(nil, "INSTANCE_HOST")
 if err != nil {
     log.Fatal(err)
 }
-
 instance.Auth.SetBasicAuth("YOUR_CLIENT_MAIL", "YOUR_APP_ACCESS_TOKEN")
 ```
+If you need to use a preconfigured HTTP client, simply pass its address to the `New` function.
 
-If you need to use a preconfigured HTTP client, simply pass its address to the
-`New` function.
-
-```go  
+```go
 transport := http.Transport{
 	Proxy: http.ProxyFromEnvironment,
 	Dial: (&net.Dialer{
@@ -74,97 +81,107 @@ transport := http.Transport{
 	}).Dial,
 	TLSHandshakeTimeout: 10 * time.Second,
 }
-
 client := http.Client{
 	Transport: &transport,
 	Timeout:   4 * time.Second,
 }
-
 instance, err := confluence.New(&client, "INSTANCE_HOST")
 if err != nil {
 	log.Fatal(err)
 }
-
 instance.Auth.SetBasicAuth("YOUR_CLIENT_MAIL", "YOUR_APP_ACCESS_TOKEN")
 ```
 
-### 🗺️ Services
+### 🌍 Services
+
+The library uses the services interfaces to provide a modular and flexible way to interact with Atlassian products' REST APIs. It defines a set of services interfaces that define the functionality of each API, and then provides implementations of those interfaces that can be used to interact with the APIs.
+
+```go
+// BoardConnector represents the Jira boards.
+// Use it to search, get, create, delete, and change boards.
+type BoardConnector interface {
+	Get(ctx context.Context, boardID int) (*model.BoardScheme, *model.ResponseScheme, error)
+	Create(ctx context.Context, payload *model.BoardPayloadScheme) (*model.BoardScheme, *model.ResponseScheme, error)
+	Filter(ctx context.Context, filterID, startAt, maxResults int) (*model.BoardPageScheme, *model.ResponseScheme, error)
+	Backlog(ctx context.Context, boardID int, opts *model.IssueOptionScheme, startAt, maxResults int) (*model.BoardIssuePageScheme, *model.ResponseScheme, error)
+	Configuration(ctx context.Context, boardID int) (*model.BoardConfigurationScheme, *model.ResponseScheme, error)
+	Epics(ctx context.Context, boardID, startAt, maxResults int, done bool) (*model.BoardEpicPageScheme, *model.ResponseScheme, error)
+	IssuesWithoutEpic(ctx context.Context, boardID int, opts *model.IssueOptionScheme, startAt, maxResults int) (
+		*model.BoardIssuePageScheme, *model.ResponseScheme, error)
+	IssuesByEpic(ctx context.Context, boardID, epicID int, opts *model.IssueOptionScheme, startAt, maxResults int) (
+		*model.BoardIssuePageScheme, *model.ResponseScheme, error)
+	Issues(ctx context.Context, boardID int, opts *model.IssueOptionScheme, startAt, maxResults int) (*model.BoardIssuePageScheme,
+		*model.ResponseScheme, error)
+	Move(ctx context.Context, boardID int, payload *model.BoardMovementPayloadScheme) (*model.ResponseScheme, error)
+	Projects(ctx context.Context, boardID, startAt, maxResults int) (*model.BoardProjectPageScheme, *model.ResponseScheme, error)
+	Sprints(ctx context.Context, boardID, startAt, maxResults int, states []string) (*model.BoardSprintPageScheme,
+		*model.ResponseScheme, error)
+	IssuesBySprint(ctx context.Context, boardID, sprintID int, opts *model.IssueOptionScheme, startAt, maxResults int) (
+		*model.BoardIssuePageScheme, *model.ResponseScheme, error)
+	Versions(ctx context.Context, boardID, startAt, maxResults int, released bool) (*model.BoardVersionPageScheme,
+		*model.ResponseScheme, error)
+	Delete(ctx context.Context, boardID int) (*model.ResponseScheme, error)
+	Gets(ctx context.Context, opts *model.GetBoardsOptions, startAt, maxResults int) (*model.BoardPageScheme,
+		*model.ResponseScheme, error)
+}
+```
 
-The client contains a distinct service for working with each of the Atlassian API's
-endpoints. Each service has a set of service functions that make specific API
-calls to their respective endpoint.
+Each service interface includes a set of methods that correspond to the available endpoints in the corresponding API. For example, the `IssueService` interface includes methods like `Create`, `Update`, and `Get` that correspond to the `POST`, `PUT`, and `GET` endpoints in the Jira Issues API.
 
-To start communicating with the **go-atlassian**, choose a service and call its service
-function. Take the Jira service for example.
+### 🎉 Implementation
 
-To get the issue with the transitions, use the **Issue** service function.
-```go  
+Behind the scenes, the `Create` method on the `IssueService` interface is implemented by the `issueService.Create` function in the go-atlassian library. This function sends an HTTP request to the relevant endpoint in the Jira Issues API, using the credentials and configuration provided by the client, and then parses the response into a usable format.
+
+Here's a little example about how to get the issue transitions using the Issue service.
+
+```go
 ctx := context.Background()
 issueKey := "KP-2"
 expand := []string{"transitions"}
-
 issue, response, err := atlassian.Issue.Get(ctx,issueKey, nil, expand)
 if err != nil {
 	log.Fatal(err)
 }
-
 log.Println(issue.Key)
-
 for _, transition := range issue.Transitions {
 	log.Println(transition.Name, transition.ID, transition.To.ID, transition.HasScreen)
 }
 ```
 
-To search issues using a JQL query, use the **Issue.Search** service function.
-```go  
-var (
-	jql    = "order by created DESC"
-	fields = []string{"status"}
-	expand = []string{"changelog", "renderedFields", "names", "schema", "transitions", "operations", "editmeta"}
-)
-
-issues, response, err := atlassian.Issue.Search.Post(context.Background(), jql, fields, expand, 0, 50, "")
-if err != nil {
-	log.Fatal(err)
-}
-
-log.Println("HTTP Endpoint Used", response.Endpoint)
-log.Println(issues.Total)
-```
-
-The rest of the service functions work much the same way; they are concise and
-behave as you would expect. The [documentation](https://docs.go-atlassian.io/)
-contains several examples on how to use each service function.
+The rest of the service functions work much the same way; they are concise and behave as you would expect. The [documentation](https://docs.go-atlassian.io/) contains several examples on how to use each service function.
 
 ## ✍️ Contributions
 
-If you would like to contribute to this project, please adhere to the following
-guidelines.
+If you would like to contribute to this project, please adhere to the following guidelines.
 
 * Submit an issue describing the problem.
 * Fork the repo and add your contribution.
-* Add appropriate tests.
-* Run go fmt, go vet, and golint.
-* Prefer idiomatic Go over non-idiomatic code.
 * Follow the basic Go conventions found [here](https://github.com/golang/go/wiki/CodeReviewComments).
-* If in doubt, try to match your code to the current codebase.
 * Create a pull request with a description of your changes.
 
 Again, contributions are greatly appreciated!
 
 
 ## 💡 Inspiration
-The project was created with the purpose to provide a unique point to provide an interface for interacting with Atlassian products. This module is highly inspired by the Go library https://github.com/andygrunwald/go-jira
-but focused on Cloud solutions.
+The project was created with the purpose to provide a unique point to provide an interface for interacting with Atlassian products. 
+
+This module is highly inspired by the Go library https://github.com/andygrunwald/go-jira
+but focused on Cloud solutions. 
+
+The library shares many similarities with go-jira, including its use of service interfaces to define the functionality of each API, its modular and flexible approach to working with Atlassian products' API's. 
+However, go-atlassian also adds several new features and improvements that are not present in go-jira.
+
+Despite these differences, go-atlassian remains heavily inspired by go-jira, and many of the core design principles and patterns used in go-jira can be found in go-atlassian as well.
 
 ## 📝 License
-Copyright © 2021 [Carlos Treminio](https://github.com/ctreminiom).
+Copyright © 2023 [Carlos Treminio](https://github.com/ctreminiom).
 This project is [MIT](https://opensource.org/licenses/MIT) licensed.
 
 [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fctreminiom%2Fgo-atlassian.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fctreminiom%2Fgo-atlassian?ref=badge_large)
 
 ## 🤝 Special Thanks
-In addition to all the contributors we would like to thanks to these companies:
+
+We would like to extend our sincere thanks to the following sponsors for their generous support:
 -   [Atlassian](https://www.atlassian.com/)  for providing us Atlassian Admin/Jira/Confluence Standard licenses.
 -   [JetBrains](https://www.jetbrains.com/)  for providing us with free licenses of  [GoLand](https://www.jetbrains.com/pycharm/)
 -   [GitBook](https://www.gitbook.com/)  for providing us non-profit / open-source plan so hence I would like to express my thanks here.
diff --git a/confluence/internal/page_impl.go b/confluence/internal/page_impl.go
new file mode 100644
index 0000000..5a117dd
--- /dev/null
+++ b/confluence/internal/page_impl.go
@@ -0,0 +1,294 @@
+package internal
+
+import (
+	"context"
+	"fmt"
+	model "github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service"
+	"github.com/ctreminiom/go-atlassian/service/confluence"
+	"net/http"
+	"net/url"
+	"strconv"
+)
+
+func NewPageService(client service.Client) *PageService {
+	return &PageService{
+		internalClient: &internalPageImpl{c: client},
+	}
+}
+
+type PageService struct {
+	internalClient confluence.PageConnector
+}
+
+// Get returns a specific page.
+//
+// GET /wiki/api/v2/pages/{id}
+//
+// https://docs.go-atlassian.io/confluence-cloud/v2/page#get-page-by-id
+func (p *PageService) Get(ctx context.Context, pageId int, format string, draft bool, version int) (*model.PageScheme, *model.ResponseScheme, error) {
+	return p.internalClient.Get(ctx, pageId, format, draft, version)
+}
+
+// Bulk returns all pages.
+//
+// # The number of results is limited by the limit parameter and additional results
+//
+// (if available) will be available through the next cursor
+//
+// GET /wiki/api/v2/pages
+//
+// https://docs.go-atlassian.io/confluence-cloud/v2/page#get-pages
+func (p *PageService) Bulk(ctx context.Context, cursor string, limit int) (*model.PageChunkScheme, *model.ResponseScheme, error) {
+	return p.internalClient.Bulk(ctx, cursor, limit)
+}
+
+// GetsByLabel returns the pages of specified label.
+//
+// # The number of results is limited by the limit parameter and additional results
+//
+// (if available) will be available through the next cursor
+//
+// GET /wiki/api/v2/labels/{id}/pages
+//
+// https://docs.go-atlassian.io/confluence-cloud/v2/page#get-pages-for-label
+func (p *PageService) GetsByLabel(ctx context.Context, labelId int, sort, cursor string, limit int) (*model.PageChunkScheme, *model.ResponseScheme, error) {
+	return p.internalClient.GetsByLabel(ctx, labelId, sort, cursor, limit)
+}
+
+// GetsBySpace returns all pages in a space.
+//
+// The number of results is limited by the limit parameter and additional results (if available)
+//
+// will be available through the next cursor
+//
+// GET /wiki/api/v2/spaces/{id}/pages
+//
+// https://docs.go-atlassian.io/confluence-cloud/v2/page#get-pages-in-space
+func (p *PageService) GetsBySpace(ctx context.Context, spaceId int, cursor string, limit int) (*model.PageChunkScheme, *model.ResponseScheme, error) {
+	return p.internalClient.GetsBySpace(ctx, spaceId, cursor, limit)
+}
+
+// Create creates a page in the space.
+//
+// Pages are created as published by default unless specified as a draft in the status field.
+//
+// If creating a published page, the title must be specified.
+//
+// POST /wiki/api/v2/pages
+//
+// https://docs.go-atlassian.io/confluence-cloud/v2/page#create-page
+func (p *PageService) Create(ctx context.Context, payload *model.PageCreatePayloadScheme) (*model.PageScheme, *model.ResponseScheme, error) {
+	return p.internalClient.Create(ctx, payload)
+}
+
+// Update updates a page by id.
+//
+// PUT /wiki/api/v2/pages/{id}
+//
+// https://docs.go-atlassian.io/confluence-cloud/v2/page#update-page
+func (p *PageService) Update(ctx context.Context, pageId int, payload *model.PageUpdatePayloadScheme) (*model.PageScheme, *model.ResponseScheme, error) {
+	return p.internalClient.Update(ctx, pageId, payload)
+}
+
+// Delete deletes a page by id.
+//
+// DELETE /wiki/api/v2/pages/{id}
+//
+// https://docs.go-atlassian.io/confluence-cloud/v2/page#delete-page
+func (p *PageService) Delete(ctx context.Context, pageId int) (*model.ResponseScheme, error) {
+	return p.internalClient.Delete(ctx, pageId)
+}
+
+type internalPageImpl struct {
+	c service.Client
+}
+
+func (i *internalPageImpl) Get(ctx context.Context, pageId int, format string, draft bool, version int) (*model.PageScheme, *model.ResponseScheme, error) {
+
+	if pageId == 0 {
+		return nil, nil, model.ErrNoPageIDError
+	}
+
+	query := url.Values{}
+
+	if format != "" {
+		query.Add("body-format", format)
+	}
+
+	if draft {
+		query.Add("get-draft", "true")
+	}
+
+	if version != 0 {
+		query.Add("version", strconv.Itoa(version))
+	}
+
+	endpoint := fmt.Sprintf("wiki/api/v2/pages/%v?%v", pageId, query.Encode())
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	page := new(model.PageScheme)
+	response, err := i.c.Call(request, page)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return page, response, nil
+}
+
+func (i *internalPageImpl) Bulk(ctx context.Context, cursor string, limit int) (*model.PageChunkScheme, *model.ResponseScheme, error) {
+
+	query := url.Values{}
+	query.Add("limit", strconv.Itoa(limit))
+
+	if cursor != "" {
+		query.Add("cursor", cursor)
+	}
+
+	endpoint := fmt.Sprintf("wiki/api/v2/pages?%v", query.Encode())
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	chunk := new(model.PageChunkScheme)
+	response, err := i.c.Call(request, chunk)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return chunk, response, nil
+}
+
+func (i *internalPageImpl) GetsByLabel(ctx context.Context, labelId int, sort, cursor string, limit int) (*model.PageChunkScheme, *model.ResponseScheme, error) {
+
+	if labelId == 0 {
+		return nil, nil, model.ErrNoLabelIDError
+	}
+
+	query := url.Values{}
+	query.Add("limit", strconv.Itoa(limit))
+
+	if cursor != "" {
+		query.Add("cursor", cursor)
+	}
+
+	if sort != "" {
+		query.Add("sort", sort)
+	}
+
+	endpoint := fmt.Sprintf("wiki/api/v2/labels/%v/pages?%v", labelId, query.Encode())
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	chunk := new(model.PageChunkScheme)
+	response, err := i.c.Call(request, chunk)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return chunk, response, nil
+}
+
+func (i *internalPageImpl) GetsBySpace(ctx context.Context, spaceId int, cursor string, limit int) (*model.PageChunkScheme, *model.ResponseScheme, error) {
+
+	if spaceId == 0 {
+		return nil, nil, model.ErrNoSpaceIDError
+	}
+
+	query := url.Values{}
+	query.Add("limit", strconv.Itoa(limit))
+
+	if cursor != "" {
+		query.Add("cursor", cursor)
+	}
+
+	endpoint := fmt.Sprintf("wiki/api/v2/spaces/%v/pages?%v", spaceId, query.Encode())
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	chunk := new(model.PageChunkScheme)
+	response, err := i.c.Call(request, chunk)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return chunk, response, nil
+}
+
+func (i *internalPageImpl) Create(ctx context.Context, payload *model.PageCreatePayloadScheme) (*model.PageScheme, *model.ResponseScheme, error) {
+
+	reader, err := i.c.TransformStructToReader(payload)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	endpoint := "wiki/api/v2/pages"
+
+	request, err := i.c.NewRequest(ctx, http.MethodPost, endpoint, reader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	page := new(model.PageScheme)
+	response, err := i.c.Call(request, page)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return page, response, nil
+}
+
+func (i *internalPageImpl) Update(ctx context.Context, pageId int, payload *model.PageUpdatePayloadScheme) (*model.PageScheme, *model.ResponseScheme, error) {
+
+	if pageId == 0 {
+		return nil, nil, model.ErrNoPageIDError
+	}
+
+	reader, err := i.c.TransformStructToReader(payload)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	endpoint := fmt.Sprintf("wiki/api/v2/pages/%v", pageId)
+
+	request, err := i.c.NewRequest(ctx, http.MethodPut, endpoint, reader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	page := new(model.PageScheme)
+	response, err := i.c.Call(request, page)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return page, response, nil
+}
+
+func (i *internalPageImpl) Delete(ctx context.Context, pageId int) (*model.ResponseScheme, error) {
+
+	if pageId == 0 {
+		return nil, model.ErrNoPageIDError
+	}
+
+	endpoint := fmt.Sprintf("wiki/api/v2/pages/%v", pageId)
+
+	request, err := i.c.NewRequest(ctx, http.MethodDelete, endpoint, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return i.c.Call(request, nil)
+}
diff --git a/confluence/internal/search_impl.go b/confluence/internal/search_impl.go
index 9f8667e..2bcff00 100644
--- a/confluence/internal/search_impl.go
+++ b/confluence/internal/search_impl.go
@@ -99,12 +99,6 @@ func (i *internalSearchImpl) Content(ctx context.Context, cql string, options *m
 		if options.SitePermissionTypeFilter != "" {
 			query.Add("sitePermissionTypeFilter", options.SitePermissionTypeFilter)
 		}
-
-		if len(options.Expand) > 0 {
-			for _, value := range options.Expand {
-				query.Add("expand", value)
-			}
-		}
 	}
 
 	endpoint := fmt.Sprintf("wiki/rest/api/search?%v", query.Encode())
diff --git a/confluence/v2/api_client_impl.go b/confluence/v2/api_client_impl.go
new file mode 100644
index 0000000..2133671
--- /dev/null
+++ b/confluence/v2/api_client_impl.go
@@ -0,0 +1,167 @@
+package v2
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"github.com/ctreminiom/go-atlassian/confluence/internal"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service/common"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"reflect"
+	"strings"
+)
+
+func New(httpClient common.HttpClient, site string) (*Client, error) {
+
+	if httpClient == nil {
+		httpClient = http.DefaultClient
+	}
+
+	if !strings.HasSuffix(site, "/") {
+		site += "/"
+	}
+
+	siteAsURL, err := url.Parse(site)
+	if err != nil {
+		return nil, err
+	}
+
+	client := &Client{
+		HTTP: httpClient,
+		Site: siteAsURL,
+	}
+
+	client.Auth = internal.NewAuthenticationService(client)
+	client.Page = internal.NewPageService(client)
+
+	return client, nil
+}
+
+type Client struct {
+	HTTP common.HttpClient
+	Site *url.URL
+	Auth common.Authentication
+	Page *internal.PageService
+}
+
+func (c *Client) NewFormRequest(ctx context.Context, method, apiEndpoint, contentType string, payload io.Reader) (*http.Request, error) {
+
+	relativePath, err := url.Parse(apiEndpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	var endpoint = c.Site.ResolveReference(relativePath).String()
+
+	request, err := http.NewRequestWithContext(ctx, method, endpoint, payload)
+	if err != nil {
+		return nil, err
+	}
+
+	request.Header.Add("Content-Type", contentType)
+	request.Header.Add("Accept", "application/json")
+	request.Header.Set("X-Atlassian-Token", "no-check")
+
+	if c.Auth.HasBasicAuth() {
+		request.SetBasicAuth(c.Auth.GetBasicAuth())
+	}
+
+	if c.Auth.HasUserAgent() {
+		request.Header.Set("User-Agent", c.Auth.GetUserAgent())
+	}
+
+	return request, nil
+}
+
+func (c *Client) NewRequest(ctx context.Context, method, apiEndpoint string, payload io.Reader) (*http.Request, error) {
+
+	relativePath, err := url.Parse(apiEndpoint)
+	if err != nil {
+		return nil, err
+	}
+
+	var endpoint = c.Site.ResolveReference(relativePath).String()
+
+	request, err := http.NewRequestWithContext(ctx, method, endpoint, payload)
+	if err != nil {
+		return nil, err
+	}
+
+	request.Header.Set("Accept", "application/json")
+
+	if payload != nil {
+		request.Header.Set("Content-Type", "application/json")
+	}
+
+	if c.Auth.HasBasicAuth() {
+		request.SetBasicAuth(c.Auth.GetBasicAuth())
+	}
+
+	if c.Auth.HasUserAgent() {
+		request.Header.Set("User-Agent", c.Auth.GetUserAgent())
+	}
+
+	return request, nil
+}
+
+func (c *Client) Call(request *http.Request, structure interface{}) (*models.ResponseScheme, error) {
+
+	response, err := c.HTTP.Do(request)
+	if err != nil {
+		return nil, err
+	}
+
+	return c.TransformTheHTTPResponse(response, structure)
+}
+
+func (c *Client) TransformTheHTTPResponse(response *http.Response, structure interface{}) (*models.ResponseScheme, error) {
+
+	responseTransformed := &models.ResponseScheme{
+		Response: response,
+		Code:     response.StatusCode,
+		Endpoint: response.Request.URL.String(),
+		Method:   response.Request.Method,
+	}
+
+	responseAsBytes, err := ioutil.ReadAll(response.Body)
+	if err != nil {
+		return responseTransformed, err
+	}
+
+	responseTransformed.Bytes.Write(responseAsBytes)
+
+	var wasSuccess = response.StatusCode >= 200 && response.StatusCode < 300
+	if !wasSuccess {
+		return responseTransformed, models.ErrInvalidStatusCodeError
+	}
+
+	if structure != nil {
+		if err = json.Unmarshal(responseAsBytes, &structure); err != nil {
+			return responseTransformed, err
+		}
+	}
+
+	return responseTransformed, nil
+}
+
+func (c *Client) TransformStructToReader(structure interface{}) (io.Reader, error) {
+
+	if structure == nil {
+		return nil, models.ErrNilPayloadError
+	}
+
+	if reflect.ValueOf(structure).Type().Kind() == reflect.Struct {
+		return nil, models.ErrNonPayloadPointerError
+	}
+
+	structureAsBodyBytes, err := json.Marshal(structure)
+	if err != nil {
+		return nil, err
+	}
+
+	return bytes.NewReader(structureAsBodyBytes), nil
+}
diff --git a/pkg/infra/models/confluence_page.go b/pkg/infra/models/confluence_page.go
new file mode 100644
index 0000000..2d9a3e6
--- /dev/null
+++ b/pkg/infra/models/confluence_page.go
@@ -0,0 +1,61 @@
+package models
+
+type PageChunkScheme struct {
+	Results []*PageScheme         `json:"results,omitempty"`
+	Links   *PageChunkLinksScheme `json:"_links,omitempty"`
+}
+
+type PageChunkLinksScheme struct {
+	Next string `json:"next,omitempty"`
+}
+
+type PageScheme struct {
+	ID        int                `json:"id,omitempty"`
+	Status    string             `json:"status,omitempty"`
+	Title     string             `json:"title,omitempty"`
+	SpaceID   int                `json:"spaceId,omitempty"`
+	ParentID  int                `json:"parentId,omitempty"`
+	AuthorID  string             `json:"authorId,omitempty"`
+	CreatedAt string             `json:"createdAt,omitempty"`
+	Version   *PageVersionScheme `json:"version,omitempty"`
+	Body      *PageBodyScheme    `json:"body,omitempty"`
+}
+
+type PageVersionScheme struct {
+	CreatedAt string `json:"createdAt,omitempty"`
+	Message   string `json:"message,omitempty"`
+	Number    int    `json:"number,omitempty"`
+	MinorEdit bool   `json:"minorEdit,omitempty"`
+	AuthorID  string `json:"authorId,omitempty"`
+}
+
+type PageBodyScheme struct {
+	Storage        *PageBodyRepresentationScheme `json:"storage,omitempty"`
+	AtlasDocFormat *PageBodyRepresentationScheme `json:"atlas_doc_format,omitempty"`
+}
+
+type PageCreatePayloadScheme struct {
+	SpaceID int                           `json:"spaceId,omitempty"`
+	Status  string                        `json:"status,omitempty"`
+	Title   string                        `json:"title,omitempty"`
+	Body    *PageBodyRepresentationScheme `json:"body,omitempty"`
+}
+
+type PageBodyRepresentationScheme struct {
+	Representation string `json:"representation,omitempty"`
+	Value          string `json:"value,omitempty"`
+}
+
+type PageUpdatePayloadScheme struct {
+	ID      int                             `json:"id,omitempty"`
+	Status  string                          `json:"status,omitempty"`
+	Title   string                          `json:"title,omitempty"`
+	SpaceID int                             `json:"spaceId,omitempty"`
+	Body    *PageBodyRepresentationScheme   `json:"body,omitempty"`
+	Version *PageUpdatePayloadVersionScheme `json:"version,omitempty"`
+}
+
+type PageUpdatePayloadVersionScheme struct {
+	Number  int    `json:"number,omitempty"`
+	Message string `json:"message,omitempty"`
+}
diff --git a/pkg/infra/models/errors.go b/pkg/infra/models/errors.go
index b209699..e32418c 100644
--- a/pkg/infra/models/errors.go
+++ b/pkg/infra/models/errors.go
@@ -36,6 +36,9 @@ var (
 	ErrNoContentAttachmentNameError        = errors.New("confluence: no attachment filename set")
 	ErrNoContentReaderError                = errors.New("confluence: no reader set")
 	ErrNoContentIDError                    = errors.New("confluence: no content id set")
+	ErrNoPageIDError                       = errors.New("confluence: no page id set")
+	ErrNoSpaceIDError                      = errors.New("confluence: no space id set")
+	ErrNoLabelIDError                      = errors.New("confluence: no label id set")
 	ErrNoCQLError                          = errors.New("confluence: no CQL query set")
 	ErrNoContentTypeError                  = errors.New("confluence: no content type set")
 	ErrInvalidContentTypeError             = errors.New("confluence: invalid content type: (page, comment, attachment)")
diff --git a/service/confluence/page.go b/service/confluence/page.go
new file mode 100644
index 0000000..32928b3
--- /dev/null
+++ b/service/confluence/page.go
@@ -0,0 +1,76 @@
+package confluence
+
+import (
+	"context"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+)
+
+// PageConnector represents the Confluence Cloud Pages.
+// Use it to search, get, create, delete, and change pages.
+type PageConnector interface {
+
+	// Get returns a specific page.
+	//
+	// GET /wiki/api/v2/pages/{id}
+	//
+	// https://docs.go-atlassian.io/confluence-cloud/v2/page#get-page-by-id
+	Get(ctx context.Context, pageId int, format string, draft bool, version int) (*models.PageScheme, *models.ResponseScheme, error)
+
+	// Bulk returns all pages.
+	//
+	// The number of results is limited by the limit parameter and additional results
+	//
+	// (if available) will be available through the next cursor
+	//
+	// GET /wiki/api/v2/pages
+	//
+	// https://docs.go-atlassian.io/confluence-cloud/v2/page#get-pages
+	Bulk(ctx context.Context, cursor string, limit int) (*models.PageChunkScheme, *models.ResponseScheme, error)
+
+	// GetsByLabel returns the pages of specified label.
+	//
+	// The number of results is limited by the limit parameter and additional results
+	//
+	// (if available) will be available through the next cursor
+	//
+	// GET /wiki/api/v2/labels/{id}/pages
+	//
+	// https://docs.go-atlassian.io/confluence-cloud/v2/page#get-pages-for-label
+	GetsByLabel(ctx context.Context, labelId int, sort, cursor string, limit int) (*models.PageChunkScheme, *models.ResponseScheme, error)
+
+	// GetsBySpace returns all pages in a space.
+	//
+	// The number of results is limited by the limit parameter and additional results (if available)
+	//
+	// will be available through the next cursor
+	//
+	// GET /wiki/api/v2/spaces/{id}/pages
+	//
+	// https://docs.go-atlassian.io/confluence-cloud/v2/page#get-pages-in-space
+	GetsBySpace(ctx context.Context, spaceId int, cursor string, limit int) (*models.PageChunkScheme, *models.ResponseScheme, error)
+
+	// Create creates a page in the space.
+	//
+	// Pages are created as published by default unless specified as a draft in the status field.
+	//
+	// If creating a published page, the title must be specified.
+	//
+	// POST /wiki/api/v2/pages
+	//
+	// https://docs.go-atlassian.io/confluence-cloud/v2/page#create-page
+	Create(ctx context.Context, payload *models.PageCreatePayloadScheme) (*models.PageScheme, *models.ResponseScheme, error)
+
+	// Update updates a page by id.
+	//
+	// PUT /wiki/api/v2/pages/{id}
+	//
+	// https://docs.go-atlassian.io/confluence-cloud/v2/page#update-page
+	Update(ctx context.Context, pageId int, payload *models.PageUpdatePayloadScheme) (*models.PageScheme, *models.ResponseScheme, error)
+
+	// Delete deletes a page by id.
+	//
+	// DELETE /wiki/api/v2/pages/{id}
+	//
+	// https://docs.go-atlassian.io/confluence-cloud/v2/page#delete-page
+	Delete(ctx context.Context, pageId int) (*models.ResponseScheme, error)
+}


Test patch:
diff --git a/confluence/internal/page_impl_test.go b/confluence/internal/page_impl_test.go
new file mode 100644
index 0000000..d5c0d94
--- /dev/null
+++ b/confluence/internal/page_impl_test.go
@@ -0,0 +1,915 @@
+package internal
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	model "github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service"
+	"github.com/ctreminiom/go-atlassian/service/mocks"
+	"github.com/stretchr/testify/assert"
+	"log"
+	"net/http"
+	"testing"
+)
+
+func Test_internalPageImpl_Get(t *testing.T) {
+
+	type fields struct {
+		c service.Client
+	}
+
+	type args struct {
+		ctx     context.Context
+		pageId  int
+		format  string
+		draft   bool
+		version int
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				ctx:     context.TODO(),
+				pageId:  200001,
+				format:  "atlas_doc_format",
+				draft:   true,
+				version: 2,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/pages/200001?body-format=atlas_doc_format&get-draft=true&version=2",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.PageScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:     context.TODO(),
+				pageId:  200001,
+				format:  "atlas_doc_format",
+				draft:   true,
+				version: 2,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/pages/200001?body-format=atlas_doc_format&get-draft=true&version=2",
+					nil).
+					Return(&http.Request{}, errors.New("error, unable to create the http request"))
+
+				fields.c = client
+
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to create the http request"),
+		},
+
+		{
+			name: "when the page id is not provided",
+			args: args{
+				ctx: context.TODO(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoPageIDError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewPageService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.Get(testCase.args.ctx, testCase.args.pageId, testCase.args.format,
+				testCase.args.draft, testCase.args.version)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+			}
+
+		})
+	}
+}
+
+func Test_internalPageImpl_Bulk(t *testing.T) {
+
+	type fields struct {
+		c service.Client
+	}
+
+	type args struct {
+		ctx    context.Context
+		cursor string
+		limit  int
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				ctx:    context.TODO(),
+				cursor: "cursor-sample",
+				limit:  200,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/pages?cursor=cursor-sample&limit=200",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.PageChunkScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:    context.TODO(),
+				cursor: "cursor-sample",
+				limit:  200,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/pages?cursor=cursor-sample&limit=200",
+					nil).
+					Return(&http.Request{}, errors.New("error, unable to create the http request"))
+
+				fields.c = client
+
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to create the http request"),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewPageService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.Bulk(testCase.args.ctx, testCase.args.cursor, testCase.args.limit)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+			}
+
+		})
+	}
+}
+
+func Test_internalPageImpl_GetsByLabel(t *testing.T) {
+
+	type fields struct {
+		c service.Client
+	}
+
+	type args struct {
+		ctx     context.Context
+		labelId int
+		sort    string
+		cursor  string
+		limit   int
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				ctx:     context.TODO(),
+				labelId: 20001,
+				sort:    "test-label",
+				cursor:  "cursor-sample",
+				limit:   200,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/labels/20001/pages?cursor=cursor-sample&limit=200&sort=test-label",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.PageChunkScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the label id is not provided",
+			args: args{
+				ctx: context.TODO(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoLabelIDError,
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:     context.TODO(),
+				labelId: 20001,
+				sort:    "test-label",
+				cursor:  "cursor-sample",
+				limit:   200,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/labels/20001/pages?cursor=cursor-sample&limit=200&sort=test-label",
+					nil).
+					Return(&http.Request{}, errors.New("error, unable to create the http request"))
+
+				fields.c = client
+
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to create the http request"),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewPageService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.GetsByLabel(testCase.args.ctx, testCase.args.labelId,
+				testCase.args.sort, testCase.args.cursor, testCase.args.limit)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+			}
+
+		})
+	}
+}
+
+func Test_internalPageImpl_GetsBySpace(t *testing.T) {
+
+	type fields struct {
+		c service.Client
+	}
+
+	type args struct {
+		ctx     context.Context
+		spaceId int
+		cursor  string
+		limit   int
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				ctx:     context.TODO(),
+				spaceId: 20001,
+				cursor:  "cursor-sample",
+				limit:   200,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/spaces/20001/pages?cursor=cursor-sample&limit=200",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.PageChunkScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the space id is not provided",
+			args: args{
+				ctx: context.TODO(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoSpaceIDError,
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:     context.TODO(),
+				spaceId: 20001,
+				cursor:  "cursor-sample",
+				limit:   200,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/spaces/20001/pages?cursor=cursor-sample&limit=200",
+					nil).
+					Return(&http.Request{}, errors.New("error, unable to create the http request"))
+
+				fields.c = client
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to create the http request"),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewPageService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.GetsBySpace(testCase.args.ctx, testCase.args.spaceId,
+				testCase.args.cursor, testCase.args.limit)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+			}
+
+		})
+	}
+}
+
+func Test_internalPageImpl_Delete(t *testing.T) {
+
+	type fields struct {
+		c service.Client
+	}
+
+	type args struct {
+		ctx    context.Context
+		pageId int
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				ctx:    context.TODO(),
+				pageId: 200001,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"wiki/api/v2/pages/200001",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:    context.TODO(),
+				pageId: 200001,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"wiki/api/v2/pages/200001",
+					nil).
+					Return(&http.Request{}, errors.New("error, unable to create the http request"))
+
+				fields.c = client
+
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to create the http request"),
+		},
+
+		{
+			name: "when the page id is not provided",
+			args: args{
+				ctx: context.TODO(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoPageIDError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewPageService(testCase.fields.c)
+
+			gotResponse, err := newService.Delete(testCase.args.ctx, testCase.args.pageId)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+			}
+		})
+	}
+}
+
+func Test_internalPageImpl_Create(t *testing.T) {
+
+	//Create the ADF body
+	mockedBody := model.CommentNodeScheme{}
+	mockedBody.Version = 1
+	mockedBody.Type = "doc"
+
+	mockedBody.AppendNode(&model.CommentNodeScheme{
+		Type: "paragraph",
+		Content: []*model.CommentNodeScheme{
+			{
+				Type: "status",
+				Attrs: map[string]interface{}{
+					"color":   "neutral",
+					"style":   "bold",
+					"text":    "Status neutral status",
+					"localId": "3378f5db-c151-4ef2-aec4-56c18a1fbd96",
+				},
+			},
+		},
+	})
+
+	mockedBodyValue, err := json.Marshal(&mockedBody)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	mockedPayload := &model.PageCreatePayloadScheme{
+		SpaceID: 203718658,
+		Status:  "current",
+		Title:   "Page create title test",
+		Body: &model.PageBodyRepresentationScheme{
+			Representation: "atlas_doc_format",
+			Value:          string(mockedBodyValue),
+		},
+	}
+
+	type fields struct {
+		c service.Client
+	}
+
+	type args struct {
+		ctx     context.Context
+		payload *model.PageCreatePayloadScheme
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				ctx:     context.TODO(),
+				payload: mockedPayload,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					mockedPayload).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"wiki/api/v2/pages",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.PageScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:     context.TODO(),
+				payload: mockedPayload,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					mockedPayload).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"wiki/api/v2/pages",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, errors.New("error, unable to create the http request"))
+
+				fields.c = client
+
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to create the http request"),
+		},
+
+		{
+			name: "when the payload is not provided",
+			args: args{
+				ctx: context.TODO(),
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					(*model.PageCreatePayloadScheme)(nil)).
+					Return(nil, model.ErrNilPayloadError)
+
+				fields.c = client
+
+			},
+			wantErr: true,
+			Err:     model.ErrNilPayloadError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewPageService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.Create(testCase.args.ctx, testCase.args.payload)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+			}
+
+		})
+	}
+}
+
+func Test_internalPageImpl_Update(t *testing.T) {
+
+	//Create the ADF body
+	mockedBody := model.CommentNodeScheme{}
+	mockedBody.Version = 1
+	mockedBody.Type = "doc"
+
+	mockedBody.AppendNode(&model.CommentNodeScheme{
+		Type: "paragraph",
+		Content: []*model.CommentNodeScheme{
+			{
+				Type: "status",
+				Attrs: map[string]interface{}{
+					"color":   "neutral",
+					"style":   "bold",
+					"text":    "Status neutral status",
+					"localId": "3378f5db-c151-4ef2-aec4-56c18a1fbd96",
+				},
+			},
+		},
+	})
+
+	mockedBodyValue, err := json.Marshal(&mockedBody)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	mockedPayload := &model.PageUpdatePayloadScheme{
+		ID:      215646235,
+		SpaceID: 203718658,
+		Status:  "current",
+		Title:   "Page create title test",
+		Body: &model.PageBodyRepresentationScheme{
+			Representation: "atlas_doc_format",
+			Value:          string(mockedBodyValue),
+		},
+		Version: &model.PageUpdatePayloadVersionScheme{
+			Number:  4,
+			Message: "Version #4",
+		},
+	}
+
+	type fields struct {
+		c service.Client
+	}
+
+	type args struct {
+		ctx     context.Context
+		pageId  int
+		payload *model.PageUpdatePayloadScheme
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				ctx:     context.TODO(),
+				pageId:  215646235,
+				payload: mockedPayload,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					mockedPayload).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPut,
+					"wiki/api/v2/pages/215646235",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.PageScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:     context.TODO(),
+				pageId:  215646235,
+				payload: mockedPayload,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					mockedPayload).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPut,
+					"wiki/api/v2/pages/215646235",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, errors.New("error, unable to create the http request"))
+
+				fields.c = client
+
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to create the http request"),
+		},
+
+		{
+			name: "when the page id is not provided",
+			args: args{
+				ctx: context.TODO(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoPageIDError,
+		},
+
+		{
+			name: "when the payload is not provided",
+			args: args{
+				ctx:    context.TODO(),
+				pageId: 215646235,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					(*model.PageUpdatePayloadScheme)(nil)).
+					Return(nil, model.ErrNilPayloadError)
+
+				fields.c = client
+
+			},
+			wantErr: true,
+			Err:     model.ErrNilPayloadError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewPageService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.Update(testCase.args.ctx, testCase.args.pageId, testCase.args.payload)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+			}
+
+		})
+	}
+}
diff --git a/confluence/internal/search_impl_test.go b/confluence/internal/search_impl_test.go
index 415d2c7..f0a64d8 100644
--- a/confluence/internal/search_impl_test.go
+++ b/confluence/internal/search_impl_test.go
@@ -57,7 +57,7 @@ func Test_internalSearchImpl_Content(t *testing.T) {
 				client.On("NewRequest",
 					context.Background(),
 					http.MethodGet,
-					"wiki/rest/api/search?cql=type%3Dpage&cqlcontext=spaceKey&cursor=raNDoMsTRiNg&excerpt=indexed&excludeCurrentSpaces=true&expand=space&includeArchivedSpaces=true&limit=20&next=true&prev=true&sitePermissionTypeFilter=externalCollaborator&start=10",
+					"wiki/rest/api/search?cql=type%3Dpage&cqlcontext=spaceKey&cursor=raNDoMsTRiNg&excerpt=indexed&excludeCurrentSpaces=true&includeArchivedSpaces=true&limit=20&next=true&prev=true&sitePermissionTypeFilter=externalCollaborator&start=10",
 					nil).
 					Return(&http.Request{}, nil)
 
@@ -97,7 +97,7 @@ func Test_internalSearchImpl_Content(t *testing.T) {
 				client.On("NewRequest",
 					context.Background(),
 					http.MethodGet,
-					"wiki/rest/api/search?cql=type%3Dpage&cqlcontext=spaceKey&cursor=raNDoMsTRiNg&excerpt=indexed&excludeCurrentSpaces=true&expand=space&includeArchivedSpaces=true&limit=20&next=true&prev=true&sitePermissionTypeFilter=externalCollaborator&start=10",
+					"wiki/rest/api/search?cql=type%3Dpage&cqlcontext=spaceKey&cursor=raNDoMsTRiNg&excerpt=indexed&excludeCurrentSpaces=true&includeArchivedSpaces=true&limit=20&next=true&prev=true&sitePermissionTypeFilter=externalCollaborator&start=10",
 					nil).
 					Return(&http.Request{}, errors.New("error, unable to create the http request"))
 
diff --git a/confluence/v2/api_client_impl_test.go b/confluence/v2/api_client_impl_test.go
new file mode 100644
index 0000000..d68a005
--- /dev/null
+++ b/confluence/v2/api_client_impl_test.go
@@ -0,0 +1,646 @@
+package v2
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"github.com/ctreminiom/go-atlassian/confluence/internal"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service/common"
+	"github.com/ctreminiom/go-atlassian/service/mocks"
+	"github.com/stretchr/testify/assert"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+)
+
+func TestClient_Call(t *testing.T) {
+
+	expectedResponse := &http.Response{
+		StatusCode: http.StatusOK,
+		Body:       ioutil.NopCloser(strings.NewReader("Hello, world!")),
+		Request: &http.Request{
+			Method: http.MethodGet,
+			URL:    &url.URL{},
+		},
+	}
+
+	nonExpectedResponse := &http.Response{
+		StatusCode: http.StatusBadRequest,
+		Body:       ioutil.NopCloser(strings.NewReader("Hello, world!")),
+		Request: &http.Request{
+			Method: http.MethodGet,
+			URL:    &url.URL{},
+		},
+	}
+
+	type fields struct {
+		HTTP           common.HttpClient
+		Site           *url.URL
+		Authentication common.Authentication
+	}
+
+	type args struct {
+		request   *http.Request
+		structure interface{}
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		on      func(*fields)
+		args    args
+		want    *models.ResponseScheme
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			on: func(fields *fields) {
+
+				client := mocks.NewHttpClient(t)
+
+				client.On("Do", (*http.Request)(nil)).
+					Return(expectedResponse, nil)
+
+				fields.HTTP = client
+			},
+			args: args{
+				request:   nil,
+				structure: nil,
+			},
+			want: &models.ResponseScheme{
+				Response: expectedResponse,
+				Code:     http.StatusOK,
+				Method:   http.MethodGet,
+				Bytes:    *bytes.NewBufferString("Hello, world!"),
+			},
+			wantErr: false,
+		},
+
+		{
+			name: "when the response status is not valid",
+			on: func(fields *fields) {
+
+				client := mocks.NewHttpClient(t)
+
+				client.On("Do", (*http.Request)(nil)).
+					Return(nonExpectedResponse, nil)
+
+				fields.HTTP = client
+			},
+			args: args{
+				request:   nil,
+				structure: nil,
+			},
+			want: &models.ResponseScheme{
+				Response: nonExpectedResponse,
+				Code:     http.StatusBadRequest,
+				Method:   http.MethodGet,
+				Bytes:    *bytes.NewBufferString("Hello, world!"),
+			},
+			wantErr: true,
+			Err:     models.ErrInvalidStatusCodeError,
+		},
+
+		{
+			name: "when the http callback cannot be executed",
+			on: func(fields *fields) {
+
+				client := mocks.NewHttpClient(t)
+
+				client.On("Do", (*http.Request)(nil)).
+					Return(nil, errors.New("error, unable to execute the http call"))
+
+				fields.HTTP = client
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to execute the http call"),
+		},
+	}
+
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			c := &Client{
+				HTTP: testCase.fields.HTTP,
+				Site: testCase.fields.Site,
+				Auth: testCase.fields.Authentication,
+			}
+
+			got, err := c.Call(testCase.args.request, testCase.args.structure)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+
+			} else {
+				assert.NoError(t, err)
+				assert.Equal(t, got, testCase.want)
+			}
+		})
+	}
+}
+
+func TestNewV2(t *testing.T) {
+
+	mockClient, err := New(http.DefaultClient, "https://ctreminiom.atlassian.net")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	mockClient.Auth.SetBasicAuth("test", "test")
+	mockClient.Auth.SetUserAgent("aaa")
+
+	mockClient2, _ := New(nil, " https://zhidao.baidu.com/special/view?id=sd&preview=1")
+
+	type args struct {
+		httpClient common.HttpClient
+		site       string
+	}
+
+	testCases := []struct {
+		name    string
+		args    args
+		on      func(*args)
+		want    *Client
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				httpClient: http.DefaultClient,
+				site:       "https://ctreminiom.atlassian.net",
+			},
+			want:    mockClient,
+			wantErr: false,
+		},
+
+		{
+			name: "when the site url is not valid",
+			args: args{
+				httpClient: http.DefaultClient,
+				site:       " https://zhidao.baidu.com/special/view?id=sd&preview=1",
+			},
+			want:    mockClient2,
+			wantErr: true,
+			Err:     errors.New("parse \" https://zhidao.baidu.com/special/view?id=sd&preview=1/\": first path segment in URL cannot contain colon"),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			gotClient, err := New(testCase.args.httpClient, testCase.args.site)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.Error(t, err)
+				assert.EqualError(t, err, testCase.Err.Error())
+
+			} else {
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotClient, nil)
+			}
+
+		})
+	}
+}
+
+func TestClient_NewFormRequest(t *testing.T) {
+
+	authMocked := internal.NewAuthenticationService(nil)
+	authMocked.SetBasicAuth("mail", "token")
+	authMocked.SetUserAgent("firefox")
+
+	siteAsURL, err := url.Parse("https://ctreminiom.atlassian.net")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	requestMocked, err := http.NewRequestWithContext(context.TODO(),
+		http.MethodGet,
+		"https://ctreminiom.atlassian.net/rest/2/issue/attachment",
+		bytes.NewReader([]byte("Hello World")),
+	)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	requestMocked.Header.Add("Content-Type", "form-type-sample")
+	requestMocked.Header.Add("Accept", "application/json")
+	requestMocked.Header.Set("X-Atlassian-Token", "no-check")
+
+	type fields struct {
+		HTTP common.HttpClient
+		Auth common.Authentication
+		Site *url.URL
+	}
+
+	type args struct {
+		ctx         context.Context
+		method      string
+		apiEndpoint string
+		contentType string
+		payload     io.Reader
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		want    *http.Request
+		wantErr bool
+	}{
+		{
+			name: "when the parameters are correct",
+			fields: fields{
+				HTTP: http.DefaultClient,
+				Auth: authMocked,
+				Site: siteAsURL,
+			},
+			args: args{
+				ctx:         context.TODO(),
+				method:      http.MethodGet,
+				apiEndpoint: "rest/2/issue/attachment",
+				contentType: "form-type-sample",
+				payload:     bytes.NewReader([]byte("Hello World")),
+			},
+			want:    requestMocked,
+			wantErr: false,
+		},
+
+		{
+			name: "when the url cannot be parsed",
+			fields: fields{
+				HTTP: http.DefaultClient,
+				Auth: internal.NewAuthenticationService(nil),
+				Site: siteAsURL,
+			},
+			args: args{
+				ctx:         context.TODO(),
+				method:      http.MethodGet,
+				apiEndpoint: " https://zhidao.baidu.com/special/view?id=49105a24626975510000&preview=1",
+				contentType: "form-type-sample",
+				payload:     bytes.NewReader([]byte("Hello World")),
+			},
+			want:    nil,
+			wantErr: true,
+		},
+
+		{
+			name: "when the request cannot be created",
+			fields: fields{
+				HTTP: http.DefaultClient,
+				Auth: internal.NewAuthenticationService(nil),
+				Site: siteAsURL,
+			},
+			args: args{
+				ctx:         nil,
+				method:      http.MethodGet,
+				apiEndpoint: "rest/2/issue/attachment",
+				contentType: "form-type-sample",
+				payload:     bytes.NewReader([]byte("Hello World")),
+			},
+			want:    requestMocked,
+			wantErr: true,
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			c := &Client{
+				HTTP: testCase.fields.HTTP,
+				Auth: testCase.fields.Auth,
+				Site: testCase.fields.Site,
+			}
+
+			got, err := c.NewFormRequest(testCase.args.ctx, testCase.args.method, testCase.args.apiEndpoint, testCase.args.contentType, testCase.args.payload)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.NotEqual(t, got, nil)
+			}
+		})
+	}
+}
+
+func TestClient_TransformTheHTTPResponse(t *testing.T) {
+
+	expectedJsonResponse := `
+	{
+	  "id": 4,
+	  "self": "https://ctreminiom.atlassian.net/rest/agile/1.0/board/4",
+	  "name": "KP - Scrum",
+	  "type": "scrum"
+	}`
+
+	expectedResponse := &http.Response{
+		StatusCode: http.StatusOK,
+		Body:       ioutil.NopCloser(strings.NewReader(expectedJsonResponse)),
+		Request: &http.Request{
+			Method: http.MethodGet,
+			URL:    &url.URL{},
+		},
+	}
+
+	type fields struct {
+		HTTP           common.HttpClient
+		Site           *url.URL
+		Authentication common.Authentication
+	}
+
+	type args struct {
+		response  *http.Response
+		structure interface{}
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		want    *models.ResponseScheme
+		wantErr bool
+		Err     error
+	}{
+		{
+			name:   "when the parameters are correct",
+			fields: fields{},
+			args: args{
+				response:  expectedResponse,
+				structure: models.BoardScheme{},
+			},
+			want: &models.ResponseScheme{
+				Response: expectedResponse,
+				Code:     http.StatusOK,
+				Method:   http.MethodGet,
+				Bytes:    *bytes.NewBufferString(expectedJsonResponse),
+			},
+			wantErr: false,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			c := &Client{
+				HTTP: testCase.fields.HTTP,
+				Site: testCase.fields.Site,
+				Auth: testCase.fields.Authentication,
+			}
+
+			got, err := c.TransformTheHTTPResponse(testCase.args.response, testCase.args.structure)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.Error(t, err)
+				assert.EqualError(t, err, testCase.Err.Error())
+
+			} else {
+				assert.NoError(t, err)
+				assert.NotEqual(t, got, nil)
+			}
+		})
+	}
+}
+
+func TestClient_TransformStructToReader(t *testing.T) {
+
+	expectedBytes, err := json.Marshal(&models.BoardScheme{
+		Name: "BoardConnector Sample",
+		Type: "Scrum",
+	})
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	type fields struct {
+		HTTP           common.HttpClient
+		Site           *url.URL
+		Authentication common.Authentication
+	}
+
+	type args struct {
+		structure interface{}
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		want    io.Reader
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				structure: &models.BoardScheme{
+					Name: "BoardConnector Sample",
+					Type: "Scrum",
+				},
+			},
+			want:    bytes.NewReader(expectedBytes),
+			wantErr: false,
+		},
+
+		{
+			name: "when the payload provided is not a pointer",
+			args: args{
+				structure: models.BoardScheme{
+					Name: "BoardConnector Sample",
+					Type: "Scrum",
+				},
+			},
+			want:    bytes.NewReader(expectedBytes),
+			wantErr: true,
+			Err:     models.ErrNonPayloadPointerError,
+		},
+
+		{
+			name: "when the payload is not provided",
+			args: args{
+				structure: nil,
+			},
+			want:    bytes.NewReader(expectedBytes),
+			wantErr: true,
+			Err:     models.ErrNilPayloadError,
+		},
+	}
+
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+			c := &Client{
+				HTTP: testCase.fields.HTTP,
+				Site: testCase.fields.Site,
+				Auth: testCase.fields.Authentication,
+			}
+
+			got, err := c.TransformStructToReader(testCase.args.structure)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.Error(t, err)
+				assert.EqualError(t, err, testCase.Err.Error())
+
+			} else {
+				assert.NoError(t, err)
+				assert.NotEqual(t, got, nil)
+			}
+		})
+	}
+}
+
+func TestClient_NewRequest(t *testing.T) {
+
+	authMocked := internal.NewAuthenticationService(nil)
+	authMocked.SetBasicAuth("mail", "token")
+	authMocked.SetUserAgent("firefox")
+
+	siteAsURL, err := url.Parse("https://ctreminiom.atlassian.net")
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	requestMocked, err := http.NewRequestWithContext(context.TODO(),
+		http.MethodGet,
+		"https://ctreminiom.atlassian.net/rest/2/issue/attachment",
+		bytes.NewReader([]byte("Hello World")),
+	)
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	requestMocked.Header.Set("Accept", "application/json")
+	requestMocked.Header.Set("Content-Type", "application/json")
+
+	type fields struct {
+		HTTP common.HttpClient
+		Auth common.Authentication
+		Site *url.URL
+	}
+
+	type args struct {
+		ctx         context.Context
+		method      string
+		apiEndpoint string
+		payload     io.Reader
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		want    *http.Request
+		wantErr bool
+	}{
+		{
+			name: "when the parameters are correct",
+			fields: fields{
+				HTTP: http.DefaultClient,
+				Auth: authMocked,
+				Site: siteAsURL,
+			},
+			args: args{
+				ctx:         context.TODO(),
+				method:      http.MethodGet,
+				apiEndpoint: "rest/2/issue/attachment",
+				payload:     bytes.NewReader([]byte("Hello World")),
+			},
+			want:    requestMocked,
+			wantErr: false,
+		},
+
+		{
+			name: "when the url cannot be parsed",
+			fields: fields{
+				HTTP: http.DefaultClient,
+				Auth: internal.NewAuthenticationService(nil),
+				Site: siteAsURL,
+			},
+			args: args{
+				ctx:         context.TODO(),
+				method:      http.MethodGet,
+				apiEndpoint: " https://zhidao.baidu.com/special/view?id=49105a24626975510000&preview=1",
+				payload:     bytes.NewReader([]byte("Hello World")),
+			},
+			want:    nil,
+			wantErr: true,
+		},
+
+		{
+			name: "when the request cannot be created",
+			fields: fields{
+				HTTP: http.DefaultClient,
+				Auth: internal.NewAuthenticationService(nil),
+				Site: siteAsURL,
+			},
+			args: args{
+				ctx:         nil,
+				method:      http.MethodGet,
+				apiEndpoint: "rest/2/issue/attachment",
+				payload:     bytes.NewReader([]byte("Hello World")),
+			},
+			want:    requestMocked,
+			wantErr: true,
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			c := &Client{
+				HTTP: testCase.fields.HTTP,
+				Auth: testCase.fields.Auth,
+				Site: testCase.fields.Site,
+			}
+
+			got, err := c.NewRequest(testCase.args.ctx, testCase.args.method, testCase.args.apiEndpoint, testCase.args.payload)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.Error(t, err)
+			} else {
+				assert.NoError(t, err)
+				assert.NotEqual(t, got, nil)
+			}
+		})
+	}
+}