From b8ed049487d35a66188dae1937b6c6fe468b0cc9 Mon Sep 17 00:00:00 2001 From: Florian Schade Date: Mon, 22 Apr 2024 17:24:45 +0200 Subject: [PATCH] Enhancement: Ability to Change Share Item Visibility in Graph API (#8750) * Enhancement: Ability to Change Share Item Visibility in Graph API Introduce the `PATCH /graph/v1beta1/drives/{driveID}/items/{itemID}` Graph API endpoint which allows updating individual Drive Items. * fix: failing tests * fix: consider siblings when updating shares * fix: reduce sharing service provider interface --- .../unreleased/graph-item-visibility-api.md | 11 + services/graph/.mockery.yaml | 1 + services/graph/mocks/base_graph_provider.go | 99 ++ .../graph/mocks/drives_drive_item_provider.go | 171 ++- .../service/v0/api_driveitem_permissions.go | 1 + .../pkg/service/v0/api_drives_drive_item.go | 434 ++++-- .../service/v0/api_drives_drive_item_test.go | 1305 +++++++++-------- services/graph/pkg/service/v0/base.go | 14 + services/graph/pkg/service/v0/service.go | 89 +- services/graph/pkg/service/v0/utils.go | 16 +- 10 files changed, 1297 insertions(+), 844 deletions(-) create mode 100644 changelog/unreleased/graph-item-visibility-api.md create mode 100644 services/graph/mocks/base_graph_provider.go diff --git a/changelog/unreleased/graph-item-visibility-api.md b/changelog/unreleased/graph-item-visibility-api.md new file mode 100644 index 0000000000..632083da13 --- /dev/null +++ b/changelog/unreleased/graph-item-visibility-api.md @@ -0,0 +1,11 @@ +Enhancement: Ability to Change Share Item Visibility in Graph API + +Introduce the `PATCH /graph/v1beta1/drives/{driveID}/items/{itemID}` Graph API endpoint which allows updating individual Drive Items. + +At the moment, only the share visibility is considered changeable, but in the future, more properties can be added to this endpoint. + +This enhancement is needed for the user interface, allowing specific shares to be hidden or unhidden as needed, +thereby improving the user experience. + +https://github.com/owncloud/ocis/pull/8750 +https://github.com/owncloud/ocis/issues/8654 diff --git a/services/graph/.mockery.yaml b/services/graph/.mockery.yaml index 86bb59dd8e..36e5b4b870 100644 --- a/services/graph/.mockery.yaml +++ b/services/graph/.mockery.yaml @@ -7,6 +7,7 @@ packages: config: dir: "mocks" interfaces: + BaseGraphProvider: DrivesDriveItemProvider: DriveItemPermissionsProvider: HTTPClient: diff --git a/services/graph/mocks/base_graph_provider.go b/services/graph/mocks/base_graph_provider.go new file mode 100644 index 0000000000..af846055d6 --- /dev/null +++ b/services/graph/mocks/base_graph_provider.go @@ -0,0 +1,99 @@ +// Code generated by mockery v2.40.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + + libregraph "github.com/owncloud/libre-graph-api-go" + + mock "github.com/stretchr/testify/mock" +) + +// BaseGraphProvider is an autogenerated mock type for the BaseGraphProvider type +type BaseGraphProvider struct { + mock.Mock +} + +type BaseGraphProvider_Expecter struct { + mock *mock.Mock +} + +func (_m *BaseGraphProvider) EXPECT() *BaseGraphProvider_Expecter { + return &BaseGraphProvider_Expecter{mock: &_m.Mock} +} + +// CS3ReceivedSharesToDriveItems provides a mock function with given fields: ctx, receivedShares +func (_m *BaseGraphProvider) CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaborationv1beta1.ReceivedShare) ([]libregraph.DriveItem, error) { + ret := _m.Called(ctx, receivedShares) + + if len(ret) == 0 { + panic("no return value specified for CS3ReceivedSharesToDriveItems") + } + + var r0 []libregraph.DriveItem + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []*collaborationv1beta1.ReceivedShare) ([]libregraph.DriveItem, error)); ok { + return rf(ctx, receivedShares) + } + if rf, ok := ret.Get(0).(func(context.Context, []*collaborationv1beta1.ReceivedShare) []libregraph.DriveItem); ok { + r0 = rf(ctx, receivedShares) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]libregraph.DriveItem) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []*collaborationv1beta1.ReceivedShare) error); ok { + r1 = rf(ctx, receivedShares) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CS3ReceivedSharesToDriveItems' +type BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call struct { + *mock.Call +} + +// CS3ReceivedSharesToDriveItems is a helper method to define mock.On call +// - ctx context.Context +// - receivedShares []*collaborationv1beta1.ReceivedShare +func (_e *BaseGraphProvider_Expecter) CS3ReceivedSharesToDriveItems(ctx interface{}, receivedShares interface{}) *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call { + return &BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call{Call: _e.mock.On("CS3ReceivedSharesToDriveItems", ctx, receivedShares)} +} + +func (_c *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call) Run(run func(ctx context.Context, receivedShares []*collaborationv1beta1.ReceivedShare)) *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]*collaborationv1beta1.ReceivedShare)) + }) + return _c +} + +func (_c *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call) Return(_a0 []libregraph.DriveItem, _a1 error) *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call) RunAndReturn(run func(context.Context, []*collaborationv1beta1.ReceivedShare) ([]libregraph.DriveItem, error)) *BaseGraphProvider_CS3ReceivedSharesToDriveItems_Call { + _c.Call.Return(run) + return _c +} + +// NewBaseGraphProvider creates a new instance of BaseGraphProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewBaseGraphProvider(t interface { + mock.TestingT + Cleanup(func()) +}) *BaseGraphProvider { + mock := &BaseGraphProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/services/graph/mocks/drives_drive_item_provider.go b/services/graph/mocks/drives_drive_item_provider.go index 1442ef3596..62c3bb7106 100644 --- a/services/graph/mocks/drives_drive_item_provider.go +++ b/services/graph/mocks/drives_drive_item_provider.go @@ -5,10 +5,13 @@ package mocks import ( context "context" - libregraph "github.com/owncloud/libre-graph-api-go" + collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" + mock "github.com/stretchr/testify/mock" providerv1beta1 "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + + svc "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0" ) // DrivesDriveItemProvider is an autogenerated mock type for the DrivesDriveItemProvider type @@ -24,26 +27,88 @@ func (_m *DrivesDriveItemProvider) EXPECT() *DrivesDriveItemProvider_Expecter { return &DrivesDriveItemProvider_Expecter{mock: &_m.Mock} } +// GetShareAndSiblings provides a mock function with given fields: ctx, shareID, filters +func (_m *DrivesDriveItemProvider) GetShareAndSiblings(ctx context.Context, shareID *collaborationv1beta1.ShareId, filters []*collaborationv1beta1.Filter) ([]*collaborationv1beta1.ReceivedShare, error) { + ret := _m.Called(ctx, shareID, filters) + + if len(ret) == 0 { + panic("no return value specified for GetShareAndSiblings") + } + + var r0 []*collaborationv1beta1.ReceivedShare + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.ShareId, []*collaborationv1beta1.Filter) ([]*collaborationv1beta1.ReceivedShare, error)); ok { + return rf(ctx, shareID, filters) + } + if rf, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.ShareId, []*collaborationv1beta1.Filter) []*collaborationv1beta1.ReceivedShare); ok { + r0 = rf(ctx, shareID, filters) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*collaborationv1beta1.ReceivedShare) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *collaborationv1beta1.ShareId, []*collaborationv1beta1.Filter) error); ok { + r1 = rf(ctx, shareID, filters) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DrivesDriveItemProvider_GetShareAndSiblings_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetShareAndSiblings' +type DrivesDriveItemProvider_GetShareAndSiblings_Call struct { + *mock.Call +} + +// GetShareAndSiblings is a helper method to define mock.On call +// - ctx context.Context +// - shareID *collaborationv1beta1.ShareId +// - filters []*collaborationv1beta1.Filter +func (_e *DrivesDriveItemProvider_Expecter) GetShareAndSiblings(ctx interface{}, shareID interface{}, filters interface{}) *DrivesDriveItemProvider_GetShareAndSiblings_Call { + return &DrivesDriveItemProvider_GetShareAndSiblings_Call{Call: _e.mock.On("GetShareAndSiblings", ctx, shareID, filters)} +} + +func (_c *DrivesDriveItemProvider_GetShareAndSiblings_Call) Run(run func(ctx context.Context, shareID *collaborationv1beta1.ShareId, filters []*collaborationv1beta1.Filter)) *DrivesDriveItemProvider_GetShareAndSiblings_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*collaborationv1beta1.ShareId), args[2].([]*collaborationv1beta1.Filter)) + }) + return _c +} + +func (_c *DrivesDriveItemProvider_GetShareAndSiblings_Call) Return(_a0 []*collaborationv1beta1.ReceivedShare, _a1 error) *DrivesDriveItemProvider_GetShareAndSiblings_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DrivesDriveItemProvider_GetShareAndSiblings_Call) RunAndReturn(run func(context.Context, *collaborationv1beta1.ShareId, []*collaborationv1beta1.Filter) ([]*collaborationv1beta1.ReceivedShare, error)) *DrivesDriveItemProvider_GetShareAndSiblings_Call { + _c.Call.Return(run) + return _c +} + // MountShare provides a mock function with given fields: ctx, resourceID, name -func (_m *DrivesDriveItemProvider) MountShare(ctx context.Context, resourceID providerv1beta1.ResourceId, name string) (libregraph.DriveItem, error) { +func (_m *DrivesDriveItemProvider) MountShare(ctx context.Context, resourceID *providerv1beta1.ResourceId, name string) ([]*collaborationv1beta1.ReceivedShare, error) { ret := _m.Called(ctx, resourceID, name) if len(ret) == 0 { panic("no return value specified for MountShare") } - var r0 libregraph.DriveItem + var r0 []*collaborationv1beta1.ReceivedShare var r1 error - if rf, ok := ret.Get(0).(func(context.Context, providerv1beta1.ResourceId, string) (libregraph.DriveItem, error)); ok { + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string) ([]*collaborationv1beta1.ReceivedShare, error)); ok { return rf(ctx, resourceID, name) } - if rf, ok := ret.Get(0).(func(context.Context, providerv1beta1.ResourceId, string) libregraph.DriveItem); ok { + if rf, ok := ret.Get(0).(func(context.Context, *providerv1beta1.ResourceId, string) []*collaborationv1beta1.ReceivedShare); ok { r0 = rf(ctx, resourceID, name) } else { - r0 = ret.Get(0).(libregraph.DriveItem) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*collaborationv1beta1.ReceivedShare) + } } - if rf, ok := ret.Get(1).(func(context.Context, providerv1beta1.ResourceId, string) error); ok { + if rf, ok := ret.Get(1).(func(context.Context, *providerv1beta1.ResourceId, string) error); ok { r1 = rf(ctx, resourceID, name) } else { r1 = ret.Error(1) @@ -59,40 +124,40 @@ type DrivesDriveItemProvider_MountShare_Call struct { // MountShare is a helper method to define mock.On call // - ctx context.Context -// - resourceID providerv1beta1.ResourceId +// - resourceID *providerv1beta1.ResourceId // - name string func (_e *DrivesDriveItemProvider_Expecter) MountShare(ctx interface{}, resourceID interface{}, name interface{}) *DrivesDriveItemProvider_MountShare_Call { return &DrivesDriveItemProvider_MountShare_Call{Call: _e.mock.On("MountShare", ctx, resourceID, name)} } -func (_c *DrivesDriveItemProvider_MountShare_Call) Run(run func(ctx context.Context, resourceID providerv1beta1.ResourceId, name string)) *DrivesDriveItemProvider_MountShare_Call { +func (_c *DrivesDriveItemProvider_MountShare_Call) Run(run func(ctx context.Context, resourceID *providerv1beta1.ResourceId, name string)) *DrivesDriveItemProvider_MountShare_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(providerv1beta1.ResourceId), args[2].(string)) + run(args[0].(context.Context), args[1].(*providerv1beta1.ResourceId), args[2].(string)) }) return _c } -func (_c *DrivesDriveItemProvider_MountShare_Call) Return(_a0 libregraph.DriveItem, _a1 error) *DrivesDriveItemProvider_MountShare_Call { +func (_c *DrivesDriveItemProvider_MountShare_Call) Return(_a0 []*collaborationv1beta1.ReceivedShare, _a1 error) *DrivesDriveItemProvider_MountShare_Call { _c.Call.Return(_a0, _a1) return _c } -func (_c *DrivesDriveItemProvider_MountShare_Call) RunAndReturn(run func(context.Context, providerv1beta1.ResourceId, string) (libregraph.DriveItem, error)) *DrivesDriveItemProvider_MountShare_Call { +func (_c *DrivesDriveItemProvider_MountShare_Call) RunAndReturn(run func(context.Context, *providerv1beta1.ResourceId, string) ([]*collaborationv1beta1.ReceivedShare, error)) *DrivesDriveItemProvider_MountShare_Call { _c.Call.Return(run) return _c } -// UnmountShare provides a mock function with given fields: ctx, resourceID -func (_m *DrivesDriveItemProvider) UnmountShare(ctx context.Context, resourceID providerv1beta1.ResourceId) error { - ret := _m.Called(ctx, resourceID) +// UnmountShare provides a mock function with given fields: ctx, shareID +func (_m *DrivesDriveItemProvider) UnmountShare(ctx context.Context, shareID *collaborationv1beta1.ShareId) error { + ret := _m.Called(ctx, shareID) if len(ret) == 0 { panic("no return value specified for UnmountShare") } var r0 error - if rf, ok := ret.Get(0).(func(context.Context, providerv1beta1.ResourceId) error); ok { - r0 = rf(ctx, resourceID) + if rf, ok := ret.Get(0).(func(context.Context, *collaborationv1beta1.ShareId) error); ok { + r0 = rf(ctx, shareID) } else { r0 = ret.Error(0) } @@ -107,14 +172,14 @@ type DrivesDriveItemProvider_UnmountShare_Call struct { // UnmountShare is a helper method to define mock.On call // - ctx context.Context -// - resourceID providerv1beta1.ResourceId -func (_e *DrivesDriveItemProvider_Expecter) UnmountShare(ctx interface{}, resourceID interface{}) *DrivesDriveItemProvider_UnmountShare_Call { - return &DrivesDriveItemProvider_UnmountShare_Call{Call: _e.mock.On("UnmountShare", ctx, resourceID)} +// - shareID *collaborationv1beta1.ShareId +func (_e *DrivesDriveItemProvider_Expecter) UnmountShare(ctx interface{}, shareID interface{}) *DrivesDriveItemProvider_UnmountShare_Call { + return &DrivesDriveItemProvider_UnmountShare_Call{Call: _e.mock.On("UnmountShare", ctx, shareID)} } -func (_c *DrivesDriveItemProvider_UnmountShare_Call) Run(run func(ctx context.Context, resourceID providerv1beta1.ResourceId)) *DrivesDriveItemProvider_UnmountShare_Call { +func (_c *DrivesDriveItemProvider_UnmountShare_Call) Run(run func(ctx context.Context, shareID *collaborationv1beta1.ShareId)) *DrivesDriveItemProvider_UnmountShare_Call { _c.Call.Run(func(args mock.Arguments) { - run(args[0].(context.Context), args[1].(providerv1beta1.ResourceId)) + run(args[0].(context.Context), args[1].(*collaborationv1beta1.ShareId)) }) return _c } @@ -124,7 +189,67 @@ func (_c *DrivesDriveItemProvider_UnmountShare_Call) Return(_a0 error) *DrivesDr return _c } -func (_c *DrivesDriveItemProvider_UnmountShare_Call) RunAndReturn(run func(context.Context, providerv1beta1.ResourceId) error) *DrivesDriveItemProvider_UnmountShare_Call { +func (_c *DrivesDriveItemProvider_UnmountShare_Call) RunAndReturn(run func(context.Context, *collaborationv1beta1.ShareId) error) *DrivesDriveItemProvider_UnmountShare_Call { + _c.Call.Return(run) + return _c +} + +// UpdateShares provides a mock function with given fields: ctx, shares, updater +func (_m *DrivesDriveItemProvider) UpdateShares(ctx context.Context, shares []*collaborationv1beta1.ReceivedShare, updater svc.UpdateShareClosure) ([]*collaborationv1beta1.ReceivedShare, error) { + ret := _m.Called(ctx, shares, updater) + + if len(ret) == 0 { + panic("no return value specified for UpdateShares") + } + + var r0 []*collaborationv1beta1.ReceivedShare + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []*collaborationv1beta1.ReceivedShare, svc.UpdateShareClosure) ([]*collaborationv1beta1.ReceivedShare, error)); ok { + return rf(ctx, shares, updater) + } + if rf, ok := ret.Get(0).(func(context.Context, []*collaborationv1beta1.ReceivedShare, svc.UpdateShareClosure) []*collaborationv1beta1.ReceivedShare); ok { + r0 = rf(ctx, shares, updater) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*collaborationv1beta1.ReceivedShare) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []*collaborationv1beta1.ReceivedShare, svc.UpdateShareClosure) error); ok { + r1 = rf(ctx, shares, updater) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DrivesDriveItemProvider_UpdateShares_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateShares' +type DrivesDriveItemProvider_UpdateShares_Call struct { + *mock.Call +} + +// UpdateShares is a helper method to define mock.On call +// - ctx context.Context +// - shares []*collaborationv1beta1.ReceivedShare +// - updater svc.UpdateShareClosure +func (_e *DrivesDriveItemProvider_Expecter) UpdateShares(ctx interface{}, shares interface{}, updater interface{}) *DrivesDriveItemProvider_UpdateShares_Call { + return &DrivesDriveItemProvider_UpdateShares_Call{Call: _e.mock.On("UpdateShares", ctx, shares, updater)} +} + +func (_c *DrivesDriveItemProvider_UpdateShares_Call) Run(run func(ctx context.Context, shares []*collaborationv1beta1.ReceivedShare, updater svc.UpdateShareClosure)) *DrivesDriveItemProvider_UpdateShares_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]*collaborationv1beta1.ReceivedShare), args[2].(svc.UpdateShareClosure)) + }) + return _c +} + +func (_c *DrivesDriveItemProvider_UpdateShares_Call) Return(_a0 []*collaborationv1beta1.ReceivedShare, _a1 error) *DrivesDriveItemProvider_UpdateShares_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *DrivesDriveItemProvider_UpdateShares_Call) RunAndReturn(run func(context.Context, []*collaborationv1beta1.ReceivedShare, svc.UpdateShareClosure) ([]*collaborationv1beta1.ReceivedShare, error)) *DrivesDriveItemProvider_UpdateShares_Call { _c.Call.Return(run) return _c } diff --git a/services/graph/pkg/service/v0/api_driveitem_permissions.go b/services/graph/pkg/service/v0/api_driveitem_permissions.go index df98081d9a..ee7916850d 100644 --- a/services/graph/pkg/service/v0/api_driveitem_permissions.go +++ b/services/graph/pkg/service/v0/api_driveitem_permissions.go @@ -19,6 +19,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/go-chi/render" libregraph "github.com/owncloud/libre-graph-api-go" + "github.com/owncloud/ocis/v2/ocis-pkg/conversions" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/graph/pkg/config" diff --git a/services/graph/pkg/service/v0/api_drives_drive_item.go b/services/graph/pkg/service/v0/api_drives_drive_item.go index c1167bfc5e..f49786cd54 100644 --- a/services/graph/pkg/service/v0/api_drives_drive_item.go +++ b/services/graph/pkg/service/v0/api_drives_drive_item.go @@ -18,223 +18,276 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" - "github.com/owncloud/ocis/v2/services/graph/pkg/identity" ) const ( _fieldMaskPathState = "state" _fieldMaskPathMountPoint = "mount_point" + _fieldMaskPathHidden = "hidden" ) -// DrivesDriveItemProvider is the interface that needs to be implemented by the individual space service -type DrivesDriveItemProvider interface { - MountShare(ctx context.Context, resourceID storageprovider.ResourceId, name string) (libregraph.DriveItem, error) - UnmountShare(ctx context.Context, resourceID storageprovider.ResourceId) error -} +var ( + // ErrNoUpdates is returned when no updates are provided + ErrNoUpdates = errors.New("no updates") + + // ErrNoUpdater is returned when no updater is provided + ErrNoUpdater = errors.New("no updater") + + // ErrNoShares is returned when no shares are found + ErrNoShares = errors.New("no shares found") + + // ErrAbsoluteNamePath is returned when the name is an absolute path + ErrAbsoluteNamePath = errors.New("name cannot be an absolute path") + + // ErrNotAShareJail is returned when the driveID does not belong to a share jail + ErrNotAShareJail = errors.New("id does not belong to a share jail") + + // ErrInvalidDriveIDOrItemID is returned when the driveID or itemID is invalid + ErrInvalidDriveIDOrItemID = errors.New("invalid driveID or itemID") + + // ErrInvalidRequestBody is returned when the request body is invalid + ErrInvalidRequestBody = errors.New("invalid request body") + + // ErrUnmountShare is returned when unmounting a share fails + ErrUnmountShare = errors.New("unmounting share failed") + + // ErrMountShare is returned when mounting a share fails + ErrMountShare = errors.New("mounting share failed") + + // ErrGetShareAndSiblings is returned when getting the share and siblings fails + ErrGetShareAndSiblings = errors.New("failed to get share and siblings") + + // ErrUpdateShares is returned when updating shares fails + ErrUpdateShares = errors.New("failed to update share") + + // ErrInvalidID is returned when the id is invalid + ErrInvalidID = errors.New("invalid id") + + // ErrDriveItemConversion is returned when converting to drive items fails + ErrDriveItemConversion = errors.New("converting to drive items failed") +) + +type ( + // UpdateShareClosure is a closure that injects required updates into the update request + UpdateShareClosure func(share *collaboration.ReceivedShare, request *collaboration.UpdateReceivedShareRequest) + + // DrivesDriveItemProvider is the interface that needs to be implemented by the individual space service + DrivesDriveItemProvider interface { + // MountShare mounts a share + MountShare(ctx context.Context, resourceID *storageprovider.ResourceId, name string) ([]*collaboration.ReceivedShare, error) + + // UnmountShare unmounts a share + UnmountShare(ctx context.Context, shareID *collaboration.ShareId) error + + // UpdateShares updates multiple shares + UpdateShares(ctx context.Context, shares []*collaboration.ReceivedShare, updater UpdateShareClosure) ([]*collaboration.ReceivedShare, error) + + // GetShareAndSiblings returns the share and all its siblings + GetShareAndSiblings(ctx context.Context, shareID *collaboration.ShareId, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) + } +) // DrivesDriveItemService contains the production business logic for everything that relates to drives type DrivesDriveItemService struct { logger log.Logger gatewaySelector pool.Selectable[gateway.GatewayAPIClient] - identityCache identity.IdentityCache } // NewDrivesDriveItemService creates a new DrivesDriveItemService -func NewDrivesDriveItemService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient], identityCache identity.IdentityCache) (DrivesDriveItemService, error) { +func NewDrivesDriveItemService(logger log.Logger, gatewaySelector pool.Selectable[gateway.GatewayAPIClient]) (DrivesDriveItemService, error) { return DrivesDriveItemService{ logger: log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemService").Logger()}, gatewaySelector: gatewaySelector, - identityCache: identityCache, }, nil } -// UnmountShare unmounts a share from the share-jail -func (s DrivesDriveItemService) UnmountShare(ctx context.Context, resourceID storageprovider.ResourceId) error { +// GetShareAndSiblings returns the share and all its siblings +func (s DrivesDriveItemService) GetShareAndSiblings(ctx context.Context, shareID *collaboration.ShareId, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) { gatewayClient, err := s.gatewaySelector.Next() if err != nil { - return err + return nil, err } - // This is a bit of a hack. We should not rely on a specific format of the item id. - // But currently there is no other way to get the ShareID. - shareId := resourceID.GetOpaqueId() - // Now, find out the resourceID of the shared resource getReceivedShareResponse, err := gatewayClient.GetReceivedShare(ctx, &collaboration.GetReceivedShareRequest{ Ref: &collaboration.ShareReference{ Spec: &collaboration.ShareReference_Id{ - Id: &collaboration.ShareId{ - OpaqueId: shareId, - }, + Id: shareID, }, }, }, ) if err := errorcode.FromCS3Status(getReceivedShareResponse.GetStatus(), err); err != nil { - s.logger.Debug().Err(err). - Str("shareid", shareId). - Msg("failed to read share") - return err + return nil, err } + return s.GetSharesByResourceID(ctx, getReceivedShareResponse.GetShare().GetShare().GetResourceId(), filters) +} + +// GetSharesByResourceID returns all shares for a given resourceID +func (s DrivesDriveItemService) GetSharesByResourceID(ctx context.Context, resourceID *storageprovider.ResourceId, filters []*collaboration.Filter) ([]*collaboration.ReceivedShare, error) { // Find all accepted shares for this resource - gatewayClient, err = s.gatewaySelector.Next() + gatewayClient, err := s.gatewaySelector.Next() if err != nil { - return err + return nil, err } + receivedSharesResponse, err := gatewayClient.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{ - Filters: []*collaboration.Filter{ - { - Type: collaboration.Filter_TYPE_STATE, - Term: &collaboration.Filter_State{ - State: collaboration.ShareState_SHARE_STATE_ACCEPTED, - }, - }, + Filters: append([]*collaboration.Filter{ { Type: collaboration.Filter_TYPE_RESOURCE_ID, Term: &collaboration.Filter_ResourceId{ - ResourceId: getReceivedShareResponse.GetShare().GetShare().GetResourceId(), + ResourceId: resourceID, }, }, - }, + }, filters...), }) - if err != nil { - return err - } - if len(receivedSharesResponse.GetShares()) == 0 { - return errorcode.New(errorcode.InvalidRequest, "invalid itemID") + switch { + case err != nil: + return nil, err + case len(receivedSharesResponse.GetShares()) == 0: + return nil, ErrNoShares + default: + return receivedSharesResponse.GetShares(), errorcode.FromCS3Status(receivedSharesResponse.GetStatus(), err) } +} - var errs []error +// UpdateShares updates multiple shares; +// it could happen that some shares are updated and some are not, +// this will return a list of updated shares and a list of errors; +// there is no guarantee that all updates are successful +func (s DrivesDriveItemService) UpdateShares(ctx context.Context, shares []*collaboration.ReceivedShare, updater UpdateShareClosure) ([]*collaboration.ReceivedShare, error) { + errs := make([]error, 0, len(shares)) + updatedShares := make([]*collaboration.ReceivedShare, 0, len(shares)) - // Reject all the shares for this resource - for _, receivedShare := range receivedSharesResponse.GetShares() { - receivedShare.State = collaboration.ShareState_SHARE_STATE_REJECTED - - updateReceivedShareRequest := &collaboration.UpdateReceivedShareRequest{ - Share: receivedShare, - UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{_fieldMaskPathState}}, - } - - _, err := gatewayClient.UpdateReceivedShare(ctx, updateReceivedShareRequest) + for _, share := range shares { + updatedShare, err := s.UpdateShare( + ctx, + share, + updater, + ) if err != nil { errs = append(errs, err) continue } + + updatedShares = append(updatedShares, updatedShare) } - return errors.Join(errs...) + return updatedShares, errors.Join(errs...) } -// MountShare mounts a share -func (s DrivesDriveItemService) MountShare(ctx context.Context, resourceID storageprovider.ResourceId, name string) (libregraph.DriveItem, error) { - if filepath.IsAbs(name) { - return libregraph.DriveItem{}, errorcode.New(errorcode.InvalidRequest, "name cannot be an absolute path") - } - if name != "" { - name = filepath.Clean(name) - } - +// UpdateShare updates a single share +func (s DrivesDriveItemService) UpdateShare(ctx context.Context, share *collaboration.ReceivedShare, updater UpdateShareClosure) (*collaboration.ReceivedShare, error) { gatewayClient, err := s.gatewaySelector.Next() if err != nil { - return libregraph.DriveItem{}, err + return nil, err } - // Get all shares that the user has received for this resource. There might be multiple - receivedSharesResponse, err := gatewayClient.ListReceivedShares(ctx, &collaboration.ListReceivedSharesRequest{ - Filters: []*collaboration.Filter{ - { - Type: collaboration.Filter_TYPE_STATE, - Term: &collaboration.Filter_State{ - State: collaboration.ShareState_SHARE_STATE_PENDING, - }, + updateReceivedShareRequest := &collaboration.UpdateReceivedShareRequest{ + Share: &collaboration.ReceivedShare{ + Share: &collaboration.Share{ + Id: share.GetShare().GetId(), }, - { - Type: collaboration.Filter_TYPE_STATE, - Term: &collaboration.Filter_State{ - State: collaboration.ShareState_SHARE_STATE_REJECTED, - }, - }, - { - Type: collaboration.Filter_TYPE_RESOURCE_ID, - Term: &collaboration.Filter_ResourceId{ - ResourceId: &resourceID, - }, + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}}, + } + + switch updater { + case nil: + return nil, ErrNoUpdater + default: + updater(share, updateReceivedShareRequest) + } + + if len(updateReceivedShareRequest.GetUpdateMask().GetPaths()) == 0 { + return nil, ErrNoUpdates + } + + updateReceivedShareResponse, err := gatewayClient.UpdateReceivedShare(ctx, updateReceivedShareRequest) + return updateReceivedShareResponse.GetShare(), errorcode.FromCS3Status(updateReceivedShareResponse.GetStatus(), err) +} + +// UnmountShare unmounts a share +func (s DrivesDriveItemService) UnmountShare(ctx context.Context, shareID *collaboration.ShareId) error { + availableShares, err := s.GetShareAndSiblings(ctx, shareID, []*collaboration.Filter{ + { + Type: collaboration.Filter_TYPE_STATE, + Term: &collaboration.Filter_State{ + State: collaboration.ShareState_SHARE_STATE_ACCEPTED, }, }, }) if err != nil { - return libregraph.DriveItem{}, err - } - if len(receivedSharesResponse.GetShares()) == 0 { - return libregraph.DriveItem{}, errorcode.New(errorcode.InvalidRequest, "invalid itemID") + return err } - var errs []error + _, err = s.UpdateShares(ctx, availableShares, func(_ *collaboration.ReceivedShare, request *collaboration.UpdateReceivedShareRequest) { + request.Share.State = collaboration.ShareState_SHARE_STATE_REJECTED + request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathState) + }) - var acceptedShares []*collaboration.ReceivedShare + return err +} - // try to accept all the received shares for this resource. So that the stat is in sync across all - // shares +// MountShare mounts a share, there is no guarantee that all siblings will be mounted +// in some rare cases it could happen that none of the siblings could be mounted, +// then the error will be returned +func (s DrivesDriveItemService) MountShare(ctx context.Context, resourceID *storageprovider.ResourceId, name string) ([]*collaboration.ReceivedShare, error) { + if filepath.IsAbs(name) { + return nil, ErrAbsoluteNamePath + } - for _, receivedShare := range receivedSharesResponse.GetShares() { - updateMask := &fieldmaskpb.FieldMask{Paths: []string{_fieldMaskPathState}} - receivedShare.State = collaboration.ShareState_SHARE_STATE_ACCEPTED + if name != "" { + name = filepath.Clean(name) + } + + availableShares, err := s.GetSharesByResourceID(ctx, resourceID, []*collaboration.Filter{ + { + Type: collaboration.Filter_TYPE_STATE, + Term: &collaboration.Filter_State{ + State: collaboration.ShareState_SHARE_STATE_PENDING, + }, + }, + { + Type: collaboration.Filter_TYPE_STATE, + Term: &collaboration.Filter_State{ + State: collaboration.ShareState_SHARE_STATE_REJECTED, + }, + }, + }) + if err != nil { + return nil, err + } + + updatedShares, err := s.UpdateShares(ctx, availableShares, func(share *collaboration.ReceivedShare, request *collaboration.UpdateReceivedShareRequest) { + request.Share.State = collaboration.ShareState_SHARE_STATE_ACCEPTED + request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathState) // only update if mountPoint name is not empty and the path has changed if name != "" { - mountPoint := receivedShare.GetMountPoint() + mountPoint := share.GetMountPoint() if mountPoint == nil { mountPoint = &storageprovider.Reference{} } if filepath.Clean(mountPoint.GetPath()) != name { mountPoint.Path = name - receivedShare.MountPoint = mountPoint - updateMask.Paths = append(updateMask.Paths, _fieldMaskPathMountPoint) + request.Share.MountPoint = mountPoint + request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathMountPoint) } } + }) - updateReceivedShareRequest := &collaboration.UpdateReceivedShareRequest{ - Share: receivedShare, - UpdateMask: updateMask, - } - - gatewayClient, err = s.gatewaySelector.Next() - if err != nil { - return libregraph.DriveItem{}, err - } - updateReceivedShareResponse, err := gatewayClient.UpdateReceivedShare(ctx, updateReceivedShareRequest) - switch errCode := errorcode.FromCS3Status(updateReceivedShareResponse.GetStatus(), err); { - case errCode == nil: - acceptedShares = append(acceptedShares, updateReceivedShareResponse.GetShare()) - default: - // Just log at debug level here. If a single accept for any of the received shares failed this - // is not a critical problem. We mainly need to handle the case where all accepts fail. (Outside - // the loop) - s.logger.Debug().Err(errCode). - Str("shareid", receivedShare.GetShare().GetId().String()). - Str("resourceid", receivedShare.GetShare().GetResourceId().String()). - Msg("failed to accept share") - errs = append(errs, errCode) - } + errs, ok := err.(interface{ Unwrap() []error }) + if ok && len(errs.Unwrap()) == len(availableShares) { + // none of the received shares could be accepted. + // this is an error, return it. + return nil, err } - if len(receivedSharesResponse.GetShares()) == len(errs) { - // none of the received shares could be accepted. This is an error. Return it. - return libregraph.DriveItem{}, errors.Join(errs...) - } - - // As the accepted shares are all for the same resource they should collapse to a single driveitem - items, err := cs3ReceivedSharesToDriveItems(ctx, &s.logger, gatewayClient, s.identityCache, acceptedShares) - switch { - case err != nil: - return libregraph.DriveItem{}, err - case len(items) != 1: - return libregraph.DriveItem{}, errorcode.New(errorcode.GeneralException, "failed to convert accepted shares into drive-item") - } - return items[0], nil + return updatedShares, nil } // DrivesDriveItemApi is the api that registers the http endpoints which expose needed operation to the graph api. @@ -242,13 +295,15 @@ func (s DrivesDriveItemService) MountShare(ctx context.Context, resourceID stora type DrivesDriveItemApi struct { logger log.Logger drivesDriveItemService DrivesDriveItemProvider + baseGraphService BaseGraphProvider } // NewDrivesDriveItemApi creates a new DrivesDriveItemApi -func NewDrivesDriveItemApi(drivesDriveItemService DrivesDriveItemProvider, logger log.Logger) (DrivesDriveItemApi, error) { +func NewDrivesDriveItemApi(drivesDriveItemService DrivesDriveItemProvider, baseGraphService BaseGraphProvider, logger log.Logger) (DrivesDriveItemApi, error) { return DrivesDriveItemApi{ logger: log.Logger{Logger: logger.With().Str("graph api", "DrivesDriveItemApi").Logger()}, drivesDriveItemService: drivesDriveItemService, + baseGraphService: baseGraphService, }, nil } @@ -257,23 +312,21 @@ func (api DrivesDriveItemApi) DeleteDriveItem(w http.ResponseWriter, r *http.Req ctx := r.Context() driveID, itemID, err := GetDriveAndItemIDParam(r, &api.logger) if err != nil { - msg := "invalid driveID or itemID" - api.logger.Debug().Err(err).Msg(msg) - errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, msg) + api.logger.Debug().Err(err).Msg(ErrInvalidDriveIDOrItemID.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, ErrInvalidDriveIDOrItemID.Error()) return } if !IsShareJail(driveID) { - msg := "invalid driveID, must be share jail" - api.logger.Debug().Interface("driveID", driveID).Msg(msg) - errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, msg) + api.logger.Debug().Interface("driveID", driveID).Msg(ErrNotAShareJail.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, ErrNotAShareJail.Error()) return } - if err := api.drivesDriveItemService.UnmountShare(ctx, itemID); err != nil { - msg := "unmounting share failed" - api.logger.Debug().Err(err).Msg(msg) - errorcode.InvalidRequest.Render(w, r, http.StatusFailedDependency, msg) + shareID := ExtractShareIdFromResourceId(itemID) + if err := api.drivesDriveItemService.UnmountShare(ctx, shareID); err != nil { + api.logger.Debug().Err(err).Msg(ErrUnmountShare.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusFailedDependency, ErrUnmountShare.Error()) return } @@ -281,49 +334,112 @@ func (api DrivesDriveItemApi) DeleteDriveItem(w http.ResponseWriter, r *http.Req render.NoContent(w, r) } +// UpdateDriveItem updates a drive item, currently only the visibility of the share is updated +func (api DrivesDriveItemApi) UpdateDriveItem(w http.ResponseWriter, r *http.Request) { + driveID, itemID, err := GetDriveAndItemIDParam(r, &api.logger) + if err != nil { + api.logger.Debug().Err(err).Msg(ErrInvalidDriveIDOrItemID.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, ErrInvalidDriveIDOrItemID.Error()) + return + } + + if !IsShareJail(driveID) { + api.logger.Debug().Interface("driveID", driveID).Msg(ErrNotAShareJail.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, ErrNotAShareJail.Error()) + return + } + + shareID := ExtractShareIdFromResourceId(itemID) + requestDriveItem := libregraph.DriveItem{} + if err := StrictJSONUnmarshal(r.Body, &requestDriveItem); err != nil { + api.logger.Debug().Err(err).Msg(ErrInvalidRequestBody.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, ErrInvalidRequestBody.Error()) + return + } + + availableShares, err := api.drivesDriveItemService.GetShareAndSiblings(r.Context(), shareID, nil) + if err != nil { + api.logger.Debug().Err(err).Msg(ErrGetShareAndSiblings.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusFailedDependency, ErrGetShareAndSiblings.Error()) + return + } + + updatedShares, err := api.drivesDriveItemService.UpdateShares( + r.Context(), + availableShares, + func(_ *collaboration.ReceivedShare, request *collaboration.UpdateReceivedShareRequest) { + request.GetShare().Hidden = requestDriveItem.GetUIHidden() + request.UpdateMask.Paths = append(request.UpdateMask.Paths, _fieldMaskPathHidden) + }, + ) + switch { + case err != nil: + break + case len(updatedShares) == 0: + err = ErrNoShares + } + if err != nil { + api.logger.Debug().Err(err).Msg(ErrUpdateShares.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusFailedDependency, ErrUpdateShares.Error()) + return + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, updatedShares[0]) +} + // CreateDriveItem creates a drive item func (api DrivesDriveItemApi) CreateDriveItem(w http.ResponseWriter, r *http.Request) { ctx := r.Context() driveID, err := parseIDParam(r, "driveID") if err != nil { - api.logger.Debug().Err(err).Msg("invalid driveID") - errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, "invalid driveID") + api.logger.Debug().Err(err).Msg(ErrInvalidDriveIDOrItemID.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, ErrInvalidDriveIDOrItemID.Error()) return } if !IsShareJail(driveID) { - msg := "invalid driveID, must be share jail" - api.logger.Debug().Interface("driveID", driveID).Msg(msg) - errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, msg) + api.logger.Debug().Interface("driveID", driveID).Msg(ErrNotAShareJail.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, ErrNotAShareJail.Error()) return } requestDriveItem := libregraph.DriveItem{} if err := StrictJSONUnmarshal(r.Body, &requestDriveItem); err != nil { - msg := "invalid request body" - api.logger.Debug().Err(err).Msg(msg) - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, msg) + api.logger.Debug().Err(err).Msg(ErrInvalidRequestBody.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusUnprocessableEntity, ErrInvalidRequestBody.Error()) return } remoteItem := requestDriveItem.GetRemoteItem() resourceId, err := storagespace.ParseID(remoteItem.GetId()) if err != nil { - msg := "invalid remote item id" - api.logger.Debug().Err(err).Msg(msg) - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, msg) + api.logger.Debug().Err(err).Msg(ErrInvalidID.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, ErrInvalidID.Error()) return } - mountShareResponse, err := api.drivesDriveItemService. - MountShare(ctx, resourceId, requestDriveItem.GetName()) + mountedShares, err := api.drivesDriveItemService. + MountShare(ctx, &resourceId, requestDriveItem.GetName()) if err != nil { - msg := "mounting share failed" - api.logger.Debug().Err(err).Msg(msg) - errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, msg) + api.logger.Debug().Err(err).Msg(ErrMountShare.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, ErrMountShare.Error()) + return + } + + driveItems, err := api.baseGraphService.CS3ReceivedSharesToDriveItems(ctx, mountedShares) + switch { + case err != nil: + break + case len(driveItems) != 1: + err = ErrDriveItemConversion + } + if err != nil { + api.logger.Debug().Err(err).Msg(ErrDriveItemConversion.Error()) + errorcode.InvalidRequest.Render(w, r, http.StatusFailedDependency, ErrDriveItemConversion.Error()) return } render.Status(r, http.StatusCreated) - render.JSON(w, r, mountShareResponse) + render.JSON(w, r, driveItems[0]) } diff --git a/services/graph/pkg/service/v0/api_drives_drive_item_test.go b/services/graph/pkg/service/v0/api_drives_drive_item_test.go index 2a932ff67b..7d7dc21529 100644 --- a/services/graph/pkg/service/v0/api_drives_drive_item_test.go +++ b/services/graph/pkg/service/v0/api_drives_drive_item_test.go @@ -5,10 +5,8 @@ import ( "context" "encoding/json" "errors" - "fmt" "net/http" "net/http/httptest" - "strconv" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" collaborationv1beta1 "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" @@ -20,15 +18,14 @@ import ( "github.com/stretchr/testify/mock" "github.com/tidwall/gjson" "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/fieldmaskpb" "github.com/cs3org/reva/v2/pkg/rgrpc/status" - "github.com/cs3org/reva/v2/pkg/storagespace" cs3mocks "github.com/cs3org/reva/v2/tests/cs3mocks/mocks" - + "github.com/owncloud/ocis/v2/ocis-pkg/conversions" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/services/graph/mocks" "github.com/owncloud/ocis/v2/services/graph/pkg/errorcode" - "github.com/owncloud/ocis/v2/services/graph/pkg/identity" svc "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0" ) @@ -46,741 +43,815 @@ var _ = Describe("DrivesDriveItemService", func() { gatewaySelector = mocks.NewSelectable[gateway.GatewayAPIClient](GinkgoT()) gatewaySelector.On("Next").Return(gatewayClient, nil) - cache := identity.NewIdentityCache(identity.IdentityCacheWithGatewaySelector(gatewaySelector)) - - service, err := svc.NewDrivesDriveItemService(logger, gatewaySelector, cache) + service, err := svc.NewDrivesDriveItemService(logger, gatewaySelector) Expect(err).ToNot(HaveOccurred()) drivesDriveItemService = service }) - Describe("UnmountShare", func() { - It("handles gateway selector related errors", func() { - gatewaySelector.ExpectedCalls = nil + var _ = Describe("GetSharesByResourceID", func() { + It("uses the correct filters to list received shares", func() { + resourceID := &storageprovider.ResourceId{ + StorageId: "1", + OpaqueId: "2", + SpaceId: "3", + } + state := collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED - expectedError := errors.New("obtaining next gatewayClient failed") - gatewaySelector.On("Next").Return(gatewayClient, expectedError) + gatewayClient. + EXPECT(). + ListReceivedShares(context.Background(), mock.Anything, mock.Anything, mock.Anything). + RunAndReturn(func(ctx context.Context, request *collaborationv1beta1.ListReceivedSharesRequest, option ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { + Expect(request.Filters).To(HaveLen(2)) + Expect(request.Filters[0].Term.(*collaborationv1beta1.Filter_ResourceId).ResourceId).To(Equal(resourceID)) + Expect(request.Filters[1].Term.(*collaborationv1beta1.Filter_State).State).To(Equal(state)) + return nil, nil + }). + Once() - _, err := drivesDriveItemService.MountShare(context.Background(), storageprovider.ResourceId{}, "") - Expect(err).To(MatchError(expectedError)) - }) - - Describe("gateway client share listing", func() { - It("handles share listing errors", func() { - expectedError := errors.New("listing shares failed") - gatewayClient. - On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). - Return(&collaborationv1beta1.ListReceivedSharesResponse{}, expectedError) - - _, err := drivesDriveItemService.MountShare(context.Background(), storageprovider.ResourceId{}, "") - Expect(err).To(MatchError(expectedError)) - }) - - It("uses the correct filters to get the shares", func() { - expectedResourceID := storageprovider.ResourceId{ - StorageId: "1", - OpaqueId: "2", - SpaceId: "3", - } - expectedShareID := collaborationv1beta1.ShareId{ - OpaqueId: "1:2:3", - } - gatewayClient. - On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { - Expect(in.Filters).To(HaveLen(3)) - - var shareStates []collaborationv1beta1.ShareState - var resourceIDs []*storageprovider.ResourceId - - for _, filter := range in.Filters { - switch filter.Term.(type) { - case *collaborationv1beta1.Filter_State: - shareStates = append(shareStates, filter.GetState()) - case *collaborationv1beta1.Filter_ResourceId: - resourceIDs = append(resourceIDs, filter.GetResourceId()) - } - } - - Expect(shareStates).To(HaveLen(2)) - Expect(shareStates).To(ContainElements( - collaborationv1beta1.ShareState_SHARE_STATE_PENDING, - collaborationv1beta1.ShareState_SHARE_STATE_REJECTED, - )) - - Expect(resourceIDs).To(HaveLen(1)) - Expect(resourceIDs[0]).To(Equal(&expectedResourceID)) - - return &collaborationv1beta1.ListReceivedSharesResponse{ - Shares: []*collaborationv1beta1.ReceivedShare{ - { - State: collaborationv1beta1.ShareState_SHARE_STATE_PENDING, - Share: &collaborationv1beta1.Share{ - Id: &expectedShareID, - }, - }, - }, - }, nil - }) - gatewayClient. - On("UpdateReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { - Expect(in.GetUpdateMask().GetPaths()).To(Equal([]string{"state"})) - Expect(in.GetShare().GetState()).To(Equal(collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED)) - Expect(in.GetShare().GetShare().GetId().GetOpaqueId()).To(Equal(expectedShareID.GetOpaqueId())) - return &collaborationv1beta1.UpdateReceivedShareResponse{ - Status: status.NewOK(ctx), - Share: &collaborationv1beta1.ReceivedShare{ - State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, - Share: &collaborationv1beta1.Share{ - Id: &expectedShareID, - ResourceId: &expectedResourceID, - }, - }, - }, nil - }) - gatewayClient. - On("Stat", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *storageprovider.StatRequest, opts ...grpc.CallOption) (*storageprovider.StatResponse, error) { - return &storageprovider.StatResponse{ - Status: status.NewOK(ctx), - Info: &storageprovider.ResourceInfo{ - Id: &expectedResourceID, - Name: "name", - }, - }, nil - }) - - _, err := drivesDriveItemService.MountShare(context.Background(), expectedResourceID, "") - Expect(err).ToNot(HaveOccurred()) + _, _ = drivesDriveItemService.GetSharesByResourceID(context.Background(), resourceID, []*collaborationv1beta1.Filter{ + { + Type: collaborationv1beta1.Filter_TYPE_STATE, + Term: &collaborationv1beta1.Filter_State{ + State: state, + }, + }, }) }) - Describe("gateway client share update", func() { - It("updates the share state to be accepted", func() { - expectedShareID := collaborationv1beta1.ShareId{ - OpaqueId: "1:2:3", - } - expectedResourceID := storageprovider.ResourceId{ - StorageId: "1", - SpaceId: "2", - OpaqueId: "3", - } + It("fails on ancestor error", func() { + someErr := errors.New("some error") + gatewayClient. + EXPECT(). + ListReceivedShares(context.Background(), mock.Anything, mock.Anything, mock.Anything). + Return(nil, someErr). + Once() - gatewayClient. - On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { - return &collaborationv1beta1.ListReceivedSharesResponse{ - Shares: []*collaborationv1beta1.ReceivedShare{ - { - State: collaborationv1beta1.ShareState_SHARE_STATE_PENDING, - Share: &collaborationv1beta1.Share{ - Id: &expectedShareID, - }, - }, - }, - }, nil - }) + _, err := drivesDriveItemService.GetSharesByResourceID(context.Background(), &storageprovider.ResourceId{}, []*collaborationv1beta1.Filter{}) + Expect(err).To(MatchError(someErr)) + }) - gatewayClient. - On("UpdateReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { - Expect(in.GetUpdateMask().GetPaths()).To(Equal([]string{"state"})) - Expect(in.GetShare().GetState()).To(Equal(collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED)) - Expect(in.GetShare().GetShare().GetId().GetOpaqueId()).To(Equal(expectedShareID.GetOpaqueId())) - return &collaborationv1beta1.UpdateReceivedShareResponse{ - Status: status.NewOK(ctx), - Share: &collaborationv1beta1.ReceivedShare{ - State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, - Share: &collaborationv1beta1.Share{ - Id: &expectedShareID, - ResourceId: &expectedResourceID, - }, - }, - }, nil - }) - gatewayClient. - On("Stat", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *storageprovider.StatRequest, opts ...grpc.CallOption) (*storageprovider.StatResponse, error) { - return &storageprovider.StatResponse{ - Status: status.NewOK(ctx), - Info: &storageprovider.ResourceInfo{ - Id: &expectedResourceID, - Name: "name", - }, - }, nil - }) - _, err := drivesDriveItemService.MountShare(context.Background(), storageprovider.ResourceId{}, "") - Expect(err).ToNot(HaveOccurred()) - }) + It("fails if no shares are found", func() { + gatewayClient. + EXPECT(). + ListReceivedShares(context.Background(), mock.Anything, mock.Anything, mock.Anything). + Return(nil, nil). + Once() - It("updates the mountPoint", func() { - expectedShareID := collaborationv1beta1.ShareId{ - OpaqueId: "1:2:3", - } - expectedResourceID := storageprovider.ResourceId{ - StorageId: "1", - SpaceId: "2", - OpaqueId: "3", - } + _, err := drivesDriveItemService.GetSharesByResourceID(context.Background(), &storageprovider.ResourceId{}, []*collaborationv1beta1.Filter{}) + Expect(err).To(MatchError(svc.ErrNoShares)) + }) - gatewayClient. - On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { - return &collaborationv1beta1.ListReceivedSharesResponse{ - Shares: []*collaborationv1beta1.ReceivedShare{ - {}, - }, - }, nil - }) + It("successfully returns shares", func() { + givenShares := []*collaborationv1beta1.ReceivedShare{ + {State: collaborationv1beta1.ShareState_SHARE_STATE_PENDING}, + {State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED}, + {State: collaborationv1beta1.ShareState_SHARE_STATE_REJECTED}, + } + gatewayClient. + EXPECT(). + ListReceivedShares(context.Background(), mock.Anything, mock.Anything, mock.Anything). + Return(&collaborationv1beta1.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: givenShares, + }, nil). + Once() - gatewayClient. - On("UpdateReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { - Expect(in.GetUpdateMask().GetPaths()).To(HaveLen(2)) - Expect(in.GetUpdateMask().GetPaths()).To(ContainElements("mount_point")) - Expect(in.GetShare().GetMountPoint().GetPath()).To(Equal("new name")) - return &collaborationv1beta1.UpdateReceivedShareResponse{ - Status: status.NewOK(ctx), - Share: &collaborationv1beta1.ReceivedShare{ - State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, - Share: &collaborationv1beta1.Share{ - Id: &expectedShareID, - ResourceId: &expectedResourceID, - }, - MountPoint: &storageprovider.Reference{ - Path: "new name", - }, - }, - }, nil - }) - gatewayClient. - On("Stat", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *storageprovider.StatRequest, opts ...grpc.CallOption) (*storageprovider.StatResponse, error) { - return &storageprovider.StatResponse{ - Status: status.NewOK(ctx), - Info: &storageprovider.ResourceInfo{ - Id: &expectedResourceID, - Name: "name", - }, - }, nil - }) - - di, err := drivesDriveItemService.MountShare(context.Background(), storageprovider.ResourceId{}, "new name") - Expect(err).ToNot(HaveOccurred()) - Expect(di.GetName()).To(Equal("new name")) - }) - - It("succeeds when any of the shares was accepted", func() { - expectedResourceID := storageprovider.ResourceId{ - StorageId: "1", - SpaceId: "2", - OpaqueId: "3", - } - - gatewayClient. - On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { - return &collaborationv1beta1.ListReceivedSharesResponse{ - Shares: []*collaborationv1beta1.ReceivedShare{ - {}, - {}, - {}, - }, - }, nil - }) - - var calls int - gatewayClient. - On("UpdateReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { - calls++ - Expect(calls).To(BeNumerically("<=", 3)) - - if calls <= 2 { - return nil, fmt.Errorf("error %d", calls) - } - - return &collaborationv1beta1.UpdateReceivedShareResponse{ - Status: status.NewOK(ctx), - Share: &collaborationv1beta1.ReceivedShare{ - State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, - Share: &collaborationv1beta1.Share{ - Id: &collaborationv1beta1.ShareId{ - OpaqueId: strconv.Itoa(calls), - }, - ResourceId: &expectedResourceID, - }, - }, - }, nil - }) - gatewayClient. - On("Stat", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *storageprovider.StatRequest, opts ...grpc.CallOption) (*storageprovider.StatResponse, error) { - return &storageprovider.StatResponse{ - Status: status.NewOK(ctx), - Info: &storageprovider.ResourceInfo{ - Id: &expectedResourceID, - Name: "name", - }, - }, nil - }) - - di, err := drivesDriveItemService.MountShare(context.Background(), storageprovider.ResourceId{}, "new name") - Expect(err).To(BeNil()) - Expect(di.GetId()).ToNot(BeEmpty()) - }) - It("errors when none of the shares can be accepted", func() { - gatewayClient. - On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { - return &collaborationv1beta1.ListReceivedSharesResponse{ - Shares: []*collaborationv1beta1.ReceivedShare{ - {}, - {}, - {}, - }, - }, nil - }) - - var calls int - gatewayClient. - On("UpdateReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { - calls++ - Expect(calls).To(BeNumerically("<=", 3)) - return nil, fmt.Errorf("error %d", calls) - }) - - _, err := drivesDriveItemService.MountShare(context.Background(), storageprovider.ResourceId{}, "new name") - Expect(fmt.Sprint(err)).To(ContainSubstring("error 1")) - Expect(fmt.Sprint(err)).To(ContainSubstring("error 2")) - Expect(fmt.Sprint(err)).To(ContainSubstring("error 3")) - }) + shares, err := drivesDriveItemService.GetSharesByResourceID(context.Background(), &storageprovider.ResourceId{}, []*collaborationv1beta1.Filter{}) + Expect(err).To(BeNil()) + Expect(shares).To(Equal(givenShares)) }) }) - Describe("UnmountShare", func() { - It("handles gateway selector related errors", func() { - gatewaySelector.ExpectedCalls = nil + var _ = Describe("GetShareAndSiblings", func() { + It("fails if share lookup reports an error", func() { + someErr := errors.New("some error") + gatewayClient. + EXPECT(). + GetReceivedShare(context.Background(), mock.Anything, mock.Anything). + Return(nil, someErr). + Once() - expectedError := errors.New("obtaining next gatewayClient failed") - gatewaySelector.On("Next").Return(gatewayClient, expectedError) - - err := drivesDriveItemService.UnmountShare(context.Background(), storageprovider.ResourceId{}) - Expect(err).To(MatchError(expectedError)) + _, err := drivesDriveItemService.GetShareAndSiblings(context.Background(), &collaborationv1beta1.ShareId{}, nil) + Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()))) }) - Describe("gateway client share listing", func() { - It("handles share listing errors", func() { - expectedError := errorcode.New(errorcode.GeneralException, "listing shares failed") - gatewayClient. - On("GetReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(&collaborationv1beta1.GetReceivedShareResponse{}, errors.New("listing shares failed")) + It("fails if share lookup does not report an error but the status is off", func() { + someErr := errors.New("some error") + gatewayClient. + EXPECT(). + GetReceivedShare(context.Background(), mock.Anything, mock.Anything). + Return(&collaborationv1beta1.GetReceivedShareResponse{ + Status: status.NewNotFound(context.Background(), someErr.Error()), + }, nil). + Once() - err := drivesDriveItemService.UnmountShare(context.Background(), storageprovider.ResourceId{}) - Expect(err).To(MatchError(expectedError)) + _, err := drivesDriveItemService.GetShareAndSiblings(context.Background(), &collaborationv1beta1.ShareId{}, nil) + Expect(err).To(MatchError(errorcode.New(errorcode.ItemNotFound, someErr.Error()))) + }) + + It("successfully returns shares", func() { + gatewayClient. + EXPECT(). + GetReceivedShare(context.Background(), mock.Anything, mock.Anything). + Return(&collaborationv1beta1.GetReceivedShareResponse{ + Status: status.NewOK(context.Background()), + }, nil). + Once() + + gatewayClient. + EXPECT(). + ListReceivedShares(context.Background(), mock.Anything, mock.Anything, mock.Anything). + Return(&collaborationv1beta1.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: make([]*collaborationv1beta1.ReceivedShare, 3), + }, nil). + Once() + + shares, err := drivesDriveItemService.GetShareAndSiblings(context.Background(), &collaborationv1beta1.ShareId{}, nil) + Expect(err).To(BeNil()) + Expect(shares).To(HaveLen(3)) + }) + }) + + var _ = Describe("UpdateShare", func() { + It("fails without an updater", func() { + _, err := drivesDriveItemService.UpdateShare(context.Background(), &collaborationv1beta1.ReceivedShare{}, nil) + Expect(err).To(MatchError(svc.ErrNoUpdater)) + }) + + It("fails without updates", func() { + _, err := drivesDriveItemService.UpdateShare(context.Background(), &collaborationv1beta1.ReceivedShare{}, func(*collaborationv1beta1.ReceivedShare, *collaborationv1beta1.UpdateReceivedShareRequest) {}) + Expect(err).To(MatchError(svc.ErrNoUpdates)) + }) + + It("fails if share updates reports an error", func() { + someErr := errors.New("some error") + gatewayClient. + EXPECT(). + UpdateReceivedShare(context.Background(), mock.Anything, mock.Anything). + Return(nil, someErr). + Once() + + _, err := drivesDriveItemService.UpdateShare(context.Background(), &collaborationv1beta1.ReceivedShare{}, func(_ *collaborationv1beta1.ReceivedShare, request *collaborationv1beta1.UpdateReceivedShareRequest) { + request.Share.State = collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED + request.UpdateMask.Paths = append(request.UpdateMask.Paths, "state") }) + Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()))) + }) - It("uses the correct filters to get the shares", func() { - driveItemResourceID := storageprovider.ResourceId{ - StorageId: "1", - SpaceId: "2", - OpaqueId: "3:4:5", - } - expectedResourceID := storageprovider.ResourceId{ - StorageId: "3", - SpaceId: "4", - OpaqueId: "5", - } - expectedShareID := collaborationv1beta1.ShareId{ - OpaqueId: "3:4:5", - } - gatewayClient. - On("GetReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.GetReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.GetReceivedShareResponse, error) { - Expect(in.Ref.GetId().GetOpaqueId()).To(Equal(driveItemResourceID.GetOpaqueId())) - return &collaborationv1beta1.GetReceivedShareResponse{ - Status: status.NewOK(ctx), - Share: &collaborationv1beta1.ReceivedShare{ - State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, - Share: &collaborationv1beta1.Share{ - Id: &collaborationv1beta1.ShareId{ - OpaqueId: driveItemResourceID.GetOpaqueId(), - }, - ResourceId: &expectedResourceID, - }, - }, - }, nil - }) + It("fails if share update does not report an error but the status is off", func() { + someErr := errors.New("some error") + gatewayClient. + EXPECT(). + UpdateReceivedShare(context.Background(), mock.Anything, mock.Anything). + Return(&collaborationv1beta1.UpdateReceivedShareResponse{ + Status: status.NewNotFound(context.Background(), someErr.Error()), + }, nil). + Once() - gatewayClient. - On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { - Expect(in.Filters).To(HaveLen(2)) + _, err := drivesDriveItemService.UpdateShare(context.Background(), &collaborationv1beta1.ReceivedShare{}, func(_ *collaborationv1beta1.ReceivedShare, request *collaborationv1beta1.UpdateReceivedShareRequest) { + request.Share.State = collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED + request.UpdateMask.Paths = append(request.UpdateMask.Paths, "state") + }) + Expect(err).To(MatchError(errorcode.New(errorcode.ItemNotFound, someErr.Error()))) + }) + }) - var shareStates []collaborationv1beta1.ShareState - var resourceIDs []*storageprovider.ResourceId - - for _, filter := range in.Filters { - switch filter.Term.(type) { - case *collaborationv1beta1.Filter_State: - shareStates = append(shareStates, filter.GetState()) - case *collaborationv1beta1.Filter_ResourceId: - resourceIDs = append(resourceIDs, filter.GetResourceId()) - } - } - - Expect(shareStates).To(HaveLen(1)) - Expect(shareStates).To(ContainElements( - collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, - )) - - Expect(resourceIDs).To(HaveLen(1)) - Expect(resourceIDs[0]).To(Equal(&expectedResourceID)) - - return &collaborationv1beta1.ListReceivedSharesResponse{ - Shares: []*collaborationv1beta1.ReceivedShare{ - { - State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, - Share: &collaborationv1beta1.Share{ - Id: &expectedShareID, - }, - }, - }, - }, nil - }) - gatewayClient. - On("UpdateReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { - Expect(in.GetUpdateMask().GetPaths()).To(Equal([]string{"state"})) - Expect(in.GetShare().GetState()).To(Equal(collaborationv1beta1.ShareState_SHARE_STATE_REJECTED)) - Expect(in.GetShare().GetShare().GetId().GetOpaqueId()).To(Equal(expectedShareID.GetOpaqueId())) + var _ = Describe("UpdateShares", func() { + It("reports some error if one or multiple shares could not be updated, successfully updates the rest", func() { + someErr := errors.New("some error") + gatewayClient. + EXPECT(). + UpdateReceivedShare(context.Background(), mock.Anything, mock.Anything). + RunAndReturn(func(ctx context.Context, request *collaborationv1beta1.UpdateReceivedShareRequest, option ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { + if request.GetShare().GetShare().GetId() != nil { return &collaborationv1beta1.UpdateReceivedShareResponse{ Status: status.NewOK(ctx), - Share: &collaborationv1beta1.ReceivedShare{ - State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, - Share: &collaborationv1beta1.Share{ - Id: &expectedShareID, - ResourceId: &expectedResourceID, - }, - }, }, nil - }) + } - err := drivesDriveItemService.UnmountShare(context.Background(), driveItemResourceID) - Expect(err).ToNot(HaveOccurred()) + return nil, someErr + }). + Times(3) + + shares, err := drivesDriveItemService.UpdateShares(context.Background(), []*collaborationv1beta1.ReceivedShare{ + {}, + {Share: &collaborationv1beta1.Share{Id: &collaborationv1beta1.ShareId{}}}, + {}, + }, func(_ *collaborationv1beta1.ReceivedShare, request *collaborationv1beta1.UpdateReceivedShareRequest) { + request.Share.State = collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED + request.UpdateMask.Paths = append(request.UpdateMask.Paths, "state") }) + Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()))) + Expect(err.(interface{ Unwrap() []error }).Unwrap()).To(HaveLen(2)) + Expect(shares).To(HaveLen(1)) + }) + }) + + var _ = Describe("UnmountShare", func() { + It("fails if get share and siblings reports an error", func() { + someErr := errors.New("some error") + gatewayClient. + EXPECT(). + GetReceivedShare(context.Background(), mock.Anything, mock.Anything). + Return(nil, someErr). + Once() + + err := drivesDriveItemService.UnmountShare(context.Background(), &collaborationv1beta1.ShareId{}) + Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()))) }) - Describe("gateway client share update", func() { - It("updates the share state to be rejected", func() { - expectedShareID := collaborationv1beta1.ShareId{ - OpaqueId: "1$2!3", - } - gatewayClient. - On("GetReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.GetReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.GetReceivedShareResponse, error) { - return &collaborationv1beta1.GetReceivedShareResponse{ + It("requests only accepted shares to be unmounted", func() { + gatewayClient. + EXPECT(). + GetReceivedShare(context.Background(), mock.Anything, mock.Anything). + Return(&collaborationv1beta1.GetReceivedShareResponse{ + Status: status.NewOK(context.Background()), + }, nil). + Once() + + gatewayClient. + EXPECT(). + ListReceivedShares(context.Background(), mock.Anything, mock.Anything, mock.Anything). + RunAndReturn(func(ctx context.Context, request *collaborationv1beta1.ListReceivedSharesRequest, option ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { + Expect(request.Filters).To(HaveLen(2)) + Expect(request.Filters[0].Type).To(Equal(collaborationv1beta1.Filter_TYPE_RESOURCE_ID)) + Expect(request.Filters[1].Term.(*collaborationv1beta1.Filter_State).State).To(Equal(collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED)) + return nil, nil + }). + Once() + + _ = drivesDriveItemService.UnmountShare(context.Background(), &collaborationv1beta1.ShareId{}) + }) + + It("reports some error if one or multiple shares could not be unmounted", func() { + gatewayClient. + EXPECT(). + GetReceivedShare(context.Background(), mock.Anything, mock.Anything). + Return(&collaborationv1beta1.GetReceivedShareResponse{ + Status: status.NewOK(context.Background()), + }, nil). + Once() + + gatewayClient. + EXPECT(). + ListReceivedShares(context.Background(), mock.Anything, mock.Anything, mock.Anything). + Return(&collaborationv1beta1.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaborationv1beta1.ReceivedShare{ + {}, + {Share: &collaborationv1beta1.Share{Id: &collaborationv1beta1.ShareId{}}}, + {}, + }, + }, nil). + Once() + + someErr := errors.New("some error") + gatewayClient. + EXPECT(). + UpdateReceivedShare(context.Background(), mock.Anything, mock.Anything). + RunAndReturn(func(ctx context.Context, request *collaborationv1beta1.UpdateReceivedShareRequest, option ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { + if request.GetShare().GetShare().GetId() != nil { + return &collaborationv1beta1.UpdateReceivedShareResponse{ Status: status.NewOK(ctx), - Share: &collaborationv1beta1.ReceivedShare{ - State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, - Share: &collaborationv1beta1.Share{ - Id: &expectedShareID, - }, - }, }, nil - }) - gatewayClient. - On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { - return &collaborationv1beta1.ListReceivedSharesResponse{ - Shares: []*collaborationv1beta1.ReceivedShare{ - { - State: collaborationv1beta1.ShareState_SHARE_STATE_PENDING, - Share: &collaborationv1beta1.Share{ - Id: &expectedShareID, - }, - }, - }, - }, nil - }) + } - gatewayClient. - On("UpdateReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { - Expect(in.GetUpdateMask().GetPaths()).To(Equal([]string{"state"})) - Expect(in.GetShare().GetState()).To(Equal(collaborationv1beta1.ShareState_SHARE_STATE_REJECTED)) - Expect(in.GetShare().GetShare().GetId().GetOpaqueId()).To(Equal(expectedShareID.GetOpaqueId())) - return &collaborationv1beta1.UpdateReceivedShareResponse{}, nil - }) + return nil, someErr + }). + Times(3) - err := drivesDriveItemService.UnmountShare(context.Background(), storageprovider.ResourceId{}) - Expect(err).ToNot(HaveOccurred()) - }) - It("succeeds when all shares could be rejected", func() { - gatewayClient. - On("GetReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.GetReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.GetReceivedShareResponse, error) { - return &collaborationv1beta1.GetReceivedShareResponse{ - Status: status.NewOK(ctx), - Share: &collaborationv1beta1.ReceivedShare{ - State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, - }, - }, nil - }) - gatewayClient. - On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { - return &collaborationv1beta1.ListReceivedSharesResponse{ - Shares: []*collaborationv1beta1.ReceivedShare{ - {}, - {}, - {}, - }, - }, nil - }) + err := drivesDriveItemService.UnmountShare(context.Background(), &collaborationv1beta1.ShareId{}) + Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()))) + Expect(err.(interface{ Unwrap() []error }).Unwrap()).To(HaveLen(2)) + }) + }) - var calls int - gatewayClient. - On("UpdateReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { - calls++ - return &collaborationv1beta1.UpdateReceivedShareResponse{}, nil - }) + var _ = Describe("MountShare", func() { + It("fails if name is interpreted as absolute path", func() { + _, _ = gatewaySelector.Next() // make mockery call count happy + _, err := drivesDriveItemService.MountShare(context.Background(), nil, "/some") + Expect(err).To(MatchError(svc.ErrAbsoluteNamePath)) + }) - err := drivesDriveItemService.UnmountShare(context.Background(), storageprovider.ResourceId{}) - Expect(calls).To(Equal(3)) - Expect(err).ToNot(HaveOccurred()) - }) + It("uses the correct filters to list received shares", func() { + gatewayClient. + EXPECT(). + ListReceivedShares(context.Background(), mock.Anything, mock.Anything, mock.Anything). + RunAndReturn(func(ctx context.Context, request *collaborationv1beta1.ListReceivedSharesRequest, option ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { + Expect(request.Filters).To(HaveLen(3)) + Expect(request.Filters[0].Type).To(Equal(collaborationv1beta1.Filter_TYPE_RESOURCE_ID)) + Expect(request.Filters[1].Term.(*collaborationv1beta1.Filter_State).State).To(Equal(collaborationv1beta1.ShareState_SHARE_STATE_PENDING)) + Expect(request.Filters[2].Term.(*collaborationv1beta1.Filter_State).State).To(Equal(collaborationv1beta1.ShareState_SHARE_STATE_REJECTED)) + return nil, nil + }). + Once() - It("bubbles errors when any share fails rejecting", func() { - gatewayClient. - On("GetReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.GetReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.GetReceivedShareResponse, error) { - return &collaborationv1beta1.GetReceivedShareResponse{ - Status: status.NewOK(ctx), - Share: &collaborationv1beta1.ReceivedShare{ - State: collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED, - }, - }, nil - }) - gatewayClient. - On("ListReceivedShares", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.ListReceivedSharesRequest, opts ...grpc.CallOption) (*collaborationv1beta1.ListReceivedSharesResponse, error) { - return &collaborationv1beta1.ListReceivedSharesResponse{ - Shares: []*collaborationv1beta1.ReceivedShare{ - {}, - {}, - {}, - }, - }, nil - }) + _, _ = drivesDriveItemService.MountShare(context.Background(), nil, "some") + }) - var calls int - gatewayClient. - On("UpdateReceivedShare", mock.Anything, mock.Anything, mock.Anything). - Return(func(ctx context.Context, in *collaborationv1beta1.UpdateReceivedShareRequest, opts ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { - calls++ - Expect(calls).To(BeNumerically("<=", 3)) + It("fails if the update instructions produce as many errors as there are shares", func() { + gatewayClient. + EXPECT(). + ListReceivedShares(context.Background(), mock.Anything, mock.Anything, mock.Anything). + Return(&collaborationv1beta1.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaborationv1beta1.ReceivedShare{ + {}, + {}, + {}, + }, + }, nil). + Once() - if calls <= 2 { - return nil, fmt.Errorf("error %d", calls) - } + someErr := errors.New("some error") + gatewayClient. + EXPECT(). + UpdateReceivedShare(context.Background(), mock.Anything, mock.Anything). + Return(nil, someErr). + Times(3) - return &collaborationv1beta1.UpdateReceivedShareResponse{}, nil - }) + shares, err := drivesDriveItemService.MountShare(context.Background(), nil, "some") + Expect(err).To(MatchError(errorcode.New(errorcode.GeneralException, someErr.Error()))) + Expect(err.(interface{ Unwrap() []error }).Unwrap()).To(HaveLen(3)) + Expect(shares).To(HaveLen(0)) + }) - err := drivesDriveItemService.UnmountShare(context.Background(), storageprovider.ResourceId{}) - Expect(fmt.Sprint(err)).To(Equal("error 1\nerror 2")) - }) + It("reports no errors if not all mount requests fail", func() { + gatewayClient. + EXPECT(). + ListReceivedShares(context.Background(), mock.Anything, mock.Anything, mock.Anything). + Return(&collaborationv1beta1.ListReceivedSharesResponse{ + Status: status.NewOK(context.Background()), + Shares: []*collaborationv1beta1.ReceivedShare{ + {}, + {Share: &collaborationv1beta1.Share{Id: &collaborationv1beta1.ShareId{}}}, + {}, + }, + }, nil). + Once() + + someErr := errors.New("some error") + gatewayClient. + EXPECT(). + UpdateReceivedShare(context.Background(), mock.Anything, mock.Anything). + RunAndReturn(func(ctx context.Context, request *collaborationv1beta1.UpdateReceivedShareRequest, option ...grpc.CallOption) (*collaborationv1beta1.UpdateReceivedShareResponse, error) { + if request.GetShare().GetShare().GetId() == nil { + return nil, someErr + } + + Expect(request.GetShare().GetState()).To(Equal(collaborationv1beta1.ShareState_SHARE_STATE_ACCEPTED)) + Expect(request.GetShare().GetMountPoint().GetPath()).To(Equal("some")) + + return &collaborationv1beta1.UpdateReceivedShareResponse{ + Status: status.NewOK(ctx), + }, nil + }). + Times(3) + + shares, err := drivesDriveItemService.MountShare(context.Background(), nil, "some") + Expect(err).To(BeNil()) + Expect(shares).To(HaveLen(1)) }) }) }) var _ = Describe("DrivesDriveItemApi", func() { var ( - mockProvider *mocks.DrivesDriveItemProvider - httpAPI svc.DrivesDriveItemApi - rCTX *chi.Context + drivesDriveItemProvider *mocks.DrivesDriveItemProvider + baseGraphProvider *mocks.BaseGraphProvider + drivesDriveItemApi svc.DrivesDriveItemApi + rCTX *chi.Context ) BeforeEach(func() { logger := log.NewLogger() - mockProvider = mocks.NewDrivesDriveItemProvider(GinkgoT()) - api, err := svc.NewDrivesDriveItemApi(mockProvider, logger) + baseGraphProvider = mocks.NewBaseGraphProvider(GinkgoT()) + + drivesDriveItemProvider = mocks.NewDrivesDriveItemProvider(GinkgoT()) + api, err := svc.NewDrivesDriveItemApi(drivesDriveItemProvider, baseGraphProvider, logger) Expect(err).ToNot(HaveOccurred()) - httpAPI = api + drivesDriveItemApi = api rCTX = chi.NewRouteContext() - rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") }) - checkDriveIDAndItemIDValidation := func(handler http.HandlerFunc) { - rCTX.URLParams.Add("driveID", "1$2") - rCTX.URLParams.Add("itemID", "3$4!5") + failOnInvalidDriveIDOrItemID := func(handler http.HandlerFunc) { + It("fails on invalid itemID or driveID", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "invalid") + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", nil). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + handler(w, r) + Expect(w.Code).To(Equal(http.StatusUnprocessableEntity)) - responseRecorder := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodPost, "/", nil). - WithContext( - context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), - ) + jsonData := gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrInvalidDriveIDOrItemID.Error())) + }) + } - handler(responseRecorder, request) + failOninvalidDriveItemBody := func(handler http.HandlerFunc) { + It("fails if the request body is not a valid DriveItem", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!1") - Expect(responseRecorder.Code).To(Equal(http.StatusUnprocessableEntity)) + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", nil). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + handler(w, r) + Expect(w.Code).To(Equal(http.StatusUnprocessableEntity)) - jsonData := gjson.Get(responseRecorder.Body.String(), "error") - Expect(jsonData.Get("message").String()).To(Equal("invalid driveID or itemID")) + jsonData := gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrInvalidRequestBody.Error())) + }) + } + + failOnNonShareJailDriveID := func(handler http.HandlerFunc) { + It("fails on non share jail driveID", func() { + rCTX.URLParams.Add("driveID", "1$2") + rCTX.URLParams.Add("itemID", "1$2!3") + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", nil). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + handler(w, r) + Expect(w.Code).To(Equal(http.StatusUnprocessableEntity)) + + jsonData := gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrNotAShareJail.Error())) + }) } Describe("DeleteDriveItem", func() { - It("validates the driveID and itemID url param", func() { - checkDriveIDAndItemIDValidation(httpAPI.DeleteDriveItem) - }) + failOnInvalidDriveIDOrItemID(drivesDriveItemApi.DeleteDriveItem) - It("uses the UnmountShare provider implementation", func() { - rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!a0ca6a90-a365-4782-871e-d44447bbc668") - responseRecorder := httptest.NewRecorder() + failOnNonShareJailDriveID(drivesDriveItemApi.DeleteDriveItem) - request := httptest.NewRequest(http.MethodDelete, "/", nil). + It("fails if unmounting the share fails", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!1") + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", nil). WithContext( context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), ) - onUnmountShare := mockProvider.On("UnmountShare", mock.Anything, mock.Anything) - onUnmountShare. - Return(func(ctx context.Context, resourceID storageprovider.ResourceId) error { - return errors.New("any") - }).Once() + drivesDriveItemProvider. + EXPECT(). + UnmountShare(mock.Anything, mock.Anything). + Return(errors.New("some error")). + Once() - httpAPI.DeleteDriveItem(responseRecorder, request) + drivesDriveItemApi.DeleteDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusFailedDependency)) - Expect(responseRecorder.Code).To(Equal(http.StatusFailedDependency)) + jsonData := gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrUnmountShare.Error())) + }) - jsonData := gjson.Get(responseRecorder.Body.String(), "error") - Expect(jsonData.Get("message").String()).To(Equal("unmounting share failed")) + It("successfully unmounts the share", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!1") + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", nil). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) - // happy path - responseRecorder = httptest.NewRecorder() + drivesDriveItemProvider. + EXPECT(). + UnmountShare(mock.Anything, mock.Anything). + Return(nil). + Once() - onUnmountShare. - Return(func(ctx context.Context, resourceID storageprovider.ResourceId) error { - Expect(storagespace.FormatResourceID(resourceID)).To(Equal("a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!a0ca6a90-a365-4782-871e-d44447bbc668")) - return nil - }).Once() + drivesDriveItemApi.DeleteDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusNoContent)) + }) + }) - httpAPI.DeleteDriveItem(responseRecorder, request) + Describe("UpdateDriveItem", func() { + failOnInvalidDriveIDOrItemID(drivesDriveItemApi.UpdateDriveItem) - Expect(responseRecorder.Code).To(Equal(http.StatusNoContent)) + failOnNonShareJailDriveID(drivesDriveItemApi.UpdateDriveItem) + + failOninvalidDriveItemBody(drivesDriveItemApi.UpdateDriveItem) + + It("fails if retrieving the share ans siblings fails", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!1") + + w := httptest.NewRecorder() + + driveItemJson, err := json.Marshal(libregraph.DriveItem{}) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + + drivesDriveItemProvider. + EXPECT(). + GetShareAndSiblings(mock.Anything, mock.Anything, mock.Anything). + Return(nil, errors.New("some error")). + Once() + + drivesDriveItemApi.UpdateDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusFailedDependency)) + + jsonData := gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrGetShareAndSiblings.Error())) + }) + + It("fails if updating the share fails", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!1") + + w := httptest.NewRecorder() + + driveItemJson, err := json.Marshal(libregraph.DriveItem{}) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + + drivesDriveItemProvider. + EXPECT(). + GetShareAndSiblings(mock.Anything, mock.Anything, mock.Anything). + Return(nil, nil). + Once() + + drivesDriveItemProvider. + EXPECT(). + UpdateShares(mock.Anything, mock.Anything, mock.Anything). + Return(nil, errors.New("some error")). + Once() + + drivesDriveItemApi.UpdateDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusFailedDependency)) + + jsonData := gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrUpdateShares.Error())) + }) + + It("fails if no shares are updated", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!1") + + w := httptest.NewRecorder() + + driveItemJson, err := json.Marshal(libregraph.DriveItem{ + UIHidden: conversions.ToPointer(true), + }) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + + drivesDriveItemProvider. + EXPECT(). + GetShareAndSiblings(mock.Anything, mock.Anything, mock.Anything). + Return([]*collaborationv1beta1.ReceivedShare{}, nil). + Once() + + drivesDriveItemProvider. + EXPECT(). + UpdateShares(mock.Anything, mock.Anything, mock.Anything). + Return([]*collaborationv1beta1.ReceivedShare{}, nil). + Once() + + drivesDriveItemApi.UpdateDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusFailedDependency)) + + jsonData := gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrUpdateShares.Error())) + }) + + It("successfully updates the share", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!1") + + w := httptest.NewRecorder() + + driveItemJson, err := json.Marshal(libregraph.DriveItem{ + UIHidden: conversions.ToPointer(true), + }) + Expect(err).ToNot(HaveOccurred()) + + r := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + + share := &collaborationv1beta1.ReceivedShare{ + Share: &collaborationv1beta1.Share{Id: &collaborationv1beta1.ShareId{ + OpaqueId: "123", + }}, + } + + drivesDriveItemProvider. + EXPECT(). + GetShareAndSiblings(mock.Anything, mock.Anything, mock.Anything). + Return([]*collaborationv1beta1.ReceivedShare{share}, nil). + Once() + + drivesDriveItemProvider. + EXPECT(). + UpdateShares(mock.Anything, mock.Anything, mock.Anything). + RunAndReturn(func(ctx context.Context, shares []*collaborationv1beta1.ReceivedShare, closure svc.UpdateShareClosure) ([]*collaborationv1beta1.ReceivedShare, error) { + + updateReceivedShareRequest := &collaborationv1beta1.UpdateReceivedShareRequest{ + Share: &collaborationv1beta1.ReceivedShare{ + Share: &collaborationv1beta1.Share{ + Id: share.GetShare().GetId(), + }, + }, + UpdateMask: &fieldmaskpb.FieldMask{Paths: []string{}}, + } + + closure(share, updateReceivedShareRequest) + + Expect(shares).To(HaveLen(1)) + Expect(updateReceivedShareRequest.GetShare().GetHidden()).To(BeTrue()) + Expect(updateReceivedShareRequest.GetUpdateMask().GetPaths()).To(HaveLen(1)) + Expect(updateReceivedShareRequest.GetUpdateMask().GetPaths()).To(ContainElements("hidden")) + + return shares, nil + }). + Once() + + drivesDriveItemApi.UpdateDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusOK)) }) }) Describe("CreateDriveItem", func() { - It("checks if the idemID and driveID is in share jail", func() { - rCTX.URLParams.Add("driveID", "1$2") - - responseRecorder := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodPost, "/", nil). + It("fails without a driveID", func() { + w := httptest.NewRecorder() + r := httptest.NewRequest(http.MethodPost, "/", nil). WithContext( context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), ) + drivesDriveItemApi.CreateDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusUnprocessableEntity)) - httpAPI.CreateDriveItem(responseRecorder, request) - - Expect(responseRecorder.Code).To(Equal(http.StatusUnprocessableEntity)) - - jsonData := gjson.Get(responseRecorder.Body.String(), "error") - Expect(jsonData.Get("message").String()).To(ContainSubstring("must be share jail")) + jsonData := gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrInvalidDriveIDOrItemID.Error())) }) - It("checks that the request body is valid", func() { - responseRecorder := httptest.NewRecorder() - request := httptest.NewRequest(http.MethodPost, "/", nil). - WithContext( - context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), - ) + failOnNonShareJailDriveID(drivesDriveItemApi.CreateDriveItem) - httpAPI.CreateDriveItem(responseRecorder, request) + failOninvalidDriveItemBody(drivesDriveItemApi.CreateDriveItem) - Expect(responseRecorder.Code).To(Equal(http.StatusBadRequest)) + It("fails on invalid request body id", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!1") - jsonData := gjson.Get(responseRecorder.Body.String(), "error") - Expect(jsonData.Get("message").String()).To(Equal("invalid request body")) + w := httptest.NewRecorder() - // valid drive item, but invalid remote item id - driveItem := libregraph.DriveItem{} - - driveItemJson, err := json.Marshal(driveItem) + driveItemJson, err := json.Marshal(libregraph.DriveItem{}) Expect(err).ToNot(HaveOccurred()) - responseRecorder = httptest.NewRecorder() - - request = httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). + r := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). WithContext( context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), ) - httpAPI.CreateDriveItem(responseRecorder, request) + drivesDriveItemApi.CreateDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusBadRequest)) - Expect(responseRecorder.Code).To(Equal(http.StatusBadRequest)) - - jsonData = gjson.Get(responseRecorder.Body.String(), "error") - Expect(jsonData.Get("message").String()).To(Equal("invalid remote item id")) + jsonData := gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrInvalidID.Error())) }) - It("uses the MountShare provider implementation", func() { - driveItemName := "a name" - remoteItemID := "d66d28d8-3558-4f0f-ba2a-34a7185b806d$831997cf-a531-491b-ae72-9037739f04e9!c131a84c-7506-46b4-8e5e-60c56382da3b" - driveItem := libregraph.DriveItem{ - Name: &driveItemName, + It("fails if mounting the share fails", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!1") + + w := httptest.NewRecorder() + + driveItemJson, err := json.Marshal(libregraph.DriveItem{ RemoteItem: &libregraph.RemoteItem{ - Id: &remoteItemID, + Id: conversions.ToPointer("123"), }, - } - - driveItemJson, err := json.Marshal(driveItem) + }) Expect(err).ToNot(HaveOccurred()) - responseRecorder := httptest.NewRecorder() + drivesDriveItemProvider. + EXPECT(). + MountShare(mock.Anything, mock.Anything, mock.Anything). + Return(nil, errors.New("some error")). + Once() - request := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). + r := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). WithContext( context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), ) - onMountShare := mockProvider.On("MountShare", mock.Anything, mock.Anything, mock.Anything) - onMountShare. - Return(func(ctx context.Context, resourceID storageprovider.ResourceId, name string) (libregraph.DriveItem, error) { - return libregraph.DriveItem{}, errors.New("any") - }).Once() + drivesDriveItemApi.CreateDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusBadRequest)) - httpAPI.CreateDriveItem(responseRecorder, request) + jsonData := gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrMountShare.Error())) + }) - Expect(responseRecorder.Code).To(Equal(http.StatusBadRequest)) + It("fails if drive item conversion fails", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!1") - jsonData := gjson.Get(responseRecorder.Body.String(), "error") - Expect(jsonData.Get("message").String()).To(Equal("mounting share failed")) + w := httptest.NewRecorder() - // happy path - responseRecorder = httptest.NewRecorder() + driveItemJson, err := json.Marshal(libregraph.DriveItem{ + RemoteItem: &libregraph.RemoteItem{ + Id: conversions.ToPointer("123"), + }, + }) + Expect(err).ToNot(HaveOccurred()) - request = httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). + drivesDriveItemProvider. + EXPECT(). + MountShare(mock.Anything, mock.Anything, mock.Anything). + Return(nil, nil) + + baseGraphProvider. + EXPECT(). + CS3ReceivedSharesToDriveItems(mock.Anything, mock.Anything). + Return(nil, errors.New("some error")). + Once() + + r := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). WithContext( context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), ) - onMountShare. - Return(func(ctx context.Context, resourceID storageprovider.ResourceId, name string) (libregraph.DriveItem, error) { - Expect(storagespace.FormatResourceID(resourceID)).To(Equal(remoteItemID)) - Expect(driveItemName).To(Equal(name)) - return libregraph.DriveItem{}, nil - }).Once() + drivesDriveItemApi.CreateDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusFailedDependency)) - httpAPI.CreateDriveItem(responseRecorder, request) + jsonData := gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrDriveItemConversion.Error())) - Expect(responseRecorder.Code).To(Equal(http.StatusCreated)) + // + baseGraphProvider. + EXPECT(). + CS3ReceivedSharesToDriveItems(mock.Anything, mock.Anything). + Return(nil, nil). + Once() + + r = httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + + drivesDriveItemApi.CreateDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusFailedDependency)) + + jsonData = gjson.Get(w.Body.String(), "error") + Expect(jsonData.Get("message").String()).To(Equal(svc.ErrDriveItemConversion.Error())) + }) + + It("successfully creates the drive item", func() { + rCTX.URLParams.Add("driveID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668") + rCTX.URLParams.Add("itemID", "a0ca6a90-a365-4782-871e-d44447bbc668$a0ca6a90-a365-4782-871e-d44447bbc668!1") + + w := httptest.NewRecorder() + + driveItemJson, err := json.Marshal(libregraph.DriveItem{ + RemoteItem: &libregraph.RemoteItem{ + Id: conversions.ToPointer("123"), + }, + }) + Expect(err).ToNot(HaveOccurred()) + + drivesDriveItemProvider. + EXPECT(). + MountShare(mock.Anything, mock.Anything, mock.Anything). + Return(nil, nil) + + baseGraphProvider. + EXPECT(). + CS3ReceivedSharesToDriveItems(mock.Anything, mock.Anything). + Return([]libregraph.DriveItem{{}}, nil). + Once() + + r := httptest.NewRequest(http.MethodPost, "/", bytes.NewBuffer(driveItemJson)). + WithContext( + context.WithValue(context.Background(), chi.RouteCtxKey, rCTX), + ) + + drivesDriveItemApi.CreateDriveItem(w, r) + Expect(w.Code).To(Equal(http.StatusCreated)) }) }) }) diff --git a/services/graph/pkg/service/v0/base.go b/services/graph/pkg/service/v0/base.go index ace9091de3..69f4d46975 100644 --- a/services/graph/pkg/service/v0/base.go +++ b/services/graph/pkg/service/v0/base.go @@ -31,6 +31,11 @@ import ( "github.com/owncloud/ocis/v2/services/graph/pkg/unifiedrole" ) +// BaseGraphProvider is the interface that wraps shared methods between the different graph providers +type BaseGraphProvider interface { + CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error) +} + // BaseGraphService implements a couple of helper functions that are // shared between the different graph services type BaseGraphService struct { @@ -72,6 +77,15 @@ func (g BaseGraphService) getDriveItem(ctx context.Context, ref storageprovider. return cs3ResourceToDriveItem(g.logger, res.GetInfo()) } +func (g BaseGraphService) CS3ReceivedSharesToDriveItems(ctx context.Context, receivedShares []*collaboration.ReceivedShare) ([]libregraph.DriveItem, error) { + gatewayClient, err := g.gatewaySelector.Next() + if err != nil { + return nil, err + } + + return cs3ReceivedSharesToDriveItems(ctx, g.logger, gatewayClient, g.identityCache, receivedShares) +} + func (g BaseGraphService) cs3SpacePermissionsToLibreGraph(ctx context.Context, space *storageprovider.StorageSpace, apiVersion APIVersion) []libregraph.Permission { if space.Opaque == nil { return nil diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index ed899a9271..d2f6101840 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -36,58 +36,58 @@ const ( ) // Service defines the service handlers. -type Service interface { - ServeHTTP(http.ResponseWriter, *http.Request) +type Service interface { //nolint:interfacebloat + ServeHTTP(w http.ResponseWriter, r *http.Request) ListApplications(w http.ResponseWriter, r *http.Request) - GetApplication(http.ResponseWriter, *http.Request) + GetApplication(w http.ResponseWriter, r *http.Request) - GetMe(http.ResponseWriter, *http.Request) - GetUsers(http.ResponseWriter, *http.Request) - GetUser(http.ResponseWriter, *http.Request) - PostUser(http.ResponseWriter, *http.Request) - DeleteUser(http.ResponseWriter, *http.Request) - PatchUser(http.ResponseWriter, *http.Request) - ChangeOwnPassword(http.ResponseWriter, *http.Request) + GetMe(w http.ResponseWriter, r *http.Request) + GetUsers(w http.ResponseWriter, r *http.Request) + GetUser(w http.ResponseWriter, r *http.Request) + PostUser(w http.ResponseWriter, r *http.Request) + DeleteUser(w http.ResponseWriter, r *http.Request) + PatchUser(w http.ResponseWriter, r *http.Request) + ChangeOwnPassword(w http.ResponseWriter, r *http.Request) - ListAppRoleAssignments(http.ResponseWriter, *http.Request) - CreateAppRoleAssignment(http.ResponseWriter, *http.Request) - DeleteAppRoleAssignment(http.ResponseWriter, *http.Request) + ListAppRoleAssignments(w http.ResponseWriter, r *http.Request) + CreateAppRoleAssignment(w http.ResponseWriter, r *http.Request) + DeleteAppRoleAssignment(w http.ResponseWriter, r *http.Request) - GetGroups(http.ResponseWriter, *http.Request) - GetGroup(http.ResponseWriter, *http.Request) - PostGroup(http.ResponseWriter, *http.Request) - PatchGroup(http.ResponseWriter, *http.Request) - DeleteGroup(http.ResponseWriter, *http.Request) - GetGroupMembers(http.ResponseWriter, *http.Request) - PostGroupMember(http.ResponseWriter, *http.Request) - DeleteGroupMember(http.ResponseWriter, *http.Request) + GetGroups(w http.ResponseWriter, r *http.Request) + GetGroup(w http.ResponseWriter, r *http.Request) + PostGroup(w http.ResponseWriter, r *http.Request) + PatchGroup(w http.ResponseWriter, r *http.Request) + DeleteGroup(w http.ResponseWriter, r *http.Request) + GetGroupMembers(w http.ResponseWriter, r *http.Request) + PostGroupMember(w http.ResponseWriter, r *http.Request) + DeleteGroupMember(w http.ResponseWriter, r *http.Request) - GetEducationSchools(http.ResponseWriter, *http.Request) - GetEducationSchool(http.ResponseWriter, *http.Request) - PostEducationSchool(http.ResponseWriter, *http.Request) - PatchEducationSchool(http.ResponseWriter, *http.Request) - DeleteEducationSchool(http.ResponseWriter, *http.Request) - GetEducationSchoolUsers(http.ResponseWriter, *http.Request) - PostEducationSchoolUser(http.ResponseWriter, *http.Request) - DeleteEducationSchoolUser(http.ResponseWriter, *http.Request) - GetEducationSchoolClasses(http.ResponseWriter, *http.Request) - PostEducationSchoolClass(http.ResponseWriter, *http.Request) - DeleteEducationSchoolClass(http.ResponseWriter, *http.Request) + GetEducationSchools(w http.ResponseWriter, r *http.Request) + GetEducationSchool(w http.ResponseWriter, r *http.Request) + PostEducationSchool(w http.ResponseWriter, r *http.Request) + PatchEducationSchool(w http.ResponseWriter, r *http.Request) + DeleteEducationSchool(w http.ResponseWriter, r *http.Request) + GetEducationSchoolUsers(w http.ResponseWriter, r *http.Request) + PostEducationSchoolUser(w http.ResponseWriter, r *http.Request) + DeleteEducationSchoolUser(w http.ResponseWriter, r *http.Request) + GetEducationSchoolClasses(w http.ResponseWriter, r *http.Request) + PostEducationSchoolClass(w http.ResponseWriter, r *http.Request) + DeleteEducationSchoolClass(w http.ResponseWriter, r *http.Request) - GetEducationClasses(http.ResponseWriter, *http.Request) - GetEducationClass(http.ResponseWriter, *http.Request) - PostEducationClass(http.ResponseWriter, *http.Request) - PatchEducationClass(http.ResponseWriter, *http.Request) + GetEducationClasses(w http.ResponseWriter, r *http.Request) + GetEducationClass(w http.ResponseWriter, r *http.Request) + PostEducationClass(w http.ResponseWriter, r *http.Request) + PatchEducationClass(w http.ResponseWriter, r *http.Request) DeleteEducationClass(w http.ResponseWriter, r *http.Request) GetEducationClassMembers(w http.ResponseWriter, r *http.Request) PostEducationClassMember(w http.ResponseWriter, r *http.Request) - GetEducationUsers(http.ResponseWriter, *http.Request) - GetEducationUser(http.ResponseWriter, *http.Request) - PostEducationUser(http.ResponseWriter, *http.Request) - DeleteEducationUser(http.ResponseWriter, *http.Request) - PatchEducationUser(http.ResponseWriter, *http.Request) + GetEducationUsers(w http.ResponseWriter, r *http.Request) + GetEducationUser(w http.ResponseWriter, r *http.Request) + PostEducationUser(w http.ResponseWriter, r *http.Request) + DeleteEducationUser(w http.ResponseWriter, r *http.Request) + PatchEducationUser(w http.ResponseWriter, r *http.Request) DeleteEducationClassMember(w http.ResponseWriter, r *http.Request) GetEducationClassTeachers(w http.ResponseWriter, r *http.Request) @@ -118,7 +118,7 @@ type Service interface { } // NewService returns a service implementation for Service. -func NewService(opts ...Option) (Graph, error) { +func NewService(opts ...Option) (Graph, error) { //nolint:maintidx options := newOptions(opts...) m := chi.NewMux() @@ -199,12 +199,12 @@ func NewService(opts ...Option) (Graph, error) { requireAdmin = options.RequireAdminMiddleware } - drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector, identityCache) + drivesDriveItemService, err := NewDrivesDriveItemService(options.Logger, options.GatewaySelector) if err != nil { return svc, err } - drivesDriveItemApi, err := NewDrivesDriveItemApi(drivesDriveItemService, options.Logger) + drivesDriveItemApi, err := NewDrivesDriveItemApi(drivesDriveItemService, svc.BaseGraphService, options.Logger) if err != nil { return svc, err } @@ -247,6 +247,7 @@ func NewService(opts ...Option) (Graph, error) { }) }) r.Route("/items/{itemID}", func(r chi.Router) { + r.Patch("/", drivesDriveItemApi.UpdateDriveItem) r.Delete("/", drivesDriveItemApi.DeleteDriveItem) r.Post("/invite", driveItemPermissionsApi.Invite) r.Post("/createLink", driveItemPermissionsApi.CreateLink) diff --git a/services/graph/pkg/service/v0/utils.go b/services/graph/pkg/service/v0/utils.go index 525c6cdafc..4a7ce93903 100644 --- a/services/graph/pkg/service/v0/utils.go +++ b/services/graph/pkg/service/v0/utils.go @@ -12,9 +12,10 @@ import ( cs3User "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" collaboration "github.com/cs3org/go-cs3apis/cs3/sharing/collaboration/v1beta1" storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "golang.org/x/sync/errgroup" + "github.com/cs3org/reva/v2/pkg/storagespace" "github.com/cs3org/reva/v2/pkg/utils" - "golang.org/x/sync/errgroup" libregraph "github.com/owncloud/libre-graph-api-go" @@ -153,6 +154,10 @@ func cs3ReceivedSharesToDriveItems(ctx context.Context, receivedSharesByResourceID := make(map[string][]*collaboration.ReceivedShare, len(receivedShares)) for _, receivedShare := range receivedShares { + if receivedShare == nil { + continue + } + rIDStr := storagespace.FormatResourceID(*receivedShare.GetShare().GetResourceId()) receivedSharesByResourceID[rIDStr] = append(receivedSharesByResourceID[rIDStr], receivedShare) } @@ -454,3 +459,12 @@ func roleConditionForResourceType(ri *storageprovider.ResourceInfo) (string, err return "", errorcode.New(errorcode.InvalidRequest, "unsupported resource type") } } + +// ExtractShareIdFromResourceId is a bit of a hack. +// We should not rely on a specific format of the item id. +// But currently there is no other way to get the ShareID. +func ExtractShareIdFromResourceId(rid storageprovider.ResourceId) *collaboration.ShareId { + return &collaboration.ShareId{ + OpaqueId: rid.GetOpaqueId(), + } +}