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

:sparkles: Created the Jira Remote Link service.
1. Added the possibility to extract the issue remote links
2. Documented the service methods on the documentation site.


Patch:
diff --git a/confluence/api_client_impl.go b/confluence/api_client_impl.go
index 0254be8..d9a917b 100644
--- a/confluence/api_client_impl.go
+++ b/confluence/api_client_impl.go
@@ -55,21 +55,19 @@ func New(httpClient common.HttpClient, site string) (*Client, error) {
 	client.Label = internal.NewLabelService(client)
 	client.Search = internal.NewSearchService(client)
 	client.LongTask = internal.NewTaskService(client)
-	client.Analytics = internal.NewAnalyticsService(client)
 
 	return client, nil
 }
 
 type Client struct {
-	HTTP      common.HttpClient
-	Site      *url.URL
-	Auth      common.Authentication
-	Content   *internal.ContentService
-	Space     *internal.SpaceService
-	Label     *internal.LabelService
-	Search    *internal.SearchService
-	LongTask  *internal.TaskService
-	Analytics *internal.AnalyticsService
+	HTTP     common.HttpClient
+	Site     *url.URL
+	Auth     common.Authentication
+	Content  *internal.ContentService
+	Space    *internal.SpaceService
+	Label    *internal.LabelService
+	Search   *internal.SearchService
+	LongTask *internal.TaskService
 }
 
 func (c *Client) NewFormRequest(ctx context.Context, method, apiEndpoint, contentType string, payload io.Reader) (*http.Request, error) {
diff --git a/confluence/internal/analytics_impl.go b/confluence/internal/analytics_impl.go
deleted file mode 100644
index 32b8fa6..0000000
--- a/confluence/internal/analytics_impl.go
+++ /dev/null
@@ -1,105 +0,0 @@
-package internal
-
-import (
-	"context"
-	"fmt"
-	model "github.com/ctreminiom/go-atlassian/pkg/infra/models"
-	"github.com/ctreminiom/go-atlassian/service"
-	"github.com/ctreminiom/go-atlassian/service/confluence"
-	"net/http"
-	"net/url"
-	"strings"
-)
-
-func NewAnalyticsService(client service.Client) *AnalyticsService {
-
-	return &AnalyticsService{
-		internalClient: &internalAnalyticsServiceImpl{c: client},
-	}
-}
-
-type AnalyticsService struct {
-	internalClient confluence.AnalyticsConnector
-}
-
-// Get gets the total number of views a piece of content has.
-//
-// GET /wiki/rest/api/analytics/content/{contentId}/views
-//
-// https://docs.go-atlassian.io/confluence-cloud/analytics#get-views
-func (a *AnalyticsService) Get(ctx context.Context, contentId, fromDate string) (*model.ContentViewScheme, *model.ResponseScheme, error) {
-	return a.internalClient.Get(ctx, contentId, fromDate)
-}
-
-// Distinct get the total number of distinct viewers a piece of content has.
-//
-// GET /wiki/rest/api/analytics/content/{contentId}/viewers
-//
-// https://docs.go-atlassian.io/confluence-cloud/analytics#get-viewers
-func (a *AnalyticsService) Distinct(ctx context.Context, contentId, fromDate string) (*model.ContentViewScheme, *model.ResponseScheme, error) {
-	return a.internalClient.Distinct(ctx, contentId, fromDate)
-}
-
-type internalAnalyticsServiceImpl struct {
-	c service.Client
-}
-
-func (i *internalAnalyticsServiceImpl) Get(ctx context.Context, contentId, fromDate string) (*model.ContentViewScheme, *model.ResponseScheme, error) {
-
-	if contentId == "" {
-		return nil, nil, model.ErrNoContentIDError
-	}
-
-	var endpoint strings.Builder
-	endpoint.WriteString(fmt.Sprintf("wiki/rest/api/analytics/content/%v/views", contentId))
-
-	if fromDate != "" {
-		query := url.Values{}
-		query.Add("fromDate", fromDate)
-
-		endpoint.WriteString(fmt.Sprintf("?%v", query.Encode()))
-	}
-
-	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint.String(), nil)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	views := new(model.ContentViewScheme)
-	response, err := i.c.Call(request, views)
-	if err != nil {
-		return nil, response, err
-	}
-
-	return views, response, nil
-}
-
-func (i *internalAnalyticsServiceImpl) Distinct(ctx context.Context, contentId, fromDate string) (*model.ContentViewScheme, *model.ResponseScheme, error) {
-
-	if contentId == "" {
-		return nil, nil, model.ErrNoContentIDError
-	}
-
-	var endpoint strings.Builder
-	endpoint.WriteString(fmt.Sprintf("wiki/rest/api/analytics/content/%v/viewers", contentId))
-
-	if fromDate != "" {
-		query := url.Values{}
-		query.Add("fromDate", fromDate)
-
-		endpoint.WriteString(fmt.Sprintf("?%v", query.Encode()))
-	}
-
-	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint.String(), nil)
-	if err != nil {
-		return nil, nil, err
-	}
-
-	views := new(model.ContentViewScheme)
-	response, err := i.c.Call(request, views)
-	if err != nil {
-		return nil, response, err
-	}
-
-	return views, response, nil
-}
diff --git a/jira/internal/link_impl.go b/jira/internal/link_impl.go
index 0b753d9..5c1b7e6 100644
--- a/jira/internal/link_impl.go
+++ b/jira/internal/link_impl.go
@@ -5,7 +5,7 @@ import (
 	"github.com/ctreminiom/go-atlassian/service"
 )
 
