Repository: ctreminiom/go-atlassian
Pull request #244 (2023-09-03 16:49:56)

:sparkles: Added the Bitbucket Workspace implementation.
1. Created a new module called bitbucket.

2. It enables the possibility to interact with the Bitbucket Cloud V2 API.

3. Created the Workspace interfaces.


Patch:
diff --git a/bitbucket/api_client_impl.go b/bitbucket/api_client_impl.go
new file mode 100644
index 0000000..8ee72c7
--- /dev/null
+++ b/bitbucket/api_client_impl.go
@@ -0,0 +1,172 @@
+package bitbucket
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"github.com/ctreminiom/go-atlassian/bitbucket/internal"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service/common"
+	"io"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+const DefaultBitbucketSite = "https://api.bitbucket.org"
+
+func New(httpClient common.HttpClient, site string) (*Client, error) {
+
+	if httpClient == nil {
+		httpClient = http.DefaultClient
+	}
+
+	if site == "" {
+		site = DefaultBitbucketSite
+	}
+
+	if !strings.HasSuffix(site, "/") {
+		site += "/"
+	}
+
+	u, err := url.Parse(site)
+	if err != nil {
+		return nil, err
+	}
+
+	client := &Client{
+		HTTP: httpClient,
+		Site: u,
+	}
+
+	client.Auth = internal.NewAuthenticationService(client)
+
+	client.Workspace = internal.NewWorkspaceService(client,
+		internal.NewWorkspaceHookService(client),
+		internal.NewWorkspacePermissionService(client),
+	)
+
+	return client, nil
+}
+
+type Client struct {
+	HTTP      common.HttpClient
+	Site      *url.URL
+	Auth      common.Authentication
+	Workspace *internal.WorkspaceService
+}
+
+func (c *Client) NewRequest(ctx context.Context, method, urlStr, type_ string, body interface{}) (*http.Request, error) {
+
+	rel, err := url.Parse(urlStr)
+	if err != nil {
+		return nil, err
+	}
+
+	u := c.Site.ResolveReference(rel)
+
+	buf := new(bytes.Buffer)
+	if body != nil {
+		if err = json.NewEncoder(buf).Encode(body); err != nil {
+			return nil, err
+		}
+	}
+
+	// If the body interface is a *bytes.Buffer type
+	// it means the NewRequest() requires to handle the RFC 1867 ISO
+	if attachBuffer, ok := body.(*bytes.Buffer); ok {
+		buf = attachBuffer
+	}
+
+	req, err := http.NewRequestWithContext(ctx, method, u.String(), buf)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Set("Accept", "application/json")
+
+	if body != nil {
+		req.Header.Set("Content-Type", "application/json")
+	}
+
+	if type_ != "" {
+		// When the type_ is provided, it means the request needs to be created to handle files
+		req.Header.Set("Content-Type", type_)
+		req.Header.Set("X-Atlassian-Token", "no-check")
+	}
+
+	if c.Auth.HasBasicAuth() {
+		req.SetBasicAuth(c.Auth.GetBasicAuth())
+	}
+
+	if c.Auth.HasUserAgent() {
+		req.Header.Set("User-Agent", c.Auth.GetUserAgent())
+	}
+
+	if c.Auth.GetBearerToken() != "" && !c.Auth.HasBasicAuth() {
+		req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", c.Auth.GetBearerToken()))
+	}
+
+	return req, 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.processResponse(response, structure)
+}
+
+func (c *Client) processResponse(response *http.Response, structure interface{}) (*models.ResponseScheme, error) {
+
+	defer response.Body.Close()
+
+	res := &models.ResponseScheme{
+		Response: response,
+		Code:     response.StatusCode,
+		Endpoint: response.Request.URL.String(),
+		Method:   response.Request.Method,
+	}
+
+	responseAsBytes, err := io.ReadAll(response.Body)
+	if err != nil {
+		return res, err
+	}
+
+	res.Bytes.Write(responseAsBytes)
+
+	wasSuccess := response.StatusCode >= 200 && response.StatusCode < 300
+
+	if !wasSuccess {
+
+		switch response.StatusCode {
+
+		case http.StatusNotFound:
+			return res, models.ErrNotFound
+
+		case http.StatusUnauthorized:
+			return res, models.ErrUnauthorized
+
+		case http.StatusInternalServerError:
+			return res, models.ErrInternalError
+
+		case http.StatusBadRequest:
+			return res, models.ErrBadRequestError
+
+		default:
+			return res, models.ErrInvalidStatusCodeError
+		}
+	}
+
+	if structure != nil {
+		if err = json.Unmarshal(responseAsBytes, &structure); err != nil {
+			return res, err
+		}
+	}
+
+	return res, nil
+}
diff --git a/bitbucket/internal/authentication_impl.go b/bitbucket/internal/authentication_impl.go
new file mode 100644
index 0000000..2bbe9ab
--- /dev/null
+++ b/bitbucket/internal/authentication_impl.go
@@ -0,0 +1,61 @@
+package internal
+
+import (
+	"github.com/ctreminiom/go-atlassian/service"
+	"github.com/ctreminiom/go-atlassian/service/common"
+)
+
+func NewAuthenticationService(client service.Connector) common.Authentication {
+	return &AuthenticationService{c: client}
+}
+
+type AuthenticationService struct {
+	c service.Connector
+
+	basicAuthProvided bool
+	mail, token       string
+
+	userAgentProvided bool
+	agent             string
+}
+
+func (a *AuthenticationService) SetBearerToken(token string) {
+	a.token = token
+}
+
+func (a *AuthenticationService) GetBearerToken() string {
+	return a.token
+}
+func (a *AuthenticationService) SetExperimentalFlag() {}
+
+func (a *AuthenticationService) HasSetExperimentalFlag() bool {
+	return false
+}
+
+func (a *AuthenticationService) SetBasicAuth(mail, token string) {
+	a.mail = mail
+	a.token = token
+
+	a.basicAuthProvided = true
+}
+
+func (a *AuthenticationService) GetBasicAuth() (string, string) {
+	return a.mail, a.token
+}
+
+func (a *AuthenticationService) HasBasicAuth() bool {
+	return a.basicAuthProvided
+}
+
+func (a *AuthenticationService) SetUserAgent(agent string) {
+	a.agent = agent
+	a.userAgentProvided = true
+}
+
+func (a *AuthenticationService) GetUserAgent() string {
+	return a.agent
+}
+
+func (a *AuthenticationService) HasUserAgent() bool {
+	return a.userAgentProvided
+}
diff --git a/bitbucket/internal/workspace_impl.go b/bitbucket/internal/workspace_impl.go
new file mode 100644
index 0000000..c6389ce
--- /dev/null
+++ b/bitbucket/internal/workspace_impl.go
@@ -0,0 +1,159 @@
+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/bitbucket"
+	"net/http"
+)
+
+func NewWorkspaceService(client service.Connector, webhook *WorkspaceHookService, permission *WorkspacePermissionService) *WorkspaceService {
+
+	return &WorkspaceService{
+		internalClient: &internalWorkspaceServiceImpl{c: client},
+		Hook:           webhook,
+		Permission:     permission,
+	}
+}
+
+type WorkspaceService struct {
+	internalClient bitbucket.WorkspaceConnector
+	Hook           *WorkspaceHookService
+	Permission     *WorkspacePermissionService
+}
+
+// Get returns the requested workspace.
+//
+// GET /2.0/workspaces/{workspace}
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace#get-a-workspace
+func (w *WorkspaceService) Get(ctx context.Context, workspace string) (*model.WorkspaceScheme, *model.ResponseScheme, error) {
+	return w.internalClient.Get(ctx, workspace)
+}
+
+// Members returns all members of the requested workspace.
+//
+// GET /2.0/workspaces/{workspace}/members
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace#get-a-workspace
+func (w *WorkspaceService) Members(ctx context.Context, workspace string) (*model.WorkspaceMembershipPageScheme, *model.ResponseScheme, error) {
+	return w.internalClient.Members(ctx, workspace)
+}
+
+// Membership returns the workspace membership,
+//
+// which includes a User object for the member and a Workspace object for the requested workspace.
+//
+// GET /2.0/workspaces/{workspace}/members/{member}
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace#get-member-in-a-workspace
+func (w *WorkspaceService) Membership(ctx context.Context, workspace, memberId string) (*model.WorkspaceMembershipScheme, *model.ResponseScheme, error) {
+	return w.internalClient.Membership(ctx, workspace, memberId)
+}
+
+// Projects returns the list of projects in this workspace.
+//
+// GET /2.0/workspaces/{workspace}/projects
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace#get-projects-in-a-workspace
+func (w *WorkspaceService) Projects(ctx context.Context, workspace string) (*model.BitbucketProjectPageScheme, *model.ResponseScheme, error) {
+	return w.internalClient.Projects(ctx, workspace)
+}
+
+type internalWorkspaceServiceImpl struct {
+	c service.Connector
+}
+
+func (i *internalWorkspaceServiceImpl) Get(ctx context.Context, workspace string) (*model.WorkspaceScheme, *model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, nil, model.ErrNoWorkspaceError
+	}
+
+	endpoint := fmt.Sprintf("2.0/workspaces/%v", workspace)
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	result := new(model.WorkspaceScheme)
+	response, err := i.c.Call(request, result)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return result, response, nil
+}
+
+func (i *internalWorkspaceServiceImpl) Members(ctx context.Context, workspace string) (*model.WorkspaceMembershipPageScheme, *model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, nil, model.ErrNoWorkspaceError
+	}
+
+	endpoint := fmt.Sprintf("2.0/workspaces/%v/members", workspace)
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	page := new(model.WorkspaceMembershipPageScheme)
+	response, err := i.c.Call(request, page)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return page, response, nil
+}
+
+func (i *internalWorkspaceServiceImpl) Membership(ctx context.Context, workspace, memberId string) (*model.WorkspaceMembershipScheme, *model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, nil, model.ErrNoWorkspaceError
+	}
+
+	if memberId == "" {
+		return nil, nil, model.ErrNoMemberIDError
+	}
+
+	endpoint := fmt.Sprintf("2.0/workspaces/%v/members/%v", workspace, memberId)
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	member := new(model.WorkspaceMembershipScheme)
+	response, err := i.c.Call(request, member)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return member, response, nil
+}
+
+func (i *internalWorkspaceServiceImpl) Projects(ctx context.Context, workspace string) (*model.BitbucketProjectPageScheme, *model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, nil, model.ErrNoWorkspaceError
+	}
+
+	endpoint := fmt.Sprintf("2.0/workspaces/%v/projects", workspace)
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	page := new(model.BitbucketProjectPageScheme)
+	response, err := i.c.Call(request, page)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return page, response, nil
+}
diff --git a/bitbucket/internal/workspace_permissions_impl.go b/bitbucket/internal/workspace_permissions_impl.go
new file mode 100644
index 0000000..8b20034
--- /dev/null
+++ b/bitbucket/internal/workspace_permissions_impl.go
@@ -0,0 +1,166 @@
+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/bitbucket"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+func NewWorkspacePermissionService(client service.Connector) *WorkspacePermissionService {
+
+	return &WorkspacePermissionService{
+		internalClient: &internalWorkspacePermissionServiceImpl{c: client},
+	}
+}
+
+type WorkspacePermissionService struct {
+	internalClient bitbucket.WorkspacePermissionConnector
+}
+
+// Members returns the list of members in a workspace and their permission levels.
+//
+// GET /2.0/workspaces/{workspace}/permissions
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace/permissions#get-user-permissions-in-a-workspace
+func (w *WorkspacePermissionService) Members(ctx context.Context, workspace, query string) (*model.WorkspaceMembershipPageScheme, *model.ResponseScheme, error) {
+	return w.internalClient.Members(ctx, workspace, query)
+}
+
+// Repositories returns an object for each repository permission for all of a workspaces repositories.
+//
+// Permissions returned are effective permissions: the highest level of permission the user has.
+//
+// NOTE: Only users with admin permission for the team may access this resource.
+//
+// GET /2.0/workspaces/{workspace}/permissions/repositories
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace/permissions#gets-all-repository-permissions-in-a-workspace
+func (w *WorkspacePermissionService) Repositories(ctx context.Context, workspace, query, sort string) (*model.RepositoryPermissionPageScheme, *model.ResponseScheme, error) {
+	return w.internalClient.Repositories(ctx, workspace, query, sort)
+}
+
+// Repository returns an object for the repository permission of each user in the requested repository.
+//
+// Permissions returned are effective permissions: the highest level of permission the user has.
+//
+// GET /2.0/workspaces/{workspace}/permissions/repositories/{repo_slug}
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace/permissions#get-repository-permission-in-a-workspace
+func (w *WorkspacePermissionService) Repository(ctx context.Context, workspace, repository, query, sort string) (*model.RepositoryPermissionPageScheme, *model.ResponseScheme, error) {
+	return w.internalClient.Repository(ctx, workspace, repository, query, sort)
+}
+
+type internalWorkspacePermissionServiceImpl struct {
+	c service.Connector
+}
+
+func (i *internalWorkspacePermissionServiceImpl) Members(ctx context.Context, workspace, query string) (*model.WorkspaceMembershipPageScheme, *model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, nil, model.ErrNoWorkspaceError
+	}
+
+	var endpoint strings.Builder
+	endpoint.WriteString(fmt.Sprintf("2.0/workspaces/%v/permissions", workspace))
+
+	if query != "" {
+
+		params := url.Values{}
+		params.Add("q", query)
+
+		endpoint.WriteString(fmt.Sprintf("?%v", params.Encode()))
+	}
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint.String(), "", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	page := new(model.WorkspaceMembershipPageScheme)
+	response, err := i.c.Call(request, page)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return page, response, nil
+}
+
+func (i *internalWorkspacePermissionServiceImpl) Repositories(ctx context.Context, workspace, query, sort string) (*model.RepositoryPermissionPageScheme, *model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, nil, model.ErrNoWorkspaceError
+	}
+
+	var endpoint strings.Builder
+	endpoint.WriteString(fmt.Sprintf("2.0/workspaces/%v/permissions/repositories", workspace))
+
+	params := url.Values{}
+	if query != "" {
+		params.Add("q", query)
+	}
+	if sort != "" {
+		params.Add("sort", sort)
+	}
+
+	if params.Encode() != "" {
+		endpoint.WriteString(fmt.Sprintf("?%v", params.Encode()))
+	}
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint.String(), "", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	page := new(model.RepositoryPermissionPageScheme)
+	response, err := i.c.Call(request, page)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return page, response, nil
+}
+
+func (i *internalWorkspacePermissionServiceImpl) Repository(ctx context.Context, workspace, repository, query, sort string) (*model.RepositoryPermissionPageScheme, *model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, nil, model.ErrNoWorkspaceError
+	}
+
+	if repository == "" {
+		return nil, nil, model.ErrNoRepositoryError
+	}
+
+	var endpoint strings.Builder
+	endpoint.WriteString(fmt.Sprintf("2.0/workspaces/%v/permissions/repositories/%v", workspace, repository))
+
+	params := url.Values{}
+
+	if query != "" {
+		params.Add("q", query)
+	}
+	if sort != "" {
+		params.Add("sort", sort)
+	}
+
+	if params.Encode() != "" {
+		endpoint.WriteString(fmt.Sprintf("?%v", params.Encode()))
+	}
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint.String(), "", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	page := new(model.RepositoryPermissionPageScheme)
+	response, err := i.c.Call(request, page)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return page, response, nil
+}
diff --git a/bitbucket/internal/workspace_webhooks_impl.go b/bitbucket/internal/workspace_webhooks_impl.go
new file mode 100644
index 0000000..8c0fd80
--- /dev/null
+++ b/bitbucket/internal/workspace_webhooks_impl.go
@@ -0,0 +1,188 @@
+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/bitbucket"
+	"net/http"
+)
+
+func NewWorkspaceHookService(client service.Connector) *WorkspaceHookService {
+
+	return &WorkspaceHookService{
+		internalClient: &internalWorkspaceHookServiceImpl{c: client},
+	}
+}
+
+type WorkspaceHookService struct {
+	internalClient bitbucket.WorkspaceHookConnector
+}
+
+// Gets returns a paginated list of webhooks installed on this workspace.
+//
+// GET /2.0/workspaces/{workspace}/hooks
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace/webhooks#list-webhooks-for-a-workspace
+func (w *WorkspaceHookService) Gets(ctx context.Context, workspace string) (*model.WebhookSubscriptionPageScheme, *model.ResponseScheme, error) {
+	return w.internalClient.Gets(ctx, workspace)
+}
+
+// Create creates a new webhook on the specified workspace.
+//
+// Workspace webhooks are fired for events from all repositories contained by that workspace.
+//
+// POST /2.0/workspaces/{workspace}/hooks
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace/webhooks#create-webhook-for-a-workspace
+func (w *WorkspaceHookService) Create(ctx context.Context, workspace string, payload *model.WebhookSubscriptionPayloadScheme) (*model.WebhookSubscriptionScheme, *model.ResponseScheme, error) {
+	return w.internalClient.Create(ctx, workspace, payload)
+}
+
+// Get returns the webhook with the specified id installed on the given workspace.
+//
+// GET /2.0/workspaces/{workspace}/hooks/{uid}
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace/webhooks#get-webhook-for-a-workspace
+func (w *WorkspaceHookService) Get(ctx context.Context, workspace, webhookId string) (*model.WebhookSubscriptionScheme, *model.ResponseScheme, error) {
+	return w.internalClient.Get(ctx, workspace, webhookId)
+}
+
+// Update updates the specified webhook subscription.
+//
+// PUT /2.0/workspaces/{workspace}/hooks/{uid}
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace/webhooks#update-webhook-for-a-workspace
+func (w *WorkspaceHookService) Update(ctx context.Context, workspace, webhookId string, payload *model.WebhookSubscriptionPayloadScheme) (*model.WebhookSubscriptionScheme, *model.ResponseScheme, error) {
+	return w.internalClient.Update(ctx, workspace, webhookId, payload)
+}
+
+// Delete deletes the specified webhook subscription from the given workspace.
+//
+// DELETE /2.0/workspaces/{workspace}/hooks/{uid}
+//
+// https://docs.go-atlassian.io/bitbucket-cloud/workspace/webhooks#delete-webhook-for-a-workspace
+func (w *WorkspaceHookService) Delete(ctx context.Context, workspace, webhookId string) (*model.ResponseScheme, error) {
+	return w.internalClient.Delete(ctx, workspace, webhookId)
+}
+
+type internalWorkspaceHookServiceImpl struct {
+	c service.Connector
+}
+
+func (i *internalWorkspaceHookServiceImpl) Gets(ctx context.Context, workspace string) (*model.WebhookSubscriptionPageScheme, *model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, nil, model.ErrNoWorkspaceError
+	}
+
+	endpoint := fmt.Sprintf("2.0/workspaces/%v/hooks", workspace)
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	page := new(model.WebhookSubscriptionPageScheme)
+	response, err := i.c.Call(request, page)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return page, response, nil
+}
+
+func (i *internalWorkspaceHookServiceImpl) Create(ctx context.Context, workspace string, payload *model.WebhookSubscriptionPayloadScheme) (*model.WebhookSubscriptionScheme, *model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, nil, model.ErrNoWorkspaceError
+	}
+
+	endpoint := fmt.Sprintf("2.0/workspaces/%v/hooks", workspace)
+
+	request, err := i.c.NewRequest(ctx, http.MethodPost, endpoint, "", payload)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	webhook := new(model.WebhookSubscriptionScheme)
+	response, err := i.c.Call(request, webhook)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return webhook, response, nil
+}
+
+func (i *internalWorkspaceHookServiceImpl) Get(ctx context.Context, workspace, webhookId string) (*model.WebhookSubscriptionScheme, *model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, nil, model.ErrNoWorkspaceError
+	}
+
+	if webhookId == "" {
+		return nil, nil, model.ErrNoWebhookIDError
+	}
+
+	endpoint := fmt.Sprintf("2.0/workspaces/%v/hooks/%v", workspace, webhookId)
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, "", nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	webhook := new(model.WebhookSubscriptionScheme)
+	response, err := i.c.Call(request, webhook)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return webhook, response, nil
+}
+
+func (i *internalWorkspaceHookServiceImpl) Update(ctx context.Context, workspace, webhookId string, payload *model.WebhookSubscriptionPayloadScheme) (*model.WebhookSubscriptionScheme, *model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, nil, model.ErrNoWorkspaceError
+	}
+
+	if webhookId == "" {
+		return nil, nil, model.ErrNoWebhookIDError
+	}
+
+	endpoint := fmt.Sprintf("2.0/workspaces/%v/hooks/%v", workspace, webhookId)
+
+	request, err := i.c.NewRequest(ctx, http.MethodPut, endpoint, "", payload)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	webhook := new(model.WebhookSubscriptionScheme)
+	response, err := i.c.Call(request, webhook)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return webhook, response, nil
+}
+
+func (i *internalWorkspaceHookServiceImpl) Delete(ctx context.Context, workspace, webhookId string) (*model.ResponseScheme, error) {
+
+	if workspace == "" {
+		return nil, model.ErrNoWorkspaceError
+	}
+
+	if webhookId == "" {
+		return nil, model.ErrNoWebhookIDError
+	}
+
+	endpoint := fmt.Sprintf("2.0/workspaces/%v/hooks/%v", workspace, webhookId)
+
+	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/pkg/infra/models/bitbucket_branch.go b/pkg/infra/models/bitbucket_branch.go
new file mode 100644
index 0000000..2de5947
--- /dev/null
+++ b/pkg/infra/models/bitbucket_branch.go
@@ -0,0 +1,6 @@
+package models
+
+type BranchScheme struct {
+	MergeStrategies      []string `json:"merge_strategies"`
+	DefaultMergeStrategy string   `json:"default_merge_strategy"`
+}
diff --git a/pkg/infra/models/bitbucket_projects.go b/pkg/infra/models/bitbucket_projects.go
new file mode 100644
index 0000000..c99b5e9
--- /dev/null
+++ b/pkg/infra/models/bitbucket_projects.go
@@ -0,0 +1,27 @@
+package models
+
+type BitbucketProjectPageScheme struct {
+	Size     int                       `json:"size,omitempty"`
+	Page     int                       `json:"page,omitempty"`
+	Pagelen  int                       `json:"pagelen,omitempty"`
+	Next     string                    `json:"next,omitempty"`
+	Previous string                    `json:"previous,omitempty"`
+	Values   []*BitbucketProjectScheme `json:"values,omitempty"`
+}
+
+type BitbucketProjectScheme struct {
+	Links                   *BitbucketProjectLinksScheme `json:"links,omitempty"`
+	Uuid                    string                       `json:"uuid,omitempty"`
+	Key                     string                       `json:"key,omitempty"`
+	Name                    string                       `json:"name,omitempty"`
+	Description             string                       `json:"description,omitempty"`
+	IsPrivate               bool                         `json:"is_private,omitempty"`
+	CreatedOn               string                       `json:"created_on,omitempty"`
+	UpdatedOn               string                       `json:"updated_on,omitempty"`
+	HasPubliclyVisibleRepos bool                         `json:"has_publicly_visible_repos,omitempty"`
+}
+
+type BitbucketProjectLinksScheme struct {
+	Html   *BitbucketLinkScheme `json:"html,omitempty"`
+	Avatar *BitbucketLinkScheme `json:"avatar,omitempty"`
+}
diff --git a/pkg/infra/models/bitbucket_repository.go b/pkg/infra/models/bitbucket_repository.go
new file mode 100644
index 0000000..d55a3f8
--- /dev/null
+++ b/pkg/infra/models/bitbucket_repository.go
@@ -0,0 +1,51 @@
+package models
+
+type RepositoryPermissionPageScheme struct {
+	Size     int                           `json:"size,omitempty"`
+	Page     int                           `json:"page,omitempty"`
+	Pagelen  int                           `json:"pagelen,omitempty"`
+	Next     string                        `json:"next,omitempty"`
+	Previous string                        `json:"previous,omitempty"`
+	Values   []*RepositoryPermissionScheme `json:"values,omitempty"`
+}
+
+type RepositoryPermissionScheme struct {
+	Type       string                  `json:"type,omitempty"`
+	Permission string                  `json:"permission,omitempty"`
+	User       *BitbucketAccountScheme `json:"user,omitempty"`
+	Repository *RepositoryScheme       `json:"repository,omitempty"`
+}
+
+type RepositoryScheme struct {
+	Type        string                  `json:"type,omitempty"`
+	Uuid        string                  `json:"uuid,omitempty"`
+	FullName    string                  `json:"full_name,omitempty"`
+	IsPrivate   bool                    `json:"is_private,omitempty"`
+	Scm         string                  `json:"scm,omitempty"`
+	Name        string                  `json:"name,omitempty"`
+	Description string                  `json:"description,omitempty"`
+	CreatedOn   string                  `json:"created_on,omitempty"`
+	UpdatedOn   string                  `json:"updated_on,omitempty"`
+	Size        int                     `json:"size,omitempty"`
+	Language    string                  `json:"language,omitempty"`
+	HasIssues   bool                    `json:"has_issues,omitempty"`
+	HasWiki     bool                    `json:"has_wiki,omitempty"`
+	ForkPolicy  string                  `json:"fork_policy,omitempty"`
+	Owner       *BitbucketAccountScheme `json:"owner,omitempty"`
+	Parent      *RepositoryScheme       `json:"parent,omitempty"`
+	Project     BitbucketProjectScheme  `json:"project,omitempty"`
+	Links       *RepositoryLinksScheme  `json:"links,omitempty"`
+}
+
+type RepositoryLinksScheme struct {
+	Self         *BitbucketLinkScheme   `json:"self,omitempty"`
+	Html         *BitbucketLinkScheme   `json:"html,omitempty"`
+	Avatar       *BitbucketLinkScheme   `json:"avatar,omitempty"`
+	PullRequests *BitbucketLinkScheme   `json:"pullrequests,omitempty"`
+	Commits      *BitbucketLinkScheme   `json:"commits,omitempty"`
+	Forks        *BitbucketLinkScheme   `json:"forks,omitempty"`
+	Watchers     *BitbucketLinkScheme   `json:"watchers,omitempty"`
+	Downloads    *BitbucketLinkScheme   `json:"downloads,omitempty"`
+	Clone        []*BitbucketLinkScheme `json:"clone,omitempty"`
+	Hooks        *BitbucketLinkScheme   `json:"hooks,omitempty"`
+}
diff --git a/pkg/infra/models/bitbucket_webhooks.go b/pkg/infra/models/bitbucket_webhooks.go
new file mode 100644
index 0000000..6f35513
--- /dev/null
+++ b/pkg/infra/models/bitbucket_webhooks.go
@@ -0,0 +1,32 @@
+package models
+
+type WebhookSubscriptionPayloadScheme struct {
+	Description string   `json:"description,omitempty"`
+	Url         string   `json:"url,omitempty"`
+	Active      bool     `json:"active,omitempty"`
+	Events      []string `json:"events,omitempty"`
+}
+
+type WebhookSubscriptionPageScheme struct {
+	Size     int                          `json:"size,omitempty"`
+	Page     int                          `json:"page,omitempty"`
+	Pagelen  int                          `json:"pagelen,omitempty"`
+	Next     string                       `json:"next,omitempty"`
+	Previous string                       `json:"previous,omitempty"`
+	Values   []*WebhookSubscriptionScheme `json:"values,omitempty"`
+}
+
+type WebhookSubscriptionScheme struct {
+	UUID        string                            `json:"uuid,omitempty"`
+	URL         string                            `json:"url,omitempty"`
+	Description string                            `json:"description,omitempty"`
+	SubjectType string                            `json:"subject_type,omitempty"`
+	Subject     *WebhookSubscriptionSubjectScheme `json:"subject,omitempty"`
+	Active      bool                              `json:"active,omitempty"`
+	CreatedAt   string                            `json:"created_at,omitempty"`
+	Events      []string                          `json:"events,omitempty"`
+}
+
+type WebhookSubscriptionSubjectScheme struct {
+	Type string `json:"type,omitempty"`
+}
diff --git a/pkg/infra/models/bitbucket_workspace.go b/pkg/infra/models/bitbucket_workspace.go
new file mode 100644
index 0000000..05dab19
--- /dev/null
+++ b/pkg/infra/models/bitbucket_workspace.go
@@ -0,0 +1,28 @@
+package models
+
+type WorkspaceScheme struct {
+	Type      string                `json:"type,omitempty"`
+	Links     *WorkspaceLinksScheme `json:"links,omitempty"`
+	Uuid      string                `json:"uuid,omitempty"`
+	Name      string                `json:"name,omitempty"`
+	Slug      string                `json:"slug,omitempty"`
+	IsPrivate bool                  `json:"is_private,omitempty"`
+	CreatedOn string                `json:"created_on,omitempty"`
+	UpdatedOn string                `json:"updated_on,omitempty"`
+}
+
+type WorkspaceLinksScheme struct {
+	Avatar       *BitbucketLinkScheme `json:"avatar,omitempty"`
+	Html         *BitbucketLinkScheme `json:"html,omitempty"`
+	Members      *BitbucketLinkScheme `json:"members,omitempty"`
+	Owners       *BitbucketLinkScheme `json:"owners,omitempty"`
+	Projects     *BitbucketLinkScheme `json:"projects,omitempty"`
+	Repositories *BitbucketLinkScheme `json:"repositories,omitempty"`
+	Snippets     *BitbucketLinkScheme `json:"snippets,omitempty"`
+	Self         *BitbucketLinkScheme `json:"self,omitempty"`
+}
+
+type BitbucketLinkScheme struct {
+	Href string `json:"href,omitempty"`
+	Name string `json:"name,omitempty"`
+}
diff --git a/pkg/infra/models/bitbucket_workspace_membership.go b/pkg/infra/models/bitbucket_workspace_membership.go
new file mode 100644
index 0000000..1017de1
--- /dev/null
+++ b/pkg/infra/models/bitbucket_workspace_membership.go
@@ -0,0 +1,73 @@
+package models
+
+import "time"
+
+type WorkspaceMembershipPageScheme struct {
+	Size     int                          `json:"size,omitempty"`
+	Page     int                          `json:"page,omitempty"`
+	Pagelen  int                          `json:"pagelen,omitempty"`
+	Next     string                       `json:"next,omitempty"`
+	Previous string                       `json:"previous,omitempty"`
+	Values   []*WorkspaceMembershipScheme `json:"values,omitempty"`
+}
+
+type WorkspaceMembershipScheme struct {
+	Links        *WorkspaceMembershipLinksScheme `json:"links,omitempty"`
+	User         *BitbucketAccountScheme         `json:"user,omitempty"`
+	Workspace    *WorkspaceScheme                `json:"workspace,omitempty"`
+	AddedOn      time.Time                       `json:"added_on,omitempty"`
+	Permission   string                          `json:"permission,omitempty"`
+	LastAccessed time.Time                       `json:"last_accessed,omitempty"`
+}
+
+type WorkspaceMembershipLinksScheme struct {
+	Self *BitbucketLinkScheme `json:"self,omitempty"`
+}
+
+type BitbucketAccountScheme struct {
+	Links       *BitbucketAccountLinksScheme `json:"links,omitempty"`
+	CreatedOn   string                       `json:"created_on,omitempty"`
+	DisplayName string                       `json:"display_name,omitempty"`
+	Username    string                       `json:"username,omitempty"`
+	Uuid        string                       `json:"uuid,omitempty"`
+	Type        string                       `json:"type,omitempty"`
+	AccountId   string                       `json:"account_id,omitempty"`
+	Nickname    string                       `json:"nickname,omitempty"`
+}
+
+type BitbucketAccountLinksScheme struct {
+	Avatar *BitbucketLinkScheme `json:"avatar,omitempty"`
+	Self   *BitbucketLinkScheme `json:"self,omitempty"`
+	Html   *BitbucketLinkScheme `json:"html,omitempty"`
+}
+
+/*
+
+ */
+
+type T struct {
+	Values []struct {
+		Repository struct {
+			Type     string `json:"type"`
+			FullName string `json:"full_name"`
+			Links    struct {
+				Self struct {
+					Href string `json:"href"`
+				} `json:"self"`
+				Html struct {
+					Href string `json:"href"`
+				} `json:"html"`
+				Avatar struct {
+					Href string `json:"href"`
+				} `json:"avatar"`
+			} `json:"links"`
+			Name string `json:"name"`
+			Uuid string `json:"uuid"`
+		} `json:"repository"`
+		Type       string `json:"type"`
+		Permission string `json:"permission"`
+	} `json:"values"`
+	Pagelen int `json:"pagelen"`
+	Size    int `json:"size"`
+	Page    int `json:"page"`
+}
diff --git a/pkg/infra/models/errors.go b/pkg/infra/models/errors.go
index f69f1cd..9c1a811 100644
--- a/pkg/infra/models/errors.go
+++ b/pkg/infra/models/errors.go
@@ -168,4 +168,9 @@ var (
 	ErrNoSiteError                         = errors.New("client: no atlassian site set")
 	ErrNoCreateIssuesError                 = errors.New("jira: no issues payload set")
 	ErrNoIssueSchemeError                  = errors.New("jira: no issue instance set")
+
+	ErrNoWorkspaceError  = errors.New("bitbucket: no workspace set")
+	ErrNoMemberIDError   = errors.New("bitbucket: no member id set")
+	ErrNoWebhookIDError  = errors.New("bitbucket: no webhook id set")
+	ErrNoRepositoryError = errors.New("bitbucket: no repository set")
 )
diff --git a/service/bitbucket/workspace.go b/service/bitbucket/workspace.go
new file mode 100644
index 0000000..6f46387
--- /dev/null
+++ b/service/bitbucket/workspace.go
@@ -0,0 +1,120 @@
+package bitbucket
+
+import (
+	"context"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+)
+
+// WorkspaceConnector is where you create repositories, collaborate on your code,
+//
+// and organize different streams of work in your Bitbucket Cloud account.
+//
+// Workspaces replace the use of teams and users in API calls.
+type WorkspaceConnector interface {
+
+	// Get returns the requested workspace.
+	//
+	// GET /2.0/workspaces/{workspace}
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace#get-a-workspace
+	Get(ctx context.Context, workspace string) (*models.WorkspaceScheme, *models.ResponseScheme, error)
+
+	// Members returns all members of the requested workspace.
+	//
+	// GET /2.0/workspaces/{workspace}/members
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace#get-a-workspace
+	Members(ctx context.Context, workspace string) (*models.WorkspaceMembershipPageScheme, *models.ResponseScheme, error)
+
+	// Membership returns the workspace membership,
+	//
+	// which includes a User object for the member and a Workspace object for the requested workspace.
+	//
+	// GET /2.0/workspaces/{workspace}/members/{member}
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace#get-member-in-a-workspace
+	Membership(ctx context.Context, workspace, memberId string) (*models.WorkspaceMembershipScheme, *models.ResponseScheme, error)
+
+	// Projects returns the list of projects in this workspace.
+	//
+	// GET /2.0/workspaces/{workspace}/projects
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace#get-projects-in-a-workspace
+	Projects(ctx context.Context, workspace string) (*models.BitbucketProjectPageScheme, *models.ResponseScheme, error)
+}
+
+// WorkspaceHookConnector is where you can manage the workspace webhook connector
+//
+// use it to retrieve, edit and delete webhook on your workspace
+type WorkspaceHookConnector interface {
+
+	// Gets returns a paginated list of webhooks installed on this workspace.
+	//
+	// GET /2.0/workspaces/{workspace}/hooks
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace/webhooks#list-webhooks-for-a-workspace
+	Gets(ctx context.Context, workspace string) (*models.WebhookSubscriptionPageScheme, *models.ResponseScheme, error)
+
+	// Create creates a new webhook on the specified workspace.
+	//
+	// Workspace webhooks are fired for events from all repositories contained by that workspace.
+	//
+	// POST /2.0/workspaces/{workspace}/hooks
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace/webhooks#create-webhook-for-a-workspace
+	Create(ctx context.Context, workspace string, payload *models.WebhookSubscriptionPayloadScheme) (*models.WebhookSubscriptionScheme, *models.ResponseScheme, error)
+
+	// Get returns the webhook with the specified id installed on the given workspace.
+	//
+	// GET /2.0/workspaces/{workspace}/hooks/{uid}
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace/webhooks#get-webhook-for-a-workspace
+	Get(ctx context.Context, workspace, webhookId string) (*models.WebhookSubscriptionScheme, *models.ResponseScheme, error)
+
+	// Update updates the specified webhook subscription.
+	//
+	// PUT /2.0/workspaces/{workspace}/hooks/{uid}
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace/webhooks#update-webhook-for-a-workspace
+	Update(ctx context.Context, workspace, webhookId string, payload *models.WebhookSubscriptionPayloadScheme) (*models.WebhookSubscriptionScheme, *models.ResponseScheme, error)
+
+	// Delete deletes the specified webhook subscription from the given workspace.
+	//
+	// DELETE /2.0/workspaces/{workspace}/hooks/{uid}
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace/webhooks#delete-webhook-for-a-workspace
+	Delete(ctx context.Context, workspace, webhookId string) (*models.ResponseScheme, error)
+}
+
+// WorkspacePermissionConnector is where you can manage the workspace permissions
+//
+// use it to retrieve the workspace permissions and the repositories linked to a workspace.
+type WorkspacePermissionConnector interface {
+
+	// Members returns the list of members in a workspace and their permission levels.
+	//
+	// GET /2.0/workspaces/{workspace}/permissions
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace/permissions#get-user-permissions-in-a-workspace
+	Members(ctx context.Context, workspace, query string) (*models.WorkspaceMembershipPageScheme, *models.ResponseScheme, error)
+
+	// Repositories returns an object for each repository permission for all of a workspaces repositories.
+	//
+	// Permissions returned are effective permissions: the highest level of permission the user has.
+	//
+	// NOTE: Only users with admin permission for the team may access this resource.
+	//
+	// GET /2.0/workspaces/{workspace}/permissions/repositories
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace/permissions#gets-all-repository-permissions-in-a-workspace
+	Repositories(ctx context.Context, workspace, query, sort string) (*models.RepositoryPermissionPageScheme, *models.ResponseScheme, error)
+
+	// Repository returns an object for the repository permission of each user in the requested repository.
+	//
+	// Permissions returned are effective permissions: the highest level of permission the user has.
+	//
+	// GET /2.0/workspaces/{workspace}/permissions/repositories/{repo_slug}
+	//
+	// https://docs.go-atlassian.io/bitbucket-cloud/workspace/permissions#get-repository-permission-in-a-workspace
+	Repository(ctx context.Context, workspace, repository, query, sort string) (*models.RepositoryPermissionPageScheme, *models.ResponseScheme, error)
+}


Test patch:
diff --git a/bitbucket/api_client_impl_test.go b/bitbucket/api_client_impl_test.go
new file mode 100644
index 0000000..4dcf696
--- /dev/null
+++ b/bitbucket/api_client_impl_test.go
@@ -0,0 +1,559 @@
+package bitbucket
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"github.com/ctreminiom/go-atlassian/bitbucket/internal"
+	model "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"
+	"net/http"
+	"net/url"
+	"strings"
+	"testing"
+)
+
+func TestClient_Call(t *testing.T) {
+
+	expectedResponse := &http.Response{
+		StatusCode: http.StatusOK,
+		Body:       io.NopCloser(strings.NewReader("Hello, world!")),
+		Request: &http.Request{
+			Method: http.MethodGet,
+			URL:    &url.URL{},
+		},
+	}
+
+	badRequestResponse := &http.Response{
+		StatusCode: http.StatusBadRequest,
+		Body:       io.NopCloser(strings.NewReader("Hello, world!")),
+		Request: &http.Request{
+			Method: http.MethodGet,
+			URL:    &url.URL{},
+		},
+	}
+
+	internalServerResponse := &http.Response{
+		StatusCode: http.StatusInternalServerError,
+		Body:       io.NopCloser(strings.NewReader("Hello, world!")),
+		Request: &http.Request{
+			Method: http.MethodGet,
+			URL:    &url.URL{},
+		},
+	}
+
+	unauthorizedResponse := &http.Response{
+		StatusCode: http.StatusUnauthorized,
+		Body:       io.NopCloser(strings.NewReader("Hello, world!")),
+		Request: &http.Request{
+			Method: http.MethodGet,
+			URL:    &url.URL{},
+		},
+	}
+
+	notFoundResponse := &http.Response{
+		StatusCode: http.StatusNotFound,
+		Body:       io.NopCloser(strings.NewReader("Hello, world!")),
+		Request: &http.Request{
+			Method: http.MethodGet,
+			URL:    &url.URL{},
+		},
+	}
+
+	type fields struct {
+		HTTP common.HttpClient
+		Site *url.URL
+		Auth common.Authentication
+	}
+	type args struct {
+		request   *http.Request
+		structure interface{}
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		want    *model.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: &model.ResponseScheme{
+				Response: expectedResponse,
+				Code:     http.StatusOK,
+				Method:   http.MethodGet,
+				Bytes:    *bytes.NewBufferString("Hello, world!"),
+			},
+			wantErr: false,
+		},
+
+		{
+			name: "when the response status is a bad request",
+			on: func(fields *fields) {
+
+				client := mocks.NewHttpClient(t)
+
+				client.On("Do", (*http.Request)(nil)).
+					Return(badRequestResponse, nil)
+
+				fields.HTTP = client
+			},
+			args: args{
+				request:   nil,
+				structure: nil,
+			},
+			want: &model.ResponseScheme{
+				Response: badRequestResponse,
+				Code:     http.StatusBadRequest,
+				Method:   http.MethodGet,
+				Bytes:    *bytes.NewBufferString("Hello, world!"),
+			},
+			wantErr: true,
+			Err:     model.ErrBadRequestError,
+		},
+
+		{
+			name: "when the response status is an internal service error",
+			on: func(fields *fields) {
+
+				client := mocks.NewHttpClient(t)
+
+				client.On("Do", (*http.Request)(nil)).
+					Return(internalServerResponse, nil)
+
+				fields.HTTP = client
+			},
+			args: args{
+				request:   nil,
+				structure: nil,
+			},
+			want: &model.ResponseScheme{
+				Response: internalServerResponse,
+				Code:     http.StatusInternalServerError,
+				Method:   http.MethodGet,
+				Bytes:    *bytes.NewBufferString("Hello, world!"),
+			},
+			wantErr: true,
+			Err:     model.ErrInternalError,
+		},
+
+		{
+			name: "when the response status is a not found",
+			on: func(fields *fields) {
+
+				client := mocks.NewHttpClient(t)
+
+				client.On("Do", (*http.Request)(nil)).
+					Return(notFoundResponse, nil)
+
+				fields.HTTP = client
+			},
+			args: args{
+				request:   nil,
+				structure: nil,
+			},
+			want: &model.ResponseScheme{
+				Response: notFoundResponse,
+				Code:     http.StatusNotFound,
+				Method:   http.MethodGet,
+				Bytes:    *bytes.NewBufferString("Hello, world!"),
+			},
+			wantErr: true,
+			Err:     model.ErrNotFound,
+		},
+
+		{
+			name: "when the response status is unauthorized",
+			on: func(fields *fields) {
+
+				client := mocks.NewHttpClient(t)
+
+				client.On("Do", (*http.Request)(nil)).
+					Return(unauthorizedResponse, nil)
+
+				fields.HTTP = client
+			},
+			args: args{
+				request:   nil,
+				structure: nil,
+			},
+			want: &model.ResponseScheme{
+				Response: unauthorizedResponse,
+				Code:     http.StatusUnauthorized,
+				Method:   http.MethodGet,
+				Bytes:    *bytes.NewBufferString("Hello, world!"),
+			},
+			wantErr: true,
+			Err:     model.ErrUnauthorized,
+		},
+	}
+	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,
+			}
+
+			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 TestClient_NewRequest(t *testing.T) {
+
+	authMocked := internal.NewAuthenticationService(nil)
+	authMocked.SetBasicAuth("mail", "token")
+	authMocked.SetUserAgent("firefox")
+	authMocked.SetBearerToken("token_sample")
+
+	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
+		urlStr string
+		type_  string
+		body   interface{}
+	}
+
+	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,
+				urlStr: "rest/2/issue/attachment",
+				type_:  "",
+				body:   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,
+				urlStr: " https://zhidao.baidu.com/special/view?id=49105a24626975510000&preview=1",
+				body:   bytes.NewReader([]byte("Hello World")),
+			},
+			want:    nil,
+			wantErr: true,
+		},
+
+		{
+			name: "when the content type is provided",
+			fields: fields{
+				HTTP: http.DefaultClient,
+				Auth: authMocked,
+				Site: siteAsURL,
+			},
+			args: args{
+				ctx:    context.TODO(),
+				method: http.MethodGet,
+				urlStr: "rest/2/issue/attachment",
+				type_:  "type_sample",
+				body:   bytes.NewReader([]byte("Hello World")),
+			},
+			want:    requestMocked,
+			wantErr: false,
+		},
+
+		{
+			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,
+				urlStr: "rest/2/issue/attachment",
+				body:   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.urlStr,
+				testCase.args.type_,
+				testCase.args.body,
+			)
+
+			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_processResponse(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:       io.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    *model.ResponseScheme
+		wantErr bool
+		Err     error
+	}{
+		{
+			name:   "when the parameters are correct",
+			fields: fields{},
+			args: args{
+				response:  expectedResponse,
+				structure: model.BoardScheme{},
+			},
+			want: &model.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.processResponse(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 TestNew(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")
+
+	invalidURLClientMocked, _ := 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
+		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 is not provided are correct",
+			args: args{
+				httpClient: http.DefaultClient,
+				site:       "",
+			},
+			want:    mockClient,
+			wantErr: false,
+		},
+
+		{
+			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:    invalidURLClientMocked,
+			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)
+			}
+		})
+	}
+}
diff --git a/bitbucket/internal/authentication_impl_test.go b/bitbucket/internal/authentication_impl_test.go
new file mode 100644
index 0000000..052f346
--- /dev/null
+++ b/bitbucket/internal/authentication_impl_test.go
@@ -0,0 +1,454 @@
+package internal
+
+import (
+	"github.com/ctreminiom/go-atlassian/service"
+	"github.com/ctreminiom/go-atlassian/service/common"
+	"github.com/ctreminiom/go-atlassian/service/mocks"
+	"github.com/stretchr/testify/assert"
+	"reflect"
+	"testing"
+)
+
+func TestAuthenticationService_GetBasicAuth(t *testing.T) {
+
+	type fields struct {
+		c                 service.Connector
+		basicAuthProvided bool
+		mail              string
+		token             string
+		userAgentProvided bool
+		agent             string
+	}
+	testCases := []struct {
+		name   string
+		fields fields
+		want   string
+		want1  string
+	}{
+		{
+			name: "when the basic auth is already set",
+			fields: fields{
+				c:     mocks.NewConnector(t),
+				mail:  "mail",
+				token: "token",
+			},
+			want:  "mail",
+			want1: "token",
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			a := &AuthenticationService{
+				c:                 testCase.fields.c,
+				basicAuthProvided: testCase.fields.basicAuthProvided,
+				mail:              testCase.fields.mail,
+				token:             testCase.fields.token,
+				userAgentProvided: testCase.fields.userAgentProvided,
+				agent:             testCase.fields.agent,
+			}
+
+			got, got1 := a.GetBasicAuth()
+			if got != testCase.want {
+				t.Errorf("GetBasicAuth() got = %v, want %v", got, testCase.want)
+			}
+
+			if got1 != testCase.want1 {
+				t.Errorf("GetBasicAuth() got1 = %v, want %v", got1, testCase.want1)
+			}
+		})
+	}
+}
+
+func TestAuthenticationService_GetUserAgent(t *testing.T) {
+	type fields struct {
+		c                 service.Connector
+		basicAuthProvided bool
+		mail              string
+		token             string
+		userAgentProvided bool
+		agent             string
+	}
+	testCases := []struct {
+		name   string
+		fields fields
+		want   string
+	}{
+		{
+			name: "when the user agent is already set",
+			fields: fields{
+				c:     mocks.NewConnector(t),
+				agent: "firefox-09",
+			},
+			want: "firefox-09",
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			a := &AuthenticationService{
+				c:                 testCase.fields.c,
+				basicAuthProvided: testCase.fields.basicAuthProvided,
+				mail:              testCase.fields.mail,
+				token:             testCase.fields.token,
+				userAgentProvided: testCase.fields.userAgentProvided,
+				agent:             testCase.fields.agent,
+			}
+			if got := a.GetUserAgent(); got != testCase.want {
+				t.Errorf("GetUserAgent() = %v, want %v", got, testCase.want)
+			}
+		})
+	}
+}
+
+func TestAuthenticationService_HasBasicAuth(t *testing.T) {
+	type fields struct {
+		c                 service.Connector
+		basicAuthProvided bool
+		mail              string
+		token             string
+		userAgentProvided bool
+		agent             string
+	}
+	testCases := []struct {
+		name   string
+		fields fields
+		want   bool
+	}{
+		{
+			name: "when the params are correct",
+			fields: fields{
+				c:                 mocks.NewConnector(t),
+				basicAuthProvided: true,
+			},
+			want: true,
+		},
+	}
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+			a := &AuthenticationService{
+				c:                 testCase.fields.c,
+				basicAuthProvided: testCase.fields.basicAuthProvided,
+				mail:              testCase.fields.mail,
+				token:             testCase.fields.token,
+				userAgentProvided: testCase.fields.userAgentProvided,
+				agent:             testCase.fields.agent,
+			}
+			if got := a.HasBasicAuth(); got != testCase.want {
+				t.Errorf("HasBasicAuth() = %v, want %v", got, testCase.want)
+			}
+		})
+	}
+}
+
+func TestAuthenticationService_HasUserAgent(t *testing.T) {
+	type fields struct {
+		c                 service.Connector
+		basicAuthProvided bool
+		mail              string
+		token             string
+		userAgentProvided bool
+		agent             string
+	}
+	testCases := []struct {
+		name   string
+		fields fields
+		want   bool
+	}{
+		{
+			name: "when the parameters are correct",
+			fields: fields{
+				userAgentProvided: true,
+			},
+			want: true,
+		},
+	}
+	for _, testCase := range testCases {
+
+		t.Run(testCase.name, func(t *testing.T) {
+			a := &AuthenticationService{
+				c:                 testCase.fields.c,
+				basicAuthProvided: testCase.fields.basicAuthProvided,
+				mail:              testCase.fields.mail,
+				token:             testCase.fields.token,
+				userAgentProvided: testCase.fields.userAgentProvided,
+				agent:             testCase.fields.agent,
+			}
+
+			if got := a.HasUserAgent(); got != testCase.want {
+				t.Errorf("HasUserAgent() = %v, want %v", got, testCase.want)
+			}
+		})
+	}
+}
+
+func TestNewAuthenticationService(t *testing.T) {
+
+	clientMocked := mocks.NewConnector(t)
+
+	type args struct {
+		client service.Connector
+	}
+	testCases := []struct {
+		name string
+		args args
+		want common.Authentication
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				client: clientMocked,
+			},
+			want: NewAuthenticationService(clientMocked),
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			if got := NewAuthenticationService(testCase.args.client); !reflect.DeepEqual(got, testCase.want) {
+				t.Errorf("NewAuthenticationService() = %v, want %v", got, testCase.want)
+			}
+		})
+	}
+}
+
+func TestAuthenticationService_SetUserAgent(t *testing.T) {
+
+	type fields struct {
+		c                 service.Connector
+		basicAuthProvided bool
+		mail              string
+		token             string
+		userAgentProvided bool
+		agent             string
+	}
+	type args struct {
+		agent string
+	}
+	testCases := []struct {
+		name   string
+		fields fields
+		args   args
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{agent: "mozilla-9"},
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			a := &AuthenticationService{
+				c:                 testCase.fields.c,
+				basicAuthProvided: testCase.fields.basicAuthProvided,
+				mail:              testCase.fields.mail,
+				token:             testCase.fields.token,
+				userAgentProvided: testCase.fields.userAgentProvided,
+				agent:             testCase.fields.agent,
+			}
+
+			a.SetUserAgent(testCase.args.agent)
+		})
+	}
+}
+
+func TestAuthenticationService_SetBasicAuth(t *testing.T) {
+
+	type fields struct {
+		c                 service.Connector
+		basicAuthProvided bool
+		mail              string
+		token             string
+		userAgentProvided bool
+		agent             string
+	}
+	type args struct {
+		mail  string
+		token string
+	}
+	testCases := []struct {
+		name   string
+		fields fields
+		args   args
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				mail:  "example@example.com",
+				token: "token-sample",
+			},
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			a := &AuthenticationService{
+				c:                 testCase.fields.c,
+				basicAuthProvided: testCase.fields.basicAuthProvided,
+				mail:              testCase.fields.mail,
+				token:             testCase.fields.token,
+				userAgentProvided: testCase.fields.userAgentProvided,
+				agent:             testCase.fields.agent,
+			}
+
+			a.SetBasicAuth(testCase.args.mail, testCase.args.token)
+		})
+	}
+}
+
+func TestAuthenticationService_HasSetExperimentalFlag(t *testing.T) {
+
+	type fields struct {
+		c                 service.Connector
+		basicAuthProvided bool
+		mail              string
+		token             string
+		userAgentProvided bool
+		agent             string
+	}
+	testCases := []struct {
+		name   string
+		fields fields
+		want   bool
+	}{
+
+		{
+			name:   "when the parameters are correct",
+			fields: fields{},
+			want:   false,
+		},
+	}
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			a := &AuthenticationService{
+				c:                 testCase.fields.c,
+				basicAuthProvided: testCase.fields.basicAuthProvided,
+				mail:              testCase.fields.mail,
+				token:             testCase.fields.token,
+				userAgentProvided: testCase.fields.userAgentProvided,
+				agent:             testCase.fields.agent,
+			}
+			if got := a.HasSetExperimentalFlag(); got != testCase.want {
+				t.Errorf("HasSetExperimentalFlag() = %v, want %v", got, testCase.want)
+			}
+		})
+	}
+}
+
+func TestAuthenticationService_GetBearerToken(t *testing.T) {
+	type fields struct {
+		c                 service.Connector
+		basicAuthProvided bool
+		mail              string
+		token             string
+		userAgentProvided bool
+		agent             string
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		want   string
+	}{
+		{
+			name: "when the parameters are correct",
+			fields: fields{
+				c:     mocks.NewConnector(t),
+				token: "token-sample",
+			},
+			want: "token-sample",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			a := &AuthenticationService{
+				c:                 tt.fields.c,
+				basicAuthProvided: tt.fields.basicAuthProvided,
+				mail:              tt.fields.mail,
+				token:             tt.fields.token,
+				userAgentProvided: tt.fields.userAgentProvided,
+				agent:             tt.fields.agent,
+			}
+			assert.Equalf(t, tt.want, a.GetBearerToken(), "GetBearerToken()")
+		})
+	}
+}
+
+func TestAuthenticationService_SetBearerToken(t *testing.T) {
+	type fields struct {
+		c                 service.Connector
+		basicAuthProvided bool
+		mail              string
+		token             string
+		userAgentProvided bool
+		agent             string
+	}
+	type args struct {
+		token string
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		args   args
+	}{
+		{
+			name: "when the parameters are correct",
+			fields: fields{
+				token: "token-sample",
+			},
+			args: args{
+				token: "token-sample",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			a := &AuthenticationService{
+				c:                 tt.fields.c,
+				basicAuthProvided: tt.fields.basicAuthProvided,
+				mail:              tt.fields.mail,
+				token:             tt.fields.token,
+				userAgentProvided: tt.fields.userAgentProvided,
+				agent:             tt.fields.agent,
+			}
+			a.SetBearerToken(tt.args.token)
+		})
+	}
+}
+
+func TestAuthenticationService_SetExperimentalFlag(t *testing.T) {
+	type fields struct {
+		c                 service.Connector
+		basicAuthProvided bool
+		mail              string
+		token             string
+		userAgentProvided bool
+		agent             string
+	}
+	tests := []struct {
+		name   string
+		fields fields
+	}{
+		{
+			name: "when the parameters are correct",
+			fields: fields{
+				c:                 nil,
+				basicAuthProvided: false,
+				mail:              "",
+				token:             "",
+				userAgentProvided: false,
+				agent:             "",
+			},
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			a := &AuthenticationService{
+				c:                 tt.fields.c,
+				basicAuthProvided: tt.fields.basicAuthProvided,
+				mail:              tt.fields.mail,
+				token:             tt.fields.token,
+				userAgentProvided: tt.fields.userAgentProvided,
+				agent:             tt.fields.agent,
+			}
+			a.SetExperimentalFlag()
+		})
+	}
+}
diff --git a/bitbucket/internal/workspace_impl_test.go b/bitbucket/internal/workspace_impl_test.go
new file mode 100644
index 0000000..5125cb2
--- /dev/null
+++ b/bitbucket/internal/workspace_impl_test.go
@@ -0,0 +1,461 @@
+package internal
+
+import (
+	"context"
+	"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"
+	"net/http"
+	"testing"
+)
+
+func Test_internalWorkspaceServiceImpl_Get(t *testing.T) {
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx       context.Context
+		workspace string
+	}
+
+	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(),
+				workspace: "work-space-name-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample",
+					"", nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.WorkspaceScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample",
+					"", 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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspaceService(testCase.fields.c, nil, nil)
+
+			gotResult, gotResponse, err := newService.Get(testCase.args.ctx, testCase.args.workspace)
+
+			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_internalWorkspaceServiceImpl_Members(t *testing.T) {
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx       context.Context
+		workspace string
+	}
+
+	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(),
+				workspace: "work-space-name-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/members",
+					"", nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.WorkspaceMembershipPageScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/members",
+					"", 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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspaceService(testCase.fields.c, nil, nil)
+
+			gotResult, gotResponse, err := newService.Members(testCase.args.ctx, testCase.args.workspace)
+
+			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_internalWorkspaceServiceImpl_Projects(t *testing.T) {
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx       context.Context
+		workspace string
+	}
+
+	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(),
+				workspace: "work-space-name-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/projects",
+					"", nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.BitbucketProjectPageScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/projects",
+					"", 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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspaceService(testCase.fields.c, nil, nil)
+
+			gotResult, gotResponse, err := newService.Projects(testCase.args.ctx, testCase.args.workspace)
+
+			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_internalWorkspaceServiceImpl_Membership(t *testing.T) {
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx       context.Context
+		workspace string
+		memberId  string
+	}
+
+	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(),
+				workspace: "work-space-name-sample",
+				memberId:  "account-id-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/members/account-id-sample",
+					"", nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.WorkspaceMembershipScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+				memberId:  "account-id-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/members/account-id-sample",
+					"", 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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+
+		{
+			name: "when the member id is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+			},
+			wantErr: true,
+			Err:     model.ErrNoMemberIDError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspaceService(testCase.fields.c, nil, nil)
+
+			gotResult, gotResponse, err := newService.Membership(testCase.args.ctx, testCase.args.workspace, testCase.args.memberId)
+
+			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/bitbucket/internal/workspace_permissions_impl_test.go b/bitbucket/internal/workspace_permissions_impl_test.go
new file mode 100644
index 0000000..bb794ad
--- /dev/null
+++ b/bitbucket/internal/workspace_permissions_impl_test.go
@@ -0,0 +1,369 @@
+package internal
+
+import (
+	"context"
+	"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"
+	"net/http"
+	"testing"
+)
+
+func Test_internalWorkspacePermissionServiceImpl_Members(t *testing.T) {
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx       context.Context
+		workspace string
+		query     string
+	}
+
+	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(),
+				workspace: "work-space-name-sample",
+				query:     "permission=\"owner\"",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/permissions?q=permission%3D%22owner%22",
+					"", nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.WorkspaceMembershipPageScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+				query:     "permission=\"owner\"",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/permissions?q=permission%3D%22owner%22",
+					"", 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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspacePermissionService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.Members(testCase.args.ctx, testCase.args.workspace, testCase.args.query)
+
+			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_internalWorkspacePermissionServiceImpl_Repositories(t *testing.T) {
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx       context.Context
+		workspace string
+		query     string
+		sort      string
+	}
+
+	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(),
+				workspace: "work-space-name-sample",
+				query:     "permission=\"owner\"",
+				sort:      "user.display_name",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/permissions/repositories?q=permission%3D%22owner%22&sort=user.display_name",
+					"", nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.RepositoryPermissionPageScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+				query:     "permission=\"owner\"",
+				sort:      "user.display_name",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/permissions/repositories?q=permission%3D%22owner%22&sort=user.display_name",
+					"", 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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspacePermissionService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.Repositories(testCase.args.ctx, testCase.args.workspace, testCase.args.query,
+				testCase.args.sort)
+
+			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_internalWorkspacePermissionServiceImpl_Repository(t *testing.T) {
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx        context.Context
+		workspace  string
+		repository string
+		query      string
+		sort       string
+	}
+
+	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(),
+				workspace:  "work-space-name-sample",
+				query:      "permission=\"owner\"",
+				sort:       "user.display_name",
+				repository: "microservice-a",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/permissions/repositories/microservice-a?q=permission%3D%22owner%22&sort=user.display_name",
+					"", nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.RepositoryPermissionPageScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:        context.TODO(),
+				workspace:  "work-space-name-sample",
+				query:      "permission=\"owner\"",
+				sort:       "user.display_name",
+				repository: "microservice-a",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/permissions/repositories/microservice-a?q=permission%3D%22owner%22&sort=user.display_name",
+					"", 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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+
+		{
+			name: "when the repository is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+			},
+			wantErr: true,
+			Err:     model.ErrNoRepositoryError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspacePermissionService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.Repository(testCase.args.ctx, testCase.args.workspace, testCase.args.repository,
+				testCase.args.query, testCase.args.sort)
+
+			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/bitbucket/internal/workspace_webhooks_impl_test.go b/bitbucket/internal/workspace_webhooks_impl_test.go
new file mode 100644
index 0000000..8e2f163
--- /dev/null
+++ b/bitbucket/internal/workspace_webhooks_impl_test.go
@@ -0,0 +1,616 @@
+package internal
+
+import (
+	"context"
+	"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"
+	"net/http"
+	"testing"
+)
+
+func Test_internalWorkspaceWebhookServiceImpl_Gets(t *testing.T) {
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx       context.Context
+		workspace string
+	}
+
+	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(),
+				workspace: "work-space-name-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/hooks",
+					"", nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.WebhookSubscriptionPageScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/hooks",
+					"", 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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspaceHookService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.Gets(testCase.args.ctx, testCase.args.workspace)
+
+			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_internalWorkspaceWebhookServiceImpl_Get(t *testing.T) {
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx       context.Context
+		workspace string
+		webhookId string
+	}
+
+	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(),
+				workspace: "work-space-name-sample",
+				webhookId: "uuid-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/hooks/uuid-sample",
+					"", nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.WebhookSubscriptionScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+				webhookId: "uuid-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"2.0/workspaces/work-space-name-sample/hooks/uuid-sample",
+					"", 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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+
+		{
+			name: "when the webhook is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWebhookIDError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspaceHookService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.Get(testCase.args.ctx, testCase.args.workspace, testCase.args.webhookId)
+
+			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_internalWorkspaceWebhookServiceImpl_Create(t *testing.T) {
+
+	payloadMocked := &model.WebhookSubscriptionPayloadScheme{
+		Description: "",
+		Url:         "",
+		Active:      false,
+		Events:      nil,
+	}
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx       context.Context
+		workspace string
+		payload   *model.WebhookSubscriptionPayloadScheme
+	}
+
+	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(),
+				workspace: "work-space-name-sample",
+				payload:   payloadMocked,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"2.0/workspaces/work-space-name-sample/hooks",
+					"", payloadMocked).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.WebhookSubscriptionScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+				payload:   payloadMocked,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"2.0/workspaces/work-space-name-sample/hooks",
+					"", payloadMocked).
+					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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspaceHookService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.Create(testCase.args.ctx, testCase.args.workspace, 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_internalWorkspaceWebhookServiceImpl_Update(t *testing.T) {
+
+	payloadMocked := &model.WebhookSubscriptionPayloadScheme{
+		Description: "",
+		Url:         "",
+		Active:      false,
+		Events:      nil,
+	}
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx       context.Context
+		workspace string
+		webhookId string
+		payload   *model.WebhookSubscriptionPayloadScheme
+	}
+
+	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(),
+				workspace: "work-space-name-sample",
+				webhookId: "webhook-uuid",
+				payload:   payloadMocked,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPut,
+					"2.0/workspaces/work-space-name-sample/hooks/webhook-uuid",
+					"", payloadMocked).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.WebhookSubscriptionScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+				webhookId: "webhook-uuid",
+				payload:   payloadMocked,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPut,
+					"2.0/workspaces/work-space-name-sample/hooks/webhook-uuid",
+					"", payloadMocked).
+					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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+
+		{
+			name: "when the webhook is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWebhookIDError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspaceHookService(testCase.fields.c)
+
+			gotResult, gotResponse, err := newService.Update(testCase.args.ctx, testCase.args.workspace,
+				testCase.args.webhookId, 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_internalWorkspaceWebhookServiceImpl_Delete(t *testing.T) {
+
+	type fields struct {
+		c service.Connector
+	}
+
+	type args struct {
+		ctx       context.Context
+		workspace string
+		webhookId string
+	}
+
+	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(),
+				workspace: "work-space-name-sample",
+				webhookId: "uuid-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"2.0/workspaces/work-space-name-sample/hooks/uuid-sample",
+					"", 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(),
+				workspace: "work-space-name-sample",
+				webhookId: "uuid-sample",
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewConnector(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"2.0/workspaces/work-space-name-sample/hooks/uuid-sample",
+					"", 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 workspace is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWorkspaceError,
+		},
+
+		{
+			name: "when the webhook is not provided",
+			args: args{
+				ctx:       context.TODO(),
+				workspace: "work-space-name-sample",
+			},
+			wantErr: true,
+			Err:     model.ErrNoWebhookIDError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			newService := NewWorkspaceHookService(testCase.fields.c)
+
+			gotResponse, err := newService.Delete(testCase.args.ctx, testCase.args.workspace, testCase.args.webhookId)
+
+			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)
+			}
+
+		})
+	}
+}