Repository: ctreminiom/go-atlassian
Pull request #109 (2022-03-07 23:02:26)

:sparkles: Added the Space Permission service.
1. Added the ability to manipulates the space permissions.
2. Added the Add, Bulk and Removes methods.


Patch:
diff --git a/confluence/confluence.go b/confluence/confluence.go
index 69bcae6..22bf045 100644
--- a/confluence/confluence.go
+++ b/confluence/confluence.go
@@ -63,7 +63,11 @@ func New(httpClient *http.Client, site string) (client *Client, err error) {
 		Version: &ContentVersionService{client: client},
 	}
 
-	client.Space = &SpaceService{client: client}
+	client.Space = &SpaceService{
+		client:     client,
+		Permission: &SpacePermissionService{client: client},
+	}
+
 	client.Label = &LabelService{client: client}
 	client.Search = &SearchService{client: client}
 	return
diff --git a/confluence/mocks/space-permission-v2.json b/confluence/mocks/space-permission-v2.json
new file mode 100644
index 0000000..77d86de
--- /dev/null
+++ b/confluence/mocks/space-permission-v2.json
@@ -0,0 +1,12 @@
+{
+  "id": 2154,
+  "subject": {
+    "type": "user",
+    "identifier": "<string>"
+  },
+  "operation": {
+    "key": "administer",
+    "target": "page"
+  },
+  "_links": {}
+}
\ No newline at end of file
diff --git a/confluence/space.go b/confluence/space.go
index fa38beb..a8f45eb 100644
--- a/confluence/space.go
+++ b/confluence/space.go
@@ -11,7 +11,8 @@ import (
 )
 
 type SpaceService struct {
-	client *Client
+	client     *Client
+	Permission *SpacePermissionService
 }
 
 // Gets returns all spaces. The returned spaces are ordered alphabetically in ascending order by space key.