-func NewLinkService(client service.Client, version string, type_ *LinkTypeService) (*LinkADFService, *LinkRichTextService, error) {
+func NewLinkService(client service.Client, version string, type_ *LinkTypeService, remote *RemoteLinkService) (*LinkADFService, *LinkRichTextService, error) {
 
 	if version == "" {
 		return nil, nil, model.ErrNoVersionProvided
@@ -16,7 +16,8 @@ func NewLinkService(client service.Client, version string, type_ *LinkTypeServic
 			c:       client,
 			version: version,
 		},
-		Type: type_,
+		Type:   type_,
+		Remote: remote,
 	}
 
 	richTextService := &LinkRichTextService{
@@ -24,7 +25,8 @@ func NewLinkService(client service.Client, version string, type_ *LinkTypeServic
 			c:       client,
 			version: version,
 		},
-		Type: type_,
+		Type:   type_,
+		Remote: remote,
 	}
 
 	return adfService, richTextService, nil
diff --git a/jira/internal/link_impl_adf.go b/jira/internal/link_impl_adf.go
index aa836f2..4016510 100644
--- a/jira/internal/link_impl_adf.go
+++ b/jira/internal/link_impl_adf.go
@@ -12,6 +12,7 @@ import (
 type LinkADFService struct {
 	internalClient jira.LinkAdfIssueConnector
 	Type           *LinkTypeService
+	Remote         *RemoteLinkService
 }
 
 type internalLinkADFServiceImpl struct {
diff --git a/jira/internal/link_impl_rich_text.go b/jira/internal/link_impl_rich_text.go
index 2c400c2..649717e 100644
--- a/jira/internal/link_impl_rich_text.go
+++ b/jira/internal/link_impl_rich_text.go
@@ -12,6 +12,7 @@ import (
 type LinkRichTextService struct {
 	internalClient jira.LinkRichTextConnector
 	Type           *LinkTypeService
+	Remote         *RemoteLinkService
 }
 
 type internalLinkRichTextServiceImpl struct {
diff --git a/jira/internal/remote_link_impl.go b/jira/internal/remote_link_impl.go
new file mode 100644
index 0000000..9e6b42d
--- /dev/null
+++ b/jira/internal/remote_link_impl.go
@@ -0,0 +1,253 @@
+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/jira"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+func NewRemoteLinkService(client service.Client, version string) (*RemoteLinkService, error) {
+
+	if version == "" {
+		return nil, model.ErrNoVersionProvided
+	}
+
+	return &RemoteLinkService{
+		internalClient: &internalRemoteLinkImpl{c: client, version: version},
+	}, nil
+}
+
+type RemoteLinkService struct {
+	internalClient jira.RemoteLinkConnector
+}
+
+// Gets returns the remote issue links for an issue.
+//
+// When a remote issue link global ID is provided the record with that global ID is returned,
+//
+// otherwise all remote issue links are returned.
+//
+// # Where a global ID includes reserved URL characters these must be escaped in the request
+//
+// GET /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink
+//
+// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#get-remote-issue-links
+func (r *RemoteLinkService) Gets(ctx context.Context, issueKeyOrId, globalId string) ([]*model.RemoteLinkScheme, *model.ResponseScheme, error) {
+	return r.internalClient.Gets(ctx, issueKeyOrId, globalId)
+}
+
+// Get returns a remote issue link for an issue.
+//
+// GET /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink/{linkId}
+//
+// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#get-remote-issue-link
+func (r *RemoteLinkService) Get(ctx context.Context, issueKeyOrId, linkId string) (*model.RemoteLinkScheme, *model.ResponseScheme, error) {
+	return r.internalClient.Get(ctx, issueKeyOrId, linkId)
+}
+
+// Create creates or updates a remote issue link for an issue.
+//
+// If a globalId is provided and a remote issue link with that global ID is found it is updated.
+//
+// Any fields without values in the request are set to null. Otherwise, the remote issue link is created.
+//
+// POST /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink
+//
+// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#create-remote-issue-link
+func (r *RemoteLinkService) Create(ctx context.Context, issueKeyOrId string, payload *model.RemoteLinkScheme) (*model.RemoteLinkIdentify, *model.ResponseScheme, error) {
+	return r.internalClient.Create(ctx, issueKeyOrId, payload)
+}
+
+// Update updates a remote issue link for an issue.
+//
+// Note: Fields without values in the request are set to null.
+//
+// PUT /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink/{linkId}
+//
+// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#update-remote-issue-link
+func (r *RemoteLinkService) Update(ctx context.Context, issueKeyOrId, linkId string, payload *model.RemoteLinkScheme) (*model.ResponseScheme, error) {
+	return r.internalClient.Update(ctx, issueKeyOrId, linkId, payload)
+}
+
+// DeleteById deletes a remote issue link from an issue.
+//
+// DELETE /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink/{linkId}
+//
+// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#delete-remote-issue-link-by-id
+func (r *RemoteLinkService) DeleteById(ctx context.Context, issueKeyOrId, linkId string) (*model.ResponseScheme, error) {
+	return r.internalClient.DeleteById(ctx, issueKeyOrId, linkId)
+}
+
+// DeleteByGlobalId deletes the remote issue link from the issue using the link's global ID.
+//
+// Where the global ID includes reserved URL characters these must be escaped in the request.
+//
+// For example, pass system=http://www.mycompany.com/support&id=1 as system%3Dhttp%3A%2F%2Fwww.mycompany.com%2Fsupport%26id%3D1.
+//
+// DELETE /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink
+//
+// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#delete-remote-issue-link-by-global-id
+func (r *RemoteLinkService) DeleteByGlobalId(ctx context.Context, issueKeyOrId, globalId string) (*model.ResponseScheme, error) {
+	return r.internalClient.DeleteByGlobalId(ctx, issueKeyOrId, globalId)
+}
+
+type internalRemoteLinkImpl struct {
+	c       service.Client
+	version string
+}
+
+func (i *internalRemoteLinkImpl) Gets(ctx context.Context, issueKeyOrId, globalId string) ([]*model.RemoteLinkScheme, *model.ResponseScheme, error) {
+
+	if issueKeyOrId == "" {
+		return nil, nil, model.ErrNoIssueKeyOrIDError
+	}
+
+	var endpoint strings.Builder
+	endpoint.WriteString(fmt.Sprintf("rest/api/%v/issue/%v/remotelink", i.version, issueKeyOrId))
+
+	if globalId != "" {
+
+		params := url.Values{}
+		params.Add("globalId", globalId)
+		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
+	}
+
+	var remoteLinks []*model.RemoteLinkScheme
+	response, err := i.c.Call(request, &remoteLinks)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return remoteLinks, response, nil
+}
+
+func (i *internalRemoteLinkImpl) Get(ctx context.Context, issueKeyOrId, linkId string) (*model.RemoteLinkScheme, *model.ResponseScheme, error) {
+
+	if issueKeyOrId == "" {
+		return nil, nil, model.ErrNoIssueKeyOrIDError
+	}
+
+	if linkId == "" {
+		return nil, nil, model.ErrNoRemoteLinkIDError
+	}
+
+	endpoint := fmt.Sprintf("rest/api/%v/issue/%v/remotelink/%v", i.version, issueKeyOrId, linkId)
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	remoteLink := new(model.RemoteLinkScheme)
+	response, err := i.c.Call(request, remoteLink)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return remoteLink, response, nil
+}
+
+func (i *internalRemoteLinkImpl) Create(ctx context.Context, issueKeyOrId string, payload *model.RemoteLinkScheme) (*model.RemoteLinkIdentify, *model.ResponseScheme, error) {
+
+	if issueKeyOrId == "" {
+		return nil, nil, model.ErrNoIssueKeyOrIDError
+	}
+
+	reader, err := i.c.TransformStructToReader(payload)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	endpoint := fmt.Sprintf("rest/api/%v/issue/%v/remotelink", i.version, issueKeyOrId)
+
+	request, err := i.c.NewRequest(ctx, http.MethodPost, endpoint, reader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	identify := new(model.RemoteLinkIdentify)
+	response, err := i.c.Call(request, identify)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return identify, response, nil
+}
+
+func (i *internalRemoteLinkImpl) Update(ctx context.Context, issueKeyOrId, linkId string, payload *model.RemoteLinkScheme) (*model.ResponseScheme, error) {
+
+	if issueKeyOrId == "" {
+		return nil, model.ErrNoIssueKeyOrIDError
+	}
+
+	if linkId == "" {
+		return nil, model.ErrNoRemoteLinkIDError
+	}
+
+	reader, err := i.c.TransformStructToReader(payload)
+	if err != nil {
+		return nil, err
+	}
+
+	endpoint := fmt.Sprintf("rest/api/%v/issue/%v/remotelink/%v", i.version, issueKeyOrId, linkId)
+
+	request, err := i.c.NewRequest(ctx, http.MethodPut, endpoint, reader)
+	if err != nil {
+		return nil, err
+	}
+
+	return i.c.Call(request, nil)
+}
+
+func (i *internalRemoteLinkImpl) DeleteById(ctx context.Context, issueKeyOrId, linkId string) (*model.ResponseScheme, error) {
+
+	if issueKeyOrId == "" {
+		return nil, model.ErrNoIssueKeyOrIDError
+	}
+
+	if linkId == "" {
+		return nil, model.ErrNoRemoteLinkIDError
+	}
+
+	endpoint := fmt.Sprintf("rest/api/%v/issue/%v/remotelink/%v", i.version, issueKeyOrId, linkId)
+
+	request, err := i.c.NewRequest(ctx, http.MethodDelete, endpoint, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	return i.c.Call(request, nil)
+}
+
+func (i *internalRemoteLinkImpl) DeleteByGlobalId(ctx context.Context, issueKeyOrId, globalId string) (*model.ResponseScheme, error) {
+
+	if issueKeyOrId == "" {
+		return nil, model.ErrNoIssueKeyOrIDError
+	}
+
+	if globalId == "" {
+		return nil, model.ErrNoRemoteLinkGlobalIDError
+	}
+
+	params := url.Values{}
+	params.Add("globalId", globalId)
+
+	endpoint := fmt.Sprintf("rest/api/%v/issue/%v/remotelink?%v", i.version, issueKeyOrId, params.Encode())
+
+	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/jira/v2/api_client_impl.go b/jira/v2/api_client_impl.go
index cb5ee9b..45b0893 100644
--- a/jira/v2/api_client_impl.go
+++ b/jira/v2/api_client_impl.go
@@ -120,7 +120,12 @@ func New(httpClient common.HttpClient, site string) (*Client, error) {
 		return nil, err
 	}
 
-	_, link, err := internal.NewLinkService(client, "2", linkType)
+	remoteLink, err := internal.NewRemoteLinkService(client, "2")
+	if err != nil {
+		return nil, err
+	}
+
+	_, link, err := internal.NewLinkService(client, "2", linkType, remoteLink)
 	if err != nil {
 		return nil, err
 	}
diff --git a/jira/v3/api_client_impl.go b/jira/v3/api_client_impl.go
index 4bfa6be..cf8d237 100644
--- a/jira/v3/api_client_impl.go
+++ b/jira/v3/api_client_impl.go
@@ -120,7 +120,12 @@ func New(httpClient common.HttpClient, site string) (*Client, error) {
 		return nil, err
 	}
 
-	link, _, err := internal.NewLinkService(client, "3", linkType)
+	remoteLink, err := internal.NewRemoteLinkService(client, "3")
+	if err != nil {
+		return nil, err
+	}
+
+	link, _, err := internal.NewLinkService(client, "3", linkType, remoteLink)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/infra/models/confluence_analytics.go b/pkg/infra/models/confluence_analytics.go
deleted file mode 100644
index ae0070f..0000000
--- a/pkg/infra/models/confluence_analytics.go
+++ /dev/null
@@ -1,6 +0,0 @@
-package models
-
-type ContentViewScheme struct {
-	ID    int `json:"id,omitempty"`
-	Count int `json:"count,omitempty"`
-}
diff --git a/pkg/infra/models/errors.go b/pkg/infra/models/errors.go
index e32418c..0c1587a 100644
--- a/pkg/infra/models/errors.go
+++ b/pkg/infra/models/errors.go
@@ -60,6 +60,8 @@ var (
 	ErrNoGroupIDError                      = errors.New("jira: no group name set")
 	ErrNoGroupsNameError                   = errors.New("jira: no groups names set")
 	ErrNoIssueKeyOrIDError                 = errors.New("jira: no issue key/id set")
+	ErrNoRemoteLinkIDError                 = errors.New("jira: no remote link id set")
+	ErrNoRemoteLinkGlobalIDError           = errors.New("jira: no global remote link id set")
 	ErrNoIssueSchemeError                  = errors.New("jira: no jira.IssueScheme set")
 	ErrNoTransitionIDError                 = errors.New("jira: no transition id set")
 	ErrNoAttachmentIDError                 = errors.New("jira: no attachment id set")
diff --git a/pkg/infra/models/jira_remote_links.go b/pkg/infra/models/jira_remote_links.go
new file mode 100644
index 0000000..b6509c2
--- /dev/null
+++ b/pkg/infra/models/jira_remote_links.go
@@ -0,0 +1,39 @@
+package models
+
+type RemoteLinkIdentify struct {
+	ID   int    `json:"id,omitempty"`
+	Self string `json:"self,omitempty"`
+}
+
+type RemoteLinkScheme struct {
+	Application  *RemoteLinkApplicationScheme `json:"application,omitempty"`
+	GlobalID     string                       `json:"globalId,omitempty"`
+	ID           int                          `json:"id,omitempty"`
+	Object       *RemoteLinkObjectScheme      `json:"object,omitempty"`
+	Relationship string                       `json:"relationship,omitempty"`
+	Self         string                       `json:"self,omitempty"`
+}
+
+type RemoteLinkObjectScheme struct {
+	Icon    *RemoteLinkObjectLinkScheme   `json:"icon,omitempty"`
+	Status  *RemoteLinkObjectStatusScheme `json:"status,omitempty"`
+	Summary string                        `json:"summary,omitempty"`
+	Title   string                        `json:"title,omitempty"`
+	URL     string                        `json:"url,omitempty"`
+}
+
+type RemoteLinkObjectStatusScheme struct {
+	Icon     *RemoteLinkObjectLinkScheme `json:"icon,omitempty"`
+	Resolved bool                        `json:"resolved,omitempty"`
+}
+
+type RemoteLinkObjectLinkScheme struct {
+	Link     string `json:"link,omitempty"`
+	Title    string `json:"title,omitempty"`
+	URL16X16 string `json:"url16x16,omitempty"`
+}
+
+type RemoteLinkApplicationScheme struct {
+	Name string `json:"name,omitempty"`
+	Type string `json:"type,omitempty"`
+}
diff --git a/service/confluence/analytics.go b/service/confluence/analytics.go
deleted file mode 100644
index 0d99b01..0000000
--- a/service/confluence/analytics.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package confluence
-
-import (
-	"context"
-	model "github.com/ctreminiom/go-atlassian/pkg/infra/models"
-)
-
-type AnalyticsConnector interface {
-
-	// Get gets the total number of views a piece of content has.
-	//
-	// GET /wiki/rest/api/analytics/content/{contentId}/views
-	//
-	// https://docs.go-atlassian.io/confluence-cloud/analytics#get-views
-	Get(ctx context.Context, contentId, fromDate string) (*model.ContentViewScheme, *model.ResponseScheme, error)
-
-	// Distinct get the total number of distinct viewers a piece of content has.
-	//
-	// GET /wiki/rest/api/analytics/content/{contentId}/viewers
-	//
-	// https://docs.go-atlassian.io/confluence-cloud/analytics#get-viewers
-	Distinct(ctx context.Context, contentId, fromDate string) (*model.ContentViewScheme, *model.ResponseScheme, error)
-}
diff --git a/service/jira/remoteLink.go b/service/jira/remoteLink.go
new file mode 100644
index 0000000..c5ded5a
--- /dev/null
+++ b/service/jira/remoteLink.go
@@ -0,0 +1,67 @@
+package jira
+
+import (
+	"context"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+)
+
+type RemoteLinkConnector interface {
+
+	// Gets returns the remote issue links for an issue.
+	//
+	// When a remote issue link global ID is provided the record with that global ID is returned,
+	//
+	// otherwise all remote issue links are returned.
+	//
+	// Where a global ID includes reserved URL characters these must be escaped in the request
+	//
+	// GET /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink
+	//
+	// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#get-remote-issue-links
+	Gets(ctx context.Context, issueKeyOrId, globalId string) ([]*models.RemoteLinkScheme, *models.ResponseScheme, error)
+
+	// Get returns a remote issue link for an issue.
+	//
+	// GET /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink/{linkId}
+	//
+	// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#get-remote-issue-link
+	Get(ctx context.Context, issueKeyOrId, linkId string) (*models.RemoteLinkScheme, *models.ResponseScheme, error)
+
+	// Create creates or updates a remote issue link for an issue.
+	//
+	// If a globalId is provided and a remote issue link with that global ID is found it is updated.
+	//
+	// Any fields without values in the request are set to null. Otherwise, the remote issue link is created.
+	//
+	// POST /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink
+	//
+	// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#create-remote-issue-link
+	Create(ctx context.Context, issueKeyOrId string, payload *models.RemoteLinkScheme) (*models.RemoteLinkIdentify, *models.ResponseScheme, error)
+
+	// Update updates a remote issue link for an issue.
+	//
+	// Note: Fields without values in the request are set to null.
+	//
+	// PUT /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink/{linkId}
+	//
+	// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#update-remote-issue-link
+	Update(ctx context.Context, issueKeyOrId, linkId string, payload *models.RemoteLinkScheme) (*models.ResponseScheme, error)
+
+	// DeleteById deletes a remote issue link from an issue.
+	//
+	// DELETE /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink/{linkId}
+	//
+	// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#delete-remote-issue-link-by-id
+	DeleteById(ctx context.Context, issueKeyOrId, linkId string) (*models.ResponseScheme, error)
+
+	// DeleteByGlobalId deletes the remote issue link from the issue using the link's global ID.
+	//
+	// Where the global ID includes reserved URL characters these must be escaped in the request.
+	//
+	// For example, pass system=http://www.mycompany.com/support&id=1 as system%3Dhttp%3A%2F%2Fwww.mycompany.com%2Fsupport%26id%3D1.
+	//
+	// DELETE /rest/api/{2-3}/issue/{issueIdOrKey}/remotelink
+	//
+	// https://docs.go-atlassian.io/jira-software-cloud/issues/link/remote#delete-remote-issue-link-by-global-id
+	DeleteByGlobalId(ctx context.Context, issueKeyOrId, globalId string) (*models.ResponseScheme, error)
+}


Test patch:
diff --git a/confluence/internal/analytics_impl_test.go b/confluence/internal/analytics_impl_test.go
deleted file mode 100644
index edc3499..0000000
--- a/confluence/internal/analytics_impl_test.go
+++ /dev/null
@@ -1,232 +0,0 @@
-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_internalAnalyticsServiceImpl_Get(t *testing.T) {
-
-	type fields struct {
-		c service.Client
-	}
-
-	type args struct {
-		ctx                 context.Context
-		contentId, fromDate 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(),
-				contentId: "2337372172371",
-				fromDate:  "2023-10-03",
-			},
-			on: func(fields *fields) {
-
-				client := mocks.NewClient(t)
-
-				client.On("NewRequest",
-					context.Background(),
-					http.MethodGet,
-					"wiki/rest/api/analytics/content/2337372172371/views?fromDate=2023-10-03",
-					nil).
-					Return(&http.Request{}, nil)
-
-				client.On("Call",
-					&http.Request{},
-					&model.ContentViewScheme{}).
-					Return(&model.ResponseScheme{}, nil)
-
-				fields.c = client
-			},
-		},
-
-		{
-			name: "when the http request cannot be created",
-			args: args{
-				ctx:       context.TODO(),
-				contentId: "2337372172371",
-				fromDate:  "2023-10-03",
-			},
-			on: func(fields *fields) {
-
-				client := mocks.NewClient(t)
-
-				client.On("NewRequest",
-					context.Background(),
-					http.MethodGet,
-					"wiki/rest/api/analytics/content/2337372172371/views?fromDate=2023-10-03",
-					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 content id is not provided",
-			args: args{
-				ctx: context.TODO(),
-			},
-			wantErr: true,
-			Err:     model.ErrNoContentIDError,
-		},
-	}
-
-	for _, testCase := range testCases {
-		t.Run(testCase.name, func(t *testing.T) {
-
-			if testCase.on != nil {
-				testCase.on(&testCase.fields)
-			}
-
-			newService := NewAnalyticsService(testCase.fields.c)
-
-			gotResult, gotResponse, err := newService.Get(testCase.args.ctx, testCase.args.contentId, testCase.args.fromDate)
-
-			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_internalAnalyticsServiceImpl_Distinct(t *testing.T) {
-
-	type fields struct {
-		c service.Client
-	}
-
-	type args struct {
-		ctx                 context.Context
-		contentId, fromDate 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(),
-				contentId: "2337372172371",
-				fromDate:  "2023-10-03",
-			},
-			on: func(fields *fields) {
-
-				client := mocks.NewClient(t)
-
-				client.On("NewRequest",
-					context.Background(),
-					http.MethodGet,
-					"wiki/rest/api/analytics/content/2337372172371/viewers?fromDate=2023-10-03",
-					nil).
-					Return(&http.Request{}, nil)
-
-				client.On("Call",
-					&http.Request{},
-					&model.ContentViewScheme{}).
-					Return(&model.ResponseScheme{}, nil)
-
-				fields.c = client
-			},
-		},
-
-		{
-			name: "when the http request cannot be created",
-			args: args{
-				ctx:       context.TODO(),
-				contentId: "2337372172371",
-				fromDate:  "2023-10-03",
-			},
-			on: func(fields *fields) {
-
-				client := mocks.NewClient(t)
-
-				client.On("NewRequest",
-					context.Background(),
-					http.MethodGet,
-					"wiki/rest/api/analytics/content/2337372172371/viewers?fromDate=2023-10-03",
-					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 content id is not provided",
-			args: args{
-				ctx: context.TODO(),
-			},
-			wantErr: true,
-			Err:     model.ErrNoContentIDError,
-		},
-	}
-
-	for _, testCase := range testCases {
-		t.Run(testCase.name, func(t *testing.T) {
-
-			if testCase.on != nil {
-				testCase.on(&testCase.fields)
-			}
-
-			newService := NewAnalyticsService(testCase.fields.c)
-
-			gotResult, gotResponse, err := newService.Distinct(testCase.args.ctx, testCase.args.contentId, testCase.args.fromDate)
-
-			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/jira/internal/link_impl_adf_test.go b/jira/internal/link_impl_adf_test.go
index 7e22d84..84d00e1 100644
--- a/jira/internal/link_impl_adf_test.go
+++ b/jira/internal/link_impl_adf_test.go
@@ -122,7 +122,7 @@ func Test_internalLinkADFServiceImpl_Get(t *testing.T) {
 				testCase.on(&testCase.fields)
 			}
 
-			linkService, _, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil)
+			linkService, _, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil, nil)
 			assert.NoError(t, err)
 
 			gotResult, gotResponse, err := linkService.Get(testCase.args.ctx, testCase.args.linkId)
@@ -267,7 +267,7 @@ func Test_internalLinkADFServiceImpl_Gets(t *testing.T) {
 				testCase.on(&testCase.fields)
 			}
 
-			linkService, _, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil)
+			linkService, _, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil, nil)
 			assert.NoError(t, err)
 
 			gotResult, gotResponse, err := linkService.Gets(testCase.args.ctx, testCase.args.issueKeyOrId)
@@ -401,7 +401,7 @@ func Test_internalLinkADFServiceImpl_Delete(t *testing.T) {
 				testCase.on(&testCase.fields)
 			}
 
-			linkService, _, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil)
+			linkService, _, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil, nil)
 			assert.NoError(t, err)
 
 			gotResponse, err := linkService.Delete(testCase.args.ctx, testCase.args.linkId)
@@ -583,7 +583,7 @@ func Test_internalLinkADFServiceImpl_Create(t *testing.T) {
 				testCase.on(&testCase.fields)
 			}
 
-			linkService, _, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil)
+			linkService, _, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil, nil)
 			assert.NoError(t, err)
 
 			gotResponse, err := linkService.Create(testCase.args.ctx, testCase.args.payload)
diff --git a/jira/internal/link_impl_rich_text_test.go b/jira/internal/link_impl_rich_text_test.go
index 0b73bd2..9ed602a 100644
--- a/jira/internal/link_impl_rich_text_test.go
+++ b/jira/internal/link_impl_rich_text_test.go
@@ -122,7 +122,7 @@ func Test_internalLinkRichTextServiceImpl_Get(t *testing.T) {
 				testCase.on(&testCase.fields)
 			}
 
-			_, linkService, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil)
+			_, linkService, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil, nil)
 			assert.NoError(t, err)
 
 			gotResult, gotResponse, err := linkService.Get(testCase.args.ctx, testCase.args.linkId)
@@ -267,7 +267,7 @@ func Test_internalLinkRichTextServiceImpl_Gets(t *testing.T) {
 				testCase.on(&testCase.fields)
 			}
 
-			_, linkService, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil)
+			_, linkService, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil, nil)
 			assert.NoError(t, err)
 
 			gotResult, gotResponse, err := linkService.Gets(testCase.args.ctx, testCase.args.issueKeyOrId)
@@ -401,7 +401,7 @@ func Test_internalLinkRichTextServiceImpl_Delete(t *testing.T) {
 				testCase.on(&testCase.fields)
 			}
 
-			_, linkService, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil)
+			_, linkService, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil, nil)
 			assert.NoError(t, err)
 
 			gotResponse, err := linkService.Delete(testCase.args.ctx, testCase.args.linkId)
@@ -561,7 +561,7 @@ func Test_internalLinkRichTextServiceImpl_Create(t *testing.T) {
 				testCase.on(&testCase.fields)
 			}
 
-			_, linkService, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil)
+			_, linkService, err := NewLinkService(testCase.fields.c, testCase.fields.version, nil, nil)
 			assert.NoError(t, err)
 
 			gotResponse, err := linkService.Create(testCase.args.ctx, testCase.args.payload)
diff --git a/jira/internal/link_impl_test.go b/jira/internal/link_impl_test.go
index df382cf..57fb254 100644
--- a/jira/internal/link_impl_test.go
+++ b/jira/internal/link_impl_test.go
@@ -41,7 +41,7 @@ func Test_NewLinkService(t *testing.T) {
 	}
 	for _, testCase := range testCases {
 		t.Run(testCase.name, func(t *testing.T) {
-			got, got1, err := NewLinkService(testCase.args.client, testCase.args.version, nil)
+			got, got1, err := NewLinkService(testCase.args.client, testCase.args.version, nil, nil)
 
 			if testCase.wantErr {
 
diff --git a/jira/internal/remote_link_impl_test.go b/jira/internal/remote_link_impl_test.go
new file mode 100644
index 0000000..0afe461
--- /dev/null
+++ b/jira/internal/remote_link_impl_test.go
@@ -0,0 +1,1191 @@
+package internal
+
+import (
+	"bytes"
+	"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"
+	"github.com/stretchr/testify/mock"
+	"net/http"
+	"testing"
+)
+
+func Test_internalRemoteLinkImpl_Gets(t *testing.T) {
+
+	type fields struct {
+		c       service.Client
+		version string
+	}
+
+	type args struct {
+		ctx                    context.Context
+		issueKeyOrId, globalId string
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		on      func(*fields)
+		args    args
+		wantErr bool
+		Err     error
+	}{
+		{
+			name:   "when the api version is v2",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/KP-23/remotelink?globalId=system%3Dhttp%3A%2F%2Fwww.mycompany.com%2Fsupport%26id%3D1",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					mock.Anything).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				globalId:     "system=http://www.mycompany.com/support&id=1",
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the api version is v3",
+			fields: fields{version: "3"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/3/issue/KP-23/remotelink?globalId=system%3Dhttp%3A%2F%2Fwww.mycompany.com%2Fsupport%26id%3D1",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					mock.Anything).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				globalId:     "system=http://www.mycompany.com/support&id=1",
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the issue key or not provided",
+			fields: fields{version: "2"},
+			args: args{
+				ctx: context.Background(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoIssueKeyOrIDError,
+		},
+
+		{
+			name:   "when the request cannot be created",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/KP-23/remotelink?globalId=system%3Dhttp%3A%2F%2Fwww.mycompany.com%2Fsupport%26id%3D1",
+					nil).
+					Return(&http.Request{}, errors.New("unable to create the http request"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				globalId:     "system=http://www.mycompany.com/support&id=1",
+			},
+			wantErr: true,
+			Err:     errors.New("unable to create the http request"),
+		},
+
+		{
+			name:   "when the http call cannot be executed",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/KP-23/remotelink?globalId=system%3Dhttp%3A%2F%2Fwww.mycompany.com%2Fsupport%26id%3D1",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					mock.Anything).
+					Return(&model.ResponseScheme{}, errors.New("error, unable to execute the http call"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				globalId:     "system=http://www.mycompany.com/support&id=1",
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to execute the http call"),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			applicationService, err := NewRemoteLinkService(testCase.fields.c, testCase.fields.version)
+			assert.NoError(t, err)
+
+			gotResult, gotResponse, err := applicationService.Gets(testCase.args.ctx, testCase.args.issueKeyOrId, testCase.args.globalId)
+
+			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_internalRemoteLinkImpl_Get(t *testing.T) {
+
+	type fields struct {
+		c       service.Client
+		version string
+	}
+
+	type args struct {
+		ctx                  context.Context
+		issueKeyOrId, linkId string
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		on      func(*fields)
+		args    args
+		wantErr bool
+		Err     error
+	}{
+		{
+			name:   "when the api version is v2",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/KP-23/remotelink/10001",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.RemoteLinkScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the api version is v3",
+			fields: fields{version: "3"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/3/issue/KP-23/remotelink/10001",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.RemoteLinkScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the issue key or not provided",
+			fields: fields{version: "2"},
+			args: args{
+				ctx: context.Background(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoIssueKeyOrIDError,
+		},
+
+		{
+			name:   "when the remote link is not provided",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-3",
+			},
+			wantErr: true,
+			Err:     model.ErrNoRemoteLinkIDError,
+		},
+
+		{
+			name:   "when the request cannot be created",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/KP-23/remotelink/10001",
+					nil).
+					Return(&http.Request{}, errors.New("unable to create the http request"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+			},
+			wantErr: true,
+			Err:     errors.New("unable to create the http request"),
+		},
+
+		{
+			name:   "when the http call cannot be executed",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"rest/api/2/issue/KP-23/remotelink/10001",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.RemoteLinkScheme{}).
+					Return(&model.ResponseScheme{}, errors.New("error, unable to execute the http call"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to execute the http call"),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			applicationService, err := NewRemoteLinkService(testCase.fields.c, testCase.fields.version)
+			assert.NoError(t, err)
+
+			gotResult, gotResponse, err := applicationService.Get(testCase.args.ctx, testCase.args.issueKeyOrId, testCase.args.linkId)
+
+			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_internalRemoteLinkImpl_Update(t *testing.T) {
+
+	payloadMocked := &model.RemoteLinkScheme{
+		Application: &model.RemoteLinkApplicationScheme{
+			Name: "My Acme Tracker",
+			Type: "com.acme.tracker",
+		},
+		GlobalID: "system=http://www.mycompany.com/support&id=1",
+		Object: &model.RemoteLinkObjectScheme{
+			Icon: &model.RemoteLinkObjectLinkScheme{
+				Title:    "Support Ticket",
+				URL16X16: "http://www.mycompany.com/support/ticket.png",
+			},
+			Status: &model.RemoteLinkObjectStatusScheme{
+				Icon: &model.RemoteLinkObjectLinkScheme{
+					Link:     "http://www.mycompany.com/support?id=1&details=closed",
+					Title:    "Case Closed",
+					URL16X16: "http://www.mycompany.com/support/resolved.png",
+				},
+				Resolved: true,
+			},
+			Summary: "Customer support issue",
+			Title:   "TSTSUP-111",
+			URL:     "http://www.mycompany.com/support?id=1",
+		},
+		Relationship: "causes",
+	}
+
+	type fields struct {
+		c       service.Client
+		version string
+	}
+
+	type args struct {
+		ctx                  context.Context
+		issueKeyOrId, linkId string
+		payload              *model.RemoteLinkScheme
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		on      func(*fields)
+		args    args
+		wantErr bool
+		Err     error
+	}{
+		{
+			name:   "when the api version is v2",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPut,
+					"rest/api/2/issue/KP-23/remotelink/10001",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+				payload:      payloadMocked,
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the api version is v3",
+			fields: fields{version: "3"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPut,
+					"rest/api/3/issue/KP-23/remotelink/10001",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+				payload:      payloadMocked,
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the issue key or not provided",
+			fields: fields{version: "2"},
+			args: args{
+				ctx: context.Background(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoIssueKeyOrIDError,
+		},
+
+		{
+			name:   "when the remote link is not provided",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-3",
+			},
+			wantErr: true,
+			Err:     model.ErrNoRemoteLinkIDError,
+		},
+
+		{
+			name:   "when the request cannot be created",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPut,
+					"rest/api/2/issue/KP-23/remotelink/10001",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, errors.New("unable to create the http request"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+				payload:      payloadMocked,
+			},
+			wantErr: true,
+			Err:     errors.New("unable to create the http request"),
+		},
+
+		{
+			name:   "when the http call cannot be executed",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPut,
+					"rest/api/2/issue/KP-23/remotelink/10001",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, errors.New("error, unable to execute the http call"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+				payload:      payloadMocked,
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to execute the http call"),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			applicationService, err := NewRemoteLinkService(testCase.fields.c, testCase.fields.version)
+			assert.NoError(t, err)
+
+			gotResponse, err := applicationService.Update(testCase.args.ctx, testCase.args.issueKeyOrId, testCase.args.linkId,
+				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)
+			}
+		})
+	}
+}
+
+func Test_internalRemoteLinkImpl_Create(t *testing.T) {
+
+	payloadMocked := &model.RemoteLinkScheme{
+		Application: &model.RemoteLinkApplicationScheme{
+			Name: "My Acme Tracker",
+			Type: "com.acme.tracker",
+		},
+		GlobalID: "system=http://www.mycompany.com/support&id=1",
+		Object: &model.RemoteLinkObjectScheme{
+			Icon: &model.RemoteLinkObjectLinkScheme{
+				Title:    "Support Ticket",
+				URL16X16: "http://www.mycompany.com/support/ticket.png",
+			},
+			Status: &model.RemoteLinkObjectStatusScheme{
+				Icon: &model.RemoteLinkObjectLinkScheme{
+					Link:     "http://www.mycompany.com/support?id=1&details=closed",
+					Title:    "Case Closed",
+					URL16X16: "http://www.mycompany.com/support/resolved.png",
+				},
+				Resolved: true,
+			},
+			Summary: "Customer support issue",
+			Title:   "TSTSUP-111",
+			URL:     "http://www.mycompany.com/support?id=1",
+		},
+		Relationship: "causes",
+	}
+
+	type fields struct {
+		c       service.Client
+		version string
+	}
+
+	type args struct {
+		ctx          context.Context
+		issueKeyOrId string
+		payload      *model.RemoteLinkScheme
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		on      func(*fields)
+		args    args
+		wantErr bool
+		Err     error
+	}{
+		{
+			name:   "when the api version is v2",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"rest/api/2/issue/KP-23/remotelink",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.RemoteLinkIdentify{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				payload:      payloadMocked,
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the api version is v3",
+			fields: fields{version: "3"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"rest/api/3/issue/KP-23/remotelink",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.RemoteLinkIdentify{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				payload:      payloadMocked,
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the issue key or not provided",
+			fields: fields{version: "2"},
+			args: args{
+				ctx: context.Background(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoIssueKeyOrIDError,
+		},
+
+		{
+			name:   "when the request cannot be created",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"rest/api/2/issue/KP-23/remotelink",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, errors.New("unable to create the http request"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				payload:      payloadMocked,
+			},
+			wantErr: true,
+			Err:     errors.New("unable to create the http request"),
+		},
+
+		{
+			name:   "when the http call cannot be executed",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("TransformStructToReader",
+					payloadMocked).
+					Return(bytes.NewReader([]byte{}), nil)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodPost,
+					"rest/api/2/issue/KP-23/remotelink",
+					bytes.NewReader([]byte{})).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.RemoteLinkIdentify{}).
+					Return(&model.ResponseScheme{}, errors.New("error, unable to execute the http call"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				payload:      payloadMocked,
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to execute the http call"),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			applicationService, err := NewRemoteLinkService(testCase.fields.c, testCase.fields.version)
+			assert.NoError(t, err)
+
+			gotResult, gotResponse, err := applicationService.Create(testCase.args.ctx, testCase.args.issueKeyOrId,
+				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_internalRemoteLinkImpl_DeleteById(t *testing.T) {
+
+	type fields struct {
+		c       service.Client
+		version string
+	}
+
+	type args struct {
+		ctx                  context.Context
+		issueKeyOrId, linkId string
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		on      func(*fields)
+		args    args
+		wantErr bool
+		Err     error
+	}{
+		{
+			name:   "when the api version is v2",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"rest/api/2/issue/KP-23/remotelink/10001",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the api version is v3",
+			fields: fields{version: "3"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"rest/api/3/issue/KP-23/remotelink/10001",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the issue key or not provided",
+			fields: fields{version: "2"},
+			args: args{
+				ctx: context.Background(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoIssueKeyOrIDError,
+		},
+
+		{
+			name:   "when the remote link is not provided",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-3",
+			},
+			wantErr: true,
+			Err:     model.ErrNoRemoteLinkIDError,
+		},
+
+		{
+			name:   "when the request cannot be created",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"rest/api/2/issue/KP-23/remotelink/10001",
+					nil).
+					Return(&http.Request{}, errors.New("unable to create the http request"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+			},
+			wantErr: true,
+			Err:     errors.New("unable to create the http request"),
+		},
+
+		{
+			name:   "when the http call cannot be executed",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"rest/api/2/issue/KP-23/remotelink/10001",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, errors.New("error, unable to execute the http call"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				linkId:       "10001",
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to execute the http call"),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			applicationService, err := NewRemoteLinkService(testCase.fields.c, testCase.fields.version)
+			assert.NoError(t, err)
+
+			gotResponse, err := applicationService.DeleteById(testCase.args.ctx, testCase.args.issueKeyOrId, testCase.args.linkId)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+			}
+		})
+	}
+}
+
+func Test_internalRemoteLinkImpl_DeleteByGlobalId(t *testing.T) {
+
+	type fields struct {
+		c       service.Client
+		version string
+	}
+
+	type args struct {
+		ctx                    context.Context
+		issueKeyOrId, globalId string
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		on      func(*fields)
+		args    args
+		wantErr bool
+		Err     error
+	}{
+		{
+			name:   "when the api version is v2",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"rest/api/2/issue/KP-23/remotelink?globalId=system%3Dhttp%3A%2F%2Fwww.mycompany.com%2Fsupport%26id%3D1",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				globalId:     "system=http://www.mycompany.com/support&id=1",
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the api version is v3",
+			fields: fields{version: "3"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"rest/api/3/issue/KP-23/remotelink?globalId=system%3Dhttp%3A%2F%2Fwww.mycompany.com%2Fsupport%26id%3D1",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				globalId:     "system=http://www.mycompany.com/support&id=1",
+			},
+			wantErr: false,
+		},
+
+		{
+			name:   "when the issue key or not provided",
+			fields: fields{version: "2"},
+			args: args{
+				ctx: context.Background(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoIssueKeyOrIDError,
+		},
+
+		{
+			name:   "when the global link is not provided",
+			fields: fields{version: "2"},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-3",
+			},
+			wantErr: true,
+			Err:     model.ErrNoRemoteLinkGlobalIDError,
+		},
+
+		{
+			name:   "when the request cannot be created",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"rest/api/2/issue/KP-23/remotelink?globalId=system%3Dhttp%3A%2F%2Fwww.mycompany.com%2Fsupport%26id%3D1",
+					nil).
+					Return(&http.Request{}, errors.New("unable to create the http request"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				globalId:     "system=http://www.mycompany.com/support&id=1",
+			},
+			wantErr: true,
+			Err:     errors.New("unable to create the http request"),
+		},
+
+		{
+			name:   "when the http call cannot be executed",
+			fields: fields{version: "2"},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodDelete,
+					"rest/api/2/issue/KP-23/remotelink?globalId=system%3Dhttp%3A%2F%2Fwww.mycompany.com%2Fsupport%26id%3D1",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					nil).
+					Return(&model.ResponseScheme{}, errors.New("error, unable to execute the http call"))
+
+				fields.c = client
+			},
+			args: args{
+				ctx:          context.Background(),
+				issueKeyOrId: "KP-23",
+				globalId:     "system=http://www.mycompany.com/support&id=1",
+			},
+			wantErr: true,
+			Err:     errors.New("error, unable to execute the http call"),
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			applicationService, err := NewRemoteLinkService(testCase.fields.c, testCase.fields.version)
+			assert.NoError(t, err)
+
+			gotResponse, err := applicationService.DeleteByGlobalId(testCase.args.ctx, testCase.args.issueKeyOrId, testCase.args.globalId)
+
+			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)
+			}
+		})
+	}
+}