From 458bc385e76f4216b64ac3ebc2d2f6748f3b1357 Mon Sep 17 00:00:00 2001 From: Carlos Treminio Date: Mon, 7 Mar 2022 17:02:01 -0600 Subject: [PATCH 1/3] :sparkles: Added the Space Permission service. 1. Added the ability to manipulates the space permissions. 2. Added the Add, Bulk and Removes methods. --- confluence/confluence.go | 6 +- confluence/mocks/space-permission-v2.json | 12 + confluence/space.go | 3 +- confluence/spacePermission.go | 95 ++++ confluence/spacePermission_test.go | 520 ++++++++++++++++++ .../models/confluence_space_permission.go | 27 + 6 files changed, 661 insertions(+), 2 deletions(-) create mode 100644 confluence/mocks/space-permission-v2.json create mode 100644 confluence/spacePermission.go create mode 100644 confluence/spacePermission_test.go create mode 100644 pkg/infra/models/confluence_space_permission.go diff --git a/confluence/confluence.go b/confluence/confluence.go index 69bcae6c..22bf045d 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 00000000..77d86de4 --- /dev/null +++ b/confluence/mocks/space-permission-v2.json @@ -0,0 +1,12 @@ +{ + "id": 2154, + "subject": { + "type": "user", + "identifier": "" + }, + "operation": { + "key": "administer", + "target": "page" + }, + "_links": {} +} \ No newline at end of file diff --git a/confluence/space.go b/confluence/space.go index fa38beba..a8f45ebc 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 00000000..8a0f2867 --- /dev/null +++ b/confluence/spacePermission.go @@ -0,0 +1,95 @@ +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. +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. +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. +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/confluence/spacePermission_test.go b/confluence/spacePermission_test.go new file mode 100644 index 00000000..dbf050c0 --- /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) + } + }) + } +} diff --git a/pkg/infra/models/confluence_space_permission.go b/pkg/infra/models/confluence_space_permission.go new file mode 100644 index 00000000..aae5b12c --- /dev/null +++ b/pkg/infra/models/confluence_space_permission.go @@ -0,0 +1,27 @@ +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"` +} + +type SpacePermissionV2Scheme struct { + Subject *PermissionSubjectScheme `json:"subject,omitempty"` + Operation *SpacePermissionOperationScheme `json:"operation,omitempty"` +} From d46b68127b225706c7d028d9c2196e730a5f783e Mon Sep 17 00:00:00 2001 From: Carlos Treminio Date: Mon, 7 Mar 2022 18:08:27 -0600 Subject: [PATCH 2/3] :pencil2: Fixed the space permission payload --- pkg/infra/models/confluence_content_permission.go | 4 ++-- pkg/infra/models/confluence_space_permission.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pkg/infra/models/confluence_content_permission.go b/pkg/infra/models/confluence_content_permission.go index ce4ee4b7..6482d4f8 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 index aae5b12c..bfbae532 100644 --- a/pkg/infra/models/confluence_space_permission.go +++ b/pkg/infra/models/confluence_space_permission.go @@ -19,6 +19,7 @@ type SpaceOperationPayloadScheme struct { type SpacePermissionOperationScheme struct { Operation string `json:"operation,omitempty"` Target string `json:"target,omitempty"` + Key string `json:"key,omitempty"` } type SpacePermissionV2Scheme struct { From d7a5bd3b73f3ad73c2aa333cc36fc9de517930d3 Mon Sep 17 00:00:00 2001 From: Carlos Treminio Date: Mon, 7 Mar 2022 18:39:46 -0600 Subject: [PATCH 3/3] :memo: Added the Service documentation 1. Added the method documentation on the Space Permission service. --- confluence/spacePermission.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/confluence/spacePermission.go b/confluence/spacePermission.go index 8a0f2867..b860cf4f 100644 --- a/confluence/spacePermission.go +++ b/confluence/spacePermission.go @@ -10,6 +10,7 @@ import ( 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) { @@ -42,6 +43,7 @@ func (s *SpacePermissionService) Add(ctx context.Context, spaceKey string, paylo // 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 { @@ -73,6 +75,7 @@ func (s *SpacePermissionService) Bulk(ctx context.Context, spaceKey string, payl // 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 {