diff --git a/services/collaboration/.mockery.yaml b/services/collaboration/.mockery.yaml new file mode 100644 index 000000000..87425509b --- /dev/null +++ b/services/collaboration/.mockery.yaml @@ -0,0 +1,12 @@ +with-expecter: true +filename: "{{.InterfaceName | snakecase }}.go" +mockname: "{{.InterfaceName}}" +outpkg: "mocks" +packages: + github.com/owncloud/ocis/v2/services/collaboration/pkg/connector: + config: + dir: "mocks" + interfaces: + ConnectorService: + ContentConnectorService: + FileConnectorService: diff --git a/services/collaboration/mocks/connector_service.go b/services/collaboration/mocks/connector_service.go new file mode 100644 index 000000000..ca93a6f56 --- /dev/null +++ b/services/collaboration/mocks/connector_service.go @@ -0,0 +1,129 @@ +// Code generated by mockery v2.40.2. DO NOT EDIT. + +package mocks + +import ( + connector "github.com/owncloud/ocis/v2/services/collaboration/pkg/connector" + mock "github.com/stretchr/testify/mock" +) + +// ConnectorService is an autogenerated mock type for the ConnectorService type +type ConnectorService struct { + mock.Mock +} + +type ConnectorService_Expecter struct { + mock *mock.Mock +} + +func (_m *ConnectorService) EXPECT() *ConnectorService_Expecter { + return &ConnectorService_Expecter{mock: &_m.Mock} +} + +// GetContentConnector provides a mock function with given fields: +func (_m *ConnectorService) GetContentConnector() connector.ContentConnectorService { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetContentConnector") + } + + var r0 connector.ContentConnectorService + if rf, ok := ret.Get(0).(func() connector.ContentConnectorService); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(connector.ContentConnectorService) + } + } + + return r0 +} + +// ConnectorService_GetContentConnector_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetContentConnector' +type ConnectorService_GetContentConnector_Call struct { + *mock.Call +} + +// GetContentConnector is a helper method to define mock.On call +func (_e *ConnectorService_Expecter) GetContentConnector() *ConnectorService_GetContentConnector_Call { + return &ConnectorService_GetContentConnector_Call{Call: _e.mock.On("GetContentConnector")} +} + +func (_c *ConnectorService_GetContentConnector_Call) Run(run func()) *ConnectorService_GetContentConnector_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ConnectorService_GetContentConnector_Call) Return(_a0 connector.ContentConnectorService) *ConnectorService_GetContentConnector_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConnectorService_GetContentConnector_Call) RunAndReturn(run func() connector.ContentConnectorService) *ConnectorService_GetContentConnector_Call { + _c.Call.Return(run) + return _c +} + +// GetFileConnector provides a mock function with given fields: +func (_m *ConnectorService) GetFileConnector() connector.FileConnectorService { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetFileConnector") + } + + var r0 connector.FileConnectorService + if rf, ok := ret.Get(0).(func() connector.FileConnectorService); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(connector.FileConnectorService) + } + } + + return r0 +} + +// ConnectorService_GetFileConnector_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFileConnector' +type ConnectorService_GetFileConnector_Call struct { + *mock.Call +} + +// GetFileConnector is a helper method to define mock.On call +func (_e *ConnectorService_Expecter) GetFileConnector() *ConnectorService_GetFileConnector_Call { + return &ConnectorService_GetFileConnector_Call{Call: _e.mock.On("GetFileConnector")} +} + +func (_c *ConnectorService_GetFileConnector_Call) Run(run func()) *ConnectorService_GetFileConnector_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *ConnectorService_GetFileConnector_Call) Return(_a0 connector.FileConnectorService) *ConnectorService_GetFileConnector_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ConnectorService_GetFileConnector_Call) RunAndReturn(run func() connector.FileConnectorService) *ConnectorService_GetFileConnector_Call { + _c.Call.Return(run) + return _c +} + +// NewConnectorService creates a new instance of ConnectorService. 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 NewConnectorService(t interface { + mock.TestingT + Cleanup(func()) +}) *ConnectorService { + mock := &ConnectorService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/services/collaboration/mocks/content_connector_service.go b/services/collaboration/mocks/content_connector_service.go new file mode 100644 index 000000000..25cd2a2ec --- /dev/null +++ b/services/collaboration/mocks/content_connector_service.go @@ -0,0 +1,143 @@ +// Code generated by mockery v2.40.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + io "io" + + mock "github.com/stretchr/testify/mock" +) + +// ContentConnectorService is an autogenerated mock type for the ContentConnectorService type +type ContentConnectorService struct { + mock.Mock +} + +type ContentConnectorService_Expecter struct { + mock *mock.Mock +} + +func (_m *ContentConnectorService) EXPECT() *ContentConnectorService_Expecter { + return &ContentConnectorService_Expecter{mock: &_m.Mock} +} + +// GetFile provides a mock function with given fields: ctx, writer +func (_m *ContentConnectorService) GetFile(ctx context.Context, writer io.Writer) error { + ret := _m.Called(ctx, writer) + + if len(ret) == 0 { + panic("no return value specified for GetFile") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, io.Writer) error); ok { + r0 = rf(ctx, writer) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// ContentConnectorService_GetFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFile' +type ContentConnectorService_GetFile_Call struct { + *mock.Call +} + +// GetFile is a helper method to define mock.On call +// - ctx context.Context +// - writer io.Writer +func (_e *ContentConnectorService_Expecter) GetFile(ctx interface{}, writer interface{}) *ContentConnectorService_GetFile_Call { + return &ContentConnectorService_GetFile_Call{Call: _e.mock.On("GetFile", ctx, writer)} +} + +func (_c *ContentConnectorService_GetFile_Call) Run(run func(ctx context.Context, writer io.Writer)) *ContentConnectorService_GetFile_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(io.Writer)) + }) + return _c +} + +func (_c *ContentConnectorService_GetFile_Call) Return(_a0 error) *ContentConnectorService_GetFile_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *ContentConnectorService_GetFile_Call) RunAndReturn(run func(context.Context, io.Writer) error) *ContentConnectorService_GetFile_Call { + _c.Call.Return(run) + return _c +} + +// PutFile provides a mock function with given fields: ctx, stream, streamLength, lockID +func (_m *ContentConnectorService) PutFile(ctx context.Context, stream io.Reader, streamLength int64, lockID string) (string, error) { + ret := _m.Called(ctx, stream, streamLength, lockID) + + if len(ret) == 0 { + panic("no return value specified for PutFile") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, io.Reader, int64, string) (string, error)); ok { + return rf(ctx, stream, streamLength, lockID) + } + if rf, ok := ret.Get(0).(func(context.Context, io.Reader, int64, string) string); ok { + r0 = rf(ctx, stream, streamLength, lockID) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, io.Reader, int64, string) error); ok { + r1 = rf(ctx, stream, streamLength, lockID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ContentConnectorService_PutFile_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutFile' +type ContentConnectorService_PutFile_Call struct { + *mock.Call +} + +// PutFile is a helper method to define mock.On call +// - ctx context.Context +// - stream io.Reader +// - streamLength int64 +// - lockID string +func (_e *ContentConnectorService_Expecter) PutFile(ctx interface{}, stream interface{}, streamLength interface{}, lockID interface{}) *ContentConnectorService_PutFile_Call { + return &ContentConnectorService_PutFile_Call{Call: _e.mock.On("PutFile", ctx, stream, streamLength, lockID)} +} + +func (_c *ContentConnectorService_PutFile_Call) Run(run func(ctx context.Context, stream io.Reader, streamLength int64, lockID string)) *ContentConnectorService_PutFile_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(io.Reader), args[2].(int64), args[3].(string)) + }) + return _c +} + +func (_c *ContentConnectorService_PutFile_Call) Return(_a0 string, _a1 error) *ContentConnectorService_PutFile_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *ContentConnectorService_PutFile_Call) RunAndReturn(run func(context.Context, io.Reader, int64, string) (string, error)) *ContentConnectorService_PutFile_Call { + _c.Call.Return(run) + return _c +} + +// NewContentConnectorService creates a new instance of ContentConnectorService. 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 NewContentConnectorService(t interface { + mock.TestingT + Cleanup(func()) +}) *ContentConnectorService { + mock := &ContentConnectorService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/services/collaboration/mocks/file_connector_service.go b/services/collaboration/mocks/file_connector_service.go new file mode 100644 index 000000000..7671d8f35 --- /dev/null +++ b/services/collaboration/mocks/file_connector_service.go @@ -0,0 +1,322 @@ +// Code generated by mockery v2.40.2. DO NOT EDIT. + +package mocks + +import ( + context "context" + + connector "github.com/owncloud/ocis/v2/services/collaboration/pkg/connector" + + mock "github.com/stretchr/testify/mock" +) + +// FileConnectorService is an autogenerated mock type for the FileConnectorService type +type FileConnectorService struct { + mock.Mock +} + +type FileConnectorService_Expecter struct { + mock *mock.Mock +} + +func (_m *FileConnectorService) EXPECT() *FileConnectorService_Expecter { + return &FileConnectorService_Expecter{mock: &_m.Mock} +} + +// CheckFileInfo provides a mock function with given fields: ctx +func (_m *FileConnectorService) CheckFileInfo(ctx context.Context) (connector.FileInfo, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for CheckFileInfo") + } + + var r0 connector.FileInfo + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (connector.FileInfo, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) connector.FileInfo); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(connector.FileInfo) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FileConnectorService_CheckFileInfo_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'CheckFileInfo' +type FileConnectorService_CheckFileInfo_Call struct { + *mock.Call +} + +// CheckFileInfo is a helper method to define mock.On call +// - ctx context.Context +func (_e *FileConnectorService_Expecter) CheckFileInfo(ctx interface{}) *FileConnectorService_CheckFileInfo_Call { + return &FileConnectorService_CheckFileInfo_Call{Call: _e.mock.On("CheckFileInfo", ctx)} +} + +func (_c *FileConnectorService_CheckFileInfo_Call) Run(run func(ctx context.Context)) *FileConnectorService_CheckFileInfo_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *FileConnectorService_CheckFileInfo_Call) Return(_a0 connector.FileInfo, _a1 error) *FileConnectorService_CheckFileInfo_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FileConnectorService_CheckFileInfo_Call) RunAndReturn(run func(context.Context) (connector.FileInfo, error)) *FileConnectorService_CheckFileInfo_Call { + _c.Call.Return(run) + return _c +} + +// GetLock provides a mock function with given fields: ctx +func (_m *FileConnectorService) GetLock(ctx context.Context) (string, error) { + ret := _m.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for GetLock") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context) (string, error)); ok { + return rf(ctx) + } + if rf, ok := ret.Get(0).(func(context.Context) string); ok { + r0 = rf(ctx) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FileConnectorService_GetLock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLock' +type FileConnectorService_GetLock_Call struct { + *mock.Call +} + +// GetLock is a helper method to define mock.On call +// - ctx context.Context +func (_e *FileConnectorService_Expecter) GetLock(ctx interface{}) *FileConnectorService_GetLock_Call { + return &FileConnectorService_GetLock_Call{Call: _e.mock.On("GetLock", ctx)} +} + +func (_c *FileConnectorService_GetLock_Call) Run(run func(ctx context.Context)) *FileConnectorService_GetLock_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context)) + }) + return _c +} + +func (_c *FileConnectorService_GetLock_Call) Return(_a0 string, _a1 error) *FileConnectorService_GetLock_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FileConnectorService_GetLock_Call) RunAndReturn(run func(context.Context) (string, error)) *FileConnectorService_GetLock_Call { + _c.Call.Return(run) + return _c +} + +// Lock provides a mock function with given fields: ctx, lockID, oldLockID +func (_m *FileConnectorService) Lock(ctx context.Context, lockID string, oldLockID string) (string, error) { + ret := _m.Called(ctx, lockID, oldLockID) + + if len(ret) == 0 { + panic("no return value specified for Lock") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (string, error)); ok { + return rf(ctx, lockID, oldLockID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) string); ok { + r0 = rf(ctx, lockID, oldLockID) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, lockID, oldLockID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FileConnectorService_Lock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Lock' +type FileConnectorService_Lock_Call struct { + *mock.Call +} + +// Lock is a helper method to define mock.On call +// - ctx context.Context +// - lockID string +// - oldLockID string +func (_e *FileConnectorService_Expecter) Lock(ctx interface{}, lockID interface{}, oldLockID interface{}) *FileConnectorService_Lock_Call { + return &FileConnectorService_Lock_Call{Call: _e.mock.On("Lock", ctx, lockID, oldLockID)} +} + +func (_c *FileConnectorService_Lock_Call) Run(run func(ctx context.Context, lockID string, oldLockID string)) *FileConnectorService_Lock_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *FileConnectorService_Lock_Call) Return(_a0 string, _a1 error) *FileConnectorService_Lock_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FileConnectorService_Lock_Call) RunAndReturn(run func(context.Context, string, string) (string, error)) *FileConnectorService_Lock_Call { + _c.Call.Return(run) + return _c +} + +// RefreshLock provides a mock function with given fields: ctx, lockID +func (_m *FileConnectorService) RefreshLock(ctx context.Context, lockID string) (string, error) { + ret := _m.Called(ctx, lockID) + + if len(ret) == 0 { + panic("no return value specified for RefreshLock") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { + return rf(ctx, lockID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { + r0 = rf(ctx, lockID) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, lockID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FileConnectorService_RefreshLock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RefreshLock' +type FileConnectorService_RefreshLock_Call struct { + *mock.Call +} + +// RefreshLock is a helper method to define mock.On call +// - ctx context.Context +// - lockID string +func (_e *FileConnectorService_Expecter) RefreshLock(ctx interface{}, lockID interface{}) *FileConnectorService_RefreshLock_Call { + return &FileConnectorService_RefreshLock_Call{Call: _e.mock.On("RefreshLock", ctx, lockID)} +} + +func (_c *FileConnectorService_RefreshLock_Call) Run(run func(ctx context.Context, lockID string)) *FileConnectorService_RefreshLock_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *FileConnectorService_RefreshLock_Call) Return(_a0 string, _a1 error) *FileConnectorService_RefreshLock_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FileConnectorService_RefreshLock_Call) RunAndReturn(run func(context.Context, string) (string, error)) *FileConnectorService_RefreshLock_Call { + _c.Call.Return(run) + return _c +} + +// UnLock provides a mock function with given fields: ctx, lockID +func (_m *FileConnectorService) UnLock(ctx context.Context, lockID string) (string, error) { + ret := _m.Called(ctx, lockID) + + if len(ret) == 0 { + panic("no return value specified for UnLock") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (string, error)); ok { + return rf(ctx, lockID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) string); ok { + r0 = rf(ctx, lockID) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, lockID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// FileConnectorService_UnLock_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UnLock' +type FileConnectorService_UnLock_Call struct { + *mock.Call +} + +// UnLock is a helper method to define mock.On call +// - ctx context.Context +// - lockID string +func (_e *FileConnectorService_Expecter) UnLock(ctx interface{}, lockID interface{}) *FileConnectorService_UnLock_Call { + return &FileConnectorService_UnLock_Call{Call: _e.mock.On("UnLock", ctx, lockID)} +} + +func (_c *FileConnectorService_UnLock_Call) Run(run func(ctx context.Context, lockID string)) *FileConnectorService_UnLock_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *FileConnectorService_UnLock_Call) Return(_a0 string, _a1 error) *FileConnectorService_UnLock_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *FileConnectorService_UnLock_Call) RunAndReturn(run func(context.Context, string) (string, error)) *FileConnectorService_UnLock_Call { + _c.Call.Return(run) + return _c +} + +// NewFileConnectorService creates a new instance of FileConnectorService. 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 NewFileConnectorService(t interface { + mock.TestingT + Cleanup(func()) +}) *FileConnectorService { + mock := &FileConnectorService{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/services/collaboration/pkg/connector/connector.go b/services/collaboration/pkg/connector/connector.go index f87a09f5f..b38eb7337 100644 --- a/services/collaboration/pkg/connector/connector.go +++ b/services/collaboration/pkg/connector/connector.go @@ -20,6 +20,15 @@ func NewConnectorError(code int, msg string) *ConnectorError { } } +// ConnectorService is the interface to implement the WOPI operations. They're +// divided into multiple endpoints. +// The IFileConnector will implement the "File" endpoint +// The IContentConnector will implement the "File content" endpoint +type ConnectorService interface { + GetFileConnector() FileConnectorService + GetContentConnector() ContentConnectorService +} + // Connector will implement the WOPI operations. // For convenience, the connector splits the operations based on the // WOPI endpoints, so you'll need to get the specific connector first. @@ -30,21 +39,21 @@ func NewConnectorError(code int, msg string) *ConnectorError { // // Other endpoints aren't available for now. type Connector struct { - fileConnector *FileConnector - contentConnector *ContentConnector + fileConnector FileConnectorService + contentConnector ContentConnectorService } -func NewConnector(fc *FileConnector, cc *ContentConnector) *Connector { +func NewConnector(fc FileConnectorService, cc ContentConnectorService) *Connector { return &Connector{ fileConnector: fc, contentConnector: cc, } } -func (c *Connector) GetFileConnector() *FileConnector { +func (c *Connector) GetFileConnector() FileConnectorService { return c.fileConnector } -func (c *Connector) GetContentConnector() *ContentConnector { +func (c *Connector) GetContentConnector() ContentConnectorService { return c.contentConnector } diff --git a/services/collaboration/pkg/connector/contentconnector.go b/services/collaboration/pkg/connector/contentconnector.go index 1559e9d31..5153e821d 100644 --- a/services/collaboration/pkg/connector/contentconnector.go +++ b/services/collaboration/pkg/connector/contentconnector.go @@ -18,6 +18,21 @@ import ( "github.com/rs/zerolog" ) +// ContentConnectorService is the interface to implement the "File contents" +// endpoint. Basically upload and download contents. +// All operations need a context containing a WOPI context and, optionally, +// a zerolog logger. +// Target file is within the WOPI context +type ContentConnectorService interface { + // GetFile downloads the file and write its contents in the provider writer + GetFile(ctx context.Context, writer io.Writer) error + // PutFile uploads the stream up to the stream length. The file should be + // locked beforehand, so the lockID needs to be provided. + // The current lockID will be returned ONLY if a conflict happens (the file is + // locked with a different lockID) + PutFile(ctx context.Context, stream io.Reader, streamLength int64, lockID string) (string, error) +} + // ContentConnector implements the "File contents" endpoint. // Basically, the ContentConnector handles downloads (GetFile) and // uploads (PutFile) diff --git a/services/collaboration/pkg/connector/fileconnector.go b/services/collaboration/pkg/connector/fileconnector.go index 6945f3b91..c16b117fb 100644 --- a/services/collaboration/pkg/connector/fileconnector.go +++ b/services/collaboration/pkg/connector/fileconnector.go @@ -25,6 +25,32 @@ const ( lockDuration time.Duration = 30 * time.Minute ) +// FileConnectorService is the interface to implement the "Files" +// endpoint. Basically lock operations on the file plus the CheckFileInfo. +// All operations need a context containing a WOPI context and, optionally, +// a zerolog logger. +// Target file is within the WOPI context +type FileConnectorService interface { + // GetLock will return the lockID present in the target file. + GetLock(ctx context.Context) (string, error) + // Lock will lock the target file with the provided lockID. If the oldLockID + // is provided (not empty), the method will perform an unlockAndRelock + // operation (unlock the file with the oldLockID and immediately relock + // the file with the new lockID). + // The current lockID will be returned if a conflict happens + Lock(ctx context.Context, lockID, oldLockID string) (string, error) + // RefreshLock will extend the lock time 30 minutes. The current lockID + // needs to be provided. + // The current lockID will be returned if a conflict happens + RefreshLock(ctx context.Context, lockID string) (string, error) + // Unlock will unlock the target file. The current lockID needs to be + // provided. + // The current lockID will be returned if a conflict happens + UnLock(ctx context.Context, lockID string) (string, error) + // CheckFileInfo will return the file information of the target file + CheckFileInfo(ctx context.Context) (FileInfo, error) +} + type FileConnector struct { gwc gatewayv1beta1.GatewayAPIClient cfg *config.Config diff --git a/services/collaboration/pkg/connector/httpadapter.go b/services/collaboration/pkg/connector/httpadapter.go index 8eda845cf..075c3d3d8 100644 --- a/services/collaboration/pkg/connector/httpadapter.go +++ b/services/collaboration/pkg/connector/httpadapter.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "net/http" + "strconv" gatewayv1beta1 "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" "github.com/owncloud/ocis/v2/services/collaboration/pkg/config" @@ -24,9 +25,11 @@ const ( // All operations are expected to follow the definitions found in // https://learn.microsoft.com/en-us/microsoft-365/cloud-storage-partner-program/rest/endpoints type HttpAdapter struct { - con *Connector + con ConnectorService } +// NewHttpAdapter will create a new HTTP adapter. A new connector using the +// provided gateway API client and configuration will be used in the adapter func NewHttpAdapter(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config) *HttpAdapter { return &HttpAdapter{ con: NewConnector( @@ -36,6 +39,14 @@ func NewHttpAdapter(gwc gatewayv1beta1.GatewayAPIClient, cfg *config.Config) *Ht } } +// NewHttpAdapterWithConnector will create a new HTTP adapter that will use +// the provided connector service +func NewHttpAdapterWithConnector(con ConnectorService) *HttpAdapter { + return &HttpAdapter{ + con: con, + } +} + func (h *HttpAdapter) GetLock(w http.ResponseWriter, r *http.Request) { fileCon := h.con.GetFileConnector() @@ -119,6 +130,9 @@ func (h *HttpAdapter) UnLock(w http.ResponseWriter, r *http.Request) { func (h *HttpAdapter) CheckFileInfo(w http.ResponseWriter, r *http.Request) { fileCon := h.con.GetFileConnector() + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", "0") + fileInfo, err := fileCon.CheckFileInfo(r.Context()) if err != nil { var conError *ConnectorError @@ -138,7 +152,7 @@ func (h *HttpAdapter) CheckFileInfo(w http.ResponseWriter, r *http.Request) { return } - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Length", strconv.Itoa(len(jsonFileInfo))) w.WriteHeader(http.StatusOK) bytes, err := w.Write(jsonFileInfo) diff --git a/services/collaboration/pkg/connector/httpadapter_test.go b/services/collaboration/pkg/connector/httpadapter_test.go new file mode 100644 index 000000000..3a277f5fc --- /dev/null +++ b/services/collaboration/pkg/connector/httpadapter_test.go @@ -0,0 +1,476 @@ +package connector_test + +import ( + "encoding/json" + "errors" + "io" + "net/http/httptest" + "strings" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/owncloud/ocis/v2/services/collaboration/mocks" + "github.com/owncloud/ocis/v2/services/collaboration/pkg/connector" + "github.com/stretchr/testify/mock" +) + +var _ = Describe("HttpAdapter", func() { + var ( + fc *mocks.FileConnectorService + cc *mocks.ContentConnectorService + con *mocks.ConnectorService + httpAdapter *connector.HttpAdapter + ) + + BeforeEach(func() { + fc = &mocks.FileConnectorService{} + cc = &mocks.ContentConnectorService{} + + con = &mocks.ConnectorService{} + con.On("GetContentConnector").Return(cc) + con.On("GetFileConnector").Return(fc) + + httpAdapter = connector.NewHttpAdapterWithConnector(con) + }) + + Describe("GetLock", func() { + It("General error", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "POST_LOCK") + + w := httptest.NewRecorder() + + fc.On("GetLock", mock.Anything).Times(1).Return("", errors.New("Something happened")) + + httpAdapter.GetLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(500)) + }) + + It("File not found", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "POST_LOCK") + + w := httptest.NewRecorder() + + fc.On("GetLock", mock.Anything).Times(1).Return("", connector.NewConnectorError(404, "Couldn't get the file")) + + httpAdapter.GetLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(404)) + }) + + It("LockId", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "POST_LOCK") + + w := httptest.NewRecorder() + + fc.On("GetLock", mock.Anything).Times(1).Return("zzz111", nil) + + httpAdapter.GetLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(200)) + Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111")) + }) + + It("Empty LockId", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "POST_LOCK") + + w := httptest.NewRecorder() + + fc.On("GetLock", mock.Anything).Times(1).Return("", nil) + + httpAdapter.GetLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(200)) + Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("")) + }) + }) + + Describe("Lock", func() { + Describe("Just lock", func() { + It("General error", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "LOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + fc.On("Lock", mock.Anything, "abc123", "").Times(1).Return("", errors.New("Something happened")) + + httpAdapter.Lock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(500)) + }) + + It("No LockId provided", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "LOCK") + req.Header.Set(connector.HeaderWopiLock, "") + + w := httptest.NewRecorder() + + fc.On("Lock", mock.Anything, "", "").Times(1).Return("", connector.NewConnectorError(400, "No lockId")) + + httpAdapter.Lock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(400)) + }) + + It("Conflict", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "LOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + fc.On("Lock", mock.Anything, "abc123", "").Times(1).Return("zzz111", connector.NewConnectorError(409, "Lock conflict")) + + httpAdapter.Lock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(409)) + Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111")) + }) + + It("Success", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "LOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + fc.On("Lock", mock.Anything, "abc123", "").Times(1).Return("", nil) + + httpAdapter.Lock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(200)) + }) + }) + + Describe("Unlock and relock", func() { + It("General error", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "LOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + req.Header.Set(connector.HeaderWopiOldLock, "qwerty") + + w := httptest.NewRecorder() + + fc.On("Lock", mock.Anything, "abc123", "qwerty").Times(1).Return("", errors.New("Something happened")) + + httpAdapter.Lock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(500)) + }) + + It("No LockId provided", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "LOCK") + req.Header.Set(connector.HeaderWopiLock, "") + req.Header.Set(connector.HeaderWopiOldLock, "") + + w := httptest.NewRecorder() + + fc.On("Lock", mock.Anything, "", "").Times(1).Return("", connector.NewConnectorError(400, "No lockId")) + + httpAdapter.Lock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(400)) + }) + + It("Conflict", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "LOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + req.Header.Set(connector.HeaderWopiOldLock, "qwerty") + + w := httptest.NewRecorder() + + fc.On("Lock", mock.Anything, "abc123", "qwerty").Times(1).Return("zzz111", connector.NewConnectorError(409, "Lock conflict")) + + httpAdapter.Lock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(409)) + Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111")) + }) + + It("Success", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "LOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + req.Header.Set(connector.HeaderWopiOldLock, "qwerty") + + w := httptest.NewRecorder() + + fc.On("Lock", mock.Anything, "abc123", "qwerty").Times(1).Return("", nil) + + httpAdapter.Lock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(200)) + }) + }) + }) + + Describe("RefreshLock", func() { + It("General error", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "REFRESH_LOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + fc.On("RefreshLock", mock.Anything, "abc123").Times(1).Return("", errors.New("Something happened")) + + httpAdapter.RefreshLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(500)) + }) + + It("No LockId provided", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "REFRESH_LOCK") + req.Header.Set(connector.HeaderWopiLock, "") + + w := httptest.NewRecorder() + + fc.On("RefreshLock", mock.Anything, "").Times(1).Return("", connector.NewConnectorError(400, "No lockId")) + + httpAdapter.RefreshLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(400)) + }) + + It("Conflict", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "REFRESH_LOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + fc.On("RefreshLock", mock.Anything, "abc123").Times(1).Return("zzz111", connector.NewConnectorError(409, "Lock conflict")) + + httpAdapter.RefreshLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(409)) + Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111")) + }) + + It("Success", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "REFRESH_LOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + fc.On("RefreshLock", mock.Anything, "abc123").Times(1).Return("", nil) + + httpAdapter.RefreshLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(200)) + }) + }) + + Describe("Unlock", func() { + It("General error", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "UNLOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + fc.On("UnLock", mock.Anything, "abc123").Times(1).Return("", errors.New("Something happened")) + + httpAdapter.UnLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(500)) + }) + + It("No LockId provided", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "UNLOCK") + req.Header.Set(connector.HeaderWopiLock, "") + + w := httptest.NewRecorder() + + fc.On("UnLock", mock.Anything, "").Times(1).Return("", connector.NewConnectorError(400, "No lockId")) + + httpAdapter.UnLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(400)) + }) + + It("Conflict", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "UNLOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + fc.On("UnLock", mock.Anything, "abc123").Times(1).Return("zzz111", connector.NewConnectorError(409, "Lock conflict")) + + httpAdapter.UnLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(409)) + Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111")) + }) + + It("Success", func() { + req := httptest.NewRequest("POST", "/wopi/files/abcdef", nil) + req.Header.Set("X-WOPI-Override", "UNLOCK") + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + fc.On("UnLock", mock.Anything, "abc123").Times(1).Return("", nil) + + httpAdapter.UnLock(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(200)) + }) + }) + + Describe("CheckFileInfo", func() { + It("General error", func() { + req := httptest.NewRequest("GET", "/wopi/files/abcdef", nil) + + w := httptest.NewRecorder() + + fc.On("CheckFileInfo", mock.Anything).Times(1).Return(connector.FileInfo{}, errors.New("Something happened")) + + httpAdapter.CheckFileInfo(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(500)) + }) + + It("Not found", func() { + // 404 isn't thrown at the moment. Test is here to prove it's possible to + // throw any error code + req := httptest.NewRequest("GET", "/wopi/files/abcdef", nil) + + w := httptest.NewRecorder() + + fc.On("CheckFileInfo", mock.Anything).Times(1).Return(connector.FileInfo{}, connector.NewConnectorError(404, "Not found")) + + httpAdapter.CheckFileInfo(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(404)) + }) + + It("Success", func() { + req := httptest.NewRequest("GET", "/wopi/files/abcdef", nil) + + w := httptest.NewRecorder() + + // might need more info, but should be enough for the test + fileinfo := connector.FileInfo{ + Size: 123456789, + BreadcrumbDocName: "testy.docx", + } + fc.On("CheckFileInfo", mock.Anything).Times(1).Return(fileinfo, nil) + + httpAdapter.CheckFileInfo(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(200)) + + jsonInfo, _ := io.ReadAll(resp.Body) + + var responseInfo connector.FileInfo + json.Unmarshal(jsonInfo, &responseInfo) + Expect(responseInfo).To(Equal(fileinfo)) + }) + }) + + Describe("GetFile", func() { + It("General error", func() { + req := httptest.NewRequest("GET", "/wopi/files/abcdef/contents", nil) + + w := httptest.NewRecorder() + + cc.On("GetFile", mock.Anything, mock.Anything).Times(1).Return(errors.New("Something happened")) + + httpAdapter.GetFile(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(500)) + }) + + It("Not found", func() { + // 404 isn't thrown at the moment. Test is here to prove it's possible to + // throw any error code + req := httptest.NewRequest("GET", "/wopi/files/abcdef/contents", nil) + + w := httptest.NewRecorder() + + cc.On("GetFile", mock.Anything, mock.Anything).Times(1).Return(connector.NewConnectorError(404, "Not found")) + + httpAdapter.GetFile(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(404)) + }) + + It("Success", func() { + req := httptest.NewRequest("GET", "/wopi/files/abcdef/contents", nil) + + w := httptest.NewRecorder() + + expectedContent := []byte("This is a fake content for a test file") + cc.On("GetFile", mock.Anything, mock.Anything).Times(1).Run(func(args mock.Arguments) { + w := args.Get(1).(io.Writer) + w.Write(expectedContent) + }).Return(nil) + + httpAdapter.GetFile(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(200)) + + content, _ := io.ReadAll(resp.Body) + Expect(content).To(Equal(expectedContent)) + }) + }) + + Describe("PutFile", func() { + It("General error", func() { + contentBody := "this is the new fake content" + req := httptest.NewRequest("GET", "/wopi/files/abcdef/contents", strings.NewReader(contentBody)) + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + cc.On("PutFile", mock.Anything, mock.Anything, int64(len(contentBody)), "abc123").Times(1).Return("", errors.New("Something happened")) + + httpAdapter.PutFile(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(500)) + }) + + It("Conflict", func() { + contentBody := "this is the new fake content" + req := httptest.NewRequest("GET", "/wopi/files/abcdef/contents", strings.NewReader(contentBody)) + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + cc.On("PutFile", mock.Anything, mock.Anything, int64(len(contentBody)), "abc123").Times(1).Return("zzz111", connector.NewConnectorError(409, "Lock conflict")) + + httpAdapter.PutFile(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(409)) + Expect(resp.Header.Get(connector.HeaderWopiLock)).To(Equal("zzz111")) + }) + + It("Success", func() { + contentBody := "this is the new fake content" + req := httptest.NewRequest("GET", "/wopi/files/abcdef/contents", strings.NewReader(contentBody)) + req.Header.Set(connector.HeaderWopiLock, "abc123") + + w := httptest.NewRecorder() + + cc.On("PutFile", mock.Anything, mock.Anything, int64(len(contentBody)), "abc123").Times(1).Return("", nil) + + httpAdapter.PutFile(w, req) + resp := w.Result() + Expect(resp.StatusCode).To(Equal(200)) + }) + }) +})