diff --git a/pkg/lumera/modules/action_msg/interface.go b/pkg/lumera/modules/action_msg/interface.go index 2a4e9c39..6fec08aa 100644 --- a/pkg/lumera/modules/action_msg/interface.go +++ b/pkg/lumera/modules/action_msg/interface.go @@ -18,19 +18,10 @@ type FinalizeActionResult struct { // Module defines the interface for action messages operations type Module interface { // FinalizeCascadeAction finalizes a CASCADE action with the given parameters - FinalizeCascadeAction( - ctx context.Context, - actionId string, - rqIdsIds []string, - ) (*FinalizeActionResult, error) + FinalizeCascadeAction(ctx context.Context, actionId string, rqIdsIds []string) (*FinalizeActionResult, error) } // NewModule creates a new ActionMsg module client -func NewModule( - conn *grpc.ClientConn, - kr keyring.Keyring, - keyName string, - chainID string, -) (Module, error) { +func NewModule(conn *grpc.ClientConn, kr keyring.Keyring, keyName string, chainID string) (Module, error) { return newModule(conn, kr, keyName, chainID) } diff --git a/pkg/testutil/lumera.go b/pkg/testutil/lumera.go index 4b583f46..57369f41 100644 --- a/pkg/testutil/lumera.go +++ b/pkg/testutil/lumera.go @@ -124,7 +124,7 @@ type MockActionMsgModule struct{} // For now, this is a placeholder implementation // FinalizeCascadeAction implements the required method from action_msg.Module interface -func (m *MockActionMsgModule) FinalizeCascadeAction(ctx context.Context, actionId string, rqIDs []string) (*action_msg.FinalizeActionResult, error) { +func (m *MockActionMsgModule) FinalizeCascadeAction(ctx context.Context, actionId string, signatures []string) (*action_msg.FinalizeActionResult, error) { // Mock implementation returns success with empty result return &action_msg.FinalizeActionResult{}, nil } diff --git a/sdk/action/client.go b/sdk/action/client.go index 060a8f2d..ba7f7c5d 100644 --- a/sdk/action/client.go +++ b/sdk/action/client.go @@ -13,6 +13,8 @@ import ( ) // Client defines the interface for action operations +// +//go:generate mockery --name=Client --output=testutil/mocks --outpkg=mocks --filename=client_mock.go type Client interface { StartCascade(ctx context.Context, data []byte, actionID string) (string, error) DeleteTask(ctx context.Context, taskID string) error @@ -52,11 +54,7 @@ func NewClient(ctx context.Context, config config.Config, logger log.Logger, key } // StartCascade initiates a cascade operation -func (c *ClientImpl) StartCascade(ctx context.Context, - data []byte, - actionID string, -) (string, error) { - +func (c *ClientImpl) StartCascade(ctx context.Context, data []byte, actionID string) (string, error) { if actionID == "" { c.logger.Error(ctx, "Empty action ID provided") return "", ErrEmptyActionID diff --git a/sdk/action/client_test.go b/sdk/action/client_test.go new file mode 100644 index 00000000..096c572c --- /dev/null +++ b/sdk/action/client_test.go @@ -0,0 +1,194 @@ +package action + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/LumeraProtocol/supernode/sdk/config" + "github.com/LumeraProtocol/supernode/sdk/event" + "github.com/LumeraProtocol/supernode/sdk/log" + "github.com/LumeraProtocol/supernode/sdk/task" + + mocktask "github.com/LumeraProtocol/supernode/sdk/task/testutil/mocks" +) + +func newClientWithMock(mgr *mocktask.Manager) *ClientImpl { + return &ClientImpl{ + config: config.Config{}, // empty fixture + taskManager: mgr, // injected mock + logger: log.NewNoopLogger(), // quiet logger + } +} + +func TestClient_StartCascade(t *testing.T) { + ctx := context.Background() + payload := []byte("hello-world") + actionID := "act-123" + mockErr := errors.New("boom") + + tests := map[string]struct { + data []byte + actionID string + setupMock func(*mocktask.Manager) + wantTaskID string + wantErrMatch func(error) bool + }{ + "happy path": { + data: payload, + actionID: actionID, + setupMock: func(m *mocktask.Manager) { + m.On("CreateCascadeTask", ctx, payload, actionID). + Return("task-42", nil) + }, + wantTaskID: "task-42", + wantErrMatch: func(err error) bool { return err == nil }, + }, + "empty action id": { + data: payload, + actionID: "", + setupMock: func(*mocktask.Manager) {}, + wantErrMatch: func(err error) bool { return errors.Is(err, ErrEmptyActionID) }, + }, + "empty data": { + data: nil, + actionID: actionID, + setupMock: func(*mocktask.Manager) {}, + wantErrMatch: func(err error) bool { return errors.Is(err, ErrEmptyData) }, + }, + "manager failure": { + data: payload, + actionID: actionID, + setupMock: func(m *mocktask.Manager) { + m.On("CreateCascadeTask", ctx, payload, actionID). + Return("", mockErr) + }, + wantErrMatch: func(err error) bool { + return err != nil && errors.Is(err, mockErr) + }, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + mockMgr := mocktask.NewManager(t) + tc.setupMock(mockMgr) + + client := newClientWithMock(mockMgr) + + gotID, err := client.StartCascade(ctx, tc.data, tc.actionID) + assert.True(t, tc.wantErrMatch(err), "error assertion failed") + assert.Equal(t, tc.wantTaskID, gotID) + }) + } +} + +func TestClient_GetTask(t *testing.T) { + ctx := context.Background() + entry := &task.TaskEntry{TaskID: "tid-7"} + + tests := map[string]struct { + taskID string + setupMock func(*mocktask.Manager) + wantEntry *task.TaskEntry + wantFound bool + }{ + "found": { + taskID: "tid-7", + setupMock: func(m *mocktask.Manager) { + m.On("GetTask", ctx, "tid-7").Return(entry, true) + }, + wantEntry: entry, wantFound: true, + }, + "missing": { + taskID: "tid-404", + setupMock: func(m *mocktask.Manager) { + m.On("GetTask", ctx, "tid-404").Return((*task.TaskEntry)(nil), false) + }, + wantEntry: nil, wantFound: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + mockMgr := mocktask.NewManager(t) + tc.setupMock(mockMgr) + + client := newClientWithMock(mockMgr) + got, ok := client.GetTask(ctx, tc.taskID) + + assert.Equal(t, tc.wantFound, ok) + assert.Equal(t, tc.wantEntry, got) + }) + } +} + +func TestClient_DeleteTask(t *testing.T) { + ctx := context.Background() + mockErr := errors.New("delete fail") + + tests := map[string]struct { + taskID string + setupMock func(*mocktask.Manager) + wantErrNil bool + }{ + "happy path": { + taskID: "tid-1", + setupMock: func(m *mocktask.Manager) { + m.On("DeleteTask", ctx, "tid-1").Return(nil) + }, + wantErrNil: true, + }, + "empty id": { + taskID: "", + setupMock: func(*mocktask.Manager) {}, + wantErrNil: false, + }, + "manager error": { + taskID: "tid-2", + setupMock: func(m *mocktask.Manager) { + m.On("DeleteTask", ctx, "tid-2").Return(mockErr) + }, + wantErrNil: false, + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + mockMgr := mocktask.NewManager(t) + tc.setupMock(mockMgr) + + client := newClientWithMock(mockMgr) + err := client.DeleteTask(ctx, tc.taskID) + + assert.Equal(t, tc.wantErrNil, err == nil) + }) + } +} + +func TestClient_Subscribe(t *testing.T) { + ctx := context.Background() + handler := func(context.Context, event.Event) {} + + mockMgr := mocktask.NewManager(t) + + mockMgr. + On("SubscribeToEvents", ctx, event.EventType("foo"), mock.AnythingOfType("event.Handler")). + Once() + mockMgr. + On("SubscribeToAllEvents", ctx, mock.AnythingOfType("event.Handler")). + Once() + + client := newClientWithMock(mockMgr) + + assert.NoError(t, client.SubscribeToEvents(ctx, "foo", handler)) + assert.NoError(t, client.SubscribeToAllEvents(ctx, handler)) + + // nil Manager branch + nilClient := &ClientImpl{} + assert.Error(t, nilClient.SubscribeToEvents(ctx, "bar", handler)) + assert.Error(t, nilClient.SubscribeToAllEvents(ctx, handler)) +} diff --git a/sdk/action/testutil/mocks/client_mock.go b/sdk/action/testutil/mocks/client_mock.go new file mode 100644 index 00000000..da7b1a12 --- /dev/null +++ b/sdk/action/testutil/mocks/client_mock.go @@ -0,0 +1,143 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + event "github.com/LumeraProtocol/supernode/sdk/event" + mock "github.com/stretchr/testify/mock" + + task "github.com/LumeraProtocol/supernode/sdk/task" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// DeleteTask provides a mock function with given fields: ctx, taskID +func (_m *Client) DeleteTask(ctx context.Context, taskID string) error { + ret := _m.Called(ctx, taskID) + + if len(ret) == 0 { + panic("no return value specified for DeleteTask") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, taskID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetTask provides a mock function with given fields: ctx, taskID +func (_m *Client) GetTask(ctx context.Context, taskID string) (*task.TaskEntry, bool) { + ret := _m.Called(ctx, taskID) + + if len(ret) == 0 { + panic("no return value specified for GetTask") + } + + var r0 *task.TaskEntry + var r1 bool + if rf, ok := ret.Get(0).(func(context.Context, string) (*task.TaskEntry, bool)); ok { + return rf(ctx, taskID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *task.TaskEntry); ok { + r0 = rf(ctx, taskID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*task.TaskEntry) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) bool); ok { + r1 = rf(ctx, taskID) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// StartCascade provides a mock function with given fields: ctx, data, actionID +func (_m *Client) StartCascade(ctx context.Context, data []byte, actionID string) (string, error) { + ret := _m.Called(ctx, data, actionID) + + if len(ret) == 0 { + panic("no return value specified for StartCascade") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []byte, string) (string, error)); ok { + return rf(ctx, data, actionID) + } + if rf, ok := ret.Get(0).(func(context.Context, []byte, string) string); ok { + r0 = rf(ctx, data, actionID) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, []byte, string) error); ok { + r1 = rf(ctx, data, actionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SubscribeToAllEvents provides a mock function with given fields: ctx, handler +func (_m *Client) SubscribeToAllEvents(ctx context.Context, handler event.Handler) error { + ret := _m.Called(ctx, handler) + + if len(ret) == 0 { + panic("no return value specified for SubscribeToAllEvents") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, event.Handler) error); ok { + r0 = rf(ctx, handler) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// SubscribeToEvents provides a mock function with given fields: ctx, eventType, handler +func (_m *Client) SubscribeToEvents(ctx context.Context, eventType event.EventType, handler event.Handler) error { + ret := _m.Called(ctx, eventType, handler) + + if len(ret) == 0 { + panic("no return value specified for SubscribeToEvents") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, event.EventType, event.Handler) error); ok { + r0 = rf(ctx, eventType, handler) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// NewClient creates a new instance of Client. 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 NewClient(t interface { + mock.TestingT + Cleanup(func()) +}) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/sdk/adapters/lumera/adapter.go b/sdk/adapters/lumera/adapter.go index 404ce3f6..7e02638a 100644 --- a/sdk/adapters/lumera/adapter.go +++ b/sdk/adapters/lumera/adapter.go @@ -14,6 +14,7 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keyring" ) +//go:generate mockery --name=Client --output=testutil/mocks --outpkg=mocks --filename=lumera_mock.go type Client interface { GetAction(ctx context.Context, actionID string) (Action, error) GetSupernodes(ctx context.Context, height int64) ([]Supernode, error) @@ -33,12 +34,7 @@ type Adapter struct { } // NewAdapter creates a new Adapter with dependencies explicitly injected -func NewAdapter( - ctx context.Context, - config ConfigParams, - kr keyring.Keyring, - logger log.Logger, -) (Client, error) { +func NewAdapter(ctx context.Context, config ConfigParams, kr keyring.Keyring, logger log.Logger) (Client, error) { // Set default logger if nil if logger == nil { logger = log.NewNoopLogger() @@ -104,10 +100,8 @@ func (a *Adapter) GetAction(ctx context.Context, actionID string) (Action, error } action := toSdkAction(resp) - a.logger.Debug(ctx, "Successfully retrieved action", - "actionID", action.ID, - "state", action.State, - "height", action.Height) + a.logger.Debug(ctx, "Successfully retrieved action", "actionID", action.ID, + "state", action.State, "height", action.Height) return action, nil } diff --git a/sdk/adapters/lumera/testutil/mocks/lumera_mock.go b/sdk/adapters/lumera/testutil/mocks/lumera_mock.go new file mode 100644 index 00000000..683bab87 --- /dev/null +++ b/sdk/adapters/lumera/testutil/mocks/lumera_mock.go @@ -0,0 +1,87 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + lumera "github.com/LumeraProtocol/supernode/sdk/adapters/lumera" + mock "github.com/stretchr/testify/mock" +) + +// Client is an autogenerated mock type for the Client type +type Client struct { + mock.Mock +} + +// GetAction provides a mock function with given fields: ctx, actionID +func (_m *Client) GetAction(ctx context.Context, actionID string) (lumera.Action, error) { + ret := _m.Called(ctx, actionID) + + if len(ret) == 0 { + panic("no return value specified for GetAction") + } + + var r0 lumera.Action + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) (lumera.Action, error)); ok { + return rf(ctx, actionID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) lumera.Action); ok { + r0 = rf(ctx, actionID) + } else { + r0 = ret.Get(0).(lumera.Action) + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, actionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetSupernodes provides a mock function with given fields: ctx, height +func (_m *Client) GetSupernodes(ctx context.Context, height int64) ([]lumera.Supernode, error) { + ret := _m.Called(ctx, height) + + if len(ret) == 0 { + panic("no return value specified for GetSupernodes") + } + + var r0 []lumera.Supernode + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, int64) ([]lumera.Supernode, error)); ok { + return rf(ctx, height) + } + if rf, ok := ret.Get(0).(func(context.Context, int64) []lumera.Supernode); ok { + r0 = rf(ctx, height) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]lumera.Supernode) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, int64) error); ok { + r1 = rf(ctx, height) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewClient creates a new instance of Client. 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 NewClient(t interface { + mock.TestingT + Cleanup(func()) +}) *Client { + mock := &Client{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/sdk/adapters/supernodeservice/testutil/mocks/cascade_service_mock.go b/sdk/adapters/supernodeservice/testutil/mocks/cascade_service_mock.go new file mode 100644 index 00000000..09562581 --- /dev/null +++ b/sdk/adapters/supernodeservice/testutil/mocks/cascade_service_mock.go @@ -0,0 +1,69 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + grpc "google.golang.org/grpc" + + mock "github.com/stretchr/testify/mock" + + supernodeservice "github.com/LumeraProtocol/supernode/sdk/adapters/supernodeservice" +) + +// CascadeServiceClient is an autogenerated mock type for the CascadeServiceClient type +type CascadeServiceClient struct { + mock.Mock +} + +// CascadeSupernodeRegister provides a mock function with given fields: ctx, in, opts +func (_m *CascadeServiceClient) CascadeSupernodeRegister(ctx context.Context, in *supernodeservice.CascadeSupernodeRegisterRequest, opts ...grpc.CallOption) (*supernodeservice.CascadeSupernodeRegisterResponse, error) { + _va := make([]interface{}, len(opts)) + for _i := range opts { + _va[_i] = opts[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, in) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for CascadeSupernodeRegister") + } + + var r0 *supernodeservice.CascadeSupernodeRegisterResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *supernodeservice.CascadeSupernodeRegisterRequest, ...grpc.CallOption) (*supernodeservice.CascadeSupernodeRegisterResponse, error)); ok { + return rf(ctx, in, opts...) + } + if rf, ok := ret.Get(0).(func(context.Context, *supernodeservice.CascadeSupernodeRegisterRequest, ...grpc.CallOption) *supernodeservice.CascadeSupernodeRegisterResponse); ok { + r0 = rf(ctx, in, opts...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*supernodeservice.CascadeSupernodeRegisterResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *supernodeservice.CascadeSupernodeRegisterRequest, ...grpc.CallOption) error); ok { + r1 = rf(ctx, in, opts...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewCascadeServiceClient creates a new instance of CascadeServiceClient. 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 NewCascadeServiceClient(t interface { + mock.TestingT + Cleanup(func()) +}) *CascadeServiceClient { + mock := &CascadeServiceClient{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/sdk/adapters/supernodeservice/types.go b/sdk/adapters/supernodeservice/types.go index cc0eb839..d64b914b 100644 --- a/sdk/adapters/supernodeservice/types.go +++ b/sdk/adapters/supernodeservice/types.go @@ -17,6 +17,7 @@ type CascadeSupernodeRegisterResponse struct { Message string } +//go:generate mockery --name=CascadeServiceClient --output=testutil/mocks --outpkg=mocks --filename=cascade_service_mock.go type CascadeServiceClient interface { CascadeSupernodeRegister(ctx context.Context, in *CascadeSupernodeRegisterRequest, opts ...grpc.CallOption) (*CascadeSupernodeRegisterResponse, error) } diff --git a/sdk/go.mod b/sdk/go.mod index 385964e4..956207e5 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -2,6 +2,8 @@ module github.com/LumeraProtocol/supernode/sdk go 1.24.1 +toolchain go1.24.2 + replace github.com/LumeraProtocol/supernode => ../ require ( @@ -10,6 +12,7 @@ require ( github.com/cosmos/cosmos-sdk v0.53.0 github.com/dgraph-io/ristretto/v2 v2.2.0 github.com/google/uuid v1.6.0 + github.com/stretchr/testify v1.10.0 google.golang.org/grpc v1.72.0 ) @@ -133,7 +136,7 @@ require ( github.com/spf13/cobra v1.9.1 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/spf13/viper v1.20.1 // indirect - github.com/stretchr/testify v1.10.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d // indirect github.com/tendermint/go-amino v0.16.0 // indirect diff --git a/sdk/task/cache_test.go b/sdk/task/cache_test.go new file mode 100644 index 00000000..18765094 --- /dev/null +++ b/sdk/task/cache_test.go @@ -0,0 +1,122 @@ +package task + +import ( + "context" + "sync" + "testing" + "time" + + "github.com/LumeraProtocol/supernode/sdk/event" + "github.com/LumeraProtocol/supernode/sdk/log" + "github.com/stretchr/testify/assert" +) + +func newTestCache(t *testing.T) *TaskCache { + t.Helper() + + tc, err := NewTaskCache(context.Background(), log.NewNoopLogger()) + if err != nil { + t.Fatalf("creating cache: %v", err) + } + return tc +} + +func TestTaskCache_ConcurrentAccess(t *testing.T) { + ctx := context.Background() + tc := newTestCache(t) + tc.Set(ctx, "conc", nil, TaskTypeCascade) + + var wg sync.WaitGroup + wg.Add(2) + + go func() { + defer wg.Done() + for i := 0; i < 50; i++ { + tc.UpdateStatus(ctx, "conc", StatusProcessing, nil) + tc.AddEvent(ctx, "conc", event.Event{Type: event.TaskStarted}) + } + }() + + go func() { + defer wg.Done() + tc.Del(ctx, "conc") // race with updates + }() + + done := make(chan struct{}) + go func() { wg.Wait(); close(done) }() + + select { + case <-done: + case <-time.After(2 * time.Second): + t.Fatal("concurrent operations deadlocked") + } +} + +func TestTaskCache_SetGet(t *testing.T) { + ctx := context.Background() + tc := newTestCache(t) + + tests := map[string]struct { + taskID string + taskType TaskType + }{ + "first insert": {"id1", TaskTypeCascade}, + "second insert": {"id2", TaskTypeCascade}, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + tc.Set(ctx, tt.taskID, nil, tt.taskType) + tc.Wait() // <-- ensure write propagated + + entry, found := tc.Get(ctx, tt.taskID) + assert.True(t, found, "entry should exist") + assert.Equal(t, tt.taskID, entry.TaskID) + assert.Equal(t, StatusPending, entry.Status) + }) + } +} + +func TestTaskCache_UpdateStatus(t *testing.T) { + ctx := context.Background() + tc := newTestCache(t) + + tc.Set(ctx, "jobX", nil, TaskTypeCascade) + tc.Wait() + + tc.UpdateStatus(ctx, "jobX", StatusProcessing, nil) + tc.Wait() + + ent, _ := tc.Get(ctx, "jobX") + assert.Equal(t, StatusProcessing, ent.Status) +} + +func TestTaskCache_AddEvent(t *testing.T) { + ctx := context.Background() + tc := newTestCache(t) + + tc.Set(ctx, "evt1", nil, TaskTypeCascade) + tc.Wait() + + ev := event.Event{Type: event.TaskStarted, TaskID: "evt1"} + tc.AddEvent(ctx, "evt1", ev) + tc.Wait() + + ent, _ := tc.Get(ctx, "evt1") + assert.Len(t, ent.Events, 1) + assert.Equal(t, ev, ent.Events[0]) +} + +func TestTaskCache_Delete(t *testing.T) { + ctx := context.Background() + tc := newTestCache(t) + + tc.Set(ctx, "gone", nil, TaskTypeCascade) + tc.Wait() + + tc.Del(ctx, "gone") + tc.Wait() + + _, found := tc.Get(ctx, "gone") + assert.False(t, found) +} diff --git a/sdk/task/cascade.go b/sdk/task/cascade.go index d666574e..c6b3276d 100644 --- a/sdk/task/cascade.go +++ b/sdk/task/cascade.go @@ -25,12 +25,7 @@ type CascadeTask struct { } // NewCascadeTask creates a new CascadeTask using a BaseTask plus cascade-specific parameters -func NewCascadeTask( - base BaseTask, - data []byte, - actionId string, - -) *CascadeTask { +func NewCascadeTask(base BaseTask, data []byte, actionId string) *CascadeTask { return &CascadeTask{ BaseTask: base, data: data, diff --git a/sdk/task/manager.go b/sdk/task/manager.go index 822633f8..c87eec87 100644 --- a/sdk/task/manager.go +++ b/sdk/task/manager.go @@ -16,6 +16,8 @@ import ( const MAX_EVENT_WORKERS = 100 // Manager handles task creation and management +// +//go:generate mockery --name=Manager --output=testutil/mocks --outpkg=mocks --filename=manager_mock.go type Manager interface { CreateCascadeTask(ctx context.Context, data []byte, actionID string) (string, error) GetTask(ctx context.Context, taskID string) (*TaskEntry, bool) @@ -33,13 +35,7 @@ type ManagerImpl struct { keyring keyring.Keyring } -func NewManager( - ctx context.Context, - config config.Config, - logger log.Logger, - kr keyring.Keyring, -) (Manager, error) { - +func NewManager(ctx context.Context, config config.Config, logger log.Logger, kr keyring.Keyring) (Manager, error) { // 1 - Logger if logger == nil { logger = log.NewNoopLogger() @@ -177,10 +173,7 @@ func (m *ManagerImpl) SubscribeToAllEvents(ctx context.Context, handler event.Ha // handleEvent processes events from tasks and updates cache func (m *ManagerImpl) handleEvent(ctx context.Context, e event.Event) { - m.logger.Debug(ctx, "Handling task event", - "taskID", e.TaskID, - "taskType", e.TaskType, - "eventType", e.Type) + m.logger.Debug(ctx, "Handling task event", "taskID", e.TaskID, "taskType", e.TaskType, "eventType", e.Type) // Update the cache with the event m.taskCache.AddEvent(ctx, e.TaskID, e) diff --git a/sdk/task/testutil/mocks/manager_mock.go b/sdk/task/testutil/mocks/manager_mock.go new file mode 100644 index 00000000..172c24bb --- /dev/null +++ b/sdk/task/testutil/mocks/manager_mock.go @@ -0,0 +1,117 @@ +// Code generated by mockery v2.53.3. DO NOT EDIT. + +package mocks + +import ( + context "context" + + event "github.com/LumeraProtocol/supernode/sdk/event" + mock "github.com/stretchr/testify/mock" + + task "github.com/LumeraProtocol/supernode/sdk/task" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// CreateCascadeTask provides a mock function with given fields: ctx, data, actionID +func (_m *Manager) CreateCascadeTask(ctx context.Context, data []byte, actionID string) (string, error) { + ret := _m.Called(ctx, data, actionID) + + if len(ret) == 0 { + panic("no return value specified for CreateCascadeTask") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []byte, string) (string, error)); ok { + return rf(ctx, data, actionID) + } + if rf, ok := ret.Get(0).(func(context.Context, []byte, string) string); ok { + r0 = rf(ctx, data, actionID) + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func(context.Context, []byte, string) error); ok { + r1 = rf(ctx, data, actionID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// DeleteTask provides a mock function with given fields: ctx, taskID +func (_m *Manager) DeleteTask(ctx context.Context, taskID string) error { + ret := _m.Called(ctx, taskID) + + if len(ret) == 0 { + panic("no return value specified for DeleteTask") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string) error); ok { + r0 = rf(ctx, taskID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// GetTask provides a mock function with given fields: ctx, taskID +func (_m *Manager) GetTask(ctx context.Context, taskID string) (*task.TaskEntry, bool) { + ret := _m.Called(ctx, taskID) + + if len(ret) == 0 { + panic("no return value specified for GetTask") + } + + var r0 *task.TaskEntry + var r1 bool + if rf, ok := ret.Get(0).(func(context.Context, string) (*task.TaskEntry, bool)); ok { + return rf(ctx, taskID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) *task.TaskEntry); ok { + r0 = rf(ctx, taskID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*task.TaskEntry) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) bool); ok { + r1 = rf(ctx, taskID) + } else { + r1 = ret.Get(1).(bool) + } + + return r0, r1 +} + +// SubscribeToAllEvents provides a mock function with given fields: ctx, handler +func (_m *Manager) SubscribeToAllEvents(ctx context.Context, handler event.Handler) { + _m.Called(ctx, handler) +} + +// SubscribeToEvents provides a mock function with given fields: ctx, eventType, handler +func (_m *Manager) SubscribeToEvents(ctx context.Context, eventType event.EventType, handler event.Handler) { + _m.Called(ctx, eventType, handler) +} + +// NewManager creates a new instance of Manager. 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 NewManager(t interface { + mock.TestingT + Cleanup(func()) +}) *Manager { + mock := &Manager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}