diff --git a/engine/access/testutil/fixture.go b/engine/access/testutil/fixture.go index aa728a28ec4..1fe5dafa486 100644 --- a/engine/access/testutil/fixture.go +++ b/engine/access/testutil/fixture.go @@ -197,7 +197,7 @@ func (tb *ProtocolDataFixtureBuilder) buildBlocks() ([]*flow.Block, []*flow.Head guarantees := make([]*flow.CollectionGuarantee, tb.colPerBlock) blockCollections := make([]*flow.Collection, tb.colPerBlock) for j := range tb.colPerBlock { - colTxs := unittest.TransactionBodyListFixture(tb.txPerCol) + colTxs := unittest.TransactionFixtures(tb.txPerCol) col := unittest.CompleteCollectionFromTransactions(colTxs) guarantees[j] = col.Guarantee diff --git a/engine/common/rpc/convert/blocks_test.go b/engine/common/rpc/convert/blocks_test.go index 7a8580051d4..b1d560e9af4 100644 --- a/engine/common/rpc/convert/blocks_test.go +++ b/engine/common/rpc/convert/blocks_test.go @@ -3,7 +3,9 @@ package convert_test import ( "bytes" "testing" + "time" + "github.com/onflow/flow/protobuf/go/flow/entities" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -26,7 +28,7 @@ func TestConvertBlock(t *testing.T) { converted, err := convert.MessageToBlock(msg) require.NoError(t, err) - assert.Equal(t, block, converted) + assert.Equal(t, block.ID(), converted.ID()) } // TestConvertBlockLight tests that converting a block to its light form results in only the correct @@ -76,3 +78,105 @@ func TestConvertRootBlock(t *testing.T) { assert.Equal(t, block.ID(), converted.ID()) } + +// TestConvertBlockStatus tests converting protobuf BlockStatus messages to flow.BlockStatus. +func TestConvertBlockStatus(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + pbStatus entities.BlockStatus + expected flow.BlockStatus + }{ + {"Unknown", entities.BlockStatus_BLOCK_UNKNOWN, flow.BlockStatusUnknown}, + {"Finalized", entities.BlockStatus_BLOCK_FINALIZED, flow.BlockStatusFinalized}, + {"Sealed", entities.BlockStatus_BLOCK_SEALED, flow.BlockStatusSealed}, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + converted := convert.MessageToBlockStatus(tc.pbStatus) + assert.Equal(t, tc.expected, converted) + }) + } +} + +// TestConvertBlockSeal tests converting a flow.Seal to and from a protobuf BlockSeal message. +func TestConvertBlockSeal(t *testing.T) { + t.Parallel() + + seal := unittest.Seal.Fixture() + + msg := convert.BlockSealToMessage(seal) + converted, err := convert.MessageToBlockSeal(msg) + require.NoError(t, err) + + assert.Equal(t, seal.ID(), converted.ID()) +} + +// TestConvertBlockSeals tests converting multiple flow.Seal to and from protobuf BlockSeal messages. +func TestConvertBlockSeals(t *testing.T) { + t.Parallel() + + seals := unittest.Seal.Fixtures(3) + + msgs := convert.BlockSealsToMessages(seals) + require.Len(t, msgs, len(seals)) + + converted, err := convert.MessagesToBlockSeals(msgs) + require.NoError(t, err) + require.Len(t, converted, len(seals)) + + for i, seal := range seals { + assert.Equal(t, seal.ID(), converted[i].ID()) + } +} + +// TestConvertPayloadFromMessage tests converting a protobuf Block message to a flow.Payload. +func TestConvertPayloadFromMessage(t *testing.T) { + t.Parallel() + + block := unittest.FullBlockFixture() + signerIDs := unittest.IdentifierListFixture(5) + + msg, err := convert.BlockToMessage(block, signerIDs) + require.NoError(t, err) + + payload, err := convert.PayloadFromMessage(msg) + require.NoError(t, err) + + assert.Equal(t, block.Payload.Hash(), payload.Hash()) +} + +// TestConvertBlockTimestamp2ProtobufTime tests converting block timestamps to protobuf Timestamp format. +func TestConvertBlockTimestamp2ProtobufTime(t *testing.T) { + t.Parallel() + + t.Run("convert current timestamp", func(t *testing.T) { + t.Parallel() + + // Use current time in unix milliseconds + now := time.Now() + timestampMillis := uint64(now.UnixMilli()) + + pbTime := convert.BlockTimestamp2ProtobufTime(timestampMillis) + require.NotNil(t, pbTime) + + // Convert back and verify + convertedTime := pbTime.AsTime() + assert.Equal(t, timestampMillis, uint64(convertedTime.UnixMilli())) + }) + + t.Run("convert zero timestamp", func(t *testing.T) { + t.Parallel() + + pbTime := convert.BlockTimestamp2ProtobufTime(0) + require.NotNil(t, pbTime) + + convertedTime := pbTime.AsTime() + assert.Equal(t, uint64(0), uint64(convertedTime.UnixMilli())) + }) +} diff --git a/engine/common/rpc/convert/compatible_range_test.go b/engine/common/rpc/convert/compatible_range_test.go index eb27dfa858b..cfa305eba67 100644 --- a/engine/common/rpc/convert/compatible_range_test.go +++ b/engine/common/rpc/convert/compatible_range_test.go @@ -4,7 +4,6 @@ import ( "math/rand" "testing" - "github.com/onflow/flow/protobuf/go/flow/entities" "github.com/stretchr/testify/assert" "github.com/onflow/flow-go/engine/common/rpc/convert" @@ -22,20 +21,20 @@ func TestConvertCompatibleRange(t *testing.T) { assert.Nil(t, convert.MessageToCompatibleRange(nil)) }) - t.Run("convert range to message", func(t *testing.T) { + t.Run("roundtrip conversion", func(t *testing.T) { + t.Parallel() + startHeight := uint64(rand.Uint32()) endHeight := uint64(rand.Uint32()) - comparableRange := &accessmodel.CompatibleRange{ - StartHeight: startHeight, - EndHeight: endHeight, - } - expected := &entities.CompatibleRange{ + original := &accessmodel.CompatibleRange{ StartHeight: startHeight, EndHeight: endHeight, } - msg := convert.CompatibleRangeToMessage(comparableRange) - assert.Equal(t, msg, expected) + msg := convert.CompatibleRangeToMessage(original) + converted := convert.MessageToCompatibleRange(msg) + + assert.Equal(t, original, converted) }) } diff --git a/engine/common/rpc/convert/convert_test.go b/engine/common/rpc/convert/convert_test.go new file mode 100644 index 00000000000..76f12f514aa --- /dev/null +++ b/engine/common/rpc/convert/convert_test.go @@ -0,0 +1,160 @@ +package convert_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow-go/engine/common/rpc/convert" + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/utils/unittest" +) + +// TestConvertIdentifier tests converting a flow.Identifier to and from a protobuf message. +func TestConvertIdentifier(t *testing.T) { + t.Parallel() + + id := unittest.IdentifierFixture() + + msg := convert.IdentifierToMessage(id) + converted := convert.MessageToIdentifier(msg) + + assert.Equal(t, id, converted) +} + +// TestConvertIdentifiers tests converting a slice of flow.Identifiers to and from protobuf messages. +func TestConvertIdentifiers(t *testing.T) { + t.Parallel() + + ids := unittest.IdentifierListFixture(10) + + msgs := convert.IdentifiersToMessages(ids) + converted := convert.MessagesToIdentifiers(msgs) + + assert.Equal(t, ids, flow.IdentifierList(converted)) +} + +// TestConvertSignature tests converting a crypto.Signature to and from a protobuf message. +func TestConvertSignature(t *testing.T) { + t.Parallel() + + sig := unittest.SignatureFixture() + + msg := convert.SignatureToMessage(sig) + converted := convert.MessageToSignature(msg) + + assert.Equal(t, sig, converted) +} + +// TestConvertSignatures tests converting a slice of crypto.Signatures to and from protobuf messages. +func TestConvertSignatures(t *testing.T) { + t.Parallel() + + sigs := unittest.SignaturesFixture(5) + + msgs := convert.SignaturesToMessages(sigs) + converted := convert.MessagesToSignatures(msgs) + + assert.Equal(t, sigs, converted) +} + +// TestConvertStateCommitment tests converting a flow.StateCommitment to and from a protobuf message. +func TestConvertStateCommitment(t *testing.T) { + t.Parallel() + + sc := unittest.StateCommitmentFixture() + + msg := convert.StateCommitmentToMessage(sc) + converted, err := convert.MessageToStateCommitment(msg) + require.NoError(t, err) + + assert.Equal(t, sc, converted) +} + +// TestConvertStateCommitmentInvalidLength tests that MessageToStateCommitment returns an error +// for invalid length byte slices. +func TestConvertStateCommitmentInvalidLength(t *testing.T) { + t.Parallel() + + invalidMsg := []byte{0x01, 0x02, 0x03} // Too short + + _, err := convert.MessageToStateCommitment(invalidMsg) + assert.Error(t, err) + assert.Contains(t, err.Error(), "invalid state commitment length") +} + +// TestConvertAggregatedSignatures tests converting a slice of flow.AggregatedSignatures to and from +// protobuf messages. +func TestConvertAggregatedSignatures(t *testing.T) { + t.Parallel() + + aggSigs := unittest.Seal.AggregatedSignatureFixtures(2) + + msgs := convert.AggregatedSignaturesToMessages(aggSigs) + converted := convert.MessagesToAggregatedSignatures(msgs) + + assert.Equal(t, aggSigs, converted) +} + +// TestConvertAggregatedSignaturesEmpty tests converting an empty slice of flow.AggregatedSignatures. +func TestConvertAggregatedSignaturesEmpty(t *testing.T) { + t.Parallel() + + aggSigs := []flow.AggregatedSignature{} + + msgs := convert.AggregatedSignaturesToMessages(aggSigs) + converted := convert.MessagesToAggregatedSignatures(msgs) + + assert.Empty(t, converted) +} + +// TestConvertChainId tests converting a valid chainId string to flow.ChainID. +func TestConvertChainId(t *testing.T) { + t.Parallel() + + t.Run("valid chain IDs", func(t *testing.T) { + t.Parallel() + + validChainIDs := []flow.ChainID{ + flow.Mainnet, + flow.Testnet, + flow.Emulator, + flow.Localnet, + flow.Sandboxnet, + flow.Previewnet, + flow.Benchnet, + flow.BftTestnet, + flow.MonotonicEmulator, + } + + for _, chainID := range validChainIDs { + t.Run(chainID.String(), func(t *testing.T) { + t.Parallel() + + result, err := convert.MessageToChainId(chainID.String()) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, chainID, *result) + }) + } + }) + + t.Run("invalid chain IDs", func(t *testing.T) { + t.Parallel() + + invalid := []string{"invalid-chain", ""} + + for _, chainID := range invalid { + t.Run(fmt.Sprintf("invalid_%q", chainID), func(t *testing.T) { + t.Parallel() + + result, err := convert.MessageToChainId(chainID) + assert.Error(t, err) + assert.Nil(t, result) + assert.Contains(t, err.Error(), "invalid chainId") + }) + } + }) +} diff --git a/engine/common/rpc/convert/events.go b/engine/common/rpc/convert/events.go index 2d78f3b62f2..65ed96b3857 100644 --- a/engine/common/rpc/convert/events.go +++ b/engine/common/rpc/convert/events.go @@ -280,21 +280,9 @@ func CcfEventToJsonEvent(e flow.Event) (*flow.Event, error) { func CcfEventsToJsonEvents(events []flow.Event) ([]flow.Event, error) { convertedEvents := make([]flow.Event, len(events)) for i, e := range events { - payload, err := CcfPayloadToJsonPayload(e.Payload) + convertedEvent, err := CcfEventToJsonEvent(e) if err != nil { - return nil, fmt.Errorf("failed to convert event payload for event %d: %w", i, err) - } - convertedEvent, err := flow.NewEvent( - flow.UntrustedEvent{ - Type: e.Type, - TransactionID: e.TransactionID, - TransactionIndex: e.TransactionIndex, - EventIndex: e.EventIndex, - Payload: payload, - }, - ) - if err != nil { - return nil, fmt.Errorf("could not construct event: %w", err) + return nil, err } convertedEvents[i] = *convertedEvent } diff --git a/engine/common/rpc/convert/events_test.go b/engine/common/rpc/convert/events_test.go index 7db322f80d4..60a57bf9a4a 100644 --- a/engine/common/rpc/convert/events_test.go +++ b/engine/common/rpc/convert/events_test.go @@ -3,6 +3,7 @@ package convert_test import ( "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/onflow/cadence" @@ -27,7 +28,6 @@ func TestConvertEventWithoutPayloadConversion(t *testing.T) { require.NoError(t, err) event := unittest.EventFixture( - unittest.Event.WithEventType(flow.EventAccountCreated), unittest.Event.WithPayload(ccfPayload), ) @@ -43,7 +43,6 @@ func TestConvertEventWithoutPayloadConversion(t *testing.T) { require.NoError(t, err) event := unittest.EventFixture( - unittest.Event.WithEventType(flow.EventAccountCreated), unittest.Event.WithPayload(jsonPayload), ) @@ -233,3 +232,313 @@ func TestConvertMessagesToBlockEvents(t *testing.T) { require.Equal(t, blockEvents, converted) } + +// TestConvertBlockEvent tests round-trip converting a single protobuf message. +func TestConvertBlockEvent(t *testing.T) { + t.Parallel() + + header := unittest.BlockHeaderFixture(unittest.WithHeaderHeight(42)) + blockEvents := unittest.BlockEventsFixture(header, 3) + + // Convert to message first + msg, err := convert.BlockEventsToMessage(blockEvents) + require.NoError(t, err) + + // Convert back to BlockEvents + converted, err := convert.MessageToBlockEvents(msg) + require.NoError(t, err) + require.NotNil(t, converted) + + require.Equal(t, blockEvents.BlockHeight, converted.BlockHeight) + require.Equal(t, blockEvents.BlockID, converted.BlockID) + require.Equal(t, blockEvents.BlockTimestamp, converted.BlockTimestamp) + require.Len(t, converted.Events, len(blockEvents.Events)) + for i, event := range blockEvents.Events { + require.Equal(t, event.ID(), converted.Events[i].ID()) + } +} + +// TestConvertCcfEventToJsonEvent tests converting a single CCF event to JSON event. +func TestConvertCcfEventToJsonEvent(t *testing.T) { + t.Parallel() + + t.Run("converts CCF event to JSON event", func(t *testing.T) { + t.Parallel() + + // Prepare input CCF event and expected JSON output + cadenceValue := cadence.NewInt(42) + ccfPayload, err := ccf.Encode(cadenceValue) + require.NoError(t, err) + + expectedJsonPayload, err := jsoncdc.Encode(cadenceValue) + require.NoError(t, err) + + ccfEvent := unittest.EventFixture( + unittest.Event.WithPayload(ccfPayload), + ) + + jsonEvent, err := convert.CcfEventToJsonEvent(ccfEvent) + require.NoError(t, err) + require.NotNil(t, jsonEvent) + + require.Equal(t, ccfEvent.Type, jsonEvent.Type) + require.Equal(t, ccfEvent.TransactionID, jsonEvent.TransactionID) + require.Equal(t, ccfEvent.TransactionIndex, jsonEvent.TransactionIndex) + require.Equal(t, ccfEvent.EventIndex, jsonEvent.EventIndex) + + // Verify payload matches expected JSON-CDC encoding + require.Equal(t, expectedJsonPayload, jsonEvent.Payload) + }) + + t.Run("returns error on invalid CCF payload", func(t *testing.T) { + t.Parallel() + + invalidEvent := unittest.EventFixture( + unittest.Event.WithPayload([]byte{0x01, 0x02, 0x03}), // Invalid CCF + ) + + jsonEvent, err := convert.CcfEventToJsonEvent(invalidEvent) + require.Nil(t, jsonEvent) + require.Error(t, err) + assert.Contains(t, err.Error(), "unable to decode from ccf format") + }) + + t.Run("returns error on empty payload", func(t *testing.T) { + t.Parallel() + + emptyEvent := unittest.EventFixture( + unittest.Event.WithPayload([]byte{}), + ) + + jsonEvent, err := convert.CcfEventToJsonEvent(emptyEvent) + require.Nil(t, jsonEvent) + require.Error(t, err, "empty payload should result in error from flow.NewEvent") + assert.Contains(t, err.Error(), "payload must not be empty") + }) +} + +// TestConvertCcfPayloadToJsonPayload tests converting CCF-encoded payloads to JSON-encoded payloads. +func TestConvertCcfPayloadToJsonPayload(t *testing.T) { + t.Parallel() + + t.Run("convert ccf payload to json payload", func(t *testing.T) { + t.Parallel() + + cadenceValue := cadence.NewInt(42) + ccfPayload, err := ccf.Encode(cadenceValue) + require.NoError(t, err) + + jsonPayload, err := convert.CcfPayloadToJsonPayload(ccfPayload) + require.NoError(t, err) + + decoded, err := jsoncdc.Decode(nil, jsonPayload) + require.NoError(t, err) + require.Equal(t, cadenceValue, decoded) + }) + + t.Run("empty payload returns empty", func(t *testing.T) { + t.Parallel() + + result, err := convert.CcfPayloadToJsonPayload([]byte{}) + require.NoError(t, err) + require.Empty(t, result) + }) + + t.Run("invalid ccf payload returns error", func(t *testing.T) { + t.Parallel() + + invalidPayload := []byte{0x01, 0x02, 0x03} + jsonPayload, err := convert.CcfPayloadToJsonPayload(invalidPayload) + require.Nil(t, jsonPayload) + require.Error(t, err) + assert.Contains(t, err.Error(), "unable to decode from ccf format") + }) +} + +// TestConvertCcfEventsToJsonEvents tests converting multiple CCF events to JSON events. +func TestConvertCcfEventsToJsonEvents(t *testing.T) { + t.Parallel() + + t.Run("converts multiple events correctly", func(t *testing.T) { + t.Parallel() + + eventCount := 5 + ccfEvents := make([]flow.Event, eventCount) + + // Create test events with varying payloads + for i := 0; i < eventCount; i++ { + cadenceValue := cadence.NewInt(i) + ccfPayload, err := ccf.Encode(cadenceValue) + require.NoError(t, err) + + ccfEvents[i] = unittest.EventFixture( + unittest.Event.WithPayload(ccfPayload), + ) + } + + converted, err := convert.CcfEventsToJsonEvents(ccfEvents) + require.NoError(t, err) + require.Len(t, converted, len(ccfEvents)) + + for i, convertedEvent := range converted { + require.Equal(t, ccfEvents[i].Type, convertedEvent.Type) + require.Equal(t, ccfEvents[i].TransactionID, convertedEvent.TransactionID) + require.Equal(t, ccfEvents[i].TransactionIndex, convertedEvent.TransactionIndex) + require.Equal(t, ccfEvents[i].EventIndex, convertedEvent.EventIndex) + + decoded, err := jsoncdc.Decode(nil, convertedEvent.Payload) + require.NoError(t, err, "payload should be valid JSON-CDC") + require.Equal(t, cadence.NewInt(i), decoded, "decoded value should match original") + } + }) + + t.Run("returns error on invalid CCF payload", func(t *testing.T) { + t.Parallel() + + invalidEvents := []flow.Event{ + unittest.EventFixture( + unittest.Event.WithPayload([]byte{0x01, 0x02, 0x03}), // Invalid CCF + ), + } + + jsonEvents, err := convert.CcfEventsToJsonEvents(invalidEvents) + require.Nil(t, jsonEvents) + require.Error(t, err) + assert.Contains(t, err.Error(), "unable to decode from ccf format") + }) +} + +// TestConvertEventToMessageFromVersion tests converting events to messages with version-specific encoding. +func TestConvertEventToMessageFromVersion(t *testing.T) { + t.Parallel() + + cadenceValue := cadence.NewInt(123) + + t.Run("convert ccf event to json message", func(t *testing.T) { + t.Parallel() + + ccfPayload, err := ccf.Encode(cadenceValue) + require.NoError(t, err) + + event := unittest.EventFixture( + unittest.Event.WithPayload(ccfPayload), + ) + + message, err := convert.EventToMessageFromVersion(event, entities.EventEncodingVersion_CCF_V0) + require.NoError(t, err) + + decoded, err := jsoncdc.Decode(nil, message.Payload) + require.NoError(t, err) + require.Equal(t, cadenceValue, decoded) + }) + + t.Run("convert json event to json message", func(t *testing.T) { + t.Parallel() + + jsonPayload, err := jsoncdc.Encode(cadenceValue) + require.NoError(t, err) + + event := unittest.EventFixture( + unittest.Event.WithPayload(jsonPayload), + ) + + message, err := convert.EventToMessageFromVersion(event, entities.EventEncodingVersion_JSON_CDC_V0) + require.NoError(t, err) + + require.Equal(t, jsonPayload, message.Payload) + }) + + t.Run("empty payload is handled correctly", func(t *testing.T) { + t.Parallel() + + event := unittest.EventFixture( + unittest.Event.WithEventType(flow.EventAccountCreated), + unittest.Event.WithPayload([]byte{}), + ) + + message, err := convert.EventToMessageFromVersion(event, entities.EventEncodingVersion_CCF_V0) + require.NoError(t, err) + require.Empty(t, message.Payload) + }) + + t.Run("invalid version returns error", func(t *testing.T) { + t.Parallel() + + event := unittest.EventFixture() + + message, err := convert.EventToMessageFromVersion(event, entities.EventEncodingVersion(999)) + require.Nil(t, message) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid encoding format") + }) +} + +// TestConvertEventsToMessagesWithEncodingConversion tests batch conversion with encoding changes. +func TestConvertEventsToMessagesWithEncodingConversion(t *testing.T) { + t.Parallel() + + eventCount := 3 + ccfEvents := make([]flow.Event, eventCount) + + for i := 0; i < eventCount; i++ { + cadenceValue := cadence.NewInt(i) + ccfPayload, err := ccf.Encode(cadenceValue) + require.NoError(t, err) + + ccfEvents[i] = unittest.EventFixture( + unittest.Event.WithEventType(flow.EventAccountCreated), + unittest.Event.WithPayload(ccfPayload), + ) + } + + t.Run("convert from ccf to json", func(t *testing.T) { + t.Parallel() + + messages, err := convert.EventsToMessagesWithEncodingConversion( + ccfEvents, + entities.EventEncodingVersion_CCF_V0, + entities.EventEncodingVersion_JSON_CDC_V0, + ) + require.NoError(t, err) + require.Len(t, messages, len(ccfEvents)) + + for i, msg := range messages { + decoded, err := jsoncdc.Decode(nil, msg.Payload) + require.NoError(t, err) + require.Equal(t, cadence.NewInt(i), decoded) + } + }) + + t.Run("same version uses passthrough", func(t *testing.T) { + t.Parallel() + + messages, err := convert.EventsToMessagesWithEncodingConversion( + ccfEvents, + entities.EventEncodingVersion_CCF_V0, + entities.EventEncodingVersion_CCF_V0, + ) + require.NoError(t, err) + require.Len(t, messages, len(ccfEvents)) + }) + + t.Run("json to ccf conversion is not supported", func(t *testing.T) { + t.Parallel() + + jsonEvents := make([]flow.Event, 1) + jsonPayload, err := jsoncdc.Encode(cadence.NewInt(1)) + require.NoError(t, err) + + jsonEvents[0] = unittest.EventFixture( + unittest.Event.WithPayload(jsonPayload), + ) + + message, err := convert.EventsToMessagesWithEncodingConversion( + jsonEvents, + entities.EventEncodingVersion_JSON_CDC_V0, + entities.EventEncodingVersion_CCF_V0, + ) + require.Nil(t, message) + require.Error(t, err) + assert.Contains(t, err.Error(), "conversion from format") + }) +} diff --git a/engine/common/rpc/convert/execution_data_test.go b/engine/common/rpc/convert/execution_data_test.go index 2d8ec9d5eb1..e093b0e475c 100644 --- a/engine/common/rpc/convert/execution_data_test.go +++ b/engine/common/rpc/convert/execution_data_test.go @@ -47,7 +47,8 @@ func TestConvertBlockExecutionDataEventPayloads(t *testing.T) { for i, e := range events { require.Equal(t, ccfEvents[i], e) - _, err := ccf.Decode(nil, e.Payload) + res, err := ccf.Decode(nil, e.Payload) + require.NotNil(t, res) require.NoError(t, err) } } @@ -64,7 +65,8 @@ func TestConvertBlockExecutionDataEventPayloads(t *testing.T) { for i, e := range events { require.Equal(t, jsonEvents[i], e) - _, err := jsoncdc.Decode(nil, e.Payload) + res, err := jsoncdc.Decode(nil, e.Payload) + require.NotNil(t, res) require.NoError(t, err) } } diff --git a/engine/common/rpc/convert/execution_state_query_test.go b/engine/common/rpc/convert/execution_state_query_test.go new file mode 100644 index 00000000000..e639510ad21 --- /dev/null +++ b/engine/common/rpc/convert/execution_state_query_test.go @@ -0,0 +1,70 @@ +package convert_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/onflow/flow/protobuf/go/flow/entities" + + "github.com/onflow/flow-go/engine/common/rpc/convert" + "github.com/onflow/flow-go/module/executiondatasync/optimistic_sync" + "github.com/onflow/flow-go/utils/unittest" +) + +// TestConvertExecutionStateQuery tests converting a protobuf ExecutionStateQuery to Criteria. +func TestConvertExecutionStateQuery(t *testing.T) { + t.Parallel() + + t.Run("non-nil query", func(t *testing.T) { + executorIDs := unittest.IdentifierListFixture(5) + agreeingCount := uint64(3) + + query := &entities.ExecutionStateQuery{ + AgreeingExecutorsCount: agreeingCount, + RequiredExecutorIds: convert.IdentifiersToMessages(executorIDs), + } + + criteria := convert.NewCriteria(query) + + assert.Equal(t, uint(agreeingCount), criteria.AgreeingExecutorsCount) + require.Equal(t, len(executorIDs), len(criteria.RequiredExecutors)) + for i, id := range executorIDs { + assert.Equal(t, id, criteria.RequiredExecutors[i]) + } + }) + + t.Run("nil query", func(t *testing.T) { + criteria := convert.NewCriteria(nil) + + expected := optimistic_sync.Criteria{} + assert.Equal(t, expected, criteria) + }) + + t.Run("empty required executors", func(t *testing.T) { + query := &entities.ExecutionStateQuery{ + AgreeingExecutorsCount: 5, + RequiredExecutorIds: [][]byte{}, + } + + criteria := convert.NewCriteria(query) + + assert.Equal(t, uint(5), criteria.AgreeingExecutorsCount) + assert.Empty(t, criteria.RequiredExecutors) + }) + + t.Run("zero agreeing executors count", func(t *testing.T) { + executorIDs := unittest.IdentifierListFixture(3) + + query := &entities.ExecutionStateQuery{ + AgreeingExecutorsCount: 0, + RequiredExecutorIds: convert.IdentifiersToMessages(executorIDs), + } + + criteria := convert.NewCriteria(query) + + assert.Equal(t, uint(0), criteria.AgreeingExecutorsCount) + assert.Equal(t, len(executorIDs), len(criteria.RequiredExecutors)) + }) +} diff --git a/engine/common/rpc/convert/validate.go b/engine/common/rpc/convert/validate.go index d27cadcca1e..d8d1edfc32c 100644 --- a/engine/common/rpc/convert/validate.go +++ b/engine/common/rpc/convert/validate.go @@ -70,7 +70,7 @@ func BlockIDs(blockIDs [][]byte) ([]flow.Identifier, error) { } func CollectionID(collectionID []byte) (flow.Identifier, error) { - if len(collectionID) == 0 { + if len(collectionID) != flow.IdentifierLen { return flow.ZeroID, status.Error(codes.InvalidArgument, "invalid collection id") } return flow.HashToID(collectionID), nil @@ -84,7 +84,7 @@ func EventType(eventType string) (string, error) { } func TransactionID(txID []byte) (flow.Identifier, error) { - if len(txID) == 0 { + if len(txID) != flow.IdentifierLen { return flow.ZeroID, status.Error(codes.InvalidArgument, "invalid transaction id") } return flow.HashToID(txID), nil diff --git a/engine/common/rpc/convert/validate_test.go b/engine/common/rpc/convert/validate_test.go new file mode 100644 index 00000000000..697a5e06244 --- /dev/null +++ b/engine/common/rpc/convert/validate_test.go @@ -0,0 +1,303 @@ +package convert_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/onflow/flow-go/engine/common/rpc/convert" + "github.com/onflow/flow-go/model/flow" + "github.com/onflow/flow-go/utils/unittest" +) + +// TestValidateAddress tests the Address validation function. +func TestValidateAddress(t *testing.T) { + t.Parallel() + + // Use Testnet chain which is what AddressFixture uses + chain := flow.Testnet.Chain() + + t.Run("valid address", func(t *testing.T) { + t.Parallel() + + validAddr := unittest.AddressFixture() + rawAddr := validAddr.Bytes() + + addr, err := convert.Address(rawAddr, chain) + require.NoError(t, err) + assert.Equal(t, validAddr, addr) + }) + + t.Run("empty address", func(t *testing.T) { + t.Parallel() + + _, err := convert.Address([]byte{}, chain) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "address cannot be empty") + }) + + t.Run("invalid address for chain", func(t *testing.T) { + t.Parallel() + + // Use an address from a different chain + mainnetAddr := unittest.RandomAddressFixtureForChain(flow.Mainnet) + rawAddr := mainnetAddr.Bytes() + + // This address should be invalid for Testnet chain + _, err := convert.Address(rawAddr, chain) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "is invalid for chain") + }) +} + +// TestValidateHexToAddress tests the HexToAddress validation function. +func TestValidateHexToAddress(t *testing.T) { + t.Parallel() + + chain := flow.Testnet.Chain() + + t.Run("valid hex address", func(t *testing.T) { + t.Parallel() + + validAddr := unittest.AddressFixture() + hexAddr := validAddr.Hex() + + addr, err := convert.HexToAddress(hexAddr, chain) + require.NoError(t, err) + assert.Equal(t, validAddr, addr) + }) + + t.Run("empty hex address", func(t *testing.T) { + t.Parallel() + + _, err := convert.HexToAddress("", chain) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "address cannot be empty") + }) + + t.Run("invalid hex address for chain", func(t *testing.T) { + t.Parallel() + + // Use an address from a different chain + mainnetAddr := unittest.RandomAddressFixtureForChain(flow.Mainnet) + hexAddr := mainnetAddr.Hex() + + // This address should be invalid for Testnet chain + _, err := convert.HexToAddress(hexAddr, chain) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "is invalid for chain") + }) +} + +// TestValidateBlockID tests the BlockID validation function. +func TestValidateBlockID(t *testing.T) { + t.Parallel() + + t.Run("valid block ID", func(t *testing.T) { + t.Parallel() + + expectedID := unittest.IdentifierFixture() + rawID := expectedID[:] + + blockID, err := convert.BlockID(rawID) + require.NoError(t, err) + assert.Equal(t, expectedID, blockID) + }) + + t.Run("invalid block ID length - too short", func(t *testing.T) { + t.Parallel() + + invalidID := []byte{0x01, 0x02, 0x03} + + _, err := convert.BlockID(invalidID) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "invalid block id") + }) + + t.Run("invalid block ID length - too long", func(t *testing.T) { + t.Parallel() + + invalidID := make([]byte, flow.IdentifierLen+1) + + _, err := convert.BlockID(invalidID) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "invalid block id") + }) + + t.Run("empty block ID", func(t *testing.T) { + t.Parallel() + + _, err := convert.BlockID([]byte{}) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + }) +} + +// TestValidateBlockIDs tests the BlockIDs validation function. +func TestValidateBlockIDs(t *testing.T) { + t.Parallel() + + t.Run("valid random block IDs lists", func(t *testing.T) { + t.Parallel() + + // Test multiple random lists to exercise conversion pathways + for count := 3; count < 8; count++ { + expectedIDs := unittest.IdentifierListFixture(count) + rawIDs := make([][]byte, len(expectedIDs)) + for j, id := range expectedIDs { + rawIDs[j] = id[:] + } + + blockIDs, err := convert.BlockIDs(rawIDs) + require.NoError(t, err) + assert.Equal(t, []flow.Identifier(expectedIDs), blockIDs) + } + }) + + t.Run("empty block IDs list", func(t *testing.T) { + t.Parallel() + + _, err := convert.BlockIDs([][]byte{}) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "empty block ids") + }) + + t.Run("one invalid block ID in list", func(t *testing.T) { + t.Parallel() + + validID := unittest.IdentifierFixture() + invalidID := []byte{0x01, 0x02} + + rawIDs := [][]byte{validID[:], invalidID} + + _, err := convert.BlockIDs(rawIDs) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + }) +} + +// TestValidateCollectionID tests the CollectionID validation function. +func TestValidateCollectionID(t *testing.T) { + t.Parallel() + + t.Run("valid collection ID", func(t *testing.T) { + t.Parallel() + + expectedID := unittest.IdentifierFixture() + rawID := expectedID[:] + + collectionID, err := convert.CollectionID(rawID) + require.NoError(t, err) + assert.Equal(t, expectedID, collectionID) + }) + + t.Run("empty collection ID", func(t *testing.T) { + t.Parallel() + + _, err := convert.CollectionID([]byte{}) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "invalid collection id") + }) + + t.Run("collection ID with different length", func(t *testing.T) { + t.Parallel() + + // so this should succeed + shortID := []byte{0x01, 0x02} + + _, err := convert.CollectionID(shortID) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "invalid collection id") + }) +} + +// TestValidateEventType tests the EventType validation function. +func TestValidateEventType(t *testing.T) { + t.Parallel() + + t.Run("valid event type", func(t *testing.T) { + t.Parallel() + + eventType := "A.0x1.MyContract.MyEvent" + + result, err := convert.EventType(eventType) + require.NoError(t, err) + assert.Equal(t, eventType, result) + }) + + t.Run("empty event type", func(t *testing.T) { + t.Parallel() + + _, err := convert.EventType("") + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "invalid event type") + }) + + t.Run("whitespace only event type", func(t *testing.T) { + t.Parallel() + + _, err := convert.EventType(" ") + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "invalid event type") + }) + + t.Run("event type with leading/trailing whitespace", func(t *testing.T) { + t.Parallel() + + eventType := " A.0x1.MyContract.MyEvent " + + result, err := convert.EventType(eventType) + require.NoError(t, err) + // The function returns the original string, not trimmed + assert.Equal(t, eventType, result) + }) +} + +// TestValidateTransactionID tests the TransactionID validation function. +func TestValidateTransactionID(t *testing.T) { + t.Parallel() + + t.Run("valid transaction ID", func(t *testing.T) { + t.Parallel() + + expectedID := unittest.IdentifierFixture() + rawID := expectedID[:] + + txID, err := convert.TransactionID(rawID) + require.NoError(t, err) + assert.Equal(t, expectedID, txID) + }) + + t.Run("empty transaction ID", func(t *testing.T) { + t.Parallel() + + _, err := convert.TransactionID([]byte{}) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "invalid transaction id") + }) + + t.Run("transaction ID with different length", func(t *testing.T) { + t.Parallel() + + shortID := []byte{0x01, 0x02} + + _, err := convert.TransactionID(shortID) + require.Error(t, err) + assert.Equal(t, codes.InvalidArgument, status.Code(err)) + assert.Contains(t, err.Error(), "invalid transaction id") + }) +} diff --git a/module/mempool/herocache/transactions_test.go b/module/mempool/herocache/transactions_test.go index b3b3841360d..90eb311ac2b 100644 --- a/module/mempool/herocache/transactions_test.go +++ b/module/mempool/herocache/transactions_test.go @@ -62,7 +62,7 @@ func TestTransactionPool(t *testing.T) { // TestConcurrentWriteAndRead checks correctness of transactions mempool under concurrent read and write. func TestConcurrentWriteAndRead(t *testing.T) { total := 100 - txs := unittest.TransactionBodyListFixture(total) + txs := unittest.TransactionFixtures(total) transactions := herocache.NewTransactions(uint32(total), unittest.Logger(), metrics.NewNoopCollector()) wg := sync.WaitGroup{} @@ -98,7 +98,7 @@ func TestConcurrentWriteAndRead(t *testing.T) { // transactions in the same order as they are returned. func TestValuesReturnsInOrder(t *testing.T) { total := 100 - txs := unittest.TransactionBodyListFixture(total) + txs := unittest.TransactionFixtures(total) transactions := herocache.NewTransactions(uint32(total), unittest.Logger(), metrics.NewNoopCollector()) // storing all transactions diff --git a/utils/unittest/fixtures.go b/utils/unittest/fixtures.go index 5410c7e036f..482877ac6da 100644 --- a/utils/unittest/fixtures.go +++ b/utils/unittest/fixtures.go @@ -1624,10 +1624,10 @@ func TransactionBodyFixture(opts ...func(*flow.TransactionBody)) flow.Transactio return tb } -func TransactionBodyListFixture(n int) []*flow.TransactionBody { +func TransactionFixtures(n int) []*flow.TransactionBody { l := make([]*flow.TransactionBody, n) for i := 0; i < n; i++ { - tx := TransactionBodyFixture() + tx := TransactionFixture() l[i] = &tx }