diff --git a/pkg/storage/rqstore/store.go b/pkg/storage/rqstore/store.go index bc62a40a..f439fe97 100644 --- a/pkg/storage/rqstore/store.go +++ b/pkg/storage/rqstore/store.go @@ -18,6 +18,7 @@ const createRQSymbolsDir string = ` PRIMARY KEY (txid) );` +//go:generate mockgen -destination=rq_mock.go -package=rqstore -source=store.go type Store interface { DeleteSymbolsByTxID(txid string) error StoreSymbolDirectory(txid, dir string) error diff --git a/supernode/cmd/keys_add_test.go b/supernode/cmd/keys_add_test.go new file mode 100644 index 00000000..6812ee93 --- /dev/null +++ b/supernode/cmd/keys_add_test.go @@ -0,0 +1,53 @@ +package cmd_test + +import ( + "bytes" + "testing" + + "github.com/LumeraProtocol/supernode/pkg/keyring" + "github.com/spf13/cobra" + "github.com/stretchr/testify/require" +) + +func TestKeysAddCmd_RunE(t *testing.T) { + cmd := getTestKeysAddCmd(t) + + tests := []struct { + name string + args []string + wantErr bool + }{ + {"no_name_arg", []string{}, false}, + {"with_name_arg", []string{"testkey"}, false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + buf := new(bytes.Buffer) + cmd.SetOut(buf) + cmd.SetArgs(tt.args) + + err := cmd.Execute() + if (err != nil) != tt.wantErr { + t.Errorf("unexpected error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +// getTestKeysAddCmd initializes and returns a test instance of the cobra.Command for keys add. +func getTestKeysAddCmd(t *testing.T) *cobra.Command { + return &cobra.Command{ + Use: "add", + RunE: func(cmd *cobra.Command, args []string) error { + name := "testkey" + if len(args) > 0 { + name = args[0] + } + kr, err := keyring.InitKeyring("test", "/tmp") + require.NoError(t, err) + _, _, err = keyring.CreateNewAccount(kr, name) + return err + }, + } +} diff --git a/supernode/cmd/service_test.go b/supernode/cmd/service_test.go new file mode 100644 index 00000000..1badf130 --- /dev/null +++ b/supernode/cmd/service_test.go @@ -0,0 +1,79 @@ +package cmd + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +// mockService implements the service interface +type mockService struct { + name string + runFunc func(ctx context.Context) error +} + +func (m *mockService) Run(ctx context.Context) error { + return m.runFunc(ctx) +} + +func TestRunServices_AllSuccessful(t *testing.T) { + s1 := &mockService{name: "s1", runFunc: func(ctx context.Context) error { + return nil + }} + s2 := &mockService{name: "s2", runFunc: func(ctx context.Context) error { + return nil + }} + + err := RunServices(context.Background(), s1, s2) + assert.NoError(t, err) +} + +func TestRunServices_OneFails(t *testing.T) { + s1 := &mockService{name: "s1", runFunc: func(ctx context.Context) error { + return errors.New("s1 failed") + }} + s2 := &mockService{name: "s2", runFunc: func(ctx context.Context) error { + return nil + }} + + err := RunServices(context.Background(), s1, s2) + assert.Error(t, err) + assert.Equal(t, "s1 failed", err.Error()) +} + +func TestRunServices_MultipleFail(t *testing.T) { + s1 := &mockService{name: "s1", runFunc: func(ctx context.Context) error { + return errors.New("s1 failed") + }} + s2 := &mockService{name: "s2", runFunc: func(ctx context.Context) error { + return errors.New("s2 failed") + }} + + err := RunServices(context.Background(), s1, s2) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed") // may not be deterministic which one returns +} + +func TestRunServices_WithCancellation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + + s1 := &mockService{name: "s1", runFunc: func(ctx context.Context) error { + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(2 * time.Second): + return nil + } + }} + s2 := &mockService{name: "s2", runFunc: func(ctx context.Context) error { + cancel() // cancel context early + return nil + }} + + err := RunServices(ctx, s1, s2) + assert.Error(t, err) + assert.Equal(t, context.Canceled, err) +} diff --git a/supernode/node/action/server/cascade/cascade_action_server_mock.go b/supernode/node/action/server/cascade/cascade_action_server_mock.go new file mode 100644 index 00000000..1f726157 --- /dev/null +++ b/supernode/node/action/server/cascade/cascade_action_server_mock.go @@ -0,0 +1,41 @@ +package cascade + +import ( + "context" + "io" + + pb "github.com/LumeraProtocol/supernode/gen/supernode/action/cascade" + "google.golang.org/grpc/metadata" +) + +// mockStream simulates pb.CascadeService_RegisterServer +type mockStream struct { + ctx context.Context + request []*pb.RegisterRequest + sent []*pb.RegisterResponse + pos int +} + +func (m *mockStream) Context() context.Context { + return m.ctx +} + +func (m *mockStream) Send(resp *pb.RegisterResponse) error { + m.sent = append(m.sent, resp) + return nil +} + +func (m *mockStream) Recv() (*pb.RegisterRequest, error) { + if m.pos >= len(m.request) { + return nil, io.EOF + } + req := m.request[m.pos] + m.pos++ + return req, nil +} + +func (m *mockStream) SetHeader(md metadata.MD) error { return nil } +func (m *mockStream) SendHeader(md metadata.MD) error { return nil } +func (m *mockStream) SetTrailer(md metadata.MD) {} +func (m *mockStream) SendMsg(_ any) error { return nil } +func (m *mockStream) RecvMsg(_ any) error { return nil } diff --git a/supernode/node/action/server/cascade/cascade_action_server_test.go b/supernode/node/action/server/cascade/cascade_action_server_test.go new file mode 100644 index 00000000..aa36a6cc --- /dev/null +++ b/supernode/node/action/server/cascade/cascade_action_server_test.go @@ -0,0 +1,97 @@ +package cascade + +import ( + "context" + "errors" + "testing" + + pb "github.com/LumeraProtocol/supernode/gen/supernode/action/cascade" + "github.com/LumeraProtocol/supernode/supernode/services/cascade" + cascademocks "github.com/LumeraProtocol/supernode/supernode/services/cascade/mocks" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func TestRegister_Success(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockTask := cascademocks.NewMockRegistrationTaskService(ctrl) + mockFactory := cascademocks.NewMockTaskFactory(ctrl) + + // Expect Register to be called with any input, respond via callback + mockTask.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn( + func(ctx context.Context, req *cascade.RegisterRequest, send func(*cascade.RegisterResponse) error) error { + return send(&cascade.RegisterResponse{ + EventType: 1, + Message: "registration successful", + TxHash: "tx123", + }) + }, + ).Times(1) + + mockFactory.EXPECT().NewCascadeRegistrationTask().Return(mockTask).Times(1) + + server := NewCascadeActionServer(mockFactory) + + stream := &mockStream{ + ctx: context.Background(), + request: []*pb.RegisterRequest{ + {RequestType: &pb.RegisterRequest_Chunk{Chunk: &pb.DataChunk{Data: []byte("abc123")}}}, + {RequestType: &pb.RegisterRequest_Metadata{ + Metadata: &pb.Metadata{TaskId: "t1", ActionId: "a1"}, + }}, + }, + } + + err := server.Register(stream) + assert.NoError(t, err) + assert.Len(t, stream.sent, 1) + assert.Equal(t, "registration successful", stream.sent[0].Message) + assert.Equal(t, "tx123", stream.sent[0].TxHash) +} + +func TestRegister_Error_NoMetadata(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := cascademocks.NewMockTaskFactory(ctrl) + server := NewCascadeActionServer(mockFactory) + + stream := &mockStream{ + ctx: context.Background(), + request: []*pb.RegisterRequest{ + {RequestType: &pb.RegisterRequest_Chunk{Chunk: &pb.DataChunk{Data: []byte("abc123")}}}, + }, + } + + err := server.Register(stream) + assert.EqualError(t, err, "no metadata received") +} + +func TestRegister_Error_TaskFails(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockTask := cascademocks.NewMockRegistrationTaskService(ctrl) + mockFactory := cascademocks.NewMockTaskFactory(ctrl) + + mockTask.EXPECT().Register(gomock.Any(), gomock.Any(), gomock.Any()).Return(errors.New("task failed")).Times(1) + mockFactory.EXPECT().NewCascadeRegistrationTask().Return(mockTask).Times(1) + + server := NewCascadeActionServer(mockFactory) + + stream := &mockStream{ + ctx: context.Background(), + request: []*pb.RegisterRequest{ + {RequestType: &pb.RegisterRequest_Chunk{Chunk: &pb.DataChunk{Data: []byte("abc123")}}}, + {RequestType: &pb.RegisterRequest_Metadata{ + Metadata: &pb.Metadata{TaskId: "t1", ActionId: "a1"}, + }}, + }, + } + + err := server.Register(stream) + assert.EqualError(t, err, "registration failed: task failed") +} diff --git a/supernode/node/supernode/server/config_test.go b/supernode/node/supernode/server/config_test.go new file mode 100644 index 00000000..80f6f3ba --- /dev/null +++ b/supernode/node/supernode/server/config_test.go @@ -0,0 +1,16 @@ +package server + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewConfig_Defaults(t *testing.T) { + cfg := NewConfig() + + assert.NotNil(t, cfg) + assert.Equal(t, "0.0.0.0", cfg.ListenAddresses, "default listen address should be 0.0.0.0") + assert.Equal(t, 4444, cfg.Port, "default port should be 4444") + assert.Equal(t, "", cfg.Identity, "default identity should be empty") +} diff --git a/supernode/node/supernode/server/mock_keyring.go b/supernode/node/supernode/server/mock_keyring.go new file mode 100644 index 00000000..2ecea380 --- /dev/null +++ b/supernode/node/supernode/server/mock_keyring.go @@ -0,0 +1,379 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/cosmos/cosmos-sdk/crypto/keyring (interfaces: Keyring) + +// Package mock_keyring is a generated GoMock package. +package server + +import ( + reflect "reflect" + + keyring "github.com/cosmos/cosmos-sdk/crypto/keyring" + types "github.com/cosmos/cosmos-sdk/crypto/types" + types0 "github.com/cosmos/cosmos-sdk/types" + signing "github.com/cosmos/cosmos-sdk/types/tx/signing" + gomock "github.com/golang/mock/gomock" +) + +// MockKeyring is a mock of Keyring interface. +type MockKeyring struct { + ctrl *gomock.Controller + recorder *MockKeyringMockRecorder +} + +// MockKeyringMockRecorder is the mock recorder for MockKeyring. +type MockKeyringMockRecorder struct { + mock *MockKeyring +} + +// NewMockKeyring creates a new mock instance. +func NewMockKeyring(ctrl *gomock.Controller) *MockKeyring { + mock := &MockKeyring{ctrl: ctrl} + mock.recorder = &MockKeyringMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockKeyring) EXPECT() *MockKeyringMockRecorder { + return m.recorder +} + +// Backend mocks base method. +func (m *MockKeyring) Backend() string { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Backend") + ret0, _ := ret[0].(string) + return ret0 +} + +// Backend indicates an expected call of Backend. +func (mr *MockKeyringMockRecorder) Backend() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Backend", reflect.TypeOf((*MockKeyring)(nil).Backend)) +} + +// Delete mocks base method. +func (m *MockKeyring) Delete(arg0 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Delete", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// Delete indicates an expected call of Delete. +func (mr *MockKeyringMockRecorder) Delete(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Delete", reflect.TypeOf((*MockKeyring)(nil).Delete), arg0) +} + +// DeleteByAddress mocks base method. +func (m *MockKeyring) DeleteByAddress(arg0 types0.Address) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteByAddress", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteByAddress indicates an expected call of DeleteByAddress. +func (mr *MockKeyringMockRecorder) DeleteByAddress(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteByAddress", reflect.TypeOf((*MockKeyring)(nil).DeleteByAddress), arg0) +} + +// ExportPrivKeyArmor mocks base method. +func (m *MockKeyring) ExportPrivKeyArmor(arg0, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExportPrivKeyArmor", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportPrivKeyArmor indicates an expected call of ExportPrivKeyArmor. +func (mr *MockKeyringMockRecorder) ExportPrivKeyArmor(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportPrivKeyArmor", reflect.TypeOf((*MockKeyring)(nil).ExportPrivKeyArmor), arg0, arg1) +} + +// ExportPrivKeyArmorByAddress mocks base method. +func (m *MockKeyring) ExportPrivKeyArmorByAddress(arg0 types0.Address, arg1 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExportPrivKeyArmorByAddress", arg0, arg1) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportPrivKeyArmorByAddress indicates an expected call of ExportPrivKeyArmorByAddress. +func (mr *MockKeyringMockRecorder) ExportPrivKeyArmorByAddress(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportPrivKeyArmorByAddress", reflect.TypeOf((*MockKeyring)(nil).ExportPrivKeyArmorByAddress), arg0, arg1) +} + +// ExportPubKeyArmor mocks base method. +func (m *MockKeyring) ExportPubKeyArmor(arg0 string) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExportPubKeyArmor", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportPubKeyArmor indicates an expected call of ExportPubKeyArmor. +func (mr *MockKeyringMockRecorder) ExportPubKeyArmor(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportPubKeyArmor", reflect.TypeOf((*MockKeyring)(nil).ExportPubKeyArmor), arg0) +} + +// ExportPubKeyArmorByAddress mocks base method. +func (m *MockKeyring) ExportPubKeyArmorByAddress(arg0 types0.Address) (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExportPubKeyArmorByAddress", arg0) + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExportPubKeyArmorByAddress indicates an expected call of ExportPubKeyArmorByAddress. +func (mr *MockKeyringMockRecorder) ExportPubKeyArmorByAddress(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportPubKeyArmorByAddress", reflect.TypeOf((*MockKeyring)(nil).ExportPubKeyArmorByAddress), arg0) +} + +// ImportPrivKey mocks base method. +func (m *MockKeyring) ImportPrivKey(arg0, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ImportPrivKey", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// ImportPrivKey indicates an expected call of ImportPrivKey. +func (mr *MockKeyringMockRecorder) ImportPrivKey(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportPrivKey", reflect.TypeOf((*MockKeyring)(nil).ImportPrivKey), arg0, arg1, arg2) +} + +// ImportPrivKeyHex mocks base method. +func (m *MockKeyring) ImportPrivKeyHex(arg0, arg1, arg2 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ImportPrivKeyHex", arg0, arg1, arg2) + ret0, _ := ret[0].(error) + return ret0 +} + +// ImportPrivKeyHex indicates an expected call of ImportPrivKeyHex. +func (mr *MockKeyringMockRecorder) ImportPrivKeyHex(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportPrivKeyHex", reflect.TypeOf((*MockKeyring)(nil).ImportPrivKeyHex), arg0, arg1, arg2) +} + +// ImportPubKey mocks base method. +func (m *MockKeyring) ImportPubKey(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ImportPubKey", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// ImportPubKey indicates an expected call of ImportPubKey. +func (mr *MockKeyringMockRecorder) ImportPubKey(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportPubKey", reflect.TypeOf((*MockKeyring)(nil).ImportPubKey), arg0, arg1) +} + +// Key mocks base method. +func (m *MockKeyring) Key(arg0 string) (*keyring.Record, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Key", arg0) + ret0, _ := ret[0].(*keyring.Record) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Key indicates an expected call of Key. +func (mr *MockKeyringMockRecorder) Key(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Key", reflect.TypeOf((*MockKeyring)(nil).Key), arg0) +} + +// KeyByAddress mocks base method. +func (m *MockKeyring) KeyByAddress(arg0 types0.Address) (*keyring.Record, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KeyByAddress", arg0) + ret0, _ := ret[0].(*keyring.Record) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// KeyByAddress indicates an expected call of KeyByAddress. +func (mr *MockKeyringMockRecorder) KeyByAddress(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyByAddress", reflect.TypeOf((*MockKeyring)(nil).KeyByAddress), arg0) +} + +// List mocks base method. +func (m *MockKeyring) List() ([]*keyring.Record, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "List") + ret0, _ := ret[0].([]*keyring.Record) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// List indicates an expected call of List. +func (mr *MockKeyringMockRecorder) List() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockKeyring)(nil).List)) +} + +// MigrateAll mocks base method. +func (m *MockKeyring) MigrateAll() ([]*keyring.Record, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "MigrateAll") + ret0, _ := ret[0].([]*keyring.Record) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// MigrateAll indicates an expected call of MigrateAll. +func (mr *MockKeyringMockRecorder) MigrateAll() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MigrateAll", reflect.TypeOf((*MockKeyring)(nil).MigrateAll)) +} + +// NewAccount mocks base method. +func (m *MockKeyring) NewAccount(arg0, arg1, arg2, arg3 string, arg4 keyring.SignatureAlgo) (*keyring.Record, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewAccount", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(*keyring.Record) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// NewAccount indicates an expected call of NewAccount. +func (mr *MockKeyringMockRecorder) NewAccount(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewAccount", reflect.TypeOf((*MockKeyring)(nil).NewAccount), arg0, arg1, arg2, arg3, arg4) +} + +// NewMnemonic mocks base method. +func (m *MockKeyring) NewMnemonic(arg0 string, arg1 keyring.Language, arg2, arg3 string, arg4 keyring.SignatureAlgo) (*keyring.Record, string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "NewMnemonic", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(*keyring.Record) + ret1, _ := ret[1].(string) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// NewMnemonic indicates an expected call of NewMnemonic. +func (mr *MockKeyringMockRecorder) NewMnemonic(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "NewMnemonic", reflect.TypeOf((*MockKeyring)(nil).NewMnemonic), arg0, arg1, arg2, arg3, arg4) +} + +// Rename mocks base method. +func (m *MockKeyring) Rename(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Rename", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// Rename indicates an expected call of Rename. +func (mr *MockKeyringMockRecorder) Rename(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Rename", reflect.TypeOf((*MockKeyring)(nil).Rename), arg0, arg1) +} + +// SaveLedgerKey mocks base method. +func (m *MockKeyring) SaveLedgerKey(arg0 string, arg1 keyring.SignatureAlgo, arg2 string, arg3, arg4, arg5 uint32) (*keyring.Record, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveLedgerKey", arg0, arg1, arg2, arg3, arg4, arg5) + ret0, _ := ret[0].(*keyring.Record) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SaveLedgerKey indicates an expected call of SaveLedgerKey. +func (mr *MockKeyringMockRecorder) SaveLedgerKey(arg0, arg1, arg2, arg3, arg4, arg5 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveLedgerKey", reflect.TypeOf((*MockKeyring)(nil).SaveLedgerKey), arg0, arg1, arg2, arg3, arg4, arg5) +} + +// SaveMultisig mocks base method. +func (m *MockKeyring) SaveMultisig(arg0 string, arg1 types.PubKey) (*keyring.Record, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveMultisig", arg0, arg1) + ret0, _ := ret[0].(*keyring.Record) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SaveMultisig indicates an expected call of SaveMultisig. +func (mr *MockKeyringMockRecorder) SaveMultisig(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveMultisig", reflect.TypeOf((*MockKeyring)(nil).SaveMultisig), arg0, arg1) +} + +// SaveOfflineKey mocks base method. +func (m *MockKeyring) SaveOfflineKey(arg0 string, arg1 types.PubKey) (*keyring.Record, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SaveOfflineKey", arg0, arg1) + ret0, _ := ret[0].(*keyring.Record) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// SaveOfflineKey indicates an expected call of SaveOfflineKey. +func (mr *MockKeyringMockRecorder) SaveOfflineKey(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SaveOfflineKey", reflect.TypeOf((*MockKeyring)(nil).SaveOfflineKey), arg0, arg1) +} + +// Sign mocks base method. +func (m *MockKeyring) Sign(arg0 string, arg1 []byte, arg2 signing.SignMode) ([]byte, types.PubKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Sign", arg0, arg1, arg2) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(types.PubKey) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// Sign indicates an expected call of Sign. +func (mr *MockKeyringMockRecorder) Sign(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Sign", reflect.TypeOf((*MockKeyring)(nil).Sign), arg0, arg1, arg2) +} + +// SignByAddress mocks base method. +func (m *MockKeyring) SignByAddress(arg0 types0.Address, arg1 []byte, arg2 signing.SignMode) ([]byte, types.PubKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SignByAddress", arg0, arg1, arg2) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(types.PubKey) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// SignByAddress indicates an expected call of SignByAddress. +func (mr *MockKeyringMockRecorder) SignByAddress(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SignByAddress", reflect.TypeOf((*MockKeyring)(nil).SignByAddress), arg0, arg1, arg2) +} + +// SupportedAlgorithms mocks base method. +func (m *MockKeyring) SupportedAlgorithms() (keyring.SigningAlgoList, keyring.SigningAlgoList) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "SupportedAlgorithms") + ret0, _ := ret[0].(keyring.SigningAlgoList) + ret1, _ := ret[1].(keyring.SigningAlgoList) + return ret0, ret1 +} + +// SupportedAlgorithms indicates an expected call of SupportedAlgorithms. +func (mr *MockKeyringMockRecorder) SupportedAlgorithms() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportedAlgorithms", reflect.TypeOf((*MockKeyring)(nil).SupportedAlgorithms)) +} diff --git a/supernode/node/supernode/server/server_test.go b/supernode/node/supernode/server/server_test.go new file mode 100644 index 00000000..de39e3c3 --- /dev/null +++ b/supernode/node/supernode/server/server_test.go @@ -0,0 +1,54 @@ +package server + +import ( + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + "google.golang.org/grpc" + "google.golang.org/grpc/health/grpc_health_v1" + "testing" +) + +// --- Mock service implementing server.service --- +type mockService struct{} + +func (m *mockService) Desc() *grpc.ServiceDesc { + return &grpc.ServiceDesc{ + ServiceName: "test.Service", + HandlerType: (*interface{})(nil), + Methods: []grpc.MethodDesc{}, + Streams: []grpc.StreamDesc{}, + } +} + +func TestNewServer_WithValidConfig(t *testing.T) { + ctl := gomock.NewController(t) + mockKeyring := NewMockKeyring(ctl) + + cfg := NewConfig() + s, err := New(cfg, "supernode-test", mockKeyring, &mockService{}) + assert.NoError(t, err) + assert.NotNil(t, s) +} + +func TestNewServer_WithNilConfig(t *testing.T) { + ctl := gomock.NewController(t) + mockKeyring := NewMockKeyring(ctl) + + s, err := New(nil, "supernode-test", mockKeyring) + assert.Nil(t, s) + assert.EqualError(t, err, "config is nil") +} + +func TestSetServiceStatusAndClose(t *testing.T) { + ctl := gomock.NewController(t) + mockKeyring := NewMockKeyring(ctl) + + cfg := NewConfig() + s, _ := New(cfg, "test", mockKeyring, &mockService{}) + _ = s.setupGRPCServer() + + s.SetServiceStatus("test.Service", grpc_health_v1.HealthCheckResponse_SERVING) + s.Close() + + // No assertion — success is no panic / crash on shutdown +} diff --git a/supernode/services/cascade/adaptors/mocks/lumera_mock.go b/supernode/services/cascade/adaptors/mocks/lumera_mock.go index e2f9399e..f24c757f 100644 --- a/supernode/services/cascade/adaptors/mocks/lumera_mock.go +++ b/supernode/services/cascade/adaptors/mocks/lumera_mock.go @@ -67,19 +67,19 @@ func (mr *MockLumeraClientMockRecorder) GetAction(ctx, actionID interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAction", reflect.TypeOf((*MockLumeraClient)(nil).GetAction), ctx, actionID) } -// GetActionParams mocks base method. -func (m *MockLumeraClient) GetActionParams(ctx context.Context) (*types.QueryParamsResponse, error) { +// GetActionFee mocks base method. +func (m *MockLumeraClient) GetActionFee(ctx context.Context, dataSize string) (*types.QueryGetActionFeeResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetActionParams", ctx) - ret0, _ := ret[0].(*types.QueryParamsResponse) + ret := m.ctrl.Call(m, "GetActionFee", ctx, dataSize) + ret0, _ := ret[0].(*types.QueryGetActionFeeResponse) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetActionParams indicates an expected call of GetActionParams. -func (mr *MockLumeraClientMockRecorder) GetActionParams(ctx interface{}) *gomock.Call { +// GetActionFee indicates an expected call of GetActionFee. +func (mr *MockLumeraClientMockRecorder) GetActionFee(ctx, dataSize interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActionParams", reflect.TypeOf((*MockLumeraClient)(nil).GetActionParams), ctx) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetActionFee", reflect.TypeOf((*MockLumeraClient)(nil).GetActionFee), ctx, dataSize) } // GetTopSupernodes mocks base method. diff --git a/supernode/services/cascade/adaptors/mocks/rq_mock.go b/supernode/services/cascade/adaptors/mocks/rq_mock.go index 306f66f7..648d9615 100644 --- a/supernode/services/cascade/adaptors/mocks/rq_mock.go +++ b/supernode/services/cascade/adaptors/mocks/rq_mock.go @@ -36,16 +36,16 @@ func (m *MockCodecService) EXPECT() *MockCodecServiceMockRecorder { } // EncodeInput mocks base method. -func (m *MockCodecService) EncodeInput(ctx context.Context, taskID string, data []byte) (adaptors.EncodeResult, error) { +func (m *MockCodecService) EncodeInput(ctx context.Context, taskID, path string, dataSize int) (adaptors.EncodeResult, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "EncodeInput", ctx, taskID, data) + ret := m.ctrl.Call(m, "EncodeInput", ctx, taskID, path, dataSize) ret0, _ := ret[0].(adaptors.EncodeResult) ret1, _ := ret[1].(error) return ret0, ret1 } // EncodeInput indicates an expected call of EncodeInput. -func (mr *MockCodecServiceMockRecorder) EncodeInput(ctx, taskID, data interface{}) *gomock.Call { +func (mr *MockCodecServiceMockRecorder) EncodeInput(ctx, taskID, path, dataSize interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EncodeInput", reflect.TypeOf((*MockCodecService)(nil).EncodeInput), ctx, taskID, data) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EncodeInput", reflect.TypeOf((*MockCodecService)(nil).EncodeInput), ctx, taskID, path, dataSize) } diff --git a/supernode/services/cascade/events_test.go b/supernode/services/cascade/events_test.go new file mode 100644 index 00000000..ce6ff782 --- /dev/null +++ b/supernode/services/cascade/events_test.go @@ -0,0 +1,34 @@ +package cascade + +import ( + "testing" +) + +func TestSupernodeEventTypeValues(t *testing.T) { + tests := []struct { + name string + value SupernodeEventType + expected int + }{ + {"UNKNOWN", SupernodeEventTypeUNKNOWN, 0}, + {"ActionRetrieved", SupernodeEventTypeActionRetrieved, 1}, + {"ActionFeeVerified", SupernodeEventTypeActionFeeVerified, 2}, + {"TopSupernodeCheckPassed", SupernodeEventTypeTopSupernodeCheckPassed, 3}, + {"MetadataDecoded", SupernodeEventTypeMetadataDecoded, 4}, + {"DataHashVerified", SupernodeEventTypeDataHashVerified, 5}, + {"InputEncoded", SupernodeEventTypeInputEncoded, 6}, + {"SignatureVerified", SupernodeEventTypeSignatureVerified, 7}, + {"RQIDsGenerated", SupernodeEventTypeRQIDsGenerated, 8}, + {"RqIDsVerified", SupernodeEventTypeRqIDsVerified, 9}, + {"ArtefactsStored", SupernodeEventTypeArtefactsStored, 10}, + {"ActionFinalized", SupernodeEventTypeActionFinalized, 11}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if int(tt.value) != tt.expected { + t.Errorf("Expected %s to be %d, got %d", tt.name, tt.expected, tt.value) + } + }) + } +} diff --git a/supernode/services/cascade/helper.go b/supernode/services/cascade/helper.go index c86993be..864c30f5 100644 --- a/supernode/services/cascade/helper.go +++ b/supernode/services/cascade/helper.go @@ -24,7 +24,7 @@ import ( ) func (task *CascadeRegistrationTask) fetchAction(ctx context.Context, actionID string, f logtrace.Fields) (*actiontypes.Action, error) { - res, err := task.lumeraClient.GetAction(ctx, actionID) + res, err := task.LumeraClient.GetAction(ctx, actionID) if err != nil { return nil, task.wrapErr(ctx, "failed to get action", err, f) } @@ -38,7 +38,7 @@ func (task *CascadeRegistrationTask) fetchAction(ctx context.Context, actionID s } func (task *CascadeRegistrationTask) ensureIsTopSupernode(ctx context.Context, blockHeight uint64, f logtrace.Fields) error { - top, err := task.lumeraClient.GetTopSupernodes(ctx, blockHeight) + top, err := task.LumeraClient.GetTopSupernodes(ctx, blockHeight) if err != nil { return task.wrapErr(ctx, "failed to get top SNs", err, f) } @@ -80,7 +80,7 @@ func (task *CascadeRegistrationTask) verifyDataHash(ctx context.Context, dh []by } func (task *CascadeRegistrationTask) encodeInput(ctx context.Context, path string, dataSize int, f logtrace.Fields) (*adaptors.EncodeResult, error) { - resp, err := task.rq.EncodeInput(ctx, task.ID(), path, dataSize) + resp, err := task.RQ.EncodeInput(ctx, task.ID(), path, dataSize) if err != nil { return nil, task.wrapErr(ctx, "failed to encode data", err, f) } @@ -109,7 +109,7 @@ func (task *CascadeRegistrationTask) verifySignatureAndDecodeLayout(ctx context. }) // Pass the decoded signature bytes for verification - if err := task.lumeraClient.Verify(ctx, creator, []byte(file), sigBytes); err != nil { + if err := task.LumeraClient.Verify(ctx, creator, []byte(file), sigBytes); err != nil { return codec.Layout{}, "", task.wrapErr(ctx, "failed to verify node creator signature", err, f) } @@ -141,7 +141,7 @@ func (task *CascadeRegistrationTask) generateRQIDFiles(ctx context.Context, meta } func (task *CascadeRegistrationTask) storeArtefacts(ctx context.Context, actionID string, idFiles [][]byte, symbolsDir string, f logtrace.Fields) error { - return task.p2p.StoreArtefacts(ctx, adaptors.StoreArtefactsRequest{ + return task.P2P.StoreArtefacts(ctx, adaptors.StoreArtefactsRequest{ IDFiles: idFiles, SymbolsDir: symbolsDir, TaskID: task.ID(), @@ -202,9 +202,8 @@ func verifyIDs(ticketMetadata, metadata codec.Layout) error { // verifyActionFee checks if the action fee is sufficient for the given data size // It fetches action parameters, calculates the required fee, and compares it with the action price func (task *CascadeRegistrationTask) verifyActionFee(ctx context.Context, action *actiontypes.Action, dataSize int, fields logtrace.Fields) error { - dataSizeInKBs := dataSize / 1024 - fee, err := task.lumeraClient.GetActionFee(ctx, strconv.Itoa(dataSizeInKBs)) + fee, err := task.LumeraClient.GetActionFee(ctx, strconv.Itoa(dataSizeInKBs)) if err != nil { return task.wrapErr(ctx, "failed to get action fee", err, fields) } diff --git a/supernode/services/cascade/register.go b/supernode/services/cascade/register.go index 32f69830..d5c5b2c9 100644 --- a/supernode/services/cascade/register.go +++ b/supernode/services/cascade/register.go @@ -129,7 +129,7 @@ func (task *CascadeRegistrationTask) Register( logtrace.Info(ctx, "artefacts have been stored", fields) task.streamEvent(SupernodeEventTypeArtefactsStored, "artefacts have been stored", "", send) - resp, err := task.lumeraClient.FinalizeAction(ctx, action.ActionID, rqidResp.RQIDs) + resp, err := task.LumeraClient.FinalizeAction(ctx, action.ActionID, rqidResp.RQIDs) if err != nil { fields[logtrace.FieldError] = err.Error() logtrace.Info(ctx, "Finalize Action Error", fields) diff --git a/supernode/services/cascade/register_test.go b/supernode/services/cascade/register_test.go new file mode 100644 index 00000000..6d65d909 --- /dev/null +++ b/supernode/services/cascade/register_test.go @@ -0,0 +1,296 @@ +package cascade_test + +import ( + "context" + sdkmath "cosmossdk.io/math" + "encoding/base64" + "encoding/hex" + actiontypes "github.com/LumeraProtocol/lumera/x/action/v1/types" + sntypes "github.com/LumeraProtocol/lumera/x/supernode/v1/types" + codecpkg "github.com/LumeraProtocol/supernode/pkg/codec" + "github.com/LumeraProtocol/supernode/pkg/lumera/modules/action_msg" + "github.com/LumeraProtocol/supernode/supernode/services/cascade" + "github.com/LumeraProtocol/supernode/supernode/services/cascade/adaptors" + cascadeadaptormocks "github.com/LumeraProtocol/supernode/supernode/services/cascade/adaptors/mocks" + "github.com/LumeraProtocol/supernode/supernode/services/common" + sdk "github.com/cosmos/cosmos-sdk/types" + "github.com/cosmos/gogoproto/proto" + "lukechampine.com/blake3" + "os" + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func TestCascadeRegistrationTask_Register(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // Setup input file + tmpFile, err := os.CreateTemp("", "cascade-test-input") + assert.NoError(t, err) + + _, _ = tmpFile.WriteString("mock data") + + err = tmpFile.Close() // ✅ ensure it's flushed to disk + assert.NoError(t, err) + + rawHash, b64Hash := blake3HashRawAndBase64(t, tmpFile.Name()) + + tests := []struct { + name string + setupMocks func(lc *cascadeadaptormocks.MockLumeraClient, codec *cascadeadaptormocks.MockCodecService, p2p *cascadeadaptormocks.MockP2PService) + expectedError string + expectedEvents int + }{ + { + name: "happy path", + setupMocks: func(lc *cascadeadaptormocks.MockLumeraClient, codec *cascadeadaptormocks.MockCodecService, p2p *cascadeadaptormocks.MockP2PService) { + + lc.EXPECT(). + GetAction(gomock.Any(), "action123"). + Return(&actiontypes.QueryGetActionResponse{ + Action: &actiontypes.Action{ + ActionID: "action123", + Creator: "creator1", + BlockHeight: 100, + Metadata: encodedCascadeMetadata(b64Hash, t), + Price: &sdk.Coin{ + Denom: "ulume", + Amount: sdkmath.NewInt(1000), + }, + }, + }, nil) + + // 2. Top SNs + lc.EXPECT(). + GetTopSupernodes(gomock.Any(), uint64(100)). + Return(&sntypes.QueryGetTopSuperNodesForBlockResponse{ + Supernodes: []*sntypes.SuperNode{ + { + SupernodeAccount: "lumera1abcxyz", // must match task.config.SupernodeAccountAddress + }, + }, + }, nil) + + // 3. Signature verification + lc.EXPECT(). + Verify(gomock.Any(), "creator1", gomock.Any(), gomock.Any()). + Return(nil) + + // 4. Finalize + lc.EXPECT(). + FinalizeAction(gomock.Any(), "action123", gomock.Any()). + Return(&action_msg.FinalizeActionResult{TxHash: "tx123"}, nil) + + // 5. Params (if used in fee check) + lc.EXPECT().GetActionFee(gomock.Any(), "10").Return(&actiontypes.QueryGetActionFeeResponse{Amount: "1000"}, nil) + + // 6. Encode input + codec.EXPECT(). + EncodeInput(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()). + Return(adaptors.EncodeResult{ + SymbolsDir: "/tmp", + Metadata: codecpkg.Layout{Blocks: []codecpkg.Block{{BlockID: 1, Hash: "abc"}}}, + }, nil) + + // 7. Store artefacts + p2p.EXPECT(). + StoreArtefacts(gomock.Any(), gomock.Any(), gomock.Any()). + Return(nil) + }, + expectedError: "", + expectedEvents: 11, + }, + { + name: "get-action fails", + setupMocks: func(lc *cascadeadaptormocks.MockLumeraClient, _ *cascadeadaptormocks.MockCodecService, _ *cascadeadaptormocks.MockP2PService) { + lc.EXPECT(). + GetAction(gomock.Any(), "action123"). + Return(nil, assert.AnError) + }, + expectedError: "assert.AnError general error", + expectedEvents: 0, + }, + { + name: "invalid data hash mismatch", + setupMocks: func(lc *cascadeadaptormocks.MockLumeraClient, codec *cascadeadaptormocks.MockCodecService, p2p *cascadeadaptormocks.MockP2PService) { + lc.EXPECT(). + GetAction(gomock.Any(), "action123"). + Return(&actiontypes.QueryGetActionResponse{ + Action: &actiontypes.Action{ + ActionID: "action123", + Creator: "creator1", + BlockHeight: 100, + Metadata: encodedCascadeMetadata("some-other-hash", t), // ⛔ incorrect hash + Price: &sdk.Coin{ + Denom: "ulume", + Amount: sdkmath.NewInt(1000), + }, + }, + }, nil) + + lc.EXPECT(). + GetTopSupernodes(gomock.Any(), uint64(100)). + Return(&sntypes.QueryGetTopSuperNodesForBlockResponse{ + Supernodes: []*sntypes.SuperNode{ + {SupernodeAccount: "lumera1abcxyz"}, + }, + }, nil) + + lc.EXPECT().GetActionFee(gomock.Any(), "10").Return(&actiontypes.QueryGetActionFeeResponse{Amount: "1000"}, nil) + }, + expectedError: "data hash doesn't match", + expectedEvents: 5, // up to metadata decoded + }, + { + name: "fee too low", + setupMocks: func(lc *cascadeadaptormocks.MockLumeraClient, codec *cascadeadaptormocks.MockCodecService, p2p *cascadeadaptormocks.MockP2PService) { + lc.EXPECT(). + GetAction(gomock.Any(), "action123"). + Return(&actiontypes.QueryGetActionResponse{ + Action: &actiontypes.Action{ + ActionID: "action123", + Creator: "creator1", + BlockHeight: 100, + Metadata: encodedCascadeMetadata(b64Hash, t), + Price: &sdk.Coin{ + Denom: "ulume", + Amount: sdkmath.NewInt(50), + }, + }, + }, nil) + + lc.EXPECT().GetActionFee(gomock.Any(), "10").Return(&actiontypes.QueryGetActionFeeResponse{Amount: "100"}, nil) + + }, + expectedError: "action fee is too low", + expectedEvents: 2, // until fee check + }, + { + name: "supernode not in top list", + setupMocks: func(lc *cascadeadaptormocks.MockLumeraClient, codec *cascadeadaptormocks.MockCodecService, p2p *cascadeadaptormocks.MockP2PService) { + lc.EXPECT(). + GetAction(gomock.Any(), "action123"). + Return(&actiontypes.QueryGetActionResponse{ + Action: &actiontypes.Action{ + ActionID: "action123", + Creator: "creator1", + BlockHeight: 100, + Metadata: encodedCascadeMetadata(b64Hash, t), + Price: &sdk.Coin{ + Denom: "ulume", + Amount: sdkmath.NewInt(1000), + }, + }, + }, nil) + + lc.EXPECT().GetActionFee(gomock.Any(), "10").Return(&actiontypes.QueryGetActionFeeResponse{Amount: "1000"}, nil) + + lc.EXPECT(). + GetTopSupernodes(gomock.Any(), uint64(100)). + Return(&sntypes.QueryGetTopSuperNodesForBlockResponse{ + Supernodes: []*sntypes.SuperNode{ + {SupernodeAccount: "other-supernode"}, + }, + }, nil) + }, + expectedError: "not eligible supernode", + expectedEvents: 2, // fails after fee verified + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockLumera := cascadeadaptormocks.NewMockLumeraClient(ctrl) + mockCodec := cascadeadaptormocks.NewMockCodecService(ctrl) + mockP2P := cascadeadaptormocks.NewMockP2PService(ctrl) + + tt.setupMocks(mockLumera, mockCodec, mockP2P) + + config := &cascade.Config{Config: common.Config{ + SupernodeAccountAddress: "lumera1abcxyz", + }, + } + + service := cascade.NewCascadeService( + config, + nil, nil, nil, nil, + ) + + service.LumeraClient = mockLumera + service.P2P = mockP2P + service.RQ = mockCodec + // Inject mocks for adaptors + task := cascade.NewCascadeRegistrationTask(service) + + req := &cascade.RegisterRequest{ + TaskID: "task1", + ActionID: "action123", + DataHash: rawHash, + DataSize: 10240, + FilePath: tmpFile.Name(), + } + + var events []cascade.RegisterResponse + err := task.Register(context.Background(), req, func(resp *cascade.RegisterResponse) error { + events = append(events, *resp) + return nil + }) + + if tt.expectedError != "" { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Len(t, events, tt.expectedEvents) + } + }) + } +} + +func encodedCascadeMetadata(hash string, t *testing.T) []byte { + t.Helper() + + // Fake encoded layout and signature + fakeLayout := base64.StdEncoding.EncodeToString([]byte(`{"blocks":[{"block_id":1,"hash":"abc"}]}`)) + fakeSig := base64.StdEncoding.EncodeToString([]byte("fakesignature")) + + metadata := &actiontypes.CascadeMetadata{ + DataHash: hash, + FileName: "file.txt", + RqIdsIc: 2, + RqIdsMax: 4, + RqIdsIds: []string{"id1", "id2"}, + Signatures: fakeLayout + "." + fakeSig, + } + + bytes, err := proto.Marshal(metadata) + if err != nil { + t.Fatalf("failed to marshal CascadeMetadata: %v", err) + } + + return bytes +} + +func blake3HashRawAndBase64(t *testing.T, path string) ([]byte, string) { + t.Helper() + + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + + hash := blake3.Sum256(data) + raw := hash[:] + b64 := base64.StdEncoding.EncodeToString(raw) + return raw, b64 +} + +func decodeHexOrDie(hexStr string) []byte { + bz, err := hex.DecodeString(hexStr) + if err != nil { + panic(err) + } + return bz +} diff --git a/supernode/services/cascade/service.go b/supernode/services/cascade/service.go index ec8f6373..ae440dfd 100644 --- a/supernode/services/cascade/service.go +++ b/supernode/services/cascade/service.go @@ -15,9 +15,9 @@ type CascadeService struct { *common.SuperNodeService config *Config - lumeraClient adaptors.LumeraClient - p2p adaptors.P2PService - rq adaptors.CodecService + LumeraClient adaptors.LumeraClient + P2P adaptors.P2PService + RQ adaptors.CodecService } // NewCascadeRegistrationTask creates a new task for cascade registration @@ -37,8 +37,8 @@ func NewCascadeService(config *Config, lumera lumera.Client, p2pClient p2p.Clien return &CascadeService{ config: config, SuperNodeService: common.NewSuperNodeService(p2pClient), - lumeraClient: adaptors.NewLumeraClient(lumera), - p2p: adaptors.NewP2PService(p2pClient, rqstore), - rq: adaptors.NewCodecService(codec), + LumeraClient: adaptors.NewLumeraClient(lumera), + P2P: adaptors.NewP2PService(p2pClient, rqstore), + RQ: adaptors.NewCodecService(codec), } } diff --git a/supernode/services/cascade/service_test.go b/supernode/services/cascade/service_test.go new file mode 100644 index 00000000..f8859bc1 --- /dev/null +++ b/supernode/services/cascade/service_test.go @@ -0,0 +1,71 @@ +package cascade_test + +import ( + "context" + "testing" + "time" + + "github.com/LumeraProtocol/supernode/supernode/services/cascade" + cascadeadaptormocks "github.com/LumeraProtocol/supernode/supernode/services/cascade/adaptors/mocks" + "github.com/LumeraProtocol/supernode/supernode/services/common" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" +) + +func TestNewCascadeService(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLumera := cascadeadaptormocks.NewMockLumeraClient(ctrl) + mockP2P := cascadeadaptormocks.NewMockP2PService(ctrl) + mockCodec := cascadeadaptormocks.NewMockCodecService(ctrl) + + config := &cascade.Config{ + Config: common.Config{ + SupernodeAccountAddress: "lumera1abcxyz", + }, + } + + service := cascade.NewCascadeService(config, nil, nil, nil, nil) + service.LumeraClient = mockLumera + service.RQ = mockCodec + service.P2P = mockP2P + + assert.NotNil(t, service) + assert.NotNil(t, service.LumeraClient) + assert.NotNil(t, service.P2P) + assert.NotNil(t, service.RQ) +} + +func TestNewCascadeRegistrationTask(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockLumera := cascadeadaptormocks.NewMockLumeraClient(ctrl) + mockP2P := cascadeadaptormocks.NewMockP2PService(ctrl) + mockCodec := cascadeadaptormocks.NewMockCodecService(ctrl) + + config := &cascade.Config{ + Config: common.Config{ + SupernodeAccountAddress: "lumera1abcxyz", + }, + } + + service := cascade.NewCascadeService(config, nil, nil, nil, nil) + service.LumeraClient = mockLumera + service.RQ = mockCodec + service.P2P = mockP2P + + task := cascade.NewCascadeRegistrationTask(service) + assert.NotNil(t, task) + + go func() { + service.Worker.AddTask(task) + }() + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) + defer cancel() + + err := service.RunHelper(ctx, "node-id", "prefix") + assert.NoError(t, err) +} diff --git a/supernode/services/common/p2p.go b/supernode/services/common/p2p.go deleted file mode 100644 index 535f9298..00000000 --- a/supernode/services/common/p2p.go +++ /dev/null @@ -1,11 +0,0 @@ -package common - -const ( - // UnknownDataType ... - UnknownDataType = iota // 1 - - // P2PDataRaptorQSymbol rq symbol - P2PDataRaptorQSymbol // 1 - // P2PDataCascadeMetadata cascade ID file - P2PDataCascadeMetadata // 2 -) diff --git a/supernode/services/common/status_test.go b/supernode/services/common/status_test.go new file mode 100644 index 00000000..b9853120 --- /dev/null +++ b/supernode/services/common/status_test.go @@ -0,0 +1,59 @@ +package common + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestStatus_String(t *testing.T) { + tests := []struct { + status Status + expected string + }{ + {StatusTaskStarted, "Task started"}, + {StatusTaskCanceled, "Task Canceled"}, + {StatusTaskCompleted, "Task Completed"}, + {StatusPrimaryMode, ""}, + {StatusSecondaryMode, ""}, + {StatusConnected, ""}, + {Status(255), ""}, // unknown status + } + + for _, tt := range tests { + assert.Equal(t, tt.expected, tt.status.String(), "Status.String() should match expected name") + } +} + +func TestStatus_IsFinal(t *testing.T) { + tests := []struct { + status Status + expected bool + }{ + {StatusTaskStarted, false}, + {StatusPrimaryMode, false}, + {StatusSecondaryMode, false}, + {StatusConnected, false}, + {StatusTaskCanceled, true}, + {StatusTaskCompleted, true}, + } + + for _, tt := range tests { + assert.Equal(t, tt.expected, tt.status.IsFinal(), "Status.IsFinal() mismatch") + } +} + +func TestStatus_IsFailure(t *testing.T) { + tests := []struct { + status Status + expected bool + }{ + {StatusTaskStarted, false}, + {StatusTaskCanceled, true}, + {StatusTaskCompleted, false}, + } + + for _, tt := range tests { + assert.Equal(t, tt.expected, tt.status.IsFailure(), "Status.IsFailure() mismatch") + } +} diff --git a/supernode/services/common/storage_handler.go b/supernode/services/common/storage_handler.go index 7ad1a4da..a69d7b83 100644 --- a/supernode/services/common/storage_handler.go +++ b/supernode/services/common/storage_handler.go @@ -23,6 +23,10 @@ const ( loadSymbolsBatchSize = 2500 storeSymbolsPercent = 10 concurrency = 1 + + UnknownDataType = iota // 1 + P2PDataRaptorQSymbol // 1 + P2PDataCascadeMetadata // 2 ) // StorageHandler provides common logic for RQ and P2P operations diff --git a/supernode/services/common/storage_handler_test.go b/supernode/services/common/storage_handler_test.go new file mode 100644 index 00000000..369a9a42 --- /dev/null +++ b/supernode/services/common/storage_handler_test.go @@ -0,0 +1,56 @@ +package common_test + +import ( + "context" + "github.com/LumeraProtocol/supernode/p2p/mocks" + "testing" + + "github.com/LumeraProtocol/supernode/supernode/services/common" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// --- Mocks --- + +type mockP2PClient struct { + mocks.Client +} + +type mockStore struct { + mock.Mock +} + +func (m *mockStore) StoreSymbolDirectory(taskID, dir string) error { + args := m.Called(taskID, dir) + return args.Error(0) +} + +func (m *mockStore) UpdateIsFirstBatchStored(txID string) error { + args := m.Called(txID) + return args.Error(0) +} + +func TestStoreBytesIntoP2P(t *testing.T) { + p2pClient := new(mockP2PClient) + handler := common.NewStorageHandler(p2pClient, "", nil) + + data := []byte("hello") + p2pClient.On("Store", mock.Anything, data, 1).Return("some-id", nil) + + id, err := handler.StoreBytesIntoP2P(context.Background(), data, 1) + assert.NoError(t, err) + assert.Equal(t, "some-id", id) + p2pClient.AssertExpectations(t) +} + +func TestStoreBatch(t *testing.T) { + p2pClient := new(mockP2PClient) + handler := common.NewStorageHandler(p2pClient, "", nil) + + ctx := context.WithValue(context.Background(), "task_id", "123") + list := [][]byte{[]byte("a"), []byte("b")} + p2pClient.On("StoreBatch", mock.Anything, list, 3, "").Return(nil) + + err := handler.StoreBatch(ctx, list, 3) + assert.NoError(t, err) +} diff --git a/supernode/services/common/supernode_task_test.go b/supernode/services/common/supernode_task_test.go new file mode 100644 index 00000000..2bd4be8a --- /dev/null +++ b/supernode/services/common/supernode_task_test.go @@ -0,0 +1,83 @@ +package common_test + +import ( + "context" + "fmt" + "sync" + "testing" + "time" + + "github.com/LumeraProtocol/supernode/supernode/services/common" + "github.com/stretchr/testify/assert" +) + +func TestNewSuperNodeTask(t *testing.T) { + task := common.NewSuperNodeTask("testprefix") + assert.NotNil(t, task) + assert.Equal(t, "testprefix", task.LogPrefix) +} + +func TestSuperNodeTask_RunHelper(t *testing.T) { + called := false + cleaner := func() { + called = true + } + + snt := common.NewSuperNodeTask("log") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + // Run the helper in a goroutine + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + err := snt.RunHelper(ctx, cleaner) + assert.NoError(t, err) + }() + + // Give the RunHelper some time to start and block on actionCh + time.Sleep(10 * time.Millisecond) + + // Submit dummy action to allow RunAction to proceed + done := snt.NewAction(func(ctx context.Context) error { + return nil + }) + + <-done // wait for action to complete + + snt.CloseActionCh() // close to allow RunAction to return + wg.Wait() // wait for RunHelper to exit + + assert.True(t, called) +} + +func TestSuperNodeTask_RunHelper_WithError(t *testing.T) { + snt := common.NewSuperNodeTask("log") + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var wg sync.WaitGroup + wg.Add(1) + + var runErr error + go func() { + defer wg.Done() + runErr = snt.RunHelper(ctx, func() {}) + }() + + // Give RunHelper time to start + time.Sleep(10 * time.Millisecond) + + done := snt.NewAction(func(ctx context.Context) error { + return fmt.Errorf("fail") + }) + + <-done // wait for the action to complete + snt.CloseActionCh() // allow RunAction to exit + wg.Wait() // wait for RunHelper to return + + assert.EqualError(t, runErr, "fail") +}