diff --git a/confluence/spacePermission.go b/confluence/spacePermission.go
new file mode 100644
index 0000000..b860cf4
--- /dev/null
+++ b/confluence/spacePermission.go
@@ -0,0 +1,98 @@
+package confluence
+
+import (
+	"context"
+	"fmt"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"net/http"
+)
+
+type SpacePermissionService struct{ client *Client }
+
+// Add adds new permission to space. If the permission to be added is a group permission, the group can be identified by its group name or group id.
+// Docs: https://docs.go-atlassian.io/confluence-cloud/space/permissions#add-new-permission-to-space
+func (s *SpacePermissionService) Add(ctx context.Context, spaceKey string, payload *models.SpacePermissionPayloadScheme) (
+	result *models.SpacePermissionV2Scheme, response *ResponseScheme, err error) {
+
+	if len(spaceKey) == 0 {
+		return nil, nil, models.ErrNoSpaceKeyError
+	}
+
+	endpoint := fmt.Sprintf("/wiki/rest/api/space/%v/permission", spaceKey)
+
+	payloadAsReader, err := transformStructToReader(payload)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	request, err := s.client.newRequest(ctx, http.MethodPost, endpoint, payloadAsReader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	request.Header.Set("Accept", "application/json")
+	request.Header.Set("Content-Type", "application/json")
+
+	response, err = s.client.Call(request, &result)
+	if err != nil {
+		return nil, response, err
+	}
+
+	return
+}
+
+// Bulk adds new custom content permission to space.
+// If the permission to be added is a group permission, the group can be identified by its group name or group id.
+// Docs: https://docs.go-atlassian.io/confluence-cloud/space/permissions#add-new-custom-content-permission-to-space
+func (s *SpacePermissionService) Bulk(ctx context.Context, spaceKey string, payload *models.SpacePermissionArrayPayloadScheme) (response *ResponseScheme, err error) {
+
+	if len(spaceKey) == 0 {
+		return nil, models.ErrNoSpaceKeyError
+	}
+
+	endpoint := fmt.Sprintf("/wiki/rest/api/space/%v/permission/custom-content", spaceKey)
+
+	payloadAsReader, err := transformStructToReader(payload)
+	if err != nil {
+		return nil, err
+	}
+
+	request, err := s.client.newRequest(ctx, http.MethodPost, endpoint, payloadAsReader)
+	if err != nil {
+		return nil, err
+	}
+
+	request.Header.Set("Accept", "application/json")
+	request.Header.Set("Content-Type", "application/json")
+
+	response, err = s.client.Call(request, nil)
+	if err != nil {
+		return response, err
+	}
+
+	return
+}
+
+// Remove removes a space permission.
+// Note that removing Read Space permission for a user or group will remove all the space permissions for that user or group.
+// Docs: https://docs.go-atlassian.io/confluence-cloud/space/permissions#remove-a-space-permission
+func (s *SpacePermissionService) Remove(ctx context.Context, spaceKey string, permissionId int) (response *ResponseScheme, err error) {
+
+	if len(spaceKey) == 0 {
+		return nil, models.ErrNoSpaceKeyError
+	}
+
+	endpoint := fmt.Sprintf("/wiki/rest/api/space/%v/permission/%v", spaceKey, permissionId)
+
+	request, err := s.client.newRequest(ctx, http.MethodDelete, endpoint, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	response, err = s.client.Call(request, nil)
+	if err != nil {
+		return response, err
+	}
+
+	return
+}
diff --git a/pkg/infra/models/confluence_content_permission.go b/pkg/infra/models/confluence_content_permission.go
index ce4ee4b..6482d4f 100644
--- a/pkg/infra/models/confluence_content_permission.go
+++ b/pkg/infra/models/confluence_content_permission.go
@@ -6,8 +6,8 @@ type CheckPermissionScheme struct {
 }
 
 type PermissionSubjectScheme struct {
-	Type       string `json:"type"`
-	Identifier string `json:"identifier"`
+	Identifier string `json:"identifier,omitempty"`
+	Type       string `json:"type,omitempty"`
 }
 
 type PermissionCheckResponseScheme struct {
diff --git a/pkg/infra/models/confluence_space_permission.go b/pkg/infra/models/confluence_space_permission.go
new file mode 100644
index 0000000..bfbae53
--- /dev/null
+++ b/pkg/infra/models/confluence_space_permission.go
@@ -0,0 +1,28 @@
+package models
+
+type SpacePermissionPayloadScheme struct {
+	Subject   *PermissionSubjectScheme        `json:"subject,omitempty"`
+	Operation *SpacePermissionOperationScheme `json:"operation,omitempty"`
+}
+
+type SpacePermissionArrayPayloadScheme struct {
+	Subject    *PermissionSubjectScheme       `json:"subject,omitempty"`
+	Operations []*SpaceOperationPayloadScheme `json:"operations,omitempty"`
+}
+
+type SpaceOperationPayloadScheme struct {
+	Key    string `json:"key,omitempty"`
+	Target string `json:"target,omitempty"`
+	Access bool   `json:"access,omitempty"`
+}
+
+type SpacePermissionOperationScheme struct {
+	Operation string `json:"operation,omitempty"`
+	Target    string `json:"target,omitempty"`
+	Key       string `json:"key,omitempty"`
+}
+
+type SpacePermissionV2Scheme struct {
+	Subject   *PermissionSubjectScheme        `json:"subject,omitempty"`
+	Operation *SpacePermissionOperationScheme `json:"operation,omitempty"`
+}


Test patch:
diff --git a/confluence/spacePermission_test.go b/confluence/spacePermission_test.go
new file mode 100644
index 0000000..dbf050c
--- /dev/null
+++ b/confluence/spacePermission_test.go
@@ -0,0 +1,520 @@
+package confluence
+
+import (
+	"context"
+	"fmt"
+	"github.com/ctreminiom/go-atlassian/pkg/infra/models"
+	"github.com/stretchr/testify/assert"
+	"net/http"
+	"net/url"
+	"testing"
+)
+
+func TestSpacePermissionService_Add(t *testing.T) {
+
+	testCases := []struct {
+		name               string
+		spaceKey           string
+		payload            *models.SpacePermissionPayloadScheme
+		mockFile           string
+		wantHTTPMethod     string
+		endpoint           string
+		context            context.Context
+		wantHTTPCodeReturn int
+		wantErr            bool
+		expectedError      string
+	}{
+		{
+			name:     "when the parameters are correct",
+			spaceKey: "DUMMY",
+			payload: &models.SpacePermissionPayloadScheme{
+				Subject: &models.PermissionSubjectScheme{
+					Type:       "user",
+					Identifier: "account-id-sample",
+				},
+				Operation: &models.SpacePermissionOperationScheme{
+					Operation: "administer",
+					Target:    "page",
+				},
+			},
+			mockFile:           "./mocks/space-permission-v2.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            false,
+		},
+
+		{
+			name:     "when space key is not provided",
+			spaceKey: "",
+			payload: &models.SpacePermissionPayloadScheme{
+				Subject: &models.PermissionSubjectScheme{
+					Type:       "user",
+					Identifier: "account-id-sample",
+				},
+				Operation: &models.SpacePermissionOperationScheme{
+					Operation: "administer",
+					Target:    "page",
+				},
+			},
+			mockFile:           "./mocks/space-permission-v2.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "confluence: no space key set",
+		},
+
+		{
+			name:               "when the payload is not provided",
+			spaceKey:           "DUMMY",
+			payload:            nil,
+			mockFile:           "./mocks/space-permission-v2.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "failed to parse the interface pointer, please provide a valid one",
+		},
+
+		{
+			name:     "when the context is not provided",
+			spaceKey: "DUMMY",
+			payload: &models.SpacePermissionPayloadScheme{
+				Subject: &models.PermissionSubjectScheme{
+					Type:       "user",
+					Identifier: "account-id-sample",
+				},
+				Operation: &models.SpacePermissionOperationScheme{
+					Operation: "administer",
+					Target:    "page",
+				},
+			},
+			mockFile:           "./mocks/space-permission-v2.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission",
+			context:            nil,
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "request creation failed: net/http: nil Context",
+		},
+
+		{
+			name:     "when response body is empty",
+			spaceKey: "DUMMY",
+			payload: &models.SpacePermissionPayloadScheme{
+				Subject: &models.PermissionSubjectScheme{
+					Type:       "user",
+					Identifier: "account-id-sample",
+				},
+				Operation: &models.SpacePermissionOperationScheme{
+					Operation: "administer",
+					Target:    "page",
+				},
+			},
+			mockFile:           "./mocks/empty-json.json",
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission",
+			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)
+			}
+
+			implementation := &SpacePermissionService{client: mockClient}
+
+			gotResult, gotResponse, err := implementation.Add(testCase.context, testCase.spaceKey, testCase.payload)
+
+			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)
+			}
+		})
+	}
+}
+
+func TestSpacePermissionService_Bulk(t *testing.T) {
+
+	testCases := []struct {
+		name               string
+		spaceKey           string
+		payload            *models.SpacePermissionArrayPayloadScheme
+		mockFile           string
+		wantHTTPMethod     string
+		endpoint           string
+		context            context.Context
+		wantHTTPCodeReturn int
+		wantErr            bool
+		expectedError      string
+	}{
+		{
+			name:     "when the parameters are correct",
+			spaceKey: "DUMMY",
+			payload: &models.SpacePermissionArrayPayloadScheme{
+				Subject: &models.PermissionSubjectScheme{
+					Type:       "user",
+					Identifier: "account-id-sample",
+				},
+				Operations: []*models.SpaceOperationPayloadScheme{
+					{
+						Key:    "read",
+						Target: "space",
+						Access: true,
+					},
+					{
+						Key:    "delete",
+						Target: "space",
+						Access: false,
+					},
+				},
+			},
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission/custom-content",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            false,
+		},
+
+		{
+			name:     "when space key is not provided",
+			spaceKey: "",
+			payload: &models.SpacePermissionArrayPayloadScheme{
+				Subject: &models.PermissionSubjectScheme{
+					Type:       "user",
+					Identifier: "account-id-sample",
+				},
+				Operations: []*models.SpaceOperationPayloadScheme{
+					{
+						Key:    "read",
+						Target: "space",
+						Access: true,
+					},
+					{
+						Key:    "delete",
+						Target: "space",
+						Access: false,
+					},
+				},
+			},
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission/custom-content",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "confluence: no space key set",
+		},
+
+		{
+			name:               "when the payload is not provided",
+			spaceKey:           "DUMMY",
+			payload:            nil,
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission/custom-content",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "failed to parse the interface pointer, please provide a valid one",
+		},
+
+		{
+			name:     "when the context is not provided",
+			spaceKey: "DUMMY",
+			payload: &models.SpacePermissionArrayPayloadScheme{
+				Subject: &models.PermissionSubjectScheme{
+					Type:       "user",
+					Identifier: "account-id-sample",
+				},
+				Operations: []*models.SpaceOperationPayloadScheme{
+					{
+						Key:    "read",
+						Target: "space",
+						Access: true,
+					},
+					{
+						Key:    "delete",
+						Target: "space",
+						Access: false,
+					},
+				},
+			},
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission/custom-content",
+			wantHTTPCodeReturn: http.StatusOK,
+			context:            nil,
+			wantErr:            true,
+			expectedError:      "request creation failed: net/http: nil Context",
+		},
+
+		{
+			name:     "when the response code is invalid",
+			spaceKey: "DUMMY",
+			payload: &models.SpacePermissionArrayPayloadScheme{
+				Subject: &models.PermissionSubjectScheme{
+					Type:       "user",
+					Identifier: "account-id-sample",
+				},
+				Operations: []*models.SpaceOperationPayloadScheme{
+					{
+						Key:    "read",
+						Target: "space",
+						Access: true,
+					},
+					{
+						Key:    "delete",
+						Target: "space",
+						Access: false,
+					},
+				},
+			},
+			wantHTTPMethod:     http.MethodPost,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission/custom-content",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusBadRequest,
+			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)
+			}
+
+			implementation := &SpacePermissionService{client: mockClient}
+
+			gotResponse, err := implementation.Bulk(testCase.context, testCase.spaceKey, testCase.payload)
+
+			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)
+			}
+		})
+	}
+}
+
+func TestSpacePermissionService_Remove(t *testing.T) {
+
+	testCases := []struct {
+		name               string
+		spaceKey           string
+		permissionId       int
+		mockFile           string
+		wantHTTPMethod     string
+		endpoint           string
+		context            context.Context
+		wantHTTPCodeReturn int
+		wantErr            bool
+		expectedError      string
+	}{
+		{
+			name:               "when the parameters are correct",
+			spaceKey:           "DUMMY",
+			permissionId:       100001,
+			wantHTTPMethod:     http.MethodDelete,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission/100001",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            false,
+		},
+
+		{
+			name:               "when space key is not provided",
+			spaceKey:           "",
+			wantHTTPMethod:     http.MethodDelete,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission/100001",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusOK,
+			wantErr:            true,
+			expectedError:      "confluence: no space key set",
+		},
+
+		{
+			name:               "when the context is not provided",
+			spaceKey:           "DUMMY",
+			wantHTTPMethod:     http.MethodDelete,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission/100001",
+			wantHTTPCodeReturn: http.StatusOK,
+			context:            nil,
+			wantErr:            true,
+			expectedError:      "request creation failed: net/http: nil Context",
+		},
+
+		{
+			name:               "when the response code is invalid",
+			spaceKey:           "DUMMY",
+			wantHTTPMethod:     http.MethodDelete,
+			endpoint:           "/wiki/rest/api/space/DUMMY/permission/100001",
+			context:            context.Background(),
+			wantHTTPCodeReturn: http.StatusBadRequest,
+			wantErr:            true,
+			expectedError:      "invalid character 'R' looking for beginning of value",
+		},
+	}
+	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)
+			}
+
+			implementation := &SpacePermissionService{client: mockClient}
+
+			gotResponse, err := implementation.Remove(testCase.context, testCase.spaceKey, testCase.permissionId)
+
+			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)
+			}
+		})
+	}
+}