Repository: ctreminiom/go-atlassian
Pull request #198 (2023-05-14 06:47:08)

:sparkles: Added the ability to interact with the Confluence V2 attac…
…hments endpoints.


Patch:
diff --git a/confluence/api_client_impl.go b/confluence/api_client_impl.go
index 19d7ecf..41e9ad4 100644
--- a/confluence/api_client_impl.go
+++ b/confluence/api_client_impl.go
@@ -35,7 +35,7 @@ func New(httpClient common.HttpClient, site string) (*Client, error) {
 	}
 
 	contentSubServices := &internal.ContentSubServices{
-		Attachment:         internal.NewAttachmentService(client),
+		Attachment:         internal.NewContentAttachmentService(client),
 		ChildrenDescendant: internal.NewChildrenDescandantsService(client),
 		Comment:            internal.NewCommentService(client),
 		Permission:         internal.NewPermissionService(client),
diff --git a/confluence/internal/attachment_content_impl.go b/confluence/internal/attachment_content_impl.go
index 0328ccc..3801a5d 100644
--- a/confluence/internal/attachment_content_impl.go
+++ b/confluence/internal/attachment_content_impl.go
@@ -15,14 +15,14 @@ import (
 	"strings"
 )
 
-func NewAttachmentService(client service.Client) *AttachmentService {
+func NewContentAttachmentService(client service.Client) *ContentAttachmentService {
 
-	return &AttachmentService{
+	return &ContentAttachmentService{
 		internalClient: &internalContentAttachmentImpl{c: client},
 	}
 }
 
-type AttachmentService struct {
+type ContentAttachmentService struct {
 	internalClient confluence.ContentAttachmentConnector
 }
 
@@ -33,7 +33,7 @@ type AttachmentService struct {
 // GET /wiki/rest/api/content/{id}/child/attachment
 //
 // https://docs.go-atlassian.io/confluence-cloud/content/attachments#get-attachments
-func (a *AttachmentService) Gets(ctx context.Context, contentID string, startAt, maxResults int, options *model.GetContentAttachmentsOptionsScheme) (*model.ContentPageScheme, *model.ResponseScheme, error) {
+func (a *ContentAttachmentService) Gets(ctx context.Context, contentID string, startAt, maxResults int, options *model.GetContentAttachmentsOptionsScheme) (*model.ContentPageScheme, *model.ResponseScheme, error) {
 	return a.internalClient.Gets(ctx, contentID, startAt, maxResults, options)
 }
 
@@ -46,7 +46,7 @@ func (a *AttachmentService) Gets(ctx context.Context, contentID string, startAt,
 // PUT /wiki/rest/api/content/{id}/child/attachment
 //
 // https://docs.go-atlassian.io/confluence-cloud/content/attachments#create-or-update-attachment
-func (a *AttachmentService) CreateOrUpdate(ctx context.Context, attachmentID, status, fileName string, file io.Reader) (*model.ContentPageScheme, *model.ResponseScheme, error) {
+func (a *ContentAttachmentService) CreateOrUpdate(ctx context.Context, attachmentID, status, fileName string, file io.Reader) (*model.ContentPageScheme, *model.ResponseScheme, error) {
 	return a.internalClient.CreateOrUpdate(ctx, attachmentID, status, fileName, file)
 }
 
@@ -59,7 +59,7 @@ func (a *AttachmentService) CreateOrUpdate(ctx context.Context, attachmentID, st
 // POST /wiki/rest/api/content/{id}/child/attachment
 //
 // https://docs.go-atlassian.io/confluence-cloud/content/attachments#create-attachment
-func (a *AttachmentService) Create(ctx context.Context, attachmentID, status, fileName string, file io.Reader) (*model.ContentPageScheme, *model.ResponseScheme, error) {
+func (a *ContentAttachmentService) Create(ctx context.Context, attachmentID, status, fileName string, file io.Reader) (*model.ContentPageScheme, *model.ResponseScheme, error) {
 	return a.internalClient.Create(ctx, attachmentID, status, fileName, file)
 }
 
diff --git a/confluence/internal/attachment_impl.go b/confluence/internal/attachment_impl.go
new file mode 100644
index 0000000..e405ace
--- /dev/null
+++ b/confluence/internal/attachment_impl.go
@@ -0,0 +1,154 @@
+package internal
+
+import (
+	"context"
+	"fmt"
+	model "github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/ctreminiom/go-atlassian/service"
+	"github.com/ctreminiom/go-atlassian/service/confluence"
+	"net/http"
+	"net/url"
+	"strconv"
+	"strings"
+)
+
+// NewAttachmentService returns a new Confluence V2 Page service
+func NewAttachmentService(client service.Client) *AttachmentService {
+	return &AttachmentService{
+		internalClient: &internalAttachmentImpl{c: client},
+	}
+}
+
+type AttachmentService struct {
+	internalClient confluence.AttachmentConnector
+}
+
+// Get returns a specific attachment.
+//
+// GET /wiki/api/v2/attachments/{id}
+//
+// https://docs.go-atlassian.io/confluence-cloud/v2/attachments#get-attachment-by-id
+func (a *AttachmentService) Get(ctx context.Context, attachmentID string, versionID int, serializeIDs bool) (*model.AttachmentScheme, *model.ResponseScheme, error) {
+	return a.internalClient.Get(ctx, attachmentID, versionID, serializeIDs)
+}
+
+// Gets returns the attachments of specific entity type.
+//
+// You can extract the attachments for blog-posts,custom-contents, labels and pages.
+//
+// Depending on the entity type, the endpoint will change based on the entity type.
+//
+// Valid entityType values: blogposts, custom-content, labels, pages.
+//
+// The number of results is limited by the limit parameter and additional results.
+//
+// (if available) will be available through the next URL present in the Link response header.
+//
+// GET /wiki/api/v2/{blogposts,custom-content,labels,pages}/{id}/attachments
+//
+// https://docs.go-atlassian.io/confluence-cloud/v2/attachments#get-attachments-by-type
+func (a *AttachmentService) Gets(ctx context.Context, entityID int, entityType string, options *model.AttachmentParamsScheme, cursor string, limit int) (*model.AttachmentPageScheme, *model.ResponseScheme, error) {
+	return a.internalClient.Gets(ctx, entityID, entityType, options, cursor, limit)
+}
+
+type internalAttachmentImpl struct {
+	c service.Client
+}
+
+func (i *internalAttachmentImpl) Get(ctx context.Context, attachmentID string, versionID int, serializeIDs bool) (*model.AttachmentScheme, *model.ResponseScheme, error) {
+
+	if attachmentID == "" {
+		return nil, nil, model.ErrNoContentAttachmentIDError
+	}
+
+	var endpoint strings.Builder
+	endpoint.WriteString(fmt.Sprintf("wiki/api/v2/attachments/%v", attachmentID))
+
+	query := url.Values{}
+	if versionID != 0 {
+		query.Add("version", strconv.Itoa(versionID))
+	}
+
+	if serializeIDs {
+		query.Add("serialize-ids-as-strings", "true")
+	}
+
+	if query.Encode() != "" {
+		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
+	}
+
+	attachment := new(model.AttachmentScheme)
+	response, err := i.c.Call(request, attachment)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return attachment, response, nil
+}
+
+func (i *internalAttachmentImpl) Gets(ctx context.Context, entityID int, entityType string, options *model.AttachmentParamsScheme, cursor string, limit int) (*model.AttachmentPageScheme, *model.ResponseScheme, error) {
+
+	if entityID == 0 {
+		return nil, nil, model.ErrNoEntityIDError
+	}
+
+	query := url.Values{}
+	query.Add("limit", strconv.Itoa(limit))
+
+	if cursor != "" {
+		query.Add("cursor", cursor)
+	}
+
+	if options != nil {
+
+		if options.SerializeIDs {
+			query.Add("serialize-ids-as-strings", "true")
+		}
+
+		if options.MediaType != "" {
+			query.Add("mediaType", options.MediaType)
+		}
+
+		if options.FileName != "" {
+			query.Add("filename", options.FileName)
+		}
+
+		if options.Sort != "" {
+			query.Add("sort", options.Sort)
+		}
+	}
+
+	// Checking if the entity type provided is supported by the library
+	var isSupported bool
+	for _, type_ := range model.ValidEntityValues {
+
+		if entityType == type_ {
+			isSupported = true
+			break
+		}
+	}
+
+	if !isSupported {
+		return nil, nil, model.ErrNoEntityValue
+	}
+
+	endpoint := fmt.Sprintf("wiki/api/v2/%v/%v/attachments?%v", entityType, entityID, query.Encode())
+
+	request, err := i.c.NewRequest(ctx, http.MethodGet, endpoint, nil)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	page := new(model.AttachmentPageScheme)
+	response, err := i.c.Call(request, page)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return page, response, nil
+}
diff --git a/confluence/internal/content_impl.go b/confluence/internal/content_impl.go
index c96b16e..1d17c31 100644
--- a/confluence/internal/content_impl.go
+++ b/confluence/internal/content_impl.go
@@ -13,7 +13,7 @@ import (
 )
 
 type ContentSubServices struct {
-	Attachment         *AttachmentService
+	Attachment         *ContentAttachmentService
 	ChildrenDescendant *ChildrenDescandantsService
 	Comment            *CommentService
 	Permission         *PermissionService
@@ -40,7 +40,7 @@ func NewContentService(client service.Client, subServices *ContentSubServices) *
 
 type ContentService struct {
 	internalClient     confluence.ContentConnector
-	Attachment         *AttachmentService
+	Attachment         *ContentAttachmentService
 	ChildrenDescendant *ChildrenDescandantsService
 	Comment            *CommentService
 	Permission         *PermissionService
diff --git a/confluence/v2/api_client_impl.go b/confluence/v2/api_client_impl.go
index 2aeaec0..db3b8b7 100644
--- a/confluence/v2/api_client_impl.go
+++ b/confluence/v2/api_client_impl.go
@@ -37,16 +37,18 @@ func New(httpClient common.HttpClient, site string) (*Client, error) {
 	client.Auth = internal.NewAuthenticationService(client)
 	client.Page = internal.NewPageService(client)
 	client.Space = internal.NewSpaceV2Service(client)
+	client.Attachment = internal.NewAttachmentService(client)
 
 	return client, nil
 }
 
 type Client struct {
-	HTTP  common.HttpClient
-	Site  *url.URL
-	Auth  common.Authentication
-	Page  *internal.PageService
-	Space *internal.SpaceV2Service
+	HTTP       common.HttpClient
+	Site       *url.URL
+	Auth       common.Authentication
+	Page       *internal.PageService
+	Space      *internal.SpaceV2Service
+	Attachment *internal.AttachmentService
 }
 
 func (c *Client) NewFormRequest(ctx context.Context, method, apiEndpoint, contentType string, payload io.Reader) (*http.Request, error) {
diff --git a/jira/internal/attachment_impl.go b/jira/internal/attachment_impl.go
index ee4602b..92cac4f 100644
--- a/jira/internal/attachment_impl.go
+++ b/jira/internal/attachment_impl.go
@@ -43,7 +43,7 @@ func (i *IssueAttachmentService) Settings(ctx context.Context) (*model.Attachmen
 // GET /rest/api/{2-3}/attachment/{id}
 //
 // https://docs.go-atlassian.io/jira-software-cloud/issues/attachments#get-attachment-metadata
-func (i *IssueAttachmentService) Metadata(ctx context.Context, attachmentId string) (*model.AttachmentMetadataScheme, *model.ResponseScheme, error) {
+func (i *IssueAttachmentService) Metadata(ctx context.Context, attachmentId string) (*model.IssueAttachmentMetadataScheme, *model.ResponseScheme, error) {
 	return i.internalClient.Metadata(ctx, attachmentId)
 }
 
@@ -62,10 +62,10 @@ func (i *IssueAttachmentService) Delete(ctx context.Context, attachmentId string
 //
 // GET /rest/api/{2-3}/attachment/{id}/expand/human
 //
-// Experimental Endpoint
+// # Experimental Endpoint
 //
 // https://docs.go-atlassian.io/jira-software-cloud/issues/attachments#get-all-metadata-for-an-expanded-attachment
-func (i *IssueAttachmentService) Human(ctx context.Context, attachmentId string) (*model.AttachmentHumanMetadataScheme, *model.ResponseScheme, error) {
+func (i *IssueAttachmentService) Human(ctx context.Context, attachmentId string) (*model.IssueAttachmentHumanMetadataScheme, *model.ResponseScheme, error) {
 	return i.internalClient.Human(ctx, attachmentId)
 }
 
@@ -137,7 +137,7 @@ func (i *internalIssueAttachmentServiceImpl) Settings(ctx context.Context) (*mod
 	return settings, response, nil
 }
 
-func (i *internalIssueAttachmentServiceImpl) Metadata(ctx context.Context, attachmentId string) (*model.AttachmentMetadataScheme, *model.ResponseScheme, error) {
+func (i *internalIssueAttachmentServiceImpl) Metadata(ctx context.Context, attachmentId string) (*model.IssueAttachmentMetadataScheme, *model.ResponseScheme, error) {
 
 	if attachmentId == "" {
 		return nil, nil, model.ErrNoAttachmentIDError
@@ -150,7 +150,7 @@ func (i *internalIssueAttachmentServiceImpl) Metadata(ctx context.Context, attac
 		return nil, nil, err
 	}
 
-	metadata := new(model.AttachmentMetadataScheme)
+	metadata := new(model.IssueAttachmentMetadataScheme)
 	response, err := i.c.Call(request, metadata)
 	if err != nil {
 		return nil, response, err
@@ -175,7 +175,7 @@ func (i *internalIssueAttachmentServiceImpl) Delete(ctx context.Context, attachm
 	return i.c.Call(request, nil)
 }
 
-func (i *internalIssueAttachmentServiceImpl) Human(ctx context.Context, attachmentId string) (*model.AttachmentHumanMetadataScheme, *model.ResponseScheme, error) {
+func (i *internalIssueAttachmentServiceImpl) Human(ctx context.Context, attachmentId string) (*model.IssueAttachmentHumanMetadataScheme, *model.ResponseScheme, error) {
 
 	if attachmentId == "" {
 		return nil, nil, model.ErrNoAttachmentIDError
@@ -188,7 +188,7 @@ func (i *internalIssueAttachmentServiceImpl) Human(ctx context.Context, attachme
 		return nil, nil, err
 	}
 
-	metadata := new(model.AttachmentHumanMetadataScheme)
+	metadata := new(model.IssueAttachmentHumanMetadataScheme)
 	response, err := i.c.Call(request, metadata)
 	if err != nil {
 		return nil, response, err
diff --git a/pkg/infra/models/confluence_attachment.go b/pkg/infra/models/confluence_attachment.go
new file mode 100644
index 0000000..18a188d
--- /dev/null
+++ b/pkg/infra/models/confluence_attachment.go
@@ -0,0 +1,54 @@
+package models
+
+type AttachmentScheme struct {
+	ID                   string                   `json:"id,omitempty"`
+	BlogPostID           string                   `json:"blogPostId,omitempty"`
+	CustomContentID      string                   `json:"customContentId,omitempty"`
+	Comment              string                   `json:"comment,omitempty"`
+	MediaTypeDescription string                   `json:"mediaTypeDescription,omitempty"`
+	WebuiLink            string                   `json:"webuiLink,omitempty"`
+	DownloadLink         string                   `json:"downloadLink,omitempty"`
+	Title                string                   `json:"title,omitempty"`
+	Status               string                   `json:"status,omitempty"`
+	FileSize             int                      `json:"fileSize,omitempty"`
+	MediaType            string                   `json:"mediaType,omitempty"`
+	PageID               int                      `json:"pageId,omitempty"`
+	FileID               string                   `json:"fileId,omitempty"`
+	Version              *AttachmentVersionScheme `json:"version,omitempty"`
+}
+
+type AttachmentVersionScheme struct {
+	CreatedAt string `json:"createdAt,omitempty"`
+	Message   string `json:"message,omitempty"`
+	Number    int    `json:"number,omitempty"`
+	MinorEdit bool   `json:"minorEdit,omitempty"`
+	AuthorID  string `json:"authorId,omitempty"`
+}
+
+type AttachmentParamsScheme struct {
+
+	// Sort it's used to sort the result by a particular field.
+	// Valid values:
+	// created-date
+	// -created-date
+	// modified-date
+	// -modified-date
+	Sort string
+
+	// MediaType filters the mediaType of attachments. Only one may be specified.
+	MediaType string
+
+	// FileName filters on the file-name of attachments. Only one may be specified.
+	FileName string
+
+	SerializeIDs bool
+}
+
+type AttachmentPageScheme struct {
+	Results []*AttachmentScheme        `json:"results,omitempty"`
+	Links   *AttachmentPageLinksScheme `json:"_links,omitempty"`
+}
+
+type AttachmentPageLinksScheme struct {
+	Next string `json:"next,omitempty"`
+}
diff --git a/pkg/infra/models/errors.go b/pkg/infra/models/errors.go
index b145b51..0031e14 100644
--- a/pkg/infra/models/errors.go
+++ b/pkg/infra/models/errors.go
@@ -41,6 +41,9 @@ var (
 	ErrNoLabelIDError                      = errors.New("confluence: no label id set")
 	ErrNoCQLError                          = errors.New("confluence: no CQL query set")
 	ErrNoContentTypeError                  = errors.New("confluence: no content type set")
+	ErrNoEntityIDError                     = errors.New("confluence: no entity id set")
+	ValidEntityValues                      = []string{"blogposts", "custom-content", "labels", "pages"}
+	ErrNoEntityValue                       = errors.New("confluence: no valid entity id set")
 	ErrInvalidContentTypeError             = errors.New("confluence: invalid content type: (page, comment, attachment)")
 	ValidContentTypes                      = []string{"page", "comment", "attachment"}
 	ErrNoContentLabelError                 = errors.New("confluence: no content label set")
diff --git a/pkg/infra/models/jira_issue_attachments.go b/pkg/infra/models/jira_issue_attachments.go
index 17e0255..be3ac9a 100644
--- a/pkg/infra/models/jira_issue_attachments.go
+++ b/pkg/infra/models/jira_issue_attachments.go
@@ -5,7 +5,7 @@ type AttachmentSettingScheme struct {
 	UploadLimit int  `json:"uploadLimit,omitempty"`
 }
 
-type AttachmentScheme struct {
+type IssueAttachmentScheme struct {
 	Self      string      `json:"self,omitempty"`
 	ID        string      `json:"id,omitempty"`
 	Filename  string      `json:"filename,omitempty"`
@@ -17,7 +17,7 @@ type AttachmentScheme struct {
 	Thumbnail string      `json:"thumbnail,omitempty"`
 }
 
-type AttachmentMetadataScheme struct {
+type IssueAttachmentMetadataScheme struct {
 	ID        int         `json:"id,omitempty"`
 	Self      string      `json:"self,omitempty"`
 	Filename  string      `json:"filename,omitempty"`
@@ -29,15 +29,15 @@ type AttachmentMetadataScheme struct {
 	Thumbnail string      `json:"thumbnail,omitempty"`
 }
 
-type AttachmentHumanMetadataScheme struct {
-	ID              int                                   `json:"id,omitempty"`
-	Name            string                                `json:"name,omitempty"`
-	Entries         []*AttachmentHumanMetadataEntryScheme `json:"entries,omitempty"`
-	TotalEntryCount int                                   `json:"totalEntryCount,omitempty"`
-	MediaType       string                                `json:"mediaType,omitempty"`
+type IssueAttachmentHumanMetadataScheme struct {
+	ID              int                                        `json:"id,omitempty"`
+	Name            string                                     `json:"name,omitempty"`
+	Entries         []*IssueAttachmentHumanMetadataEntryScheme `json:"entries,omitempty"`
+	TotalEntryCount int                                        `json:"totalEntryCount,omitempty"`
+	MediaType       string                                     `json:"mediaType,omitempty"`
 }
 
-type AttachmentHumanMetadataEntryScheme struct {
+type IssueAttachmentHumanMetadataEntryScheme struct {
 	Path      string `json:"path,omitempty"`
 	Index     int    `json:"index,omitempty"`
 	Size      string `json:"size,omitempty"`
diff --git a/service/confluence/attachment.go b/service/confluence/attachment.go
index 44a22e7..8fd5c2c 100644
--- a/service/confluence/attachment.go
+++ b/service/confluence/attachment.go
@@ -39,3 +39,30 @@ type ContentAttachmentConnector interface {
 	// https://docs.go-atlassian.io/confluence-cloud/content/attachments#create-attachment
 	Create(ctx context.Context, attachmentID, status, fileName string, file io.Reader) (*model.ContentPageScheme, *model.ResponseScheme, error)
 }
+
+type AttachmentConnector interface {
+
+	// Get returns a specific attachment.
+	//
+	// GET /wiki/api/v2/attachments/{id}
+	//
+	// https://docs.go-atlassian.io/confluence-cloud/v2/attachments#get-attachment-by-id
+	Get(ctx context.Context, attachmentID string, versionID int, serializeIDs bool) (*model.AttachmentScheme, *model.ResponseScheme, error)
+
+	// Gets returns the attachments of specific entity type.
+	//
+	// You can extract the attachments for blog-posts,custom-contents, labels and pages.
+	//
+	// Depending on the entity type, the endpoint will change based on the entity type.
+	//
+	// Valid entityType values: blogposts, custom-content, labels, pages.
+	//
+	// The number of results is limited by the limit parameter and additional results.
+	//
+	// (if available) will be available through the next URL present in the Link response header.
+	//
+	// GET /wiki/api/v2/{blogposts,custom-content,labels,pages}/{id}/attachments
+	//
+	// https://docs.go-atlassian.io/confluence-cloud/v2/attachments#get-attachments-by-type
+	Gets(ctx context.Context, entityID int, entityType string, options *model.AttachmentParamsScheme, cursor string, limit int) (*model.AttachmentPageScheme, *model.ResponseScheme, error)
+}
diff --git a/service/jira/attachment.go b/service/jira/attachment.go
index 16794cc..f5e83db 100644
--- a/service/jira/attachment.go
+++ b/service/jira/attachment.go
@@ -20,7 +20,7 @@ type AttachmentConnector interface {
 	// GET /rest/api/{2-3}/attachment/{id}
 	//
 	// https://docs.go-atlassian.io/jira-software-cloud/issues/attachments#get-attachment-metadata
-	Metadata(ctx context.Context, attachmentId string) (*model.AttachmentMetadataScheme, *model.ResponseScheme, error)
+	Metadata(ctx context.Context, attachmentId string) (*model.IssueAttachmentMetadataScheme, *model.ResponseScheme, error)
 
 	// Delete deletes an attachment from an issue.
 	//
@@ -38,7 +38,7 @@ type AttachmentConnector interface {
 	// Experimental Endpoint
 	//
 	// https://docs.go-atlassian.io/jira-software-cloud/issues/attachments#get-all-metadata-for-an-expanded-attachment
-	Human(ctx context.Context, attachmentId string) (*model.AttachmentHumanMetadataScheme, *model.ResponseScheme, error)
+	Human(ctx context.Context, attachmentId string) (*model.IssueAttachmentHumanMetadataScheme, *model.ResponseScheme, error)
 
 	// Add adds one attachment to an issue. Attachments are posted as multipart/form-data (RFC 1867).
 	//


Test patch:
diff --git a/confluence/internal/attachment_content_impl_test.go b/confluence/internal/attachment_content_impl_test.go
index 09c4e08..a2e5f4a 100644
--- a/confluence/internal/attachment_content_impl_test.go
+++ b/confluence/internal/attachment_content_impl_test.go
@@ -118,7 +118,7 @@ func Test_internalContentAttachmentImpl_Gets(t *testing.T) {
 				testCase.on(&testCase.fields)
 			}
 
-			attachmentService := NewAttachmentService(testCase.fields.c)
+			attachmentService := NewContentAttachmentService(testCase.fields.c)
 
 			gotResult, gotResponse, err := attachmentService.Gets(testCase.args.ctx, testCase.args.contentID, testCase.args.startAt,
 				testCase.args.maxResults, testCase.args.options)
@@ -269,7 +269,7 @@ func Test_internalContentAttachmentImpl_CreateOrUpdate(t *testing.T) {
 				testCase.on(&testCase.fields)
 			}
 
-			attachmentService := NewAttachmentService(testCase.fields.c)
+			attachmentService := NewContentAttachmentService(testCase.fields.c)
 
 			gotResult, gotResponse, err := attachmentService.CreateOrUpdate(testCase.args.ctx, testCase.args.attachmentID,
 				testCase.args.status, testCase.args.fileName, testCase.args.file)
@@ -420,7 +420,7 @@ func Test_internalContentAttachmentImpl_Create(t *testing.T) {
 				testCase.on(&testCase.fields)
 			}
 
-			attachmentService := NewAttachmentService(testCase.fields.c)
+			attachmentService := NewContentAttachmentService(testCase.fields.c)
 
 			gotResult, gotResponse, err := attachmentService.Create(testCase.args.ctx, testCase.args.attachmentID,
 				testCase.args.status, testCase.args.fileName, testCase.args.file)
diff --git a/confluence/internal/attachment_impl_test.go b/confluence/internal/attachment_impl_test.go
new file mode 100644
index 0000000..2527557
--- /dev/null
+++ b/confluence/internal/attachment_impl_test.go
@@ -0,0 +1,273 @@
+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_internalAttachmentImpl_Get(t *testing.T) {
+
+	type fields struct {
+		c service.Client
+	}
+
+	type args struct {
+		ctx          context.Context
+		attachmentID string
+		versionID    int
+		serializeIDs bool
+	}
+
+	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(),
+				attachmentID: "100001",
+				versionID:    25,
+				serializeIDs: true,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/attachments/100001?serialize-ids-as-strings=true&version=25",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.AttachmentScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:          context.TODO(),
+				attachmentID: "100001",
+				versionID:    25,
+				serializeIDs: true,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/attachments/100001?serialize-ids-as-strings=true&version=25",
+					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 attachment id is not provided",
+			args: args{
+				ctx: context.TODO(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoContentAttachmentIDError,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			attachmentService := NewAttachmentService(testCase.fields.c)
+
+			gotResult, gotResponse, err := attachmentService.Get(testCase.args.ctx, testCase.args.attachmentID, testCase.args.versionID,
+				testCase.args.serializeIDs)
+
+			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_internalAttachmentImpl_Gets(t *testing.T) {
+
+	type fields struct {
+		c service.Client
+	}
+
+	type args struct {
+		ctx        context.Context
+		entityID   int
+		entityType string
+		options    *model.AttachmentParamsScheme
+		cursor     string
+		limit      int
+	}
+
+	testCases := []struct {
+		name    string
+		fields  fields
+		args    args
+		on      func(*fields)
+		wantErr bool
+		Err     error
+	}{
+		{
+			name: "when the parameters are correct",
+			args: args{
+				ctx:        context.TODO(),
+				entityID:   10001,
+				entityType: "labels",
+				options: &model.AttachmentParamsScheme{
+					Sort:         "created-date",
+					MediaType:    "json",
+					FileName:     "credit-info",
+					SerializeIDs: true,
+				},
+				cursor: "uuid-sample",
+				limit:  100,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/labels/10001/attachments?cursor=uuid-sample&filename=credit-info&limit=100&mediaType=json&serialize-ids-as-strings=true&sort=created-date",
+					nil).
+					Return(&http.Request{}, nil)
+
+				client.On("Call",
+					&http.Request{},
+					&model.AttachmentPageScheme{}).
+					Return(&model.ResponseScheme{}, nil)
+
+				fields.c = client
+
+			},
+		},
+
+		{
+			name: "when the http request cannot be created",
+			args: args{
+				ctx:        context.TODO(),
+				entityID:   10001,
+				entityType: "labels",
+				options: &model.AttachmentParamsScheme{
+					Sort:         "created-date",
+					MediaType:    "json",
+					FileName:     "credit-info",
+					SerializeIDs: true,
+				},
+				cursor: "uuid-sample",
+				limit:  100,
+			},
+			on: func(fields *fields) {
+
+				client := mocks.NewClient(t)
+
+				client.On("NewRequest",
+					context.Background(),
+					http.MethodGet,
+					"wiki/api/v2/labels/10001/attachments?cursor=uuid-sample&filename=credit-info&limit=100&mediaType=json&serialize-ids-as-strings=true&sort=created-date",
+					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 entity id is not provided",
+			args: args{
+				ctx: context.TODO(),
+			},
+			wantErr: true,
+			Err:     model.ErrNoEntityIDError,
+		},
+
+		{
+			name: "when the entity type provided is not valid",
+			args: args{
+				ctx:        context.TODO(),
+				entityID:   1002,
+				entityType: "questions",
+			},
+			wantErr: true,
+			Err:     model.ErrNoEntityValue,
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+
+			if testCase.on != nil {
+				testCase.on(&testCase.fields)
+			}
+
+			attachmentService := NewAttachmentService(testCase.fields.c)
+
+			gotResult, gotResponse, err := attachmentService.Gets(testCase.args.ctx, testCase.args.entityID, testCase.args.entityType,
+				testCase.args.options, testCase.args.cursor, testCase.args.limit)
+
+			if testCase.wantErr {
+
+				if err != nil {
+					t.Logf("error returned: %v", err.Error())
+				}
+
+				assert.EqualError(t, err, testCase.Err.Error())
+
+			} else {
+
+				assert.NoError(t, err)
+				assert.NotEqual(t, gotResponse, nil)
+				assert.NotEqual(t, gotResult, nil)
+			}
+
+		})
+	}
+}
diff --git a/jira/internal/attachment_impl_test.go b/jira/internal/attachment_impl_test.go
index 2a568da..4a32279 100644
--- a/jira/internal/attachment_impl_test.go
+++ b/jira/internal/attachment_impl_test.go
@@ -184,7 +184,7 @@ func Test_internalIssueAttachmentServiceImpl_Metadata(t *testing.T) {
 
 				client.On("Call",
 					&http.Request{},
-					&model.AttachmentMetadataScheme{}).
+					&model.IssueAttachmentMetadataScheme{}).
 					Return(&model.ResponseScheme{}, nil)
 
 				fields.c = client
@@ -211,7 +211,7 @@ func Test_internalIssueAttachmentServiceImpl_Metadata(t *testing.T) {
 
 				client.On("Call",
 					&http.Request{},
-					&model.AttachmentMetadataScheme{}).
+					&model.IssueAttachmentMetadataScheme{}).
 					Return(&model.ResponseScheme{}, nil)
 
 				fields.c = client
@@ -330,7 +330,7 @@ func Test_internalIssueAttachmentServiceImpl_Human(t *testing.T) {
 
 				client.On("Call",
 					&http.Request{},
-					&model.AttachmentHumanMetadataScheme{}).
+					&model.IssueAttachmentHumanMetadataScheme{}).
 					Return(&model.ResponseScheme{}, nil)
 
 				fields.c = client
@@ -357,7 +357,7 @@ func Test_internalIssueAttachmentServiceImpl_Human(t *testing.T) {
 
 				client.On("Call",
 					&http.Request{},
-					&model.AttachmentHumanMetadataScheme{}).
+					&model.IssueAttachmentHumanMetadataScheme{}).
 					Return(&model.ResponseScheme{}, nil)
 
 				fields.c = client