From bca35773dc116d98447f8d49d9ae8e9900adcd1d Mon Sep 17 00:00:00 2001 From: Carlos Treminio Date: Sun, 23 Jan 2022 14:41:59 -0600 Subject: [PATCH 1/5] :sparkles: Added the Confluence Content Version service --- confluence/confluence.go | 1 + confluence/content.go | 1 + confluence/contentVersion.go | 147 +++ confluence/contentVersion_test.go | 642 ++++++++++++ confluence/mocks/get-content-version.json | 929 ++++++++++++++++++ confluence/mocks/get-content-versions.json | 196 ++++ pkg/infra/models/confluence_content.go | 21 - .../models/confluence_content_version.go | 40 + 8 files changed, 1956 insertions(+), 21 deletions(-) create mode 100644 confluence/contentVersion.go create mode 100644 confluence/contentVersion_test.go create mode 100644 confluence/mocks/get-content-version.json create mode 100644 confluence/mocks/get-content-versions.json create mode 100644 pkg/infra/models/confluence_content_version.go diff --git a/confluence/confluence.go b/confluence/confluence.go index 125300ef..5ef04db0 100644 --- a/confluence/confluence.go +++ b/confluence/confluence.go @@ -58,6 +58,7 @@ func New(httpClient *http.Client, site string) (client *Client, err error) { Group: &ContentRestrictionOperationGroupService{client: client}, User: &ContentRestrictionOperationUserService{client: client}, }}, + Version: &ContentVersionService{client: client}, } client.Space = &SpaceService{client: client} diff --git a/confluence/content.go b/confluence/content.go index 9bd04027..706ac478 100644 --- a/confluence/content.go +++ b/confluence/content.go @@ -19,6 +19,7 @@ type ContentService struct { Label *ContentLabelService Property *ContentPropertyService Restriction *ContentRestrictionService + Version *ContentVersionService } // Gets returns all content in a Confluence instance. diff --git a/confluence/contentVersion.go b/confluence/contentVersion.go new file mode 100644 index 00000000..206715ec --- /dev/null +++ b/confluence/contentVersion.go @@ -0,0 +1,147 @@ +package confluence + +import ( + "context" + "fmt" + "github.com/ctreminiom/go-atlassian/pkg/infra/models" + "net/http" + "net/url" + "strconv" + "strings" +) + +type ContentVersionService struct{ client *Client } + +// Gets returns the versions for a piece of content in descending order. +func (c *ContentVersionService) Gets(ctx context.Context, contentID string, expand []string, start, limit int) ( + result *models.ContentVersionPageScheme, response *ResponseScheme, err error) { + + if len(contentID) == 0 { + return nil, nil, models.ErrNoContentIDError + } + + query := url.Values{} + query.Add("start", strconv.Itoa(start)) + query.Add("limit", strconv.Itoa(limit)) + + if len(expand) != 0 { + query.Add("expand", strings.Join(expand, ",")) + } + + endpoint := fmt.Sprintf("wiki/rest/api/content/%v/version?%v", contentID, query.Encode()) + + request, err := c.client.newRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, nil, err + } + + request.Header.Set("Accept", "application/json") + + response, err = c.client.Call(request, &result) + if err != nil { + return nil, response, err + } + + return +} + +// Get returns a version for a piece of content. +func (c *ContentVersionService) Get(ctx context.Context, contentID string, versionNumber int, expand []string) ( + result *models.ContentVersionScheme, response *ResponseScheme, err error) { + + if len(contentID) == 0 { + return nil, nil, models.ErrNoContentIDError + } + + var endpoint strings.Builder + endpoint.WriteString(fmt.Sprintf("wiki/rest/api/content/%v/version/%v", contentID, versionNumber)) + + if len(expand) != 0 { + query := url.Values{} + query.Add("expand", strings.Join(expand, ",")) + endpoint.WriteString(fmt.Sprintf("?%v", query.Encode())) + } + + request, err := c.client.newRequest(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, nil, err + } + + request.Header.Set("Accept", "application/json") + + response, err = c.client.Call(request, &result) + if err != nil { + return nil, response, err + } + + return +} + +// Restore restores a historical version to be the latest version. +// That is, a new version is created with the content of the historical version. +func (c *ContentVersionService) Restore(ctx context.Context, contentID string, payload *models.ContentRestorePayloadScheme, + expand []string) (result *models.ContentVersionScheme, response *ResponseScheme, err error) { + + if len(contentID) == 0 { + return nil, nil, models.ErrNoContentIDError + } + + var endpoint strings.Builder + endpoint.WriteString(fmt.Sprintf("wiki/rest/api/content/%v/version", contentID)) + + query := url.Values{} + if len(expand) != 0 { + query.Add("expand", strings.Join(expand, ",")) + } + + if query.Encode() != "" { + endpoint.WriteString(fmt.Sprintf("?%v", query.Encode())) + } + + payloadAsReader, err := transformStructToReader(payload) + if err != nil { + return nil, nil, err + } + + request, err := c.client.newRequest(ctx, http.MethodPost, endpoint.String(), payloadAsReader) + if err != nil { + return nil, nil, err + } + + request.Header.Set("Accept", "application/json") + request.Header.Set("Content-Type", "application/json") + + response, err = c.client.Call(request, &result) + if err != nil { + return nil, response, err + } + + return +} + +// Delete deletes a historical version. +// This does not delete the changes made to the content in that version, rather the changes for the deleted version +// are rolled up into the next version. Note, you cannot delete the current version. +func (c *ContentVersionService) Delete(ctx context.Context, contentID string, versionNumber int) (response *ResponseScheme, err error) { + + if len(contentID) == 0 { + return nil, models.ErrNoContentIDError + } + + endpoint := fmt.Sprintf("wiki/rest/api/content/%v/version/%v", contentID, versionNumber) + + request, err := c.client.newRequest(ctx, http.MethodDelete, endpoint, nil) + if err != nil { + return nil, err + } + + request.Header.Set("Accept", "application/json") + request.Header.Set("Content-Type", "application/json") + + response, err = c.client.Call(request, nil) + if err != nil { + return response, err + } + + return +} diff --git a/confluence/contentVersion_test.go b/confluence/contentVersion_test.go new file mode 100644 index 00000000..96c18153 --- /dev/null +++ b/confluence/contentVersion_test.go @@ -0,0 +1,642 @@ +package confluence + +import ( + "context" + "fmt" + "github.com/ctreminiom/go-atlassian/pkg/infra/models" + "github.com/stretchr/testify/assert" + "net/http" + "net/url" + "testing" +) + +func Test_Content_Version_Service_Gets(t *testing.T) { + + testCases := []struct { + name string + contentID string + expand []string + start int + limit int + mockFile string + wantHTTPMethod string + endpoint string + context context.Context + wantHTTPCodeReturn int + wantErr bool + expectedError string + }{ + { + name: "when the parameters are correct", + contentID: "233838383", + expand: []string{"restrictions.user", "restrictions.group"}, + start: 0, + limit: 50, + mockFile: "./mocks/get-content-versions.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/content/233838383/version?expand=restrictions.user%2Crestrictions.group&limit=50&start=0", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + }, + + { + name: "when the content id is not provided", + contentID: "", + expand: []string{"restrictions.user", "restrictions.group"}, + start: 0, + limit: 50, + mockFile: "./mocks/get-content-versions.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/content/233838383/version?expand=restrictions.user%2Crestrictions.group&limit=50&start=0", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "confluence: no content id set", + }, + + { + name: "when the context is not provided", + contentID: "233838383", + expand: []string{"restrictions.user", "restrictions.group"}, + start: 0, + limit: 50, + mockFile: "./mocks/get-content-versions.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/content/233838383/version?expand=restrictions.user%2Crestrictions.group&limit=50&start=0", + context: nil, + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "request creation failed: net/http: nil Context", + }, + + { + name: "when the response status is not correct", + contentID: "233838383", + expand: []string{"restrictions.user", "restrictions.group"}, + start: 0, + limit: 50, + mockFile: "./mocks/get-content-versions.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/content/233838383/version?expand=restrictions.user%2Crestrictions.group&limit=50&start=0", + context: context.Background(), + wantHTTPCodeReturn: http.StatusBadRequest, + wantErr: true, + expectedError: "request failed. Please analyze the request body for more details. Status Code: 400", + }, + + { + name: "when the response body is not empty", + contentID: "233838383", + expand: []string{"restrictions.user", "restrictions.group"}, + start: 0, + limit: 50, + mockFile: "./mocks/empty-json.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/content/233838383/version?expand=restrictions.user%2Crestrictions.group&limit=50&start=0", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "unexpected end of JSON input", + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + //Init a new HTTP mock server + mockOptions := mockServerOptions{ + Endpoint: testCase.endpoint, + MockFilePath: testCase.mockFile, + MethodAccepted: testCase.wantHTTPMethod, + ResponseCodeWanted: testCase.wantHTTPCodeReturn, + } + + mockServer, err := startMockServer(&mockOptions) + if err != nil { + t.Fatal(err) + } + + defer mockServer.Close() + + //Init the library instance + mockClient, err := startMockClient(mockServer.URL) + if err != nil { + t.Fatal(err) + } + + service := &ContentVersionService{client: mockClient} + + gotResult, gotResponse, err := service.Gets(testCase.context, testCase.contentID, testCase.expand, + testCase.start, testCase.limit) + + if testCase.wantErr { + + if err != nil { + t.Logf("error returned: %v", err.Error()) + } + + assert.EqualError(t, err, testCase.expectedError) + + if gotResponse != nil { + t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code) + } + + } else { + + assert.NoError(t, err) + assert.NotEqual(t, gotResponse, nil) + assert.NotEqual(t, gotResult, nil) + + apiEndpoint, err := url.Parse(gotResponse.Endpoint) + if err != nil { + t.Fatal(err) + } + + var endpointToAssert string + + if apiEndpoint.Query().Encode() != "" { + endpointToAssert = fmt.Sprintf("%v?%v", apiEndpoint.Path, apiEndpoint.Query().Encode()) + } else { + endpointToAssert = apiEndpoint.Path + } + + t.Logf("HTTP Endpoint Wanted: %v, HTTP Endpoint Returned: %v", testCase.endpoint, endpointToAssert) + assert.Equal(t, testCase.endpoint, endpointToAssert) + + t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code) + assert.Equal(t, gotResponse.Code, testCase.wantHTTPCodeReturn) + } + }) + } +} + +func Test_Content_Version_Service_Get(t *testing.T) { + + testCases := []struct { + name string + contentID string + expand []string + versionNumber int + mockFile string + wantHTTPMethod string + endpoint string + context context.Context + wantHTTPCodeReturn int + wantErr bool + expectedError string + }{ + { + name: "when the parameters are correct", + contentID: "233838383", + expand: []string{"collaborators", "content"}, + mockFile: "./mocks/get-content-version.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/content/233838383/version/0?expand=collaborators%2Ccontent", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + }, + + { + name: "when the content id is not provided", + contentID: "", + expand: []string{"collaborators", "content"}, + mockFile: "./mocks/get-content-version.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/content/233838383/version/0?expand=collaborators%2Ccontent", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "confluence: no content id set", + }, + + { + name: "when the context is not provided", + contentID: "233838383", + expand: []string{"collaborators", "content"}, + mockFile: "./mocks/get-content-version.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/content/233838383/version/0?expand=collaborators%2Ccontent", + context: nil, + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "request creation failed: net/http: nil Context", + }, + + { + name: "when the response status is not correct", + contentID: "233838383", + expand: []string{"collaborators", "content"}, + mockFile: "./mocks/get-content-version.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/content/233838383/version/0?expand=collaborators%2Ccontent", + context: context.Background(), + wantHTTPCodeReturn: http.StatusBadRequest, + wantErr: true, + expectedError: "request failed. Please analyze the request body for more details. Status Code: 400", + }, + + { + name: "when the response body is not empty", + contentID: "233838383", + expand: []string{"collaborators", "content"}, + mockFile: "./mocks/empty-json.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/content/233838383/version/0?expand=collaborators%2Ccontent", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "unexpected end of JSON input", + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + //Init a new HTTP mock server + mockOptions := mockServerOptions{ + Endpoint: testCase.endpoint, + MockFilePath: testCase.mockFile, + MethodAccepted: testCase.wantHTTPMethod, + ResponseCodeWanted: testCase.wantHTTPCodeReturn, + } + + mockServer, err := startMockServer(&mockOptions) + if err != nil { + t.Fatal(err) + } + + defer mockServer.Close() + + //Init the library instance + mockClient, err := startMockClient(mockServer.URL) + if err != nil { + t.Fatal(err) + } + + service := &ContentVersionService{client: mockClient} + + gotResult, gotResponse, err := service.Get(testCase.context, testCase.contentID, testCase.versionNumber, testCase.expand) + + if testCase.wantErr { + + if err != nil { + t.Logf("error returned: %v", err.Error()) + } + + assert.EqualError(t, err, testCase.expectedError) + + if gotResponse != nil { + t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code) + } + + } else { + + assert.NoError(t, err) + assert.NotEqual(t, gotResponse, nil) + assert.NotEqual(t, gotResult, nil) + + apiEndpoint, err := url.Parse(gotResponse.Endpoint) + if err != nil { + t.Fatal(err) + } + + var endpointToAssert string + + if apiEndpoint.Query().Encode() != "" { + endpointToAssert = fmt.Sprintf("%v?%v", apiEndpoint.Path, apiEndpoint.Query().Encode()) + } else { + endpointToAssert = apiEndpoint.Path + } + + t.Logf("HTTP Endpoint Wanted: %v, HTTP Endpoint Returned: %v", testCase.endpoint, endpointToAssert) + assert.Equal(t, testCase.endpoint, endpointToAssert) + + t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code) + assert.Equal(t, gotResponse.Code, testCase.wantHTTPCodeReturn) + } + }) + } +} + +func Test_Content_Version_Service_Restore(t *testing.T) { + + testCases := []struct { + name string + contentID string + payload *models.ContentRestorePayloadScheme + expand []string + mockFile string + wantHTTPMethod string + endpoint string + context context.Context + wantHTTPCodeReturn int + wantErr bool + expectedError string + }{ + { + name: "when the parameters are correct", + contentID: "233838383", + payload: &models.ContentRestorePayloadScheme{ + OperationKey: "restore", + Params: &models.ContentRestoreParamsPayloadScheme{ + VersionNumber: 034, + Message: "message sample :)", + RestoreTitle: true, + }, + }, + expand: []string{"collaborators", "content"}, + mockFile: "./mocks/get-content-version.json", + wantHTTPMethod: http.MethodPost, + endpoint: "/wiki/rest/api/content/233838383/version?expand=collaborators%2Ccontent", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + }, + + { + name: "when the paylod is not provided", + contentID: "233838383", + payload: nil, + expand: []string{"collaborators", "content"}, + mockFile: "./mocks/get-content-version.json", + wantHTTPMethod: http.MethodPost, + endpoint: "/wiki/rest/api/content/233838383/version?expand=collaborators%2Ccontent", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "failed to parse the interface pointer, please provide a valid one", + }, + + { + name: "when the content id is not provided", + contentID: "", + payload: &models.ContentRestorePayloadScheme{ + OperationKey: "restore", + Params: &models.ContentRestoreParamsPayloadScheme{ + VersionNumber: 034, + Message: "message sample :)", + RestoreTitle: true, + }, + }, + expand: []string{"collaborators", "content"}, + mockFile: "./mocks/get-content-version.json", + wantHTTPMethod: http.MethodPost, + endpoint: "/wiki/rest/api/content/233838383/version?expand=collaborators%2Ccontent", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "confluence: no content id set", + }, + + { + name: "when the context is not provided", + contentID: "233838383", + payload: &models.ContentRestorePayloadScheme{ + OperationKey: "restore", + Params: &models.ContentRestoreParamsPayloadScheme{ + VersionNumber: 034, + Message: "message sample :)", + RestoreTitle: true, + }, + }, + expand: []string{"collaborators", "content"}, + mockFile: "./mocks/get-content-version.json", + wantHTTPMethod: http.MethodPost, + endpoint: "/wiki/rest/api/content/233838383/version?expand=collaborators%2Ccontent", + context: nil, + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "request creation failed: net/http: nil Context", + }, + + { + name: "when the response status is not correct", + contentID: "233838383", + payload: &models.ContentRestorePayloadScheme{ + OperationKey: "restore", + Params: &models.ContentRestoreParamsPayloadScheme{ + VersionNumber: 034, + Message: "message sample :)", + RestoreTitle: true, + }, + }, + expand: []string{"collaborators", "content"}, + mockFile: "./mocks/get-content-version.json", + wantHTTPMethod: http.MethodPost, + endpoint: "/wiki/rest/api/content/233838383/version?expand=collaborators%2Ccontent", + context: context.Background(), + wantHTTPCodeReturn: http.StatusBadRequest, + wantErr: true, + expectedError: "request failed. Please analyze the request body for more details. Status Code: 400", + }, + + { + name: "when the response body is not empty", + contentID: "233838383", + payload: &models.ContentRestorePayloadScheme{ + OperationKey: "restore", + Params: &models.ContentRestoreParamsPayloadScheme{ + VersionNumber: 034, + Message: "message sample :)", + RestoreTitle: true, + }, + }, + expand: []string{"collaborators", "content"}, + mockFile: "./mocks/empty-json.json", + wantHTTPMethod: http.MethodPost, + endpoint: "/wiki/rest/api/content/233838383/version?expand=collaborators%2Ccontent", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "unexpected end of JSON input", + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + //Init a new HTTP mock server + mockOptions := mockServerOptions{ + Endpoint: testCase.endpoint, + MockFilePath: testCase.mockFile, + MethodAccepted: testCase.wantHTTPMethod, + ResponseCodeWanted: testCase.wantHTTPCodeReturn, + } + + mockServer, err := startMockServer(&mockOptions) + if err != nil { + t.Fatal(err) + } + + defer mockServer.Close() + + //Init the library instance + mockClient, err := startMockClient(mockServer.URL) + if err != nil { + t.Fatal(err) + } + + service := &ContentVersionService{client: mockClient} + + gotResult, gotResponse, err := service.Restore(testCase.context, testCase.contentID, testCase.payload, testCase.expand) + + if testCase.wantErr { + + if err != nil { + t.Logf("error returned: %v", err.Error()) + } + + assert.EqualError(t, err, testCase.expectedError) + + if gotResponse != nil { + t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code) + } + + } else { + + assert.NoError(t, err) + assert.NotEqual(t, gotResponse, nil) + assert.NotEqual(t, gotResult, nil) + + apiEndpoint, err := url.Parse(gotResponse.Endpoint) + if err != nil { + t.Fatal(err) + } + + var endpointToAssert string + + if apiEndpoint.Query().Encode() != "" { + endpointToAssert = fmt.Sprintf("%v?%v", apiEndpoint.Path, apiEndpoint.Query().Encode()) + } else { + endpointToAssert = apiEndpoint.Path + } + + t.Logf("HTTP Endpoint Wanted: %v, HTTP Endpoint Returned: %v", testCase.endpoint, endpointToAssert) + assert.Equal(t, testCase.endpoint, endpointToAssert) + + t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code) + assert.Equal(t, gotResponse.Code, testCase.wantHTTPCodeReturn) + } + }) + } +} + +func Test_Content_Version_Service_Delete(t *testing.T) { + + testCases := []struct { + name string + contentID string + versionNumber int + mockFile string + wantHTTPMethod string + endpoint string + context context.Context + wantHTTPCodeReturn int + wantErr bool + expectedError string + }{ + { + name: "when the parameters are correct", + contentID: "233838383", + wantHTTPMethod: http.MethodDelete, + endpoint: "/wiki/rest/api/content/233838383/version/0", + context: context.Background(), + wantHTTPCodeReturn: http.StatusNoContent, + }, + + { + name: "when the content id is not provided", + contentID: "", + wantHTTPMethod: http.MethodDelete, + endpoint: "/wiki/rest/api/content/233838383/version/0", + context: context.Background(), + wantHTTPCodeReturn: http.StatusNoContent, + wantErr: true, + expectedError: "confluence: no content id set", + }, + + { + name: "when the context is not provided", + contentID: "233838383", + wantHTTPMethod: http.MethodDelete, + endpoint: "/wiki/rest/api/content/233838383/version/0", + context: nil, + wantHTTPCodeReturn: http.StatusNoContent, + wantErr: true, + expectedError: "request creation failed: net/http: nil Context", + }, + + { + name: "when the response status is not correct", + contentID: "233838383", + wantHTTPMethod: http.MethodDelete, + endpoint: "/wiki/rest/api/content/233838383/version/0", + context: context.Background(), + wantHTTPCodeReturn: http.StatusInternalServerError, + wantErr: true, + expectedError: "request failed. Please analyze the request body for more details. Status Code: 500", + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + //Init a new HTTP mock server + mockOptions := mockServerOptions{ + Endpoint: testCase.endpoint, + MockFilePath: testCase.mockFile, + MethodAccepted: testCase.wantHTTPMethod, + ResponseCodeWanted: testCase.wantHTTPCodeReturn, + } + + mockServer, err := startMockServer(&mockOptions) + if err != nil { + t.Fatal(err) + } + + defer mockServer.Close() + + //Init the library instance + mockClient, err := startMockClient(mockServer.URL) + if err != nil { + t.Fatal(err) + } + + service := &ContentVersionService{client: mockClient} + + gotResponse, err := service.Delete(testCase.context, testCase.contentID, testCase.versionNumber) + + if testCase.wantErr { + + if err != nil { + t.Logf("error returned: %v", err.Error()) + } + + assert.EqualError(t, err, testCase.expectedError) + + if gotResponse != nil { + t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code) + } + + } else { + + assert.NoError(t, err) + assert.NotEqual(t, gotResponse, nil) + + apiEndpoint, err := url.Parse(gotResponse.Endpoint) + if err != nil { + t.Fatal(err) + } + + var endpointToAssert string + + if apiEndpoint.Query().Encode() != "" { + endpointToAssert = fmt.Sprintf("%v?%v", apiEndpoint.Path, apiEndpoint.Query().Encode()) + } else { + endpointToAssert = apiEndpoint.Path + } + + t.Logf("HTTP Endpoint Wanted: %v, HTTP Endpoint Returned: %v", testCase.endpoint, endpointToAssert) + assert.Equal(t, testCase.endpoint, endpointToAssert) + + t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code) + assert.Equal(t, gotResponse.Code, testCase.wantHTTPCodeReturn) + } + }) + } +} diff --git a/confluence/mocks/get-content-version.json b/confluence/mocks/get-content-version.json new file mode 100644 index 00000000..41f63c55 --- /dev/null +++ b/confluence/mocks/get-content-version.json @@ -0,0 +1,929 @@ +{ + "by": { + "type": "known", + "username": "", + "userKey": "", + "accountId": "", + "accountType": "atlassian", + "email": "", + "publicName": "", + "profilePicture": { + "path": "", + "width": 2154, + "height": 2154, + "isDefault": true + }, + "displayName": "", + "timeZone": "", + "isExternalCollaborator": true, + "externalCollaborator": true, + "operations": [ + { + "operation": "administer", + "targetType": "" + } + ], + "details": { + "business": { + "position": "", + "department": "", + "location": "" + }, + "personal": { + "phone": "", + "im": "", + "website": "", + "email": "" + } + }, + "personalSpace": { + "id": 2154, + "key": "", + "name": "", + "icon": { + "path": "", + "width": 2154, + "height": 2154, + "isDefault": true + }, + "description": { + "plain": { + "value": "", + "representation": "plain", + "embeddedContent": [ + {} + ] + }, + "view": { + "value": "", + "representation": "plain", + "embeddedContent": [ + {} + ] + }, + "_expandable": { + "view": "", + "plain": "" + } + }, + "homepage": { + "type": "", + "status": "" + }, + "type": "", + "metadata": { + "labels": { + "results": [ + { + "prefix": "", + "name": "", + "id": "", + "label": "" + } + ], + "size": 2154 + }, + "_expandable": {} + }, + "operations": [ + { + "operation": "administer", + "targetType": "" + } + ], + "permissions": [ + { + "operation": { + "operation": "administer", + "targetType": "" + }, + "anonymousAccess": true, + "unlicensedAccess": true + } + ], + "status": "", + "settings": { + "routeOverrideEnabled": true, + "_links": {} + }, + "theme": { + "themeKey": "" + }, + "lookAndFeel": { + "headings": { + "color": "" + }, + "links": { + "color": "" + }, + "menus": { + "hoverOrFocus": { + "backgroundColor": "" + }, + "color": "" + }, + "header": { + "backgroundColor": "", + "button": { + "backgroundColor": "", + "color": "" + }, + "primaryNavigation": { + "color": "", + "hoverOrFocus": { + "backgroundColor": "", + "color": "" + } + }, + "secondaryNavigation": { + "color": "", + "hoverOrFocus": { + "backgroundColor": "", + "color": "" + } + }, + "search": { + "backgroundColor": "", + "color": "" + } + }, + "content": {}, + "bordersAndDividers": { + "color": "" + } + }, + "history": { + "createdDate": "" + }, + "_expandable": { + "settings": "", + "metadata": "", + "operations": "", + "lookAndFeel": "", + "permissions": "", + "icon": "", + "description": "", + "theme": "", + "history": "", + "homepage": "", + "identifiers": "" + }, + "_links": {} + }, + "_expandable": { + "operations": "", + "details": "", + "personalSpace": "" + }, + "_links": {} + }, + "when": "", + "friendlyWhen": "", + "message": "", + "number": 57, + "minorEdit": true, + "content": { + "id": "", + "type": "", + "status": "", + "title": "", + "space": { + "id": 2154, + "key": "", + "name": "", + "icon": { + "path": "", + "width": 2154, + "height": 2154, + "isDefault": true + }, + "description": { + "plain": { + "value": "", + "representation": "plain", + "embeddedContent": [ + {} + ] + }, + "view": { + "value": "", + "representation": "plain", + "embeddedContent": [ + {} + ] + }, + "_expandable": { + "view": "", + "plain": "" + } + }, + "type": "", + "metadata": { + "labels": { + "results": [ + { + "prefix": "", + "name": "", + "id": "", + "label": "" + } + ], + "size": 2154 + }, + "_expandable": {} + }, + "operations": [ + { + "operation": "administer", + "targetType": "" + } + ], + "permissions": [ + { + "operation": { + "operation": "administer", + "targetType": "" + }, + "anonymousAccess": true, + "unlicensedAccess": true + } + ], + "status": "", + "settings": { + "routeOverrideEnabled": true, + "_links": {} + }, + "theme": { + "themeKey": "" + }, + "lookAndFeel": { + "headings": { + "color": "" + }, + "links": { + "color": "" + }, + "menus": { + "hoverOrFocus": { + "backgroundColor": "" + }, + "color": "" + }, + "header": { + "backgroundColor": "", + "button": { + "backgroundColor": "", + "color": "" + }, + "primaryNavigation": { + "color": "", + "hoverOrFocus": { + "backgroundColor": "", + "color": "" + } + }, + "secondaryNavigation": { + "color": "", + "hoverOrFocus": { + "backgroundColor": "", + "color": "" + } + }, + "search": { + "backgroundColor": "", + "color": "" + } + }, + "content": {}, + "bordersAndDividers": { + "color": "" + } + }, + "history": { + "createdDate": "", + "createdBy": { + "type": "known" + } + }, + "_expandable": { + "settings": "", + "metadata": "", + "operations": "", + "lookAndFeel": "", + "permissions": "", + "icon": "", + "description": "", + "theme": "", + "history": "", + "homepage": "", + "identifiers": "" + }, + "_links": {} + }, + "history": { + "latest": true, + "createdBy": { + "type": "known" + }, + "createdDate": "", + "lastUpdated": { + "when": "", + "number": 57, + "minorEdit": true + }, + "previousVersion": { + "when": "", + "number": 57, + "minorEdit": true + }, + "contributors": { + "publishers": { + "userKeys": [ + "" + ] + } + }, + "nextVersion": { + "when": "", + "number": 57, + "minorEdit": true + }, + "_expandable": { + "lastUpdated": "", + "previousVersion": "", + "contributors": "", + "nextVersion": "" + }, + "_links": {} + }, + "version": { + "by": { + "type": "known" + }, + "when": "", + "friendlyWhen": "", + "message": "", + "number": 57, + "minorEdit": true, + "collaborators": { + "userKeys": [ + "" + ] + }, + "_expandable": { + "content": "", + "collaborators": "" + }, + "_links": {}, + "contentTypeModified": true, + "confRev": "", + "syncRev": "", + "syncRevSource": "" + }, + "ancestors": [], + "operations": [ + { + "operation": "administer", + "targetType": "" + } + ], + "children": { + "attachment": { + "results": [], + "size": 2154, + "_links": {} + }, + "comment": { + "results": [], + "size": 2154, + "_links": {} + }, + "page": { + "results": [], + "size": 2154, + "_links": {} + }, + "_expandable": { + "attachment": "", + "comment": "", + "page": "" + }, + "_links": {} + }, + "childTypes": { + "attachment": { + "value": true, + "_links": {} + }, + "comment": { + "value": true, + "_links": {} + }, + "page": { + "value": true, + "_links": {} + }, + "_expandable": { + "all": {}, + "attachment": {}, + "comment": {}, + "page": {} + } + }, + "descendants": { + "attachment": { + "results": [], + "size": 2154, + "_links": {} + }, + "comment": { + "results": [], + "size": 2154, + "_links": {} + }, + "page": { + "results": [], + "size": 2154, + "_links": {} + }, + "_expandable": { + "attachment": "", + "comment": "", + "page": "" + }, + "_links": {} + }, + "container": {}, + "body": { + "view": { + "value": "", + "representation": "view", + "embeddedContent": [ + {} + ], + "webresource": {}, + "mediaToken": { + "collectionIds": [ + "" + ], + "contentId": "", + "expiryDateTime": "", + "fileIds": [ + "" + ], + "token": "" + }, + "_expandable": { + "content": "", + "embeddedContent": "", + "webresource": "", + "mediaToken": "" + }, + "_links": {} + }, + "export_view": { + "value": "", + "representation": "view", + "embeddedContent": [ + {} + ], + "webresource": {}, + "mediaToken": { + "collectionIds": [ + "" + ], + "contentId": "", + "expiryDateTime": "", + "fileIds": [ + "" + ], + "token": "" + }, + "_expandable": { + "content": "", + "embeddedContent": "", + "webresource": "", + "mediaToken": "" + }, + "_links": {} + }, + "styled_view": { + "value": "", + "representation": "view", + "embeddedContent": [ + {} + ], + "webresource": {}, + "mediaToken": { + "collectionIds": [ + "" + ], + "contentId": "", + "expiryDateTime": "", + "fileIds": [ + "" + ], + "token": "" + }, + "_expandable": { + "content": "", + "embeddedContent": "", + "webresource": "", + "mediaToken": "" + }, + "_links": {} + }, + "storage": { + "value": "", + "representation": "view", + "embeddedContent": [ + {} + ], + "webresource": {}, + "mediaToken": { + "collectionIds": [ + "" + ], + "contentId": "", + "expiryDateTime": "", + "fileIds": [ + "" + ], + "token": "" + }, + "_expandable": { + "content": "", + "embeddedContent": "", + "webresource": "", + "mediaToken": "" + }, + "_links": {} + }, + "wiki": { + "value": "", + "representation": "view", + "embeddedContent": [ + {} + ], + "webresource": {}, + "mediaToken": { + "collectionIds": [ + "" + ], + "contentId": "", + "expiryDateTime": "", + "fileIds": [ + "" + ], + "token": "" + }, + "_expandable": { + "content": "", + "embeddedContent": "", + "webresource": "", + "mediaToken": "" + }, + "_links": {} + }, + "editor": { + "value": "", + "representation": "view", + "embeddedContent": [ + {} + ], + "webresource": {}, + "mediaToken": { + "collectionIds": [ + "" + ], + "contentId": "", + "expiryDateTime": "", + "fileIds": [ + "" + ], + "token": "" + }, + "_expandable": { + "content": "", + "embeddedContent": "", + "webresource": "", + "mediaToken": "" + }, + "_links": {} + }, + "editor2": { + "value": "", + "representation": "view", + "embeddedContent": [ + {} + ], + "webresource": {}, + "mediaToken": { + "collectionIds": [ + "" + ], + "contentId": "", + "expiryDateTime": "", + "fileIds": [ + "" + ], + "token": "" + }, + "_expandable": { + "content": "", + "embeddedContent": "", + "webresource": "", + "mediaToken": "" + }, + "_links": {} + }, + "anonymous_export_view": { + "value": "", + "representation": "view", + "embeddedContent": [ + {} + ], + "webresource": {}, + "mediaToken": { + "collectionIds": [ + "" + ], + "contentId": "", + "expiryDateTime": "", + "fileIds": [ + "" + ], + "token": "" + }, + "_expandable": { + "content": "", + "embeddedContent": "", + "webresource": "", + "mediaToken": "" + }, + "_links": {} + }, + "atlas_doc_format": { + "value": "", + "representation": "view", + "embeddedContent": [ + {} + ], + "webresource": {}, + "mediaToken": { + "collectionIds": [ + "" + ], + "contentId": "", + "expiryDateTime": "", + "fileIds": [ + "" + ], + "token": "" + }, + "_expandable": { + "content": "", + "embeddedContent": "", + "webresource": "", + "mediaToken": "" + }, + "_links": {} + }, + "dynamic": { + "value": "", + "representation": "view", + "embeddedContent": [ + {} + ], + "webresource": {}, + "mediaToken": { + "collectionIds": [ + "" + ], + "contentId": "", + "expiryDateTime": "", + "fileIds": [ + "" + ], + "token": "" + }, + "_expandable": { + "content": "", + "embeddedContent": "", + "webresource": "", + "mediaToken": "" + }, + "_links": {} + }, + "_expandable": { + "editor": "", + "view": "", + "export_view": "", + "styled_view": "", + "storage": "", + "editor2": "", + "anonymous_export_view": "", + "atlas_doc_format": "", + "wiki": "", + "dynamic": "", + "raw": "" + } + }, + "restrictions": { + "read": { + "operation": "administer", + "restrictions": { + "user": { + "results": [ + { + "type": "known" + } + ] + }, + "group": { + "results": [ + { + "type": "group", + "name": "" + } + ], + "start": 2154, + "limit": 2154, + "size": 2154 + }, + "_expandable": { + "user": "", + "group": "" + } + }, + "_expandable": { + "restrictions": "", + "content": "" + }, + "_links": {} + }, + "update": { + "operation": "administer", + "restrictions": { + "user": { + "results": [ + { + "type": "known" + } + ] + }, + "group": { + "results": [ + { + "type": "group", + "name": "" + } + ], + "start": 2154, + "limit": 2154, + "size": 2154 + }, + "_expandable": { + "user": "", + "group": "" + } + }, + "_expandable": { + "restrictions": "", + "content": "" + }, + "_links": {} + }, + "_expandable": { + "read": "", + "update": "" + }, + "_links": {} + }, + "metadata": { + "currentuser": { + "favourited": { + "isFavourite": true, + "favouritedDate": "" + }, + "lastmodified": { + "version": { + "when": "", + "number": 57, + "minorEdit": true + }, + "friendlyLastModified": "" + }, + "lastcontributed": { + "status": "", + "when": "" + }, + "viewed": { + "lastSeen": "", + "friendlyLastSeen": "" + }, + "scheduled": {}, + "_expandable": { + "favourited": "", + "lastmodified": "", + "lastcontributed": "", + "viewed": "", + "scheduled": "" + } + }, + "properties": {}, + "frontend": {}, + "labels": { + "results": [ + { + "prefix": "", + "name": "", + "id": "", + "label": "" + } + ], + "start": 2154, + "limit": 2154, + "size": 2154, + "_links": {} + } + }, + "macroRenderedOutput": {}, + "extensions": {}, + "_expandable": { + "childTypes": "", + "container": "", + "metadata": "", + "operations": "", + "children": "", + "restrictions": "", + "history": "", + "ancestors": "", + "body": "", + "version": "", + "descendants": "", + "space": "", + "extensions": "", + "schedulePublishDate": "", + "macroRenderedOutput": "" + }, + "_links": {} + }, + "collaborators": { + "users": [ + { + "type": "known", + "username": "", + "userKey": "", + "accountId": "", + "accountType": "atlassian", + "email": "", + "publicName": "", + "profilePicture": { + "path": "", + "width": 2154, + "height": 2154, + "isDefault": true + }, + "displayName": "", + "timeZone": "", + "isExternalCollaborator": true, + "externalCollaborator": true, + "operations": [ + { + "operation": "administer", + "targetType": "" + } + ], + "details": {}, + "personalSpace": { + "key": "", + "name": "", + "type": "", + "status": "", + "_expandable": {}, + "_links": {} + }, + "_expandable": { + "operations": "", + "details": "", + "personalSpace": "" + }, + "_links": {} + } + ], + "userKeys": [ + "" + ], + "_links": {} + }, + "_expandable": { + "content": "", + "collaborators": "" + }, + "_links": {}, + "contentTypeModified": true, + "confRev": "", + "syncRev": "", + "syncRevSource": "" +} \ No newline at end of file diff --git a/confluence/mocks/get-content-versions.json b/confluence/mocks/get-content-versions.json new file mode 100644 index 00000000..8dfdf8b7 --- /dev/null +++ b/confluence/mocks/get-content-versions.json @@ -0,0 +1,196 @@ +{ + "results": [ + { + "by": { + "type": "known", + "username": "", + "userKey": "", + "accountId": "", + "accountType": "atlassian", + "email": "", + "publicName": "", + "profilePicture": { + "path": "", + "width": 2154, + "height": 2154, + "isDefault": true + }, + "displayName": "", + "timeZone": "", + "isExternalCollaborator": true, + "externalCollaborator": true, + "operations": [ + { + "operation": "administer", + "targetType": "" + } + ], + "details": {}, + "personalSpace": { + "key": "", + "name": "", + "type": "", + "status": "", + "_expandable": {}, + "_links": {} + }, + "_expandable": { + "operations": "", + "details": "", + "personalSpace": "" + }, + "_links": {} + }, + "when": "", + "friendlyWhen": "", + "message": "", + "number": 57, + "minorEdit": true, + "content": { + "id": "", + "type": "", + "status": "", + "title": "", + "space": { + "key": "", + "name": "", + "type": "", + "status": "", + "_expandable": {}, + "_links": {} + }, + "history": { + "latest": true + }, + "ancestors": [], + "operations": [ + { + "operation": "administer", + "targetType": "" + } + ], + "children": {}, + "childTypes": {}, + "descendants": {}, + "container": {}, + "body": { + "view": { + "value": "", + "representation": "view" + }, + "export_view": { + "value": "", + "representation": "view" + }, + "styled_view": { + "value": "", + "representation": "view" + }, + "storage": { + "value": "", + "representation": "view" + }, + "wiki": { + "value": "", + "representation": "view" + }, + "editor": { + "value": "", + "representation": "view" + }, + "editor2": { + "value": "", + "representation": "view" + }, + "anonymous_export_view": { + "value": "", + "representation": "view" + }, + "atlas_doc_format": { + "value": "", + "representation": "view" + }, + "dynamic": { + "value": "", + "representation": "view" + }, + "_expandable": { + "editor": "", + "view": "", + "export_view": "", + "styled_view": "", + "storage": "", + "editor2": "", + "anonymous_export_view": "", + "atlas_doc_format": "", + "wiki": "", + "dynamic": "", + "raw": "" + } + }, + "restrictions": { + "read": { + "operation": "administer", + "_expandable": {}, + "_links": {} + }, + "update": { + "operation": "administer", + "_expandable": {}, + "_links": {} + }, + "_expandable": { + "read": "", + "update": "" + }, + "_links": {} + }, + "metadata": {}, + "macroRenderedOutput": {}, + "extensions": {}, + "_expandable": { + "childTypes": "", + "container": "", + "metadata": "", + "operations": "", + "children": "", + "restrictions": "", + "history": "", + "ancestors": "", + "body": "", + "version": "", + "descendants": "", + "space": "", + "extensions": "", + "schedulePublishDate": "", + "macroRenderedOutput": "" + }, + "_links": {} + }, + "collaborators": { + "users": [ + { + "type": "known" + } + ], + "userKeys": [ + "" + ], + "_links": {} + }, + "_expandable": { + "content": "", + "collaborators": "" + }, + "_links": {}, + "contentTypeModified": true, + "confRev": "", + "syncRev": "", + "syncRevSource": "" + } + ], + "start": 2154, + "limit": 2154, + "size": 2154, + "_links": {} +} \ No newline at end of file diff --git a/pkg/infra/models/confluence_content.go b/pkg/infra/models/confluence_content.go index d4129a78..c78e1a30 100644 --- a/pkg/infra/models/confluence_content.go +++ b/pkg/infra/models/confluence_content.go @@ -56,27 +56,6 @@ type ContentExtensionScheme struct { FileID string `json:"fileId,omitempty"` } -type ContentVersionScheme struct { - By *ContentUserScheme `json:"by,omitempty"` - Number int `json:"number,omitempty"` - When string `json:"when,omitempty"` - FriendlyWhen string `json:"friendlyWhen,omitempty"` - Message string `json:"message,omitempty"` - MinorEdit bool `json:"minorEdit,omitempty"` - Content *ContentScheme `json:"content,omitempty"` - Collaborators *VersionCollaboratorsScheme `json:"collaborators,omitempty"` - Expandable struct { - Content string `json:"content"` - Collaborators string `json:"collaborators"` - } `json:"_expandable"` - ContentTypeModified bool `json:"contentTypeModified"` -} - -type VersionCollaboratorsScheme struct { - Users []*ContentUserScheme `json:"users,omitempty"` - UserKeys []string `json:"userKeys,omitempty"` -} - type BodyScheme struct { View *BodyNodeScheme `json:"view"` ExportView *BodyNodeScheme `json:"export_view"` diff --git a/pkg/infra/models/confluence_content_version.go b/pkg/infra/models/confluence_content_version.go new file mode 100644 index 00000000..1db47713 --- /dev/null +++ b/pkg/infra/models/confluence_content_version.go @@ -0,0 +1,40 @@ +package models + +type ContentVersionPageScheme struct { + Results []*ContentVersionScheme `json:"results,omitempty"` + Start int `json:"start,omitempty"` + Limit int `json:"limit,omitempty"` + Size int `json:"size,omitempty"` +} + +type ContentVersionScheme struct { + By *ContentUserScheme `json:"by,omitempty"` + Number int `json:"number,omitempty"` + When string `json:"when,omitempty"` + FriendlyWhen string `json:"friendlyWhen,omitempty"` + Message string `json:"message,omitempty"` + MinorEdit bool `json:"minorEdit,omitempty"` + Content *ContentScheme `json:"content,omitempty"` + Collaborators *VersionCollaboratorsScheme `json:"collaborators,omitempty"` + Expandable struct { + Content string `json:"content"` + Collaborators string `json:"collaborators"` + } `json:"_expandable"` + ContentTypeModified bool `json:"contentTypeModified"` +} + +type VersionCollaboratorsScheme struct { + Users []*ContentUserScheme `json:"users,omitempty"` + UserKeys []string `json:"userKeys,omitempty"` +} + +type ContentRestorePayloadScheme struct { + OperationKey string `json:"operationKey,omitempty"` + Params *ContentRestoreParamsPayloadScheme `json:"params,omitempty"` +} + +type ContentRestoreParamsPayloadScheme struct { + VersionNumber int `json:"versionNumber,omitempty"` + Message string `json:"message,omitempty"` + RestoreTitle bool `json:"restoreTitle,omitempty"` +} From 610f65c0da2fc54e9dabdec3869b192d8894ea0d Mon Sep 17 00:00:00 2001 From: Carlos Treminio Date: Sun, 23 Jan 2022 15:03:29 -0600 Subject: [PATCH 2/5] :sparkles: Added the Confluence Label Service --- confluence/confluence.go | 2 + confluence/label.go | 43 ++++++ confluence/label_test.go | 169 ++++++++++++++++++++++++ confluence/mocks/get-label-details.json | 20 +++ pkg/infra/models/confluence_label.go | 19 +++ pkg/infra/models/errors.go | 1 + 6 files changed, 254 insertions(+) create mode 100644 confluence/label.go create mode 100644 confluence/label_test.go create mode 100644 confluence/mocks/get-label-details.json create mode 100644 pkg/infra/models/confluence_label.go diff --git a/confluence/confluence.go b/confluence/confluence.go index 5ef04db0..dcd3a3c8 100644 --- a/confluence/confluence.go +++ b/confluence/confluence.go @@ -21,6 +21,7 @@ type Client struct { Auth *AuthenticationService Content *ContentService Space *SpaceService + Label *LabelService } func New(httpClient *http.Client, site string) (client *Client, err error) { @@ -62,6 +63,7 @@ func New(httpClient *http.Client, site string) (client *Client, err error) { } client.Space = &SpaceService{client: client} + client.Label = &LabelService{client: client} return } diff --git a/confluence/label.go b/confluence/label.go new file mode 100644 index 00000000..f9dbe457 --- /dev/null +++ b/confluence/label.go @@ -0,0 +1,43 @@ +package confluence + +import ( + "context" + "fmt" + "github.com/ctreminiom/go-atlassian/pkg/infra/models" + "net/http" + "net/url" + "strconv" +) + +type LabelService struct{ client *Client } + +// Get returns label information and a list of contents associated with the label. +func (l *LabelService) Get(ctx context.Context, labelName, labelType string, start, limit int) (result *models.LabelDetailsScheme, + response *ResponseScheme, err error) { + + if labelName == "" { + return nil, nil, models.ErrNoLabelNameError + } + + query := url.Values{} + query.Add("start", strconv.Itoa(start)) + query.Add("limit", strconv.Itoa(limit)) + query.Add("name", labelName) + query.Add("type", labelType) + + endpoint := fmt.Sprintf("wiki/rest/api/label?%v", query.Encode()) + + request, err := l.client.newRequest(ctx, http.MethodGet, endpoint, nil) + if err != nil { + return nil, nil, err + } + + request.Header.Set("Accept", "application/json") + + response, err = l.client.Call(request, &result) + if err != nil { + return nil, response, err + } + + return +} diff --git a/confluence/label_test.go b/confluence/label_test.go new file mode 100644 index 00000000..dcaf33c3 --- /dev/null +++ b/confluence/label_test.go @@ -0,0 +1,169 @@ +package confluence + +import ( + "context" + "fmt" + "github.com/stretchr/testify/assert" + "net/http" + "net/url" + "testing" +) + +func Test_Label_Service_Get(t *testing.T) { + + testCases := []struct { + name string + labelName string + labelType string + start int + limit int + mockFile string + wantHTTPMethod string + endpoint string + context context.Context + wantHTTPCodeReturn int + wantErr bool + expectedError string + }{ + { + name: "when the parameters are correct", + start: 0, + limit: 50, + labelName: "tracking", + labelType: "page", + mockFile: "./mocks/get-label-details.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/label?limit=50&name=tracking&start=0&type=page", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + }, + + { + name: "when the label name is not provided", + start: 0, + limit: 50, + labelName: "", + labelType: "page", + mockFile: "./mocks/get-label-details.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/label?limit=50&name=tracking&start=0&type=page", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "confluence: no label name set", + }, + + { + name: "when the context is not provided", + start: 0, + limit: 50, + labelName: "tracking", + labelType: "page", + mockFile: "./mocks/get-label-details.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/label?limit=50&name=tracking&start=0&type=page", + context: nil, + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "request creation failed: net/http: nil Context", + }, + + { + name: "when the response status is not correct", + start: 0, + limit: 50, + labelName: "tracking", + labelType: "page", + mockFile: "./mocks/get-label-details.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/label?limit=50&name=tracking&start=0&type=page", + context: context.Background(), + wantHTTPCodeReturn: http.StatusBadRequest, + wantErr: true, + expectedError: "request failed. Please analyze the request body for more details. Status Code: 400", + }, + + { + name: "when the response body is not empty", + start: 0, + limit: 50, + labelName: "tracking", + labelType: "page", + mockFile: "./mocks/empty-json.json", + wantHTTPMethod: http.MethodGet, + endpoint: "/wiki/rest/api/label?limit=50&name=tracking&start=0&type=page", + context: context.Background(), + wantHTTPCodeReturn: http.StatusOK, + wantErr: true, + expectedError: "unexpected end of JSON input", + }, + } + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + + //Init a new HTTP mock server + mockOptions := mockServerOptions{ + Endpoint: testCase.endpoint, + MockFilePath: testCase.mockFile, + MethodAccepted: testCase.wantHTTPMethod, + ResponseCodeWanted: testCase.wantHTTPCodeReturn, + } + + mockServer, err := startMockServer(&mockOptions) + if err != nil { + t.Fatal(err) + } + + defer mockServer.Close() + + //Init the library instance + mockClient, err := startMockClient(mockServer.URL) + if err != nil { + t.Fatal(err) + } + + service := &LabelService{client: mockClient} + + gotResult, gotResponse, err := service.Get(testCase.context, testCase.labelName, testCase.labelType, + testCase.start, testCase.limit) + + if testCase.wantErr { + + if err != nil { + t.Logf("error returned: %v", err.Error()) + } + + assert.EqualError(t, err, testCase.expectedError) + + if gotResponse != nil { + t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code) + } + + } else { + + assert.NoError(t, err) + assert.NotEqual(t, gotResponse, nil) + assert.NotEqual(t, gotResult, nil) + + apiEndpoint, err := url.Parse(gotResponse.Endpoint) + if err != nil { + t.Fatal(err) + } + + var endpointToAssert string + + if apiEndpoint.Query().Encode() != "" { + endpointToAssert = fmt.Sprintf("%v?%v", apiEndpoint.Path, apiEndpoint.Query().Encode()) + } else { + endpointToAssert = apiEndpoint.Path + } + + t.Logf("HTTP Endpoint Wanted: %v, HTTP Endpoint Returned: %v", testCase.endpoint, endpointToAssert) + assert.Equal(t, testCase.endpoint, endpointToAssert) + + t.Logf("HTTP Code Wanted: %v, HTTP Code Returned: %v", testCase.wantHTTPCodeReturn, gotResponse.Code) + assert.Equal(t, gotResponse.Code, testCase.wantHTTPCodeReturn) + } + }) + } +} diff --git a/confluence/mocks/get-label-details.json b/confluence/mocks/get-label-details.json new file mode 100644 index 00000000..754bfcfa --- /dev/null +++ b/confluence/mocks/get-label-details.json @@ -0,0 +1,20 @@ +{ + "label": { + "prefix": "", + "name": "", + "id": "", + "label": "" + }, + "associatedContents": { + "results": [ + { + "contentType": "page", + "contentId": 2154, + "title": "" + } + ], + "start": 2154, + "limit": 2154, + "size": 2154 + } +} \ No newline at end of file diff --git a/pkg/infra/models/confluence_label.go b/pkg/infra/models/confluence_label.go new file mode 100644 index 00000000..c6372a5c --- /dev/null +++ b/pkg/infra/models/confluence_label.go @@ -0,0 +1,19 @@ +package models + +type LabelDetailsScheme struct { + Label *ContentLabelScheme `json:"label"` + AssociatedContents *LabelAssociatedContentPageScheme `json:"associatedContents"` +} + +type LabelAssociatedContentPageScheme struct { + Results []*LabelAssociatedContentScheme `json:"results,omitempty"` + Start int `json:"start,omitempty"` + Limit int `json:"limit,omitempty"` + Size int `json:"size,omitempty"` +} + +type LabelAssociatedContentScheme struct { + ContentType string `json:"contentType,omitempty"` + ContentID int `json:"contentId,omitempty"` + Title string `json:"title,omitempty"` +} diff --git a/pkg/infra/models/errors.go b/pkg/infra/models/errors.go index 2b53c64c..e7e82938 100644 --- a/pkg/infra/models/errors.go +++ b/pkg/infra/models/errors.go @@ -37,6 +37,7 @@ var ( ErrNoSpaceKeyError = errors.New("confluence: no space key set") ErrNoContentRestrictionKeyError = errors.New("confluence: no content restriction operation key set") ErrNoConfluenceGroupError = errors.New("confluence: no group id or name set") + ErrNoLabelNameError = errors.New("confluence: no label name set") ErrNoBoardIDError = errors.New("agile: no board id set") ErrNoFilterIDError = errors.New("agile: no filter id set") From ec6c4c650888b969b0a98e74fb1d2989f4d5166a Mon Sep 17 00:00:00 2001 From: Carlos Treminio Date: Sun, 23 Jan 2022 16:08:42 -0600 Subject: [PATCH 3/5] :memo: README.md updated --- README.md | 197 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 126 insertions(+), 71 deletions(-) diff --git a/README.md b/README.md index a025a5aa..5668d86e 100644 --- a/README.md +++ b/README.md @@ -13,104 +13,159 @@

