diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index e3f2e380c5..c9b40ffd8a 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -137,6 +137,10 @@ func DefaultPolicies() []config.Policy { Endpoint: "/branding/logo", Service: "eu.opencloud.web.web", }, + { + Endpoint: "/announcement", + Service: "eu.opencloud.web.web", + }, { Endpoint: "/konnect/", Service: "eu.opencloud.web.idp", diff --git a/services/settings/pkg/store/defaults/defaults.go b/services/settings/pkg/store/defaults/defaults.go index 628c07e581..c8d144eefd 100644 --- a/services/settings/pkg/store/defaults/defaults.go +++ b/services/settings/pkg/store/defaults/defaults.go @@ -80,6 +80,7 @@ func ServiceAccountBundle() *settingsmsg.Bundle { }, Settings: []*settingsmsg.Setting{ AccountManagementPermission(All), + AnnouncementReadWritePermission(All), ChangeLogoPermission(All), CollaborationPublishNotificationPermission(All), CollaborationManageFontsPermission(All), @@ -117,6 +118,7 @@ func generateBundleAdminRole() *settingsmsg.Bundle { }, Settings: []*settingsmsg.Setting{ AccountManagementPermission(All), + AnnouncementReadWritePermission(All), AutoAcceptSharesPermission(Own), ChangeLogoPermission(All), CollaborationPublishNotificationPermission(All), diff --git a/services/settings/pkg/store/defaults/permissions.go b/services/settings/pkg/store/defaults/permissions.go index 4f80c1d124..7d6ee14de6 100644 --- a/services/settings/pkg/store/defaults/permissions.go +++ b/services/settings/pkg/store/defaults/permissions.go @@ -29,6 +29,25 @@ func AccountManagementPermission(c settingsmsg.Permission_Constraint) *settingsm } } +// AnnouncementReadWritePermission is the permission to read and manage the web announcement banner +func AnnouncementReadWritePermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting { + return &settingsmsg.Setting{ + Id: "52b1994b-1bdb-4c8d-a887-1967dbe8cb11", + Name: "Announcement.ReadWrite", + DisplayName: "Manage announcement", + Description: "This permission permits to read and manage the announcement banner shown to all users.", + Resource: &settingsmsg.Resource{ + Type: settingsmsg.Resource_TYPE_SYSTEM, + }, + Value: &settingsmsg.Setting_PermissionValue{ + PermissionValue: &settingsmsg.Permission{ + Operation: settingsmsg.Permission_OPERATION_READWRITE, + Constraint: c, + }, + }, + } +} + // AutoAcceptSharesPermission is the permission to enable share auto-accept func AutoAcceptSharesPermission(c settingsmsg.Permission_Constraint) *settingsmsg.Setting { return &settingsmsg.Setting{ diff --git a/services/web/.mockery.yaml b/services/web/.mockery.yaml new file mode 100644 index 0000000000..04c898f18f --- /dev/null +++ b/services/web/.mockery.yaml @@ -0,0 +1,14 @@ +# maintain v2 separate mocks dir +dir: "{{.InterfaceDir}}/mocks" +structname: "{{.InterfaceName}}" +filename: "{{.InterfaceName | snakecase }}.go" +pkgname: mocks + +template: testify +packages: + github.com/nats-io/nats.go/jetstream: + config: + dir: mocks + interfaces: + KeyValue: {} + KeyValueEntry: {} diff --git a/services/web/mocks/key_value.go b/services/web/mocks/key_value.go new file mode 100644 index 0000000000..042e5dcc54 --- /dev/null +++ b/services/web/mocks/key_value.go @@ -0,0 +1,1363 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "context" + + "github.com/nats-io/nats.go/jetstream" + mock "github.com/stretchr/testify/mock" +) + +// NewKeyValue creates a new instance of KeyValue. 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 NewKeyValue(t interface { + mock.TestingT + Cleanup(func()) +}) *KeyValue { + mock := &KeyValue{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// KeyValue is an autogenerated mock type for the KeyValue type +type KeyValue struct { + mock.Mock +} + +type KeyValue_Expecter struct { + mock *mock.Mock +} + +func (_m *KeyValue) EXPECT() *KeyValue_Expecter { + return &KeyValue_Expecter{mock: &_m.Mock} +} + +// Bucket provides a mock function for the type KeyValue +func (_mock *KeyValue) Bucket() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Bucket") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// KeyValue_Bucket_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Bucket' +type KeyValue_Bucket_Call struct { + *mock.Call +} + +// Bucket is a helper method to define mock.On call +func (_e *KeyValue_Expecter) Bucket() *KeyValue_Bucket_Call { + return &KeyValue_Bucket_Call{Call: _e.mock.On("Bucket")} +} + +func (_c *KeyValue_Bucket_Call) Run(run func()) *KeyValue_Bucket_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *KeyValue_Bucket_Call) Return(s string) *KeyValue_Bucket_Call { + _c.Call.Return(s) + return _c +} + +func (_c *KeyValue_Bucket_Call) RunAndReturn(run func() string) *KeyValue_Bucket_Call { + _c.Call.Return(run) + return _c +} + +// Create provides a mock function for the type KeyValue +func (_mock *KeyValue) Create(ctx context.Context, key string, value []byte, opts ...jetstream.KVCreateOpt) (uint64, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, key, value, opts) + } else { + tmpRet = _mock.Called(ctx, key, value) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 uint64 + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []byte, ...jetstream.KVCreateOpt) (uint64, error)); ok { + return returnFunc(ctx, key, value, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []byte, ...jetstream.KVCreateOpt) uint64); ok { + r0 = returnFunc(ctx, key, value, opts...) + } else { + r0 = ret.Get(0).(uint64) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, []byte, ...jetstream.KVCreateOpt) error); ok { + r1 = returnFunc(ctx, key, value, opts...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type KeyValue_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value []byte +// - opts ...jetstream.KVCreateOpt +func (_e *KeyValue_Expecter) Create(ctx interface{}, key interface{}, value interface{}, opts ...interface{}) *KeyValue_Create_Call { + return &KeyValue_Create_Call{Call: _e.mock.On("Create", + append([]interface{}{ctx, key, value}, opts...)...)} +} + +func (_c *KeyValue_Create_Call) Run(run func(ctx context.Context, key string, value []byte, opts ...jetstream.KVCreateOpt)) *KeyValue_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []byte + if args[2] != nil { + arg2 = args[2].([]byte) + } + var arg3 []jetstream.KVCreateOpt + var variadicArgs []jetstream.KVCreateOpt + if len(args) > 3 { + variadicArgs = args[3].([]jetstream.KVCreateOpt) + } + arg3 = variadicArgs + run( + arg0, + arg1, + arg2, + arg3..., + ) + }) + return _c +} + +func (_c *KeyValue_Create_Call) Return(v uint64, err error) *KeyValue_Create_Call { + _c.Call.Return(v, err) + return _c +} + +func (_c *KeyValue_Create_Call) RunAndReturn(run func(ctx context.Context, key string, value []byte, opts ...jetstream.KVCreateOpt) (uint64, error)) *KeyValue_Create_Call { + _c.Call.Return(run) + return _c +} + +// Delete provides a mock function for the type KeyValue +func (_mock *KeyValue) Delete(ctx context.Context, key string, opts ...jetstream.KVDeleteOpt) error { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, key, opts) + } else { + tmpRet = _mock.Called(ctx, key) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for Delete") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, ...jetstream.KVDeleteOpt) error); ok { + r0 = returnFunc(ctx, key, opts...) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// KeyValue_Delete_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delete' +type KeyValue_Delete_Call struct { + *mock.Call +} + +// Delete is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - opts ...jetstream.KVDeleteOpt +func (_e *KeyValue_Expecter) Delete(ctx interface{}, key interface{}, opts ...interface{}) *KeyValue_Delete_Call { + return &KeyValue_Delete_Call{Call: _e.mock.On("Delete", + append([]interface{}{ctx, key}, opts...)...)} +} + +func (_c *KeyValue_Delete_Call) Run(run func(ctx context.Context, key string, opts ...jetstream.KVDeleteOpt)) *KeyValue_Delete_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []jetstream.KVDeleteOpt + var variadicArgs []jetstream.KVDeleteOpt + if len(args) > 2 { + variadicArgs = args[2].([]jetstream.KVDeleteOpt) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *KeyValue_Delete_Call) Return(err error) *KeyValue_Delete_Call { + _c.Call.Return(err) + return _c +} + +func (_c *KeyValue_Delete_Call) RunAndReturn(run func(ctx context.Context, key string, opts ...jetstream.KVDeleteOpt) error) *KeyValue_Delete_Call { + _c.Call.Return(run) + return _c +} + +// Get provides a mock function for the type KeyValue +func (_mock *KeyValue) Get(ctx context.Context, key string) (jetstream.KeyValueEntry, error) { + ret := _mock.Called(ctx, key) + + if len(ret) == 0 { + panic("no return value specified for Get") + } + + var r0 jetstream.KeyValueEntry + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string) (jetstream.KeyValueEntry, error)); ok { + return returnFunc(ctx, key) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string) jetstream.KeyValueEntry); ok { + r0 = returnFunc(ctx, key) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(jetstream.KeyValueEntry) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = returnFunc(ctx, key) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_Get_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Get' +type KeyValue_Get_Call struct { + *mock.Call +} + +// Get is a helper method to define mock.On call +// - ctx context.Context +// - key string +func (_e *KeyValue_Expecter) Get(ctx interface{}, key interface{}) *KeyValue_Get_Call { + return &KeyValue_Get_Call{Call: _e.mock.On("Get", ctx, key)} +} + +func (_c *KeyValue_Get_Call) Run(run func(ctx context.Context, key string)) *KeyValue_Get_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + run( + arg0, + arg1, + ) + }) + return _c +} + +func (_c *KeyValue_Get_Call) Return(keyValueEntry jetstream.KeyValueEntry, err error) *KeyValue_Get_Call { + _c.Call.Return(keyValueEntry, err) + return _c +} + +func (_c *KeyValue_Get_Call) RunAndReturn(run func(ctx context.Context, key string) (jetstream.KeyValueEntry, error)) *KeyValue_Get_Call { + _c.Call.Return(run) + return _c +} + +// GetRevision provides a mock function for the type KeyValue +func (_mock *KeyValue) GetRevision(ctx context.Context, key string, revision uint64) (jetstream.KeyValueEntry, error) { + ret := _mock.Called(ctx, key, revision) + + if len(ret) == 0 { + panic("no return value specified for GetRevision") + } + + var r0 jetstream.KeyValueEntry + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, uint64) (jetstream.KeyValueEntry, error)); ok { + return returnFunc(ctx, key, revision) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, uint64) jetstream.KeyValueEntry); ok { + r0 = returnFunc(ctx, key, revision) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(jetstream.KeyValueEntry) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, uint64) error); ok { + r1 = returnFunc(ctx, key, revision) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_GetRevision_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetRevision' +type KeyValue_GetRevision_Call struct { + *mock.Call +} + +// GetRevision is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - revision uint64 +func (_e *KeyValue_Expecter) GetRevision(ctx interface{}, key interface{}, revision interface{}) *KeyValue_GetRevision_Call { + return &KeyValue_GetRevision_Call{Call: _e.mock.On("GetRevision", ctx, key, revision)} +} + +func (_c *KeyValue_GetRevision_Call) Run(run func(ctx context.Context, key string, revision uint64)) *KeyValue_GetRevision_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 uint64 + if args[2] != nil { + arg2 = args[2].(uint64) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *KeyValue_GetRevision_Call) Return(keyValueEntry jetstream.KeyValueEntry, err error) *KeyValue_GetRevision_Call { + _c.Call.Return(keyValueEntry, err) + return _c +} + +func (_c *KeyValue_GetRevision_Call) RunAndReturn(run func(ctx context.Context, key string, revision uint64) (jetstream.KeyValueEntry, error)) *KeyValue_GetRevision_Call { + _c.Call.Return(run) + return _c +} + +// History provides a mock function for the type KeyValue +func (_mock *KeyValue) History(ctx context.Context, key string, opts ...jetstream.WatchOpt) ([]jetstream.KeyValueEntry, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, key, opts) + } else { + tmpRet = _mock.Called(ctx, key) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for History") + } + + var r0 []jetstream.KeyValueEntry + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, ...jetstream.WatchOpt) ([]jetstream.KeyValueEntry, error)); ok { + return returnFunc(ctx, key, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, ...jetstream.WatchOpt) []jetstream.KeyValueEntry); ok { + r0 = returnFunc(ctx, key, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]jetstream.KeyValueEntry) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, ...jetstream.WatchOpt) error); ok { + r1 = returnFunc(ctx, key, opts...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_History_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'History' +type KeyValue_History_Call struct { + *mock.Call +} + +// History is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - opts ...jetstream.WatchOpt +func (_e *KeyValue_Expecter) History(ctx interface{}, key interface{}, opts ...interface{}) *KeyValue_History_Call { + return &KeyValue_History_Call{Call: _e.mock.On("History", + append([]interface{}{ctx, key}, opts...)...)} +} + +func (_c *KeyValue_History_Call) Run(run func(ctx context.Context, key string, opts ...jetstream.WatchOpt)) *KeyValue_History_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []jetstream.WatchOpt + var variadicArgs []jetstream.WatchOpt + if len(args) > 2 { + variadicArgs = args[2].([]jetstream.WatchOpt) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *KeyValue_History_Call) Return(keyValueEntrys []jetstream.KeyValueEntry, err error) *KeyValue_History_Call { + _c.Call.Return(keyValueEntrys, err) + return _c +} + +func (_c *KeyValue_History_Call) RunAndReturn(run func(ctx context.Context, key string, opts ...jetstream.WatchOpt) ([]jetstream.KeyValueEntry, error)) *KeyValue_History_Call { + _c.Call.Return(run) + return _c +} + +// Keys provides a mock function for the type KeyValue +func (_mock *KeyValue) Keys(ctx context.Context, opts ...jetstream.WatchOpt) ([]string, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, opts) + } else { + tmpRet = _mock.Called(ctx) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for Keys") + } + + var r0 []string + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, ...jetstream.WatchOpt) ([]string, error)); ok { + return returnFunc(ctx, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, ...jetstream.WatchOpt) []string); ok { + r0 = returnFunc(ctx, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, ...jetstream.WatchOpt) error); ok { + r1 = returnFunc(ctx, opts...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_Keys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Keys' +type KeyValue_Keys_Call struct { + *mock.Call +} + +// Keys is a helper method to define mock.On call +// - ctx context.Context +// - opts ...jetstream.WatchOpt +func (_e *KeyValue_Expecter) Keys(ctx interface{}, opts ...interface{}) *KeyValue_Keys_Call { + return &KeyValue_Keys_Call{Call: _e.mock.On("Keys", + append([]interface{}{ctx}, opts...)...)} +} + +func (_c *KeyValue_Keys_Call) Run(run func(ctx context.Context, opts ...jetstream.WatchOpt)) *KeyValue_Keys_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []jetstream.WatchOpt + var variadicArgs []jetstream.WatchOpt + if len(args) > 1 { + variadicArgs = args[1].([]jetstream.WatchOpt) + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *KeyValue_Keys_Call) Return(strings []string, err error) *KeyValue_Keys_Call { + _c.Call.Return(strings, err) + return _c +} + +func (_c *KeyValue_Keys_Call) RunAndReturn(run func(ctx context.Context, opts ...jetstream.WatchOpt) ([]string, error)) *KeyValue_Keys_Call { + _c.Call.Return(run) + return _c +} + +// ListKeys provides a mock function for the type KeyValue +func (_mock *KeyValue) ListKeys(ctx context.Context, opts ...jetstream.WatchOpt) (jetstream.KeyLister, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, opts) + } else { + tmpRet = _mock.Called(ctx) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for ListKeys") + } + + var r0 jetstream.KeyLister + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, ...jetstream.WatchOpt) (jetstream.KeyLister, error)); ok { + return returnFunc(ctx, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, ...jetstream.WatchOpt) jetstream.KeyLister); ok { + r0 = returnFunc(ctx, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(jetstream.KeyLister) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, ...jetstream.WatchOpt) error); ok { + r1 = returnFunc(ctx, opts...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_ListKeys_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListKeys' +type KeyValue_ListKeys_Call struct { + *mock.Call +} + +// ListKeys is a helper method to define mock.On call +// - ctx context.Context +// - opts ...jetstream.WatchOpt +func (_e *KeyValue_Expecter) ListKeys(ctx interface{}, opts ...interface{}) *KeyValue_ListKeys_Call { + return &KeyValue_ListKeys_Call{Call: _e.mock.On("ListKeys", + append([]interface{}{ctx}, opts...)...)} +} + +func (_c *KeyValue_ListKeys_Call) Run(run func(ctx context.Context, opts ...jetstream.WatchOpt)) *KeyValue_ListKeys_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []jetstream.WatchOpt + var variadicArgs []jetstream.WatchOpt + if len(args) > 1 { + variadicArgs = args[1].([]jetstream.WatchOpt) + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *KeyValue_ListKeys_Call) Return(keyLister jetstream.KeyLister, err error) *KeyValue_ListKeys_Call { + _c.Call.Return(keyLister, err) + return _c +} + +func (_c *KeyValue_ListKeys_Call) RunAndReturn(run func(ctx context.Context, opts ...jetstream.WatchOpt) (jetstream.KeyLister, error)) *KeyValue_ListKeys_Call { + _c.Call.Return(run) + return _c +} + +// ListKeysFiltered provides a mock function for the type KeyValue +func (_mock *KeyValue) ListKeysFiltered(ctx context.Context, filters ...string) (jetstream.KeyLister, error) { + var tmpRet mock.Arguments + if len(filters) > 0 { + tmpRet = _mock.Called(ctx, filters) + } else { + tmpRet = _mock.Called(ctx) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for ListKeysFiltered") + } + + var r0 jetstream.KeyLister + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) (jetstream.KeyLister, error)); ok { + return returnFunc(ctx, filters...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, ...string) jetstream.KeyLister); ok { + r0 = returnFunc(ctx, filters...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(jetstream.KeyLister) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, ...string) error); ok { + r1 = returnFunc(ctx, filters...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_ListKeysFiltered_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListKeysFiltered' +type KeyValue_ListKeysFiltered_Call struct { + *mock.Call +} + +// ListKeysFiltered is a helper method to define mock.On call +// - ctx context.Context +// - filters ...string +func (_e *KeyValue_Expecter) ListKeysFiltered(ctx interface{}, filters ...interface{}) *KeyValue_ListKeysFiltered_Call { + return &KeyValue_ListKeysFiltered_Call{Call: _e.mock.On("ListKeysFiltered", + append([]interface{}{ctx}, filters...)...)} +} + +func (_c *KeyValue_ListKeysFiltered_Call) Run(run func(ctx context.Context, filters ...string)) *KeyValue_ListKeysFiltered_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + var variadicArgs []string + if len(args) > 1 { + variadicArgs = args[1].([]string) + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *KeyValue_ListKeysFiltered_Call) Return(keyLister jetstream.KeyLister, err error) *KeyValue_ListKeysFiltered_Call { + _c.Call.Return(keyLister, err) + return _c +} + +func (_c *KeyValue_ListKeysFiltered_Call) RunAndReturn(run func(ctx context.Context, filters ...string) (jetstream.KeyLister, error)) *KeyValue_ListKeysFiltered_Call { + _c.Call.Return(run) + return _c +} + +// Purge provides a mock function for the type KeyValue +func (_mock *KeyValue) Purge(ctx context.Context, key string, opts ...jetstream.KVDeleteOpt) error { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, key, opts) + } else { + tmpRet = _mock.Called(ctx, key) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for Purge") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, ...jetstream.KVDeleteOpt) error); ok { + r0 = returnFunc(ctx, key, opts...) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// KeyValue_Purge_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Purge' +type KeyValue_Purge_Call struct { + *mock.Call +} + +// Purge is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - opts ...jetstream.KVDeleteOpt +func (_e *KeyValue_Expecter) Purge(ctx interface{}, key interface{}, opts ...interface{}) *KeyValue_Purge_Call { + return &KeyValue_Purge_Call{Call: _e.mock.On("Purge", + append([]interface{}{ctx, key}, opts...)...)} +} + +func (_c *KeyValue_Purge_Call) Run(run func(ctx context.Context, key string, opts ...jetstream.KVDeleteOpt)) *KeyValue_Purge_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []jetstream.KVDeleteOpt + var variadicArgs []jetstream.KVDeleteOpt + if len(args) > 2 { + variadicArgs = args[2].([]jetstream.KVDeleteOpt) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *KeyValue_Purge_Call) Return(err error) *KeyValue_Purge_Call { + _c.Call.Return(err) + return _c +} + +func (_c *KeyValue_Purge_Call) RunAndReturn(run func(ctx context.Context, key string, opts ...jetstream.KVDeleteOpt) error) *KeyValue_Purge_Call { + _c.Call.Return(run) + return _c +} + +// PurgeDeletes provides a mock function for the type KeyValue +func (_mock *KeyValue) PurgeDeletes(ctx context.Context, opts ...jetstream.KVPurgeOpt) error { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, opts) + } else { + tmpRet = _mock.Called(ctx) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for PurgeDeletes") + } + + var r0 error + if returnFunc, ok := ret.Get(0).(func(context.Context, ...jetstream.KVPurgeOpt) error); ok { + r0 = returnFunc(ctx, opts...) + } else { + r0 = ret.Error(0) + } + return r0 +} + +// KeyValue_PurgeDeletes_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PurgeDeletes' +type KeyValue_PurgeDeletes_Call struct { + *mock.Call +} + +// PurgeDeletes is a helper method to define mock.On call +// - ctx context.Context +// - opts ...jetstream.KVPurgeOpt +func (_e *KeyValue_Expecter) PurgeDeletes(ctx interface{}, opts ...interface{}) *KeyValue_PurgeDeletes_Call { + return &KeyValue_PurgeDeletes_Call{Call: _e.mock.On("PurgeDeletes", + append([]interface{}{ctx}, opts...)...)} +} + +func (_c *KeyValue_PurgeDeletes_Call) Run(run func(ctx context.Context, opts ...jetstream.KVPurgeOpt)) *KeyValue_PurgeDeletes_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []jetstream.KVPurgeOpt + var variadicArgs []jetstream.KVPurgeOpt + if len(args) > 1 { + variadicArgs = args[1].([]jetstream.KVPurgeOpt) + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *KeyValue_PurgeDeletes_Call) Return(err error) *KeyValue_PurgeDeletes_Call { + _c.Call.Return(err) + return _c +} + +func (_c *KeyValue_PurgeDeletes_Call) RunAndReturn(run func(ctx context.Context, opts ...jetstream.KVPurgeOpt) error) *KeyValue_PurgeDeletes_Call { + _c.Call.Return(run) + return _c +} + +// Put provides a mock function for the type KeyValue +func (_mock *KeyValue) Put(ctx context.Context, key string, value []byte) (uint64, error) { + ret := _mock.Called(ctx, key, value) + + if len(ret) == 0 { + panic("no return value specified for Put") + } + + var r0 uint64 + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []byte) (uint64, error)); ok { + return returnFunc(ctx, key, value) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []byte) uint64); ok { + r0 = returnFunc(ctx, key, value) + } else { + r0 = ret.Get(0).(uint64) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, []byte) error); ok { + r1 = returnFunc(ctx, key, value) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_Put_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Put' +type KeyValue_Put_Call struct { + *mock.Call +} + +// Put is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value []byte +func (_e *KeyValue_Expecter) Put(ctx interface{}, key interface{}, value interface{}) *KeyValue_Put_Call { + return &KeyValue_Put_Call{Call: _e.mock.On("Put", ctx, key, value)} +} + +func (_c *KeyValue_Put_Call) Run(run func(ctx context.Context, key string, value []byte)) *KeyValue_Put_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []byte + if args[2] != nil { + arg2 = args[2].([]byte) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *KeyValue_Put_Call) Return(v uint64, err error) *KeyValue_Put_Call { + _c.Call.Return(v, err) + return _c +} + +func (_c *KeyValue_Put_Call) RunAndReturn(run func(ctx context.Context, key string, value []byte) (uint64, error)) *KeyValue_Put_Call { + _c.Call.Return(run) + return _c +} + +// PutString provides a mock function for the type KeyValue +func (_mock *KeyValue) PutString(ctx context.Context, key string, value string) (uint64, error) { + ret := _mock.Called(ctx, key, value) + + if len(ret) == 0 { + panic("no return value specified for PutString") + } + + var r0 uint64 + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) (uint64, error)); ok { + return returnFunc(ctx, key, value) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, string) uint64); ok { + r0 = returnFunc(ctx, key, value) + } else { + r0 = ret.Get(0).(uint64) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = returnFunc(ctx, key, value) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_PutString_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'PutString' +type KeyValue_PutString_Call struct { + *mock.Call +} + +// PutString is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value string +func (_e *KeyValue_Expecter) PutString(ctx interface{}, key interface{}, value interface{}) *KeyValue_PutString_Call { + return &KeyValue_PutString_Call{Call: _e.mock.On("PutString", ctx, key, value)} +} + +func (_c *KeyValue_PutString_Call) Run(run func(ctx context.Context, key string, value string)) *KeyValue_PutString_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 string + if args[2] != nil { + arg2 = args[2].(string) + } + run( + arg0, + arg1, + arg2, + ) + }) + return _c +} + +func (_c *KeyValue_PutString_Call) Return(v uint64, err error) *KeyValue_PutString_Call { + _c.Call.Return(v, err) + return _c +} + +func (_c *KeyValue_PutString_Call) RunAndReturn(run func(ctx context.Context, key string, value string) (uint64, error)) *KeyValue_PutString_Call { + _c.Call.Return(run) + return _c +} + +// Status provides a mock function for the type KeyValue +func (_mock *KeyValue) Status(ctx context.Context) (jetstream.KeyValueStatus, error) { + ret := _mock.Called(ctx) + + if len(ret) == 0 { + panic("no return value specified for Status") + } + + var r0 jetstream.KeyValueStatus + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context) (jetstream.KeyValueStatus, error)); ok { + return returnFunc(ctx) + } + if returnFunc, ok := ret.Get(0).(func(context.Context) jetstream.KeyValueStatus); ok { + r0 = returnFunc(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(jetstream.KeyValueStatus) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = returnFunc(ctx) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_Status_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Status' +type KeyValue_Status_Call struct { + *mock.Call +} + +// Status is a helper method to define mock.On call +// - ctx context.Context +func (_e *KeyValue_Expecter) Status(ctx interface{}) *KeyValue_Status_Call { + return &KeyValue_Status_Call{Call: _e.mock.On("Status", ctx)} +} + +func (_c *KeyValue_Status_Call) Run(run func(ctx context.Context)) *KeyValue_Status_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + run( + arg0, + ) + }) + return _c +} + +func (_c *KeyValue_Status_Call) Return(keyValueStatus jetstream.KeyValueStatus, err error) *KeyValue_Status_Call { + _c.Call.Return(keyValueStatus, err) + return _c +} + +func (_c *KeyValue_Status_Call) RunAndReturn(run func(ctx context.Context) (jetstream.KeyValueStatus, error)) *KeyValue_Status_Call { + _c.Call.Return(run) + return _c +} + +// Update provides a mock function for the type KeyValue +func (_mock *KeyValue) Update(ctx context.Context, key string, value []byte, revision uint64) (uint64, error) { + ret := _mock.Called(ctx, key, value, revision) + + if len(ret) == 0 { + panic("no return value specified for Update") + } + + var r0 uint64 + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []byte, uint64) (uint64, error)); ok { + return returnFunc(ctx, key, value, revision) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, []byte, uint64) uint64); ok { + r0 = returnFunc(ctx, key, value, revision) + } else { + r0 = ret.Get(0).(uint64) + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, []byte, uint64) error); ok { + r1 = returnFunc(ctx, key, value, revision) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_Update_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Update' +type KeyValue_Update_Call struct { + *mock.Call +} + +// Update is a helper method to define mock.On call +// - ctx context.Context +// - key string +// - value []byte +// - revision uint64 +func (_e *KeyValue_Expecter) Update(ctx interface{}, key interface{}, value interface{}, revision interface{}) *KeyValue_Update_Call { + return &KeyValue_Update_Call{Call: _e.mock.On("Update", ctx, key, value, revision)} +} + +func (_c *KeyValue_Update_Call) Run(run func(ctx context.Context, key string, value []byte, revision uint64)) *KeyValue_Update_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []byte + if args[2] != nil { + arg2 = args[2].([]byte) + } + var arg3 uint64 + if args[3] != nil { + arg3 = args[3].(uint64) + } + run( + arg0, + arg1, + arg2, + arg3, + ) + }) + return _c +} + +func (_c *KeyValue_Update_Call) Return(v uint64, err error) *KeyValue_Update_Call { + _c.Call.Return(v, err) + return _c +} + +func (_c *KeyValue_Update_Call) RunAndReturn(run func(ctx context.Context, key string, value []byte, revision uint64) (uint64, error)) *KeyValue_Update_Call { + _c.Call.Return(run) + return _c +} + +// Watch provides a mock function for the type KeyValue +func (_mock *KeyValue) Watch(ctx context.Context, keys string, opts ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, keys, opts) + } else { + tmpRet = _mock.Called(ctx, keys) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for Watch") + } + + var r0 jetstream.KeyWatcher + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, string, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error)); ok { + return returnFunc(ctx, keys, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, string, ...jetstream.WatchOpt) jetstream.KeyWatcher); ok { + r0 = returnFunc(ctx, keys, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(jetstream.KeyWatcher) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, string, ...jetstream.WatchOpt) error); ok { + r1 = returnFunc(ctx, keys, opts...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_Watch_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Watch' +type KeyValue_Watch_Call struct { + *mock.Call +} + +// Watch is a helper method to define mock.On call +// - ctx context.Context +// - keys string +// - opts ...jetstream.WatchOpt +func (_e *KeyValue_Expecter) Watch(ctx interface{}, keys interface{}, opts ...interface{}) *KeyValue_Watch_Call { + return &KeyValue_Watch_Call{Call: _e.mock.On("Watch", + append([]interface{}{ctx, keys}, opts...)...)} +} + +func (_c *KeyValue_Watch_Call) Run(run func(ctx context.Context, keys string, opts ...jetstream.WatchOpt)) *KeyValue_Watch_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 string + if args[1] != nil { + arg1 = args[1].(string) + } + var arg2 []jetstream.WatchOpt + var variadicArgs []jetstream.WatchOpt + if len(args) > 2 { + variadicArgs = args[2].([]jetstream.WatchOpt) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *KeyValue_Watch_Call) Return(keyWatcher jetstream.KeyWatcher, err error) *KeyValue_Watch_Call { + _c.Call.Return(keyWatcher, err) + return _c +} + +func (_c *KeyValue_Watch_Call) RunAndReturn(run func(ctx context.Context, keys string, opts ...jetstream.WatchOpt) (jetstream.KeyWatcher, error)) *KeyValue_Watch_Call { + _c.Call.Return(run) + return _c +} + +// WatchAll provides a mock function for the type KeyValue +func (_mock *KeyValue) WatchAll(ctx context.Context, opts ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, opts) + } else { + tmpRet = _mock.Called(ctx) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for WatchAll") + } + + var r0 jetstream.KeyWatcher + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error)); ok { + return returnFunc(ctx, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, ...jetstream.WatchOpt) jetstream.KeyWatcher); ok { + r0 = returnFunc(ctx, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(jetstream.KeyWatcher) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, ...jetstream.WatchOpt) error); ok { + r1 = returnFunc(ctx, opts...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_WatchAll_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WatchAll' +type KeyValue_WatchAll_Call struct { + *mock.Call +} + +// WatchAll is a helper method to define mock.On call +// - ctx context.Context +// - opts ...jetstream.WatchOpt +func (_e *KeyValue_Expecter) WatchAll(ctx interface{}, opts ...interface{}) *KeyValue_WatchAll_Call { + return &KeyValue_WatchAll_Call{Call: _e.mock.On("WatchAll", + append([]interface{}{ctx}, opts...)...)} +} + +func (_c *KeyValue_WatchAll_Call) Run(run func(ctx context.Context, opts ...jetstream.WatchOpt)) *KeyValue_WatchAll_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []jetstream.WatchOpt + var variadicArgs []jetstream.WatchOpt + if len(args) > 1 { + variadicArgs = args[1].([]jetstream.WatchOpt) + } + arg1 = variadicArgs + run( + arg0, + arg1..., + ) + }) + return _c +} + +func (_c *KeyValue_WatchAll_Call) Return(keyWatcher jetstream.KeyWatcher, err error) *KeyValue_WatchAll_Call { + _c.Call.Return(keyWatcher, err) + return _c +} + +func (_c *KeyValue_WatchAll_Call) RunAndReturn(run func(ctx context.Context, opts ...jetstream.WatchOpt) (jetstream.KeyWatcher, error)) *KeyValue_WatchAll_Call { + _c.Call.Return(run) + return _c +} + +// WatchFiltered provides a mock function for the type KeyValue +func (_mock *KeyValue) WatchFiltered(ctx context.Context, keys []string, opts ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) { + var tmpRet mock.Arguments + if len(opts) > 0 { + tmpRet = _mock.Called(ctx, keys, opts) + } else { + tmpRet = _mock.Called(ctx, keys) + } + ret := tmpRet + + if len(ret) == 0 { + panic("no return value specified for WatchFiltered") + } + + var r0 jetstream.KeyWatcher + var r1 error + if returnFunc, ok := ret.Get(0).(func(context.Context, []string, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error)); ok { + return returnFunc(ctx, keys, opts...) + } + if returnFunc, ok := ret.Get(0).(func(context.Context, []string, ...jetstream.WatchOpt) jetstream.KeyWatcher); ok { + r0 = returnFunc(ctx, keys, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(jetstream.KeyWatcher) + } + } + if returnFunc, ok := ret.Get(1).(func(context.Context, []string, ...jetstream.WatchOpt) error); ok { + r1 = returnFunc(ctx, keys, opts...) + } else { + r1 = ret.Error(1) + } + return r0, r1 +} + +// KeyValue_WatchFiltered_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'WatchFiltered' +type KeyValue_WatchFiltered_Call struct { + *mock.Call +} + +// WatchFiltered is a helper method to define mock.On call +// - ctx context.Context +// - keys []string +// - opts ...jetstream.WatchOpt +func (_e *KeyValue_Expecter) WatchFiltered(ctx interface{}, keys interface{}, opts ...interface{}) *KeyValue_WatchFiltered_Call { + return &KeyValue_WatchFiltered_Call{Call: _e.mock.On("WatchFiltered", + append([]interface{}{ctx, keys}, opts...)...)} +} + +func (_c *KeyValue_WatchFiltered_Call) Run(run func(ctx context.Context, keys []string, opts ...jetstream.WatchOpt)) *KeyValue_WatchFiltered_Call { + _c.Call.Run(func(args mock.Arguments) { + var arg0 context.Context + if args[0] != nil { + arg0 = args[0].(context.Context) + } + var arg1 []string + if args[1] != nil { + arg1 = args[1].([]string) + } + var arg2 []jetstream.WatchOpt + var variadicArgs []jetstream.WatchOpt + if len(args) > 2 { + variadicArgs = args[2].([]jetstream.WatchOpt) + } + arg2 = variadicArgs + run( + arg0, + arg1, + arg2..., + ) + }) + return _c +} + +func (_c *KeyValue_WatchFiltered_Call) Return(keyWatcher jetstream.KeyWatcher, err error) *KeyValue_WatchFiltered_Call { + _c.Call.Return(keyWatcher, err) + return _c +} + +func (_c *KeyValue_WatchFiltered_Call) RunAndReturn(run func(ctx context.Context, keys []string, opts ...jetstream.WatchOpt) (jetstream.KeyWatcher, error)) *KeyValue_WatchFiltered_Call { + _c.Call.Return(run) + return _c +} diff --git a/services/web/mocks/key_value_entry.go b/services/web/mocks/key_value_entry.go new file mode 100644 index 0000000000..7decc5725a --- /dev/null +++ b/services/web/mocks/key_value_entry.go @@ -0,0 +1,349 @@ +// Code generated by mockery; DO NOT EDIT. +// github.com/vektra/mockery +// template: testify + +package mocks + +import ( + "time" + + "github.com/nats-io/nats.go/jetstream" + mock "github.com/stretchr/testify/mock" +) + +// NewKeyValueEntry creates a new instance of KeyValueEntry. 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 NewKeyValueEntry(t interface { + mock.TestingT + Cleanup(func()) +}) *KeyValueEntry { + mock := &KeyValueEntry{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} + +// KeyValueEntry is an autogenerated mock type for the KeyValueEntry type +type KeyValueEntry struct { + mock.Mock +} + +type KeyValueEntry_Expecter struct { + mock *mock.Mock +} + +func (_m *KeyValueEntry) EXPECT() *KeyValueEntry_Expecter { + return &KeyValueEntry_Expecter{mock: &_m.Mock} +} + +// Bucket provides a mock function for the type KeyValueEntry +func (_mock *KeyValueEntry) Bucket() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Bucket") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// KeyValueEntry_Bucket_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Bucket' +type KeyValueEntry_Bucket_Call struct { + *mock.Call +} + +// Bucket is a helper method to define mock.On call +func (_e *KeyValueEntry_Expecter) Bucket() *KeyValueEntry_Bucket_Call { + return &KeyValueEntry_Bucket_Call{Call: _e.mock.On("Bucket")} +} + +func (_c *KeyValueEntry_Bucket_Call) Run(run func()) *KeyValueEntry_Bucket_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *KeyValueEntry_Bucket_Call) Return(s string) *KeyValueEntry_Bucket_Call { + _c.Call.Return(s) + return _c +} + +func (_c *KeyValueEntry_Bucket_Call) RunAndReturn(run func() string) *KeyValueEntry_Bucket_Call { + _c.Call.Return(run) + return _c +} + +// Created provides a mock function for the type KeyValueEntry +func (_mock *KeyValueEntry) Created() time.Time { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Created") + } + + var r0 time.Time + if returnFunc, ok := ret.Get(0).(func() time.Time); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(time.Time) + } + return r0 +} + +// KeyValueEntry_Created_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Created' +type KeyValueEntry_Created_Call struct { + *mock.Call +} + +// Created is a helper method to define mock.On call +func (_e *KeyValueEntry_Expecter) Created() *KeyValueEntry_Created_Call { + return &KeyValueEntry_Created_Call{Call: _e.mock.On("Created")} +} + +func (_c *KeyValueEntry_Created_Call) Run(run func()) *KeyValueEntry_Created_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *KeyValueEntry_Created_Call) Return(time1 time.Time) *KeyValueEntry_Created_Call { + _c.Call.Return(time1) + return _c +} + +func (_c *KeyValueEntry_Created_Call) RunAndReturn(run func() time.Time) *KeyValueEntry_Created_Call { + _c.Call.Return(run) + return _c +} + +// Delta provides a mock function for the type KeyValueEntry +func (_mock *KeyValueEntry) Delta() uint64 { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Delta") + } + + var r0 uint64 + if returnFunc, ok := ret.Get(0).(func() uint64); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(uint64) + } + return r0 +} + +// KeyValueEntry_Delta_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Delta' +type KeyValueEntry_Delta_Call struct { + *mock.Call +} + +// Delta is a helper method to define mock.On call +func (_e *KeyValueEntry_Expecter) Delta() *KeyValueEntry_Delta_Call { + return &KeyValueEntry_Delta_Call{Call: _e.mock.On("Delta")} +} + +func (_c *KeyValueEntry_Delta_Call) Run(run func()) *KeyValueEntry_Delta_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *KeyValueEntry_Delta_Call) Return(v uint64) *KeyValueEntry_Delta_Call { + _c.Call.Return(v) + return _c +} + +func (_c *KeyValueEntry_Delta_Call) RunAndReturn(run func() uint64) *KeyValueEntry_Delta_Call { + _c.Call.Return(run) + return _c +} + +// Key provides a mock function for the type KeyValueEntry +func (_mock *KeyValueEntry) Key() string { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Key") + } + + var r0 string + if returnFunc, ok := ret.Get(0).(func() string); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(string) + } + return r0 +} + +// KeyValueEntry_Key_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Key' +type KeyValueEntry_Key_Call struct { + *mock.Call +} + +// Key is a helper method to define mock.On call +func (_e *KeyValueEntry_Expecter) Key() *KeyValueEntry_Key_Call { + return &KeyValueEntry_Key_Call{Call: _e.mock.On("Key")} +} + +func (_c *KeyValueEntry_Key_Call) Run(run func()) *KeyValueEntry_Key_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *KeyValueEntry_Key_Call) Return(s string) *KeyValueEntry_Key_Call { + _c.Call.Return(s) + return _c +} + +func (_c *KeyValueEntry_Key_Call) RunAndReturn(run func() string) *KeyValueEntry_Key_Call { + _c.Call.Return(run) + return _c +} + +// Operation provides a mock function for the type KeyValueEntry +func (_mock *KeyValueEntry) Operation() jetstream.KeyValueOp { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Operation") + } + + var r0 jetstream.KeyValueOp + if returnFunc, ok := ret.Get(0).(func() jetstream.KeyValueOp); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(jetstream.KeyValueOp) + } + return r0 +} + +// KeyValueEntry_Operation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Operation' +type KeyValueEntry_Operation_Call struct { + *mock.Call +} + +// Operation is a helper method to define mock.On call +func (_e *KeyValueEntry_Expecter) Operation() *KeyValueEntry_Operation_Call { + return &KeyValueEntry_Operation_Call{Call: _e.mock.On("Operation")} +} + +func (_c *KeyValueEntry_Operation_Call) Run(run func()) *KeyValueEntry_Operation_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *KeyValueEntry_Operation_Call) Return(keyValueOp jetstream.KeyValueOp) *KeyValueEntry_Operation_Call { + _c.Call.Return(keyValueOp) + return _c +} + +func (_c *KeyValueEntry_Operation_Call) RunAndReturn(run func() jetstream.KeyValueOp) *KeyValueEntry_Operation_Call { + _c.Call.Return(run) + return _c +} + +// Revision provides a mock function for the type KeyValueEntry +func (_mock *KeyValueEntry) Revision() uint64 { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Revision") + } + + var r0 uint64 + if returnFunc, ok := ret.Get(0).(func() uint64); ok { + r0 = returnFunc() + } else { + r0 = ret.Get(0).(uint64) + } + return r0 +} + +// KeyValueEntry_Revision_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Revision' +type KeyValueEntry_Revision_Call struct { + *mock.Call +} + +// Revision is a helper method to define mock.On call +func (_e *KeyValueEntry_Expecter) Revision() *KeyValueEntry_Revision_Call { + return &KeyValueEntry_Revision_Call{Call: _e.mock.On("Revision")} +} + +func (_c *KeyValueEntry_Revision_Call) Run(run func()) *KeyValueEntry_Revision_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *KeyValueEntry_Revision_Call) Return(v uint64) *KeyValueEntry_Revision_Call { + _c.Call.Return(v) + return _c +} + +func (_c *KeyValueEntry_Revision_Call) RunAndReturn(run func() uint64) *KeyValueEntry_Revision_Call { + _c.Call.Return(run) + return _c +} + +// Value provides a mock function for the type KeyValueEntry +func (_mock *KeyValueEntry) Value() []byte { + ret := _mock.Called() + + if len(ret) == 0 { + panic("no return value specified for Value") + } + + var r0 []byte + if returnFunc, ok := ret.Get(0).(func() []byte); ok { + r0 = returnFunc() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]byte) + } + } + return r0 +} + +// KeyValueEntry_Value_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Value' +type KeyValueEntry_Value_Call struct { + *mock.Call +} + +// Value is a helper method to define mock.On call +func (_e *KeyValueEntry_Expecter) Value() *KeyValueEntry_Value_Call { + return &KeyValueEntry_Value_Call{Call: _e.mock.On("Value")} +} + +func (_c *KeyValueEntry_Value_Call) Run(run func()) *KeyValueEntry_Value_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *KeyValueEntry_Value_Call) Return(bytes []byte) *KeyValueEntry_Value_Call { + _c.Call.Return(bytes) + return _c +} + +func (_c *KeyValueEntry_Value_Call) RunAndReturn(run func() []byte) *KeyValueEntry_Value_Call { + _c.Call.Return(run) + return _c +} diff --git a/services/web/pkg/announcement/announcement.go b/services/web/pkg/announcement/announcement.go new file mode 100644 index 0000000000..085fd16a71 --- /dev/null +++ b/services/web/pkg/announcement/announcement.go @@ -0,0 +1,69 @@ +// Package announcement persists and serves the web announcement banner. +package announcement + +import ( + "context" + "encoding/json" + "errors" + + "github.com/nats-io/nats.go/jetstream" +) + +// _storeKey is the single key under which the announcement is persisted. +const _storeKey = "announcement" + +// Announcement is a banner message shown above the top bar to all users. +type Announcement struct { + // Enabled controls whether the announcement is live (injected into config.json). + Enabled bool `json:"enabled"` + BannerText string `json:"bannerText"` + InfoText string `json:"infoText"` +} + +// Store persists a single announcement in a NATS JetStream key-value bucket. +type Store struct { + kv jetstream.KeyValue +} + +// NewStore returns a new announcement Store backed by the given key-value bucket. +func NewStore(kv jetstream.KeyValue) *Store { + return &Store{kv: kv} +} + +// Get returns the currently stored announcement. An unset announcement is returned as the zero value. +func (s *Store) Get(ctx context.Context) (Announcement, error) { + var a Announcement + + entry, err := s.kv.Get(ctx, _storeKey) + if err != nil { + if errors.Is(err, jetstream.ErrKeyNotFound) { + return a, nil + } + return a, err + } + + if err := json.Unmarshal(entry.Value(), &a); err != nil { + return a, err + } + + return a, nil +} + +// Set persists the given announcement, overwriting any existing one. +func (s *Store) Set(ctx context.Context, a Announcement) error { + value, err := json.Marshal(a) + if err != nil { + return err + } + + _, err = s.kv.Put(ctx, _storeKey, value) + return err +} + +// Delete removes the stored announcement. Deleting a missing announcement is a no-op. +func (s *Store) Delete(ctx context.Context) error { + if err := s.kv.Delete(ctx, _storeKey); err != nil && !errors.Is(err, jetstream.ErrKeyNotFound) { + return err + } + return nil +} diff --git a/services/web/pkg/announcement/announcement_suite_test.go b/services/web/pkg/announcement/announcement_suite_test.go new file mode 100644 index 0000000000..0931590df3 --- /dev/null +++ b/services/web/pkg/announcement/announcement_suite_test.go @@ -0,0 +1,13 @@ +package announcement_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAnnouncement(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Announcement Suite") +} diff --git a/services/web/pkg/announcement/announcement_test.go b/services/web/pkg/announcement/announcement_test.go new file mode 100644 index 0000000000..2cfe22dd44 --- /dev/null +++ b/services/web/pkg/announcement/announcement_test.go @@ -0,0 +1,207 @@ +package announcement_test + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + cs3permissions "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1" + cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/nats-io/nats.go/jetstream" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + cs3mocks "github.com/opencloud-eu/reva/v2/tests/cs3mocks/mocks" + "github.com/stretchr/testify/mock" + "google.golang.org/grpc" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/web/mocks" + "github.com/opencloud-eu/opencloud/services/web/pkg/announcement" +) + +func newGatewaySelector(allowed bool) pool.Selectable[gateway.GatewayAPIClient] { + code := cs3rpc.Code_CODE_OK + name := "announcement-test-allowed" + if !allowed { + code = cs3rpc.Code_CODE_PERMISSION_DENIED + name = "announcement-test-denied" + } + + client := &cs3mocks.GatewayAPIClient{} + client.On("CheckPermission", mock.Anything, mock.Anything).Return( + &cs3permissions.CheckPermissionResponse{Status: &cs3rpc.Status{Code: code}}, nil) + + // pool.GetSelector caches by name, so allow/deny must use distinct names + return pool.GetSelector[gateway.GatewayAPIClient]( + name, + "eu.opencloud.api.gateway", + func(cc grpc.ClientConnInterface) gateway.GatewayAPIClient { return client }, + ) +} + +func withUser(r *http.Request) *http.Request { + return r.WithContext(revactx.ContextSetUser(r.Context(), &userpb.User{ + Id: &userpb.UserId{OpaqueId: "user"}, + })) +} + +func newService(store *announcement.Store, allowed bool) announcement.Service { + svc, err := announcement.NewService(announcement.ServiceOptions{}. + WithLogger(log.NopLogger()). + WithStore(store). + WithGatewaySelector(newGatewaySelector(allowed))) + Expect(err).ToNot(HaveOccurred()) + return svc +} + +var _ = Describe("Store", func() { + It("reads the stored announcement", func() { + entry := mocks.NewKeyValueEntry(GinkgoT()) + entry.EXPECT().Value().Return([]byte(`{"enabled":true,"bannerText":"hello","infoText":"world"}`)) + kv := mocks.NewKeyValue(GinkgoT()) + kv.EXPECT().Get(mock.Anything, "announcement").Return(entry, nil) + + got, err := announcement.NewStore(kv).Get(context.Background()) + Expect(err).ToNot(HaveOccurred()) + Expect(got.Enabled).To(BeTrue()) + Expect(got.BannerText).To(Equal("hello")) + Expect(got.InfoText).To(Equal("world")) + }) + + It("returns the zero value when unset", func() { + kv := mocks.NewKeyValue(GinkgoT()) + kv.EXPECT().Get(mock.Anything, "announcement").Return(nil, jetstream.ErrKeyNotFound) + + got, err := announcement.NewStore(kv).Get(context.Background()) + Expect(err).ToNot(HaveOccurred()) + Expect(got.BannerText).To(BeEmpty()) + }) + + It("writes the announcement", func() { + kv := mocks.NewKeyValue(GinkgoT()) + kv.EXPECT().Put(mock.Anything, "announcement", mock.Anything).Return(uint64(1), nil) + + Expect(announcement.NewStore(kv).Set(context.Background(), announcement.Announcement{BannerText: "hello"})).To(Succeed()) + }) + + It("deletes the announcement", func() { + kv := mocks.NewKeyValue(GinkgoT()) + kv.EXPECT().Delete(mock.Anything, "announcement").Return(nil) + + Expect(announcement.NewStore(kv).Delete(context.Background())).To(Succeed()) + }) + + It("treats deleting a missing announcement as a no-op", func() { + kv := mocks.NewKeyValue(GinkgoT()) + kv.EXPECT().Delete(mock.Anything, "announcement").Return(jetstream.ErrKeyNotFound) + + Expect(announcement.NewStore(kv).Delete(context.Background())).To(Succeed()) + }) +}) + +var _ = Describe("Service", func() { + Describe("NewService", func() { + It("fails when options are missing", func() { + _, err := announcement.NewService(announcement.ServiceOptions{}) + Expect(err).To(HaveOccurred()) + }) + + It("succeeds when options are valid", func() { + _, err := announcement.NewService(announcement.ServiceOptions{}. + WithStore(announcement.NewStore(mocks.NewKeyValue(GinkgoT()))). + WithGatewaySelector(newGatewaySelector(true))) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Describe("Get", func() { + It("returns the full stored announcement when permitted", func() { + entry := mocks.NewKeyValueEntry(GinkgoT()) + entry.EXPECT().Value().Return([]byte(`{"enabled":true,"bannerText":"hello","infoText":"world"}`)) + kv := mocks.NewKeyValue(GinkgoT()) + kv.EXPECT().Get(mock.Anything, "announcement").Return(entry, nil) + + req := withUser(httptest.NewRequest(http.MethodGet, "/announcement", nil)) + resp := httptest.NewRecorder() + + newService(announcement.NewStore(kv), true).Get(resp, req) + + Expect(resp.Code).To(Equal(http.StatusOK)) + var got announcement.Announcement + Expect(json.Unmarshal(resp.Body.Bytes(), &got)).To(Succeed()) + Expect(got.Enabled).To(BeTrue()) + Expect(got.BannerText).To(Equal("hello")) + Expect(got.InfoText).To(Equal("world")) + }) + + It("is forbidden without permission", func() { + req := withUser(httptest.NewRequest(http.MethodGet, "/announcement", nil)) + resp := httptest.NewRecorder() + + newService(announcement.NewStore(mocks.NewKeyValue(GinkgoT())), false).Get(resp, req) + + Expect(resp.Code).To(Equal(http.StatusForbidden)) + }) + }) + + Describe("Set", func() { + It("persists the message when permitted", func() { + kv := mocks.NewKeyValue(GinkgoT()) + kv.EXPECT().Put(mock.Anything, "announcement", mock.Anything).Return(uint64(1), nil) + + req := withUser(httptest.NewRequest(http.MethodPut, "/announcement", strings.NewReader(`{"bannerText":"hello"}`))) + resp := httptest.NewRecorder() + + newService(announcement.NewStore(kv), true).Set(resp, req) + + Expect(resp.Code).To(Equal(http.StatusNoContent)) + }) + + It("is forbidden without permission", func() { + req := withUser(httptest.NewRequest(http.MethodPut, "/announcement", strings.NewReader(`{"bannerText":"hello"}`))) + resp := httptest.NewRecorder() + + newService(announcement.NewStore(mocks.NewKeyValue(GinkgoT())), false).Set(resp, req) + + Expect(resp.Code).To(Equal(http.StatusForbidden)) + }) + + It("rejects an invalid body", func() { + req := withUser(httptest.NewRequest(http.MethodPut, "/announcement", strings.NewReader(`not json`))) + resp := httptest.NewRecorder() + + newService(announcement.NewStore(mocks.NewKeyValue(GinkgoT())), true).Set(resp, req) + + Expect(resp.Code).To(Equal(http.StatusBadRequest)) + }) + + It("rejects an oversized body", func() { + body := `{"bannerText":"` + strings.Repeat("a", 300000) + `"}` + req := withUser(httptest.NewRequest(http.MethodPut, "/announcement", strings.NewReader(body))) + resp := httptest.NewRecorder() + + newService(announcement.NewStore(mocks.NewKeyValue(GinkgoT())), true).Set(resp, req) + + Expect(resp.Code).To(Equal(http.StatusRequestEntityTooLarge)) + }) + }) + + Describe("Set with an empty banner text", func() { + It("removes the stored announcement", func() { + kv := mocks.NewKeyValue(GinkgoT()) + kv.EXPECT().Delete(mock.Anything, "announcement").Return(nil) + + req := withUser(httptest.NewRequest(http.MethodPut, "/announcement", strings.NewReader(`{"enabled":false,"bannerText":"","infoText":""}`))) + resp := httptest.NewRecorder() + newService(announcement.NewStore(kv), true).Set(resp, req) + + Expect(resp.Code).To(Equal(http.StatusNoContent)) + }) + }) +}) diff --git a/services/web/pkg/announcement/service.go b/services/web/pkg/announcement/service.go new file mode 100644 index 0000000000..d20512eb18 --- /dev/null +++ b/services/web/pkg/announcement/service.go @@ -0,0 +1,192 @@ +package announcement + +import ( + "encoding/json" + "net/http" + + gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + permissionsapi "github.com/cs3org/go-cs3apis/cs3/permissions/v1beta1" + rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" + "github.com/pkg/errors" + + revactx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" + + "github.com/opencloud-eu/opencloud/pkg/log" +) + +// _permission is the settings permission required to read and manage the announcement. +const _permission = "Announcement.ReadWrite" + +// _maxBodySize caps the announcement request body. The info text is Markdown and ends up in +// the public config.json that every client loads on bootstrap, so it must stay small. +const _maxBodySize = 50 << 10 // 50 KiB + +// ServiceOptions defines the options to configure the Service. +type ServiceOptions struct { + logger log.Logger + store *Store + gatewaySelector pool.Selectable[gateway.GatewayAPIClient] +} + +// WithLogger sets the logger. +func (o ServiceOptions) WithLogger(l log.Logger) ServiceOptions { + o.logger = l + return o +} + +// WithStore sets the announcement store. +func (o ServiceOptions) WithStore(s *Store) ServiceOptions { + o.store = s + return o +} + +// WithGatewaySelector sets the gateway selector. +func (o ServiceOptions) WithGatewaySelector(gws pool.Selectable[gateway.GatewayAPIClient]) ServiceOptions { + o.gatewaySelector = gws + return o +} + +// validate validates the input parameters. +func (o ServiceOptions) validate() error { + if o.store == nil { + return errors.New("store is required") + } + + if o.gatewaySelector == nil { + return errors.New("gatewaySelector is required") + } + + return nil +} + +// Service exposes the http handlers to manage the announcement. +type Service struct { + logger log.Logger + store *Store + gatewaySelector pool.Selectable[gateway.GatewayAPIClient] +} + +// NewService initializes a new Service. +func NewService(options ServiceOptions) (Service, error) { + if err := options.validate(); err != nil { + return Service{}, err + } + + return Service{ + logger: options.logger, + store: options.store, + gatewaySelector: options.gatewaySelector, + }, nil +} + +// logError logs a server-side error together with the request id, so it can be correlated with +// the request log line emitted by the logging middleware. +func (s Service) logError(r *http.Request, err error, msg string) { + s.logger.Error().Err(err).Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Msg(msg) +} + +// Get returns the full stored announcement (including disabled ones) for management. +func (s Service) Get(w http.ResponseWriter, r *http.Request) { + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + s.logError(r, err, "could not select next gateway client") + w.WriteHeader(http.StatusInternalServerError) + return + } + + user, ok := revactx.ContextGetUser(r.Context()) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{ + Permission: _permission, + SubjectRef: &permissionsapi.SubjectReference{ + Spec: &permissionsapi.SubjectReference_UserId{ + UserId: user.GetId(), + }, + }, + }) + if err != nil { + s.logError(r, err, "could not check permission") + w.WriteHeader(http.StatusInternalServerError) + return + } + if rsp.GetStatus().GetCode() != rpc.Code_CODE_OK { + w.WriteHeader(http.StatusForbidden) + return + } + + a, err := s.store.Get(r.Context()) + if err != nil { + s.logError(r, err, "could not read announcement from store") + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(a); err != nil { + s.logError(r, err, "could not encode announcement") + w.WriteHeader(http.StatusInternalServerError) + } +} + +// Set persists the announcement provided in the request body. +func (s Service) Set(w http.ResponseWriter, r *http.Request) { + gatewayClient, err := s.gatewaySelector.Next() + if err != nil { + s.logError(r, err, "could not select next gateway client") + w.WriteHeader(http.StatusInternalServerError) + return + } + + user, ok := revactx.ContextGetUser(r.Context()) + if !ok { + w.WriteHeader(http.StatusUnauthorized) + return + } + rsp, err := gatewayClient.CheckPermission(r.Context(), &permissionsapi.CheckPermissionRequest{ + Permission: _permission, + SubjectRef: &permissionsapi.SubjectReference{ + Spec: &permissionsapi.SubjectReference_UserId{ + UserId: user.GetId(), + }, + }, + }) + if err != nil { + s.logError(r, err, "could not check permission") + w.WriteHeader(http.StatusInternalServerError) + return + } + if rsp.GetStatus().GetCode() != rpc.Code_CODE_OK { + w.WriteHeader(http.StatusForbidden) + return + } + + var body Announcement + if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, _maxBodySize)).Decode(&body); err != nil { + var maxBytesErr *http.MaxBytesError + if errors.As(err, &maxBytesErr) { + w.WriteHeader(http.StatusRequestEntityTooLarge) + return + } + w.WriteHeader(http.StatusBadRequest) + return + } + + // an announcement without a banner text is nothing to show, so remove it entirely + if body.BannerText == "" { + if err := s.store.Delete(r.Context()); err != nil { + s.logError(r, err, "could not delete announcement from store") + w.WriteHeader(http.StatusInternalServerError) + return + } + } else if err := s.store.Set(r.Context(), body); err != nil { + s.logError(r, err, "could not write announcement to store") + w.WriteHeader(http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/services/web/pkg/config/config.go b/services/web/pkg/config/config.go index 91630f9baf..b643c56f8c 100644 --- a/services/web/pkg/config/config.go +++ b/services/web/pkg/config/config.go @@ -25,9 +25,22 @@ type Config struct { TokenManager *TokenManager `yaml:"token_manager"` GatewayAddress string `yaml:"gateway_addr" env:"WEB_GATEWAY_GRPC_ADDR" desc:"The bind address of the GRPC service." introductionVersion:"1.0.0"` + Store Store `yaml:"store"` Context context.Context `yaml:"-"` } +// Store configures the NATS JetStream key-value store used to keep runtime managed web settings, +// e.g. the announcement banner. +type Store struct { + Nodes []string `yaml:"nodes" env:"OC_PERSISTENT_STORE_NODES;WEB_STORE_NODES" desc:"A list of nodes to access the NATS JetStream store. See the Environment Variable Types description for more details." introductionVersion:"%%NEXT%%"` + Database string `yaml:"database" env:"WEB_STORE_DATABASE" desc:"The bucket name the store should use." introductionVersion:"%%NEXT%%"` + AuthUsername string `yaml:"username" env:"OC_PERSISTENT_STORE_AUTH_USERNAME;WEB_STORE_AUTH_USERNAME" desc:"The username to authenticate with the store." introductionVersion:"%%NEXT%%"` + AuthPassword string `yaml:"password" env:"OC_PERSISTENT_STORE_AUTH_PASSWORD;WEB_STORE_AUTH_PASSWORD" desc:"The password to authenticate with the store." introductionVersion:"%%NEXT%%"` + EnableTLS bool `yaml:"enable_tls" env:"OC_PERSISTENT_STORE_ENABLE_TLS;WEB_STORE_ENABLE_TLS" desc:"Enable TLS for the connection to the store." introductionVersion:"%%NEXT%%"` + TLSInsecure bool `yaml:"tls_insecure" env:"OC_INSECURE;OC_PERSISTENT_STORE_TLS_INSECURE;WEB_STORE_TLS_INSECURE" desc:"Whether to verify the server TLS certificates." introductionVersion:"%%NEXT%%"` + TLSRootCACertificate string `yaml:"tls_root_ca_certificate" env:"OC_PERSISTENT_STORE_TLS_ROOT_CA_CERTIFICATE;WEB_STORE_TLS_ROOT_CA_CERTIFICATE" desc:"The root CA certificate used to validate the server's TLS certificate. If provided WEB_STORE_TLS_INSECURE will be seen as false." introductionVersion:"%%NEXT%%"` +} + // Asset defines the available asset configuration. type Asset struct { CorePath string `yaml:"core_path" env:"WEB_ASSET_CORE_PATH" desc:"Serve OpenCloud Web assets from a path on the filesystem instead of the builtin assets. If not defined, the root directory derives from $OC_BASE_DATA_PATH/web/assets/core" introductionVersion:"1.0.0"` diff --git a/services/web/pkg/config/defaults/defaultconfig.go b/services/web/pkg/config/defaults/defaultconfig.go index 4c18aae0bd..0460bcef26 100644 --- a/services/web/pkg/config/defaults/defaultconfig.go +++ b/services/web/pkg/config/defaults/defaultconfig.go @@ -85,6 +85,10 @@ func DefaultConfig() *config.Config { ThemesPath: filepath.Join(defaults.BaseDataPath(), "web/assets/themes"), }, GatewayAddress: "eu.opencloud.api.gateway", + Store: config.Store{ + Nodes: []string{"127.0.0.1:9233"}, + Database: "web", + }, Web: config.Web{ ThemeServer: "https://localhost:9200", ThemePath: "/themes/opencloud/theme.json", diff --git a/services/web/pkg/config/options.go b/services/web/pkg/config/options.go index 736b232b20..3a4f037ed9 100644 --- a/services/web/pkg/config/options.go +++ b/services/web/pkg/config/options.go @@ -2,6 +2,7 @@ package config // Options are the option for the web type Options struct { + Announcement *Announcement `json:"announcement,omitempty" yaml:"-"` AccountEditLink *AccountEditLink `json:"accountEditLink,omitempty" yaml:"accountEditLink"` DisableFeedbackLink bool `json:"disableFeedbackLink,omitempty" yaml:"disableFeedbackLink" env:"WEB_OPTION_DISABLE_FEEDBACK_LINK" desc:"Set this option to 'true' to disable the feedback link in the top bar. Keeping it enabled by setting the value to 'false' or with the absence of the option, allows OpenCloud to get feedback from your user base through a dedicated survey website." introductionVersion:"1.0.0"` DisableSponsorLink bool `json:"disableSponsorLink,omitempty" yaml:"disableSponsorLink" env:"WEB_OPTION_DISABLE_SPONSOR_LINK" desc:"Set this option to 'true' to disable the sponsor link in the left sidebar. Keeping it enabled by setting the value to 'false' or by leaving the option unset allows OpenCloud to get support from the community through a dedicated sponsorship program on GitHub." introductionVersion:"7.3.0"` @@ -23,6 +24,16 @@ type Options struct { OxAppSuite *OxAppSuite `json:"oxAppSuite,omitempty" yaml:"oxAppSuite"` } +// Announcement is a banner message shown above the top bar to all users. It is managed at runtime +// (via the web service's store and the admin settings UI) and injected into config.json here; a +// value configured statically is ignored. +type Announcement struct { + // BannerText is the short line shown in the banner. + BannerText string `json:"bannerText,omitempty" yaml:"-"` + // InfoText is the (Markdown) detail shown in a dialog when the banner is clicked. + InfoText string `json:"infoText,omitempty" yaml:"-"` +} + // AccountEditLink are the AccountEditLink options type AccountEditLink struct { Href string `json:"href,omitempty" yaml:"href" env:"WEB_OPTION_ACCOUNT_EDIT_LINK_HREF" desc:"Set a different target URL for the edit link. Make sure to prepend it with 'http(s)://'." introductionVersion:"1.0.0"` diff --git a/services/web/pkg/server/http/server.go b/services/web/pkg/server/http/server.go index 25f210b13b..0b9621c798 100644 --- a/services/web/pkg/server/http/server.go +++ b/services/web/pkg/server/http/server.go @@ -1,20 +1,26 @@ package http import ( + "errors" "fmt" "path" + "strings" chimiddleware "github.com/go-chi/chi/v5/middleware" + "github.com/nats-io/nats.go" + "github.com/nats-io/nats.go/jetstream" "github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool" "go-micro.dev/v4" "github.com/opencloud-eu/opencloud/pkg/cors" "github.com/opencloud-eu/opencloud/pkg/middleware" + natspkg "github.com/opencloud-eu/opencloud/pkg/nats" "github.com/opencloud-eu/opencloud/pkg/registry" "github.com/opencloud-eu/opencloud/pkg/service/http" "github.com/opencloud-eu/opencloud/pkg/version" "github.com/opencloud-eu/opencloud/pkg/x/io/fsx" "github.com/opencloud-eu/opencloud/services/web" + "github.com/opencloud-eu/opencloud/services/web/pkg/announcement" "github.com/opencloud-eu/opencloud/services/web/pkg/apps" svc "github.com/opencloud-eu/opencloud/services/web/pkg/service/v0" ) @@ -76,11 +82,38 @@ func Server(opts ...Option) (http.Service, error) { fsx.NewBasePathFs(fsx.FromIOFS(web.Assets), "assets/themes"), ) + // NATS JetStream key-value store for runtime managed web settings, e.g. the announcement banner. + // Connect eagerly and fail fast: an unreachable store means the feature would be broken, so a + // clear startup error is preferable to silently degrading. + natsConn, err := nats.Connect( + strings.Join(options.Config.Store.Nodes, ","), + natspkg.Secure(options.Config.Store.EnableTLS, options.Config.Store.TLSInsecure, options.Config.Store.TLSRootCACertificate), + nats.UserInfo(options.Config.Store.AuthUsername, options.Config.Store.AuthPassword), + ) + if err != nil { + return http.Service{}, fmt.Errorf("could not connect to nats for the announcement store: %w", err) + } + js, err := jetstream.New(natsConn) + if err != nil { + return http.Service{}, fmt.Errorf("could not create jetstream context for the announcement store: %w", err) + } + kv, err := js.KeyValue(options.Context, options.Config.Store.Database) + if err != nil { + if !errors.Is(err, jetstream.ErrBucketNotFound) { + return http.Service{}, fmt.Errorf("could not open the announcement store bucket %q: %w", options.Config.Store.Database, err) + } + if kv, err = js.CreateKeyValue(options.Context, jetstream.KeyValueConfig{Bucket: options.Config.Store.Database}); err != nil { + return http.Service{}, fmt.Errorf("could not create the announcement store bucket %q: %w", options.Config.Store.Database, err) + } + } + announcementStore := announcement.NewStore(kv) + handle, err := svc.NewService( svc.Logger(options.Logger), svc.CoreFS(coreFS.IOFS()), svc.AppFS(appsFS.IOFS()), svc.ThemeFS(themeFS), + svc.AnnouncementStore(announcementStore), svc.AppsHTTPEndpoint(_customAppsEndpoint), svc.Config(options.Config), svc.GatewaySelector(gatewaySelector), diff --git a/services/web/pkg/service/v0/option.go b/services/web/pkg/service/v0/option.go index 033c139bf7..dcacf6048e 100644 --- a/services/web/pkg/service/v0/option.go +++ b/services/web/pkg/service/v0/option.go @@ -10,6 +10,7 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/x/io/fsx" + "github.com/opencloud-eu/opencloud/services/web/pkg/announcement" "github.com/opencloud-eu/opencloud/services/web/pkg/config" ) @@ -18,15 +19,16 @@ type Option func(o *Options) // Options define the available options for this package. type Options struct { - Logger log.Logger - Config *config.Config - Middleware []func(http.Handler) http.Handler - GatewaySelector pool.Selectable[gateway.GatewayAPIClient] - TraceProvider trace.TracerProvider - AppsHTTPEndpoint string - CoreFS fs.FS - AppFS fs.FS - ThemeFS *fsx.FallbackFS + Logger log.Logger + Config *config.Config + Middleware []func(http.Handler) http.Handler + GatewaySelector pool.Selectable[gateway.GatewayAPIClient] + TraceProvider trace.TracerProvider + AppsHTTPEndpoint string + CoreFS fs.FS + AppFS fs.FS + ThemeFS *fsx.FallbackFS + AnnouncementStore *announcement.Store } // newOptions initializes the available default options. @@ -89,6 +91,13 @@ func ThemeFS(val *fsx.FallbackFS) Option { } } +// AnnouncementStore provides a function to set the announcement store option. +func AnnouncementStore(val *announcement.Store) Option { + return func(o *Options) { + o.AnnouncementStore = val + } +} + // AppsHTTPEndpoint provides a function to set the appsHTTPEndpoint option. func AppsHTTPEndpoint(val string) Option { return func(o *Options) { diff --git a/services/web/pkg/service/v0/service.go b/services/web/pkg/service/v0/service.go index e21e9bc637..5454c15c07 100644 --- a/services/web/pkg/service/v0/service.go +++ b/services/web/pkg/service/v0/service.go @@ -1,6 +1,7 @@ package svc import ( + "context" "encoding/json" "io/fs" "net/http" @@ -19,6 +20,7 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/middleware" "github.com/opencloud-eu/opencloud/pkg/tracing" + "github.com/opencloud-eu/opencloud/services/web/pkg/announcement" "github.com/opencloud-eu/opencloud/services/web/pkg/assets" "github.com/opencloud-eu/opencloud/services/web/pkg/config" "github.com/opencloud-eu/opencloud/services/web/pkg/theme" @@ -50,10 +52,11 @@ func NewService(opts ...Option) (Service, error) { ) svc := Web{ - logger: options.Logger, - config: options.Config, - mux: m, - gatewaySelector: options.GatewaySelector, + logger: options.Logger, + config: options.Config, + mux: m, + gatewaySelector: options.GatewaySelector, + announcementStore: options.AnnouncementStore, } themeService, err := theme.NewService( @@ -65,6 +68,16 @@ func NewService(opts ...Option) (Service, error) { return svc, err } + announcementService, err := announcement.NewService( + announcement.ServiceOptions{}. + WithLogger(options.Logger). + WithStore(options.AnnouncementStore). + WithGatewaySelector(options.GatewaySelector), + ) + if err != nil { + return svc, err + } + m.Route(options.Config.HTTP.Root, func(r chi.Router) { r.Get("/config.json", svc.Config) r.Route("/branding/logo", func(r chi.Router) { @@ -75,6 +88,14 @@ func NewService(opts ...Option) (Service, error) { r.Post("/", themeService.LogoUpload) r.Delete("/", themeService.LogoReset) }) + r.Route("/announcement", func(r chi.Router) { + r.Use(middleware.ExtractAccountUUID( + account.Logger(options.Logger), + account.JWTSecret(options.Config.TokenManager.JWTSecret), + )) + r.Get("/", announcementService.Get) + r.Put("/", announcementService.Set) + }) r.Route("/themes", func(r chi.Router) { r.Get("/{id}/theme.json", themeService.Get) r.Mount("/", svc.Static( @@ -104,10 +125,11 @@ func NewService(opts ...Option) (Service, error) { // Web defines the handlers for the web service. type Web struct { - logger log.Logger - config *config.Config - mux *chi.Mux - gatewaySelector pool.Selectable[gateway.GatewayAPIClient] + logger log.Logger + config *config.Config + mux *chi.Mux + gatewaySelector pool.Selectable[gateway.GatewayAPIClient] + announcementStore *announcement.Store } // ServeHTTP implements the Service interface. @@ -115,31 +137,58 @@ func (p Web) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.mux.ServeHTTP(w, r) } -func (p Web) getPayload() (payload []byte, err error) { - // render dynamically using config +func (p Web) getPayload(ctx context.Context) (payload []byte, err error) { + // render dynamically using a copy of the config, so per-request values (e.g. the + // announcement) are not written into the shared config concurrently. + webConfig := p.config.Web.Config // build theme url if themeServer, err := url.Parse(p.config.Web.ThemeServer); err == nil { - p.config.Web.Config.Theme = themeServer.String() + p.config.Web.ThemePath + webConfig.Theme = themeServer.String() + p.config.Web.ThemePath } else { - p.config.Web.Config.Theme = p.config.Web.ThemePath + webConfig.Theme = p.config.Web.ThemePath } // make apps render as empty array if it is empty // TODO remove once https://github.com/golang/go/issues/27589 is fixed - if len(p.config.Web.Config.Apps) == 0 { - p.config.Web.Config.Apps = make([]string, 0) + if len(webConfig.Apps) == 0 { + webConfig.Apps = make([]string, 0) } // ensure that the server url has a trailing slash - p.config.Web.Config.Server = strings.TrimRight(p.config.Web.Config.Server, "/") + "/" + webConfig.Server = strings.TrimRight(webConfig.Server, "/") + "/" - return json.Marshal(p.config.Web.Config) + // the runtime store is the single source of truth for the announcement banner: expose it + // when live, clear it otherwise. A statically configured value is not supported. + webConfig.Options.Announcement = p.currentAnnouncement(ctx) + + return json.Marshal(webConfig) +} + +// currentAnnouncement returns the stored announcement for config.json, or nil if unset, disabled +// or unavailable. +func (p Web) currentAnnouncement(ctx context.Context) *config.Announcement { + if p.announcementStore == nil { + return nil + } + + a, err := p.announcementStore.Get(ctx) + if err != nil { + p.logger.Error().Err(err).Msg("could not read announcement from store") + return nil + } + + // only live (enabled) announcements with a banner line are exposed in the public config.json + if !a.Enabled || a.BannerText == "" { + return nil + } + + return &config.Announcement{BannerText: a.BannerText, InfoText: a.InfoText} } // Config implements the Service interface. -func (p Web) Config(w http.ResponseWriter, _ *http.Request) { - payload, err := p.getPayload() +func (p Web) Config(w http.ResponseWriter, r *http.Request) { + payload, err := p.getPayload(r.Context()) if err != nil { http.Error(w, ErrConfigInvalid, http.StatusUnprocessableEntity) return diff --git a/services/web/pkg/service/v0/service_test.go b/services/web/pkg/service/v0/service_test.go new file mode 100644 index 0000000000..a61b7ec648 --- /dev/null +++ b/services/web/pkg/service/v0/service_test.go @@ -0,0 +1,58 @@ +package svc + +import ( + "context" + "testing" + + "github.com/nats-io/nats.go/jetstream" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/services/web/mocks" + "github.com/opencloud-eu/opencloud/services/web/pkg/announcement" + "github.com/opencloud-eu/opencloud/services/web/pkg/config" +) + +func TestCurrentAnnouncement(t *testing.T) { + newWeb := func(store *announcement.Store) Web { + return Web{logger: log.NopLogger(), announcementStore: store} + } + // storeReturning builds a store whose backing bucket returns the given JSON for the announcement key. + storeReturning := func(t *testing.T, value string) *announcement.Store { + entry := mocks.NewKeyValueEntry(t) + entry.EXPECT().Value().Return([]byte(value)) + kv := mocks.NewKeyValue(t) + kv.EXPECT().Get(mock.Anything, "announcement").Return(entry, nil) + return announcement.NewStore(kv) + } + // emptyStore builds a store whose backing bucket has no announcement. + emptyStore := func(t *testing.T) *announcement.Store { + kv := mocks.NewKeyValue(t) + kv.EXPECT().Get(mock.Anything, "announcement").Return(nil, jetstream.ErrKeyNotFound) + return announcement.NewStore(kv) + } + + t.Run("nil when there is no store", func(t *testing.T) { + require.Nil(t, newWeb(nil).currentAnnouncement(context.Background())) + }) + + t.Run("nil when the store is empty", func(t *testing.T) { + require.Nil(t, newWeb(emptyStore(t)).currentAnnouncement(context.Background())) + }) + + t.Run("nil when disabled", func(t *testing.T) { + s := storeReturning(t, `{"enabled":false,"bannerText":"hi","infoText":"info"}`) + require.Nil(t, newWeb(s).currentAnnouncement(context.Background())) + }) + + t.Run("nil when enabled but the banner text is empty", func(t *testing.T) { + s := storeReturning(t, `{"enabled":true,"bannerText":"","infoText":"info"}`) + require.Nil(t, newWeb(s).currentAnnouncement(context.Background())) + }) + + t.Run("returns banner and info text when enabled with a banner text", func(t *testing.T) { + s := storeReturning(t, `{"enabled":true,"bannerText":"hi","infoText":"info"}`) + require.Equal(t, &config.Announcement{BannerText: "hi", InfoText: "info"}, newWeb(s).currentAnnouncement(context.Background())) + }) +}