Skip to content

add indexer client unit tests #2000

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Apr 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion api/indexer/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"github.com/ava-labs/hypersdk/requester"
)

var errTxUnmarshalingFailed = errors.New("transaction unmarshaling failed")

func NewClient(uri string) *Client {
uri = strings.TrimSuffix(uri, "/")
uri += Endpoint
Expand Down Expand Up @@ -122,7 +124,7 @@ func (c *Client) GetTx(ctx context.Context, txID ids.ID, parser chain.Parser) (G
var tx *chain.Transaction
tx, err = chain.UnmarshalTx(resp.TxBytes, parser)
if err != nil {
return GetTxResponse{}, nil, false, fmt.Errorf("failed to unmarshal tx %s: %w", txID, err)
return GetTxResponse{}, nil, false, fmt.Errorf("%w: tx %s: %w", errTxUnmarshalingFailed, txID, err)
}

resp.Result.CalculateCanotoCache()
Expand Down
186 changes: 135 additions & 51 deletions api/indexer/client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,54 @@ import (
"testing"
"time"

"github.com/ava-labs/avalanchego/database"
"github.com/ava-labs/avalanchego/ids"
"github.com/ava-labs/avalanchego/trace"
"github.com/stretchr/testify/require"

"github.com/ava-labs/hypersdk/api"
"github.com/ava-labs/hypersdk/chain"
"github.com/ava-labs/hypersdk/chain/chaintest"
"github.com/ava-labs/hypersdk/codec"
)

func TestIndexerClientBlocks(t *testing.T) {
const (
numExecutedBlocks = 4
blockWindow = 2
numTxs = 3
blockWindow = 2
numTxs = 3
)

testCases := []struct {
name string
blkHeight uint64
err error
name string
blkHeight uint64
blkHeightErr error
numExecutedBlocks int
}{
{
name: "success",
blkHeight: numExecutedBlocks - 1,
err: nil,
name: "success",
blkHeight: 3,
blkHeightErr: nil,
numExecutedBlocks: 4,
},
{
name: "missing block",
blkHeight: 0,
blkHeightErr: errBlockNotFound,
numExecutedBlocks: 4,
},
{
name: "missing block",
blkHeight: 0,
err: errBlockNotFound,
name: "no blocks",
blkHeight: 0,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In production, we should be able to serve the genesis block, should we generate a sequence of blocks that includes a genesis block?

blkHeightErr: errBlockNotFound,
numExecutedBlocks: 0,
},
}

for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
r := require.New(t)
ctx := context.Background()
indexer, executedBlocks, _ := createTestIndexer(t, ctx, numExecutedBlocks, blockWindow, numTxs)
indexer, executedBlocks, _ := createTestIndexer(t, ctx, tt.numExecutedBlocks, blockWindow, numTxs)

jsonHandler, err := api.NewJSONRPCHandler(Name, NewServer(trace.Noop, indexer))
r.NoError(err)
Expand All @@ -59,26 +70,36 @@ func TestIndexerClientBlocks(t *testing.T) {

client := NewClient(httpServer.URL)

expectedBlock := executedBlocks[tt.blkHeight]
executedBlock, err := client.GetBlockByHeight(ctx, expectedBlock.Block.Hght, parser)
if tt.err == nil {
executedBlock, err := client.GetBlockByHeight(ctx, tt.blkHeight, parser)
if tt.blkHeightErr == nil {
r.NoError(err)
expectedBlock := executedBlocks[tt.blkHeight-1]
r.Equal(expectedBlock.Block, executedBlock.Block)
} else {
r.ErrorContains(err, tt.err.Error())
r.ErrorContains(err, tt.blkHeightErr.Error())
}

expectedBlkID := ids.Empty
if tt.blkHeight > 0 {
expectedBlock := executedBlocks[tt.blkHeight-1]
expectedBlkID = expectedBlock.Block.GetID()
}

executedBlock, err = client.GetBlock(ctx, expectedBlock.Block.GetID(), parser)
if tt.err == nil {
executedBlock, err = client.GetBlock(ctx, expectedBlkID, parser)
if tt.blkHeightErr == nil {
r.NoError(err)
r.Equal(expectedBlock.Block, executedBlock.Block)
r.Equal(executedBlocks[tt.blkHeight-1].Block, executedBlock.Block)
} else {
r.ErrorContains(err, tt.err.Error())
r.ErrorContains(err, tt.blkHeightErr.Error())
}

executedBlock, err = client.GetLatestBlock(ctx, parser)
r.NoError(err)
r.Equal(executedBlocks[numExecutedBlocks-1].Block, executedBlock.Block)
if tt.numExecutedBlocks == 0 {
r.ErrorContains(err, database.ErrNotFound.Error())
Comment on lines +97 to +98
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Handling the special case this way makes it seem like we default to not including the genesis. Could we include at least the genesis block (we should receive a notification of at least the genesis block on startup)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not intended to cover the genesis block; it's really about covering the no-blocks-of-any-kind usecaswe.
Adding genesis is ok of course, but I don't feel that it's needed since it wouldn't be treated any differently from the indexer perspective.

} else {
r.NoError(err)
r.Equal(executedBlocks[tt.numExecutedBlocks-1].Block, executedBlock.Block)
}
})
}
}
Expand All @@ -90,26 +111,61 @@ func TestIndexerClientTransactions(t *testing.T) {
numTxs = 3
)

parser := chaintest.NewTestParser()
badParser := &chain.TxTypeParser{
ActionRegistry: codec.NewTypeParser[chain.Action](),
AuthRegistry: codec.NewTypeParser[chain.Auth](),
}

testCases := []struct {
name string
blockIndex int
txIndex int
err error
found bool
name string
blockIndex int
txIndex int
getTxResultsErr error
getTxErr error
found bool
parser *chain.TxTypeParser
malformedBlock bool
}{
{
name: "success",
blockIndex: numExecutedBlocks - 1,
txIndex: 0,
found: true,
err: nil,
name: "success",
blockIndex: numExecutedBlocks - 1,
txIndex: 0,
found: true,
getTxResultsErr: nil,
getTxErr: nil,
parser: parser,
malformedBlock: false,
},
{
name: "missing transaction",
blockIndex: 0,
txIndex: 0,
found: false,
getTxResultsErr: nil,
getTxErr: nil,
parser: parser,
malformedBlock: false,
},
{
name: "badParser",
blockIndex: numExecutedBlocks - 1,
txIndex: numTxs - 1,
found: true,
getTxResultsErr: nil,
getTxErr: errTxUnmarshalingFailed,
parser: badParser,
malformedBlock: false,
},
{
name: "missing transaction",
blockIndex: 0,
txIndex: 0,
found: false,
err: nil,
name: "malformed block",
blockIndex: numExecutedBlocks - 1,
txIndex: 0,
found: false,
getTxResultsErr: errTxResultNotFound,
getTxErr: errTxResultNotFound,
parser: parser,
malformedBlock: true,
},
}

Expand All @@ -127,14 +183,26 @@ func TestIndexerClientTransactions(t *testing.T) {
httpServer.Close()
})

parser := chaintest.NewTestParser()

client := NewClient(httpServer.URL)

executedBlock := executedBlocks[tt.blockIndex]
executedTx := executedBlock.Block.Txs[tt.txIndex]

if tt.malformedBlock {
// create a malformed block and have the indexer add it.
// clearing out the results ensure that we won't be able to find the result corresponding to any
// of the transactions.
executedBlock.ExecutionResults.Results = []*chain.Result{}
r.NoError(indexer.Notify(ctx, executedBlock))
}

txResponse, found, err := client.GetTxResults(ctx, executedTx.GetID())
r.Equal(tt.err, err)
if tt.getTxResultsErr != nil {
r.Error(err)
r.ErrorContains(err, tt.getTxResultsErr.Error())
err = nil
}
r.NoError(err)
r.Equal(tt.found, found)
if tt.found {
r.Equal(GetTxResponse{
Expand All @@ -144,17 +212,24 @@ func TestIndexerClientTransactions(t *testing.T) {
}, txResponse)
}

txResponse, tx, found, err := client.GetTx(ctx, executedTx.GetID(), parser)
r.Equal(tt.err, err)
txResponse, tx, found, err := client.GetTx(ctx, executedTx.GetID(), tt.parser)
if tt.getTxErr != nil {
r.Error(err)
r.ErrorContains(err, tt.getTxErr.Error())
return
}
r.NoError(err)
r.Equal(tt.found, found)
if tt.found {
r.Equal(GetTxResponse{
TxBytes: executedTx.Bytes(),
Timestamp: executedBlock.Block.Tmstmp,
Result: executedBlock.ExecutionResults.Results[tt.txIndex],
}, txResponse)
r.Equal(executedTx, tx)
if !tt.found {
return
}

r.Equal(GetTxResponse{
TxBytes: executedTx.Bytes(),
Timestamp: executedBlock.Block.Tmstmp,
Result: executedBlock.ExecutionResults.Results[tt.txIndex],
}, txResponse)
r.Equal(executedTx, tx)
})
}
}
Expand All @@ -167,7 +242,7 @@ func TestIndexerClientWaitForTransaction(t *testing.T) {
)
r := require.New(t)
ctx := context.Background()
indexer, _, _ := createTestIndexer(t, ctx, numExecutedBlocks, blockWindow, numTxs)
indexer, executedBlocks, _ := createTestIndexer(t, ctx, numExecutedBlocks, blockWindow, numTxs)

jsonHandler, err := api.NewJSONRPCHandler(Name, NewServer(trace.Noop, indexer))
r.NoError(err)
Expand All @@ -183,4 +258,13 @@ func TestIndexerClientWaitForTransaction(t *testing.T) {
defer ctxCancel()
_, _, err = client.WaitForTransaction(timeoutCtx, 1*time.Millisecond, ids.GenerateTestID())
r.ErrorIs(err, context.DeadlineExceeded)

// wait for a past transaction.
lastExecutedBlock := executedBlocks[numExecutedBlocks-1]
lastTx := lastExecutedBlock.Block.Txs[numTxs-1]
lastTxResult := lastExecutedBlock.ExecutionResults.Results[numTxs-1]
success, fee, err := client.WaitForTransaction(ctx, 1*time.Millisecond, lastTx.GetID())
r.NoError(err)
r.Equal(lastTxResult.Success, success)
r.Equal(lastTxResult.Fee, fee)
}