-go-atlassian is a Go module that enables the interaction with the Atlassian Cloud Services. It provides the Go implementation for operating the Atlassian Cloud platform. - -## ✨ Features -- Supports Jira Software V2/V3 endpoints. -- Support the Jira Agile endpoints -- Interacts with the Jira Service Management entities. -- Manages the Atlassian Admin Cloud organizations. -- Manages the Atlassian Admin SCIM workflow. -- Checks Confluence Cloud content permissions. -- CRUD Confluence Cloud content (page, blogpost, comment, question). -- Add attachment into Confluence Cloud contents. -- Search contents and spaces. -- Support the Atlassian Document Format (ADF). -- Every method has their corresponding example documented. - -## πŸ”° Installation -Make sure you have Go installed (download). Version `1.13` or higher is required. -```sh -$ go get -u -v github.com/ctreminiom/go-atlassian +Communicate with the [Atlassian API's](https://developer.atlassian.com/cloud/) quickly and easily +with the **go-atlassian** module. With the **go-atlassian** client, you can retrieve and manipulate +the extensive Atlassian Cloud API's like Jira, Confluence, Jira Agile, Jira Service Management, Atlassian Admin and much more!. + +If you find an endpoint not supported, please submit a pull request or raise a feature issue - it's always greatly appreciated. + +## Installation + +If you do not have [Go](https://golang.org/) installed yet, you can find installation instructions +[here](https://golang.org/doc/install). Please note that the package requires Go version +1.13 or later for module support. + +To pull the most recent version of **go-atlassian**, use `go get`. + +``` +go get github.com/ctreminiom/go-atlassian ``` -## πŸ““ Documentation -Documentation is hosted live at https://docs.go-atlassian.io/ +Then import the package into your project as you normally would. You can import the following packages -## πŸ“ Using the library -````go -package main +|Package|import path | +|--|--| +|Jira v2|`github.com/ctreminiom/go-atlassian/jira/v2`| +|Jira v3|`github.com/ctreminiom/go-atlassian/jira/v3`| +|Jira Agile|`github.com/ctreminiom/go-atlassian/jira/agile`| +|Jira ITSM|`github.com/ctreminiom/go-atlassian/jira/sm`| +|Confluence|`github.com/ctreminiom/go-atlassian/confluence`| +|Cloud Admin|`github.com/ctreminiom/go-atlassian/admin`| -import ( - "context" - "github.com/ctreminiom/go-atlassian/jira/v2" - "log" - "os" -) +Now you're ready to Go. -func main() { +## Usage - var ( - host = os.Getenv("HOST") - mail = os.Getenv("MAIL") - token = os.Getenv("TOKEN") - ) +### Creating A Client - atlassian, err := v2.New(nil, host) - if err != nil { - return - } +Before using the **go-atlassian** package, you need to have an Atlassian API key. If you do +not have a key yet, you can sign up [here](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/). - atlassian.Auth.SetBasicAuth(mail, token) +Create a client with your instance host and access token to start communicating with the Atlassian API's. - issue, response, err := atlassian.Issue.Get(context.Background(), "KP-2", nil, []string{"transitions"}) - if err != nil { - log.Fatal(err) - } +```go +instance, err := confluence.New(nil, "INSTANCE_HOST") +if err != nil { + log.Fatal(err) +} - log.Println("HTTP Endpoint Used", response.Endpoint) +instance.Auth.SetBasicAuth("YOUR_CLIENT_MAIL", "YOUR_APP_ACCESS_TOKEN") +``` - log.Println(issue.Key) - log.Println(issue.Fields.Reporter.AccountID) +If you need to use a preconfigured HTTP client, simply pass its address to the +`New` function. + +```go +transport := http.Transport{ + Proxy: http.ProxyFromEnvironment, + Dial: (&net.Dialer{ + // Modify the time to wait for a connection to establish + Timeout: 1 * time.Second, + KeepAlive: 30 * time.Second, + }).Dial, + TLSHandshakeTimeout: 10 * time.Second, +} - for _, transition := range issue.Transitions { - log.Println(transition.Name, transition.ID, transition.To.ID, transition.HasScreen) - } +client := http.Client{ + Transport: &transport, + Timeout: 4 * time.Second, +} - // Check if the issue contains sub-tasks - if issue.Fields.Subtasks != nil { - for _, subTask := range issue.Fields.Subtasks { - log.Println("Sub-Task: ", subTask.Key, subTask.Fields.Summary) - } - } +instance, err := confluence.New(&client, "INSTANCE_HOST") +if err != nil { + log.Fatal(err) } -```` -## ⭐️ Project assistance -If you want to say **thank you** or/and support active development of `go-atlassian`: +instance.Auth.SetBasicAuth("YOUR_CLIENT_MAIL", "YOUR_APP_ACCESS_TOKEN") +``` + +### Services + +The client contains a distinct service for working with each of the Atlassian API's +endpoints. Each service has a set of service functions that make specific API +calls to their respective endpoint. + +To start communicating with the **go-atlassian**, choose a service and call its service +function. Take the Jira service for example. + +To get the issue with the transitions, use the Issue service function. +```go +ctx := context.Background() +issueKey := "KP-2" +expand := []string{"transitions"} + +issue, response, err := atlassian.Issue.Get(ctx,issueKey, nil, expand) +if err != nil { + log.Fatal(err) +} + +log.Println(issue.Key) + +for _, transition := range issue.Transitions { + log.Println(transition.Name, transition.ID, transition.To.ID, transition.HasScreen) +} +``` + +To search issue using a JQL query, use the Issue.Search service function. +```go +var ( + jql = "order by created DESC" + fields = []string{"status"} + expand = []string{"changelog", "renderedFields", "names", "schema", "transitions", "operations", "editmeta"} +) + +issues, response, err := atlassian.Issue.Search.Post(context.Background(), jql, fields, expand, 0, 50, "") +if err != nil { + log.Fatal(err) +} + +log.Println("HTTP Endpoint Used", response.Endpoint) +log.Println(issues.Total) +``` + +The rest of the service functions work much the same way; they are concise and +behave as you would expect. The [documentation](https://docs.go-atlassian.io/) +contains several examples on how to use each service function. + +## Contributions + +If you would like to contribute to this project, please adhere to the following +guidelines. + +* Submit an issue describing the problem. +* Fork the repo and add your contribution. +* Add appropriate tests. +* Run go fmt, go vet, and golint. +* Prefer idiomatic Go over non-idiomatic code. +* Follow the basic Go conventions found [here](https://github.com/golang/go/wiki/CodeReviewComments). +* If in doubt, try to match your code to the current codebase. +* Create a pull request with a description of your changes. + +Again, contributions are greatly appreciated! -- Add a [GitHub Star](https://github.com/ctreminiom/go-atlassian) to the project. -- Write interesting articles about project on [Dev.to](https://dev.to/), [Medium](https://medium.com/) or personal blog. -- Contributions, issues and feature requests are welcome! -- Feel free to check [issues page](https://github.com/ctreminiom/go-atlassian/issues). ## πŸ’‘ Inspiration The project was created with the purpose to provide a unique point to provide an interface for interacting with Atlassian products. This module is highly inspired by the Go library https://github.com/andygrunwald/go-jira but focused on Cloud solutions. -## πŸ§ͺ Run Test Cases -```sh -go test -v ./... -``` - ## πŸ“ License Copyright Β© 2021 [Carlos Treminio](https://github.com/ctreminiom). This project is [MIT](https://opensource.org/licenses/MIT) licensed. [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fctreminiom%2Fgo-atlassian.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fctreminiom%2Fgo-atlassian?ref=badge_large) -## 🌐 Credits -In addition to all the contributors we would like to thank to these companies: +## Special Thanks +In addition to all the contributors we would like to thanks to these companies: - [Atlassian](https://www.atlassian.com/) for providing us Atlassian Admin/Jira/Confluence Standard licenses. - [JetBrains](https://www.jetbrains.com/) for providing us with free licenses of [GoLand](https://www.jetbrains.com/pycharm/) - [GitBook](https://www.gitbook.com/) for providing us non-profit / open-source plan so hence I would like to express my thanks here. From d6845c569318f3a31d8abca919f2879f5bf40de5 Mon Sep 17 00:00:00 2001 From: Carlos Treminio Date: Sun, 23 Jan 2022 16:16:26 -0600 Subject: [PATCH 4/5] :memo: README.md updated --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 5668d86e..abf0e413 100644 --- a/README.md +++ b/README.md @@ -44,9 +44,7 @@ Then import the package into your project as you normally would. You can import Now you're ready to Go. -## Usage - -### Creating A Client +### 🧳 Creating A Client Before using the **go-atlassian** package, you need to have an Atlassian API key. If you do not have a key yet, you can sign up [here](https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/). @@ -89,7 +87,7 @@ if err != nil { instance.Auth.SetBasicAuth("YOUR_CLIENT_MAIL", "YOUR_APP_ACCESS_TOKEN") ``` -### Services +### πŸ—ΊοΈ Services The client contains a distinct service for working with each of the Atlassian API's endpoints. Each service has a set of service functions that make specific API @@ -98,7 +96,7 @@ calls to their respective endpoint. To start communicating with the **go-atlassian**, choose a service and call its service function. Take the Jira service for example. -To get the issue with the transitions, use the Issue service function. +To get the issue with the transitions, use the **Issue** service function. ```go ctx := context.Background() issueKey := "KP-2" @@ -116,7 +114,7 @@ for _, transition := range issue.Transitions { } ``` -To search issue using a JQL query, use the Issue.Search service function. +To search issues using a JQL query, use the **Issue.Search** service function. ```go var ( jql = "order by created DESC" @@ -137,7 +135,7 @@ The rest of the service functions work much the same way; they are concise and behave as you would expect. The [documentation](https://docs.go-atlassian.io/) contains several examples on how to use each service function. -## Contributions +## ✍️ Contributions If you would like to contribute to this project, please adhere to the following guidelines. @@ -164,11 +162,11 @@ This project is [MIT](https://opensource.org/licenses/MIT) licensed. [![FOSSA Status](https://app.fossa.com/api/projects/git%2Bgithub.com%2Fctreminiom%2Fgo-atlassian.svg?type=large)](https://app.fossa.com/projects/git%2Bgithub.com%2Fctreminiom%2Fgo-atlassian?ref=badge_large) -## Special Thanks +## 🀝 Special Thanks In addition to all the contributors we would like to thanks to these companies: - [Atlassian](https://www.atlassian.com/) for providing us Atlassian Admin/Jira/Confluence Standard licenses. - [JetBrains](https://www.jetbrains.com/) for providing us with free licenses of [GoLand](https://www.jetbrains.com/pycharm/) - [GitBook](https://www.gitbook.com/) for providing us non-profit / open-source plan so hence I would like to express my thanks here. - - \ No newline at end of file + + \ No newline at end of file From c8161d013895987f84e95aedbaf3cfcba8882f9d Mon Sep 17 00:00:00 2001 From: Carlos Treminio Date: Sun, 23 Jan 2022 16:52:17 -0600 Subject: [PATCH 5/5] :memo: Added the Confluence Version doc's links --- confluence/contentVersion.go | 4 ++++ confluence/label.go | 1 + 2 files changed, 5 insertions(+) diff --git a/confluence/contentVersion.go b/confluence/contentVersion.go index 206715ec..aa6d29fb 100644 --- a/confluence/contentVersion.go +++ b/confluence/contentVersion.go @@ -13,6 +13,7 @@ import ( type ContentVersionService struct{ client *Client } // Gets returns the versions for a piece of content in descending order. +// Docs: https://docs.go-atlassian.io/confluence-cloud/content/versions#get-content-versions func (c *ContentVersionService) Gets(ctx context.Context, contentID string, expand []string, start, limit int) ( result *models.ContentVersionPageScheme, response *ResponseScheme, err error) { @@ -46,6 +47,7 @@ func (c *ContentVersionService) Gets(ctx context.Context, contentID string, expa } // Get returns a version for a piece of content. +// Docs: https://docs.go-atlassian.io/confluence-cloud/content/versions#get-content-version func (c *ContentVersionService) Get(ctx context.Context, contentID string, versionNumber int, expand []string) ( result *models.ContentVersionScheme, response *ResponseScheme, err error) { @@ -79,6 +81,7 @@ func (c *ContentVersionService) Get(ctx context.Context, contentID string, versi // Restore restores a historical version to be the latest version. // That is, a new version is created with the content of the historical version. +// Docs: https://docs.go-atlassian.io/confluence-cloud/content/versions#restore-content-version func (c *ContentVersionService) Restore(ctx context.Context, contentID string, payload *models.ContentRestorePayloadScheme, expand []string) (result *models.ContentVersionScheme, response *ResponseScheme, err error) { @@ -122,6 +125,7 @@ func (c *ContentVersionService) Restore(ctx context.Context, contentID string, p // Delete deletes a historical version. // This does not delete the changes made to the content in that version, rather the changes for the deleted version // are rolled up into the next version. Note, you cannot delete the current version. +// Docs: https://docs.go-atlassian.io/confluence-cloud/content/versions#delete-content-version func (c *ContentVersionService) Delete(ctx context.Context, contentID string, versionNumber int) (response *ResponseScheme, err error) { if len(contentID) == 0 { diff --git a/confluence/label.go b/confluence/label.go index f9dbe457..216c1fc7 100644 --- a/confluence/label.go +++ b/confluence/label.go @@ -12,6 +12,7 @@ import ( type LabelService struct{ client *Client } // Get returns label information and a list of contents associated with the label. +// Docs: https://docs.go-atlassian.io/confluence-cloud/label#get-label-information func (l *LabelService) Get(ctx context.Context, labelName, labelType string, start, limit int) (result *models.LabelDetailsScheme, response *ResponseScheme, err error) {