From 1ed5a6ad21881aba88753606a8f58d1578ded16b Mon Sep 17 00:00:00 2001 From: Paolo Galli Date: Mon, 22 Apr 2024 14:01:37 +0200 Subject: [PATCH] Test coverage api package [accounts, blocks, events, node, transactions, transfers] (#681) * test: add test coverage for api events, transactions and transfers packages * test: refactored httpGet util function to check also response status code * test: refactored httpPost util function to check also response status code * test: refactored errors as exportable vars * refactor: replace POST and GET string with http.Method * test: raised package account coverage at 83.6% * test: raise api/blocks test coverage to 82.8% * test: raise node code coverage to 100% * test: add more tests * Fix: expected response error status * style: remove unnecessary trailing newline * refactor: rollback function extraction * tests: add new tests due to transactions.go rollback * refactor: rename test packages by appending _test * style: add comments to separate api/transactions tests * refactor: rollback export_test.go file exporting private function * typo: remove typo in test * refactor: fixes as per review * refactor: make errNotFound private again * tweak api/transaction tests, disable log for tests --------- Co-authored-by: tony --- api/accounts/accounts.go | 8 +- api/accounts/accounts_test.go | 89 ++++++++++- api/blocks/blocks.go | 2 +- api/blocks/blocks_test.go | 121 +++++++++++++-- api/events/events.go | 2 +- api/events/events_test.go | 206 ++++++++++++++++++++++++- api/events/types_test.go | 120 +++++++++++++++ api/node/node.go | 2 +- api/node/types_test.go | 75 +++++++++ api/transactions/transactions.go | 8 +- api/transactions/transactions_test.go | 214 ++++++++++++++++++++++++-- api/transactions/types_test.go | 116 ++++++++++++++ api/transfers/transfers.go | 2 +- api/transfers/transfers_test.go | 177 ++++++++++++++++++++- 14 files changed, 1091 insertions(+), 51 deletions(-) create mode 100644 api/events/types_test.go create mode 100644 api/node/types_test.go create mode 100644 api/transactions/types_test.go diff --git a/api/accounts/accounts.go b/api/accounts/accounts.go index e4842c74d..e4dfe803b 100644 --- a/api/accounts/accounts.go +++ b/api/accounts/accounts.go @@ -349,10 +349,10 @@ func (a *Accounts) handleRevision(revision string) (*chain.BlockSummary, error) func (a *Accounts) Mount(root *mux.Router, pathPrefix string) { sub := root.PathPrefix(pathPrefix).Subrouter() - sub.Path("/*").Methods("POST").HandlerFunc(utils.WrapHandlerFunc(a.handleCallBatchCode)) + sub.Path("/*").Methods(http.MethodPost).HandlerFunc(utils.WrapHandlerFunc(a.handleCallBatchCode)) sub.Path("/{address}").Methods(http.MethodGet).HandlerFunc(utils.WrapHandlerFunc(a.handleGetAccount)) sub.Path("/{address}/code").Methods(http.MethodGet).HandlerFunc(utils.WrapHandlerFunc(a.handleGetCode)) - sub.Path("/{address}/storage/{key}").Methods("GET").HandlerFunc(utils.WrapHandlerFunc(a.handleGetStorage)) - sub.Path("").Methods("POST").HandlerFunc(utils.WrapHandlerFunc(a.handleCallContract)) - sub.Path("/{address}").Methods("POST").HandlerFunc(utils.WrapHandlerFunc(a.handleCallContract)) + sub.Path("/{address}/storage/{key}").Methods(http.MethodGet).HandlerFunc(utils.WrapHandlerFunc(a.handleGetStorage)) + sub.Path("").Methods(http.MethodPost).HandlerFunc(utils.WrapHandlerFunc(a.handleCallContract)) + sub.Path("/{address}").Methods(http.MethodPost).HandlerFunc(utils.WrapHandlerFunc(a.handleCallContract)) } diff --git a/api/accounts/accounts_test.go b/api/accounts/accounts_test.go index 9697501d9..2139e045b 100644 --- a/api/accounts/accounts_test.go +++ b/api/accounts/accounts_test.go @@ -8,6 +8,7 @@ package accounts_test import ( "bytes" "encoding/json" + "fmt" "io" "math/big" "net/http" @@ -23,6 +24,7 @@ import ( "github.com/stretchr/testify/assert" ABI "github.com/vechain/thor/v2/abi" "github.com/vechain/thor/v2/api/accounts" + "github.com/vechain/thor/v2/block" "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/genesis" "github.com/vechain/thor/v2/muxdb" @@ -86,6 +88,8 @@ var addr = thor.BytesToAddress([]byte("to")) var value = big.NewInt(10000) var storageKey = thor.Bytes32{} var storageValue = byte(1) +var gasLimit uint64 +var genesisBlock *block.Block var contractAddr thor.Address @@ -97,12 +101,15 @@ var invalidAddr = "abc" var invalidBytes32 = "0x000000000000000000000000000000000000000000000000000000000000000g" //invlaid bytes32 var invalidNumberRevision = "4294967296" //invalid block number +var acc *accounts.Accounts var ts *httptest.Server func TestAccount(t *testing.T) { initAccountServer(t) defer ts.Close() getAccount(t) + getAccountWithNonExisitingRevision(t) + getAccountWithGenesisRevision(t) getCode(t) getStorage(t) deployContractWithCall(t) @@ -127,6 +134,34 @@ func getAccount(t *testing.T) { assert.Equal(t, http.StatusOK, statusCode, "OK") } +func getAccountWithNonExisitingRevision(t *testing.T) { + revision64Len := "0123456789012345678901234567890123456789012345678901234567890123" + + _, statusCode := httpGet(t, ts.URL+"/accounts/"+addr.String()+"?revision="+revision64Len) + + assert.Equal(t, http.StatusBadRequest, statusCode, "bad revision") +} + +func getAccountWithGenesisRevision(t *testing.T) { + res, statusCode := httpGet(t, ts.URL+"/accounts/"+addr.String()+"?revision="+genesisBlock.Header().ID().String()) + assert.Equal(t, http.StatusOK, statusCode, "bad revision") + + var acc accounts.Account + if err := json.Unmarshal(res, &acc); err != nil { + t.Fatal(err) + } + + balance, err := acc.Balance.MarshalText() + assert.NoError(t, err) + assert.Equal(t, "0x0", string(balance), "balance should be 0") + + energy, err := acc.Energy.MarshalText() + assert.NoError(t, err) + assert.Equal(t, "0x0", string(energy), "energy should be 0") + + assert.Equal(t, false, acc.HasCode, "hasCode should be false") +} + func getCode(t *testing.T) { _, statusCode := httpGet(t, ts.URL+"/accounts/"+invalidAddr+"/code") assert.Equal(t, http.StatusBadRequest, statusCode, "bad address") @@ -181,6 +216,7 @@ func initAccountServer(t *testing.T) { if err != nil { t.Fatal(err) } + genesisBlock = b repo, _ := chain.NewRepository(db, b) claTransfer := tx.NewClause(&addr).WithValue(value) claDeploy := tx.NewClause(nil).WithData(bytecode) @@ -200,7 +236,9 @@ func initAccountServer(t *testing.T) { packTx(repo, stater, transactionCall, t) router := mux.NewRouter() - accounts.New(repo, stater, math.MaxUint64, thor.NoFork).Mount(router, "/accounts") + gasLimit = math.MaxUint32 + acc = accounts.New(repo, stater, gasLimit, thor.NoFork) + acc.Mount(router, "/accounts") ts = httptest.NewServer(router) } @@ -275,6 +313,13 @@ func callContract(t *testing.T) { _, statusCode := httpPost(t, ts.URL+"/accounts/"+invalidAddr, nil) assert.Equal(t, http.StatusBadRequest, statusCode, "invalid address") + malFormedBody := 123 + _, statusCode = httpPost(t, ts.URL+"/accounts", malFormedBody) + assert.Equal(t, http.StatusBadRequest, statusCode, "invalid address") + + _, statusCode = httpPost(t, ts.URL+"/accounts/"+contractAddr.String(), malFormedBody) + assert.Equal(t, http.StatusBadRequest, statusCode, "invalid address") + badBody := &accounts.CallData{ Data: "input", } @@ -312,6 +357,12 @@ func callContract(t *testing.T) { } func batchCall(t *testing.T) { + // Request body is not a valid JSON + malformedBody := 123 + _, statusCode := httpPost(t, ts.URL+"/accounts/*", malformedBody) + assert.Equal(t, http.StatusBadRequest, statusCode, "malformed data") + + // Request body is not a valid BatchCallData badBody := &accounts.BatchCallData{ Clauses: accounts.Clauses{ accounts.Clause{ @@ -325,9 +376,25 @@ func batchCall(t *testing.T) { Value: nil, }}, } - _, statusCode := httpPost(t, ts.URL+"/accounts/*", badBody) + _, statusCode = httpPost(t, ts.URL+"/accounts/*", badBody) assert.Equal(t, http.StatusBadRequest, statusCode, "invalid data") + // Request body has an invalid blockRef + badBlockRef := &accounts.BatchCallData{ + BlockRef: "0x00", + } + _, statusCode = httpPost(t, ts.URL+"/accounts/*", badBlockRef) + assert.Equal(t, http.StatusInternalServerError, statusCode, "invalid blockRef") + + // Request body has an invalid malformed revision + _, statusCode = httpPost(t, fmt.Sprintf("%s/accounts/*?revision=%d", ts.URL, malformedBody), badBody) + assert.Equal(t, http.StatusBadRequest, statusCode, "invalid revision") + + // Request body has an invalid revision number + _, statusCode = httpPost(t, ts.URL+"/accounts/*?revision="+invalidNumberRevision, badBody) + assert.Equal(t, http.StatusBadRequest, statusCode, "invalid revision") + + // Valid request a := uint8(1) b := uint8(2) method := "add" @@ -351,9 +418,6 @@ func batchCall(t *testing.T) { }}, } - _, statusCode = httpPost(t, ts.URL+"/accounts/*?revision="+invalidNumberRevision, badBody) - assert.Equal(t, http.StatusBadRequest, statusCode, "invalid revision") - res, statusCode := httpPost(t, ts.URL+"/accounts/*", reqBody) var results accounts.BatchCallResults if err = json.Unmarshal(res, &results); err != nil { @@ -373,6 +437,7 @@ func batchCall(t *testing.T) { } assert.Equal(t, http.StatusOK, statusCode) + // Valid request big := math.HexOrDecimal256(*big.NewInt(1000)) fullBody := &accounts.BatchCallData{ Clauses: accounts.Clauses{}, @@ -386,6 +451,20 @@ func batchCall(t *testing.T) { } _, statusCode = httpPost(t, ts.URL+"/accounts/*", fullBody) assert.Equal(t, http.StatusOK, statusCode) + + // Request with not enough gas + tooMuchGasBody := &accounts.BatchCallData{ + Clauses: accounts.Clauses{}, + Gas: math.MaxUint64, + GasPrice: &big, + ProvedWork: &big, + Caller: &contractAddr, + GasPayer: &contractAddr, + Expiration: 100, + BlockRef: "0x00000000aabbccdd", + } + _, statusCode = httpPost(t, ts.URL+"/accounts/*", tooMuchGasBody) + assert.Equal(t, http.StatusForbidden, statusCode) } func httpPost(t *testing.T, url string, body interface{}) ([]byte, int) { diff --git a/api/blocks/blocks.go b/api/blocks/blocks.go index 280db69f6..eece00001 100644 --- a/api/blocks/blocks.go +++ b/api/blocks/blocks.go @@ -136,5 +136,5 @@ func (b *Blocks) isTrunk(blkID thor.Bytes32, blkNum uint32) (bool, error) { func (b *Blocks) Mount(root *mux.Router, pathPrefix string) { sub := root.PathPrefix(pathPrefix).Subrouter() - sub.Path("/{revision}").Methods("GET").HandlerFunc(utils.WrapHandlerFunc(b.handleGetBlock)) + sub.Path("/{revision}").Methods(http.MethodGet).HandlerFunc(utils.WrapHandlerFunc(b.handleGetBlock)) } diff --git a/api/blocks/blocks_test.go b/api/blocks/blocks_test.go index f7079bfb4..f5e2edcd3 100644 --- a/api/blocks/blocks_test.go +++ b/api/blocks/blocks_test.go @@ -3,20 +3,24 @@ // Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying // file LICENSE or -package blocks +package blocks_test import ( "encoding/json" "io" + "math" "math/big" "net/http" "net/http/httptest" + "strconv" + "strings" "testing" "time" "github.com/ethereum/go-ethereum/crypto" "github.com/gorilla/mux" "github.com/stretchr/testify/assert" + "github.com/vechain/thor/v2/api/blocks" "github.com/vechain/thor/v2/block" "github.com/vechain/thor/v2/chain" "github.com/vechain/thor/v2/cmd/thor/solo" @@ -28,43 +32,105 @@ import ( "github.com/vechain/thor/v2/tx" ) +var genesisBlock *block.Block var blk *block.Block var ts *httptest.Server var invalidBytes32 = "0x000000000000000000000000000000000000000000000000000000000000000g" //invlaid bytes32 -var invalidNumberRevision = "4294967296" //invalid block number func TestBlock(t *testing.T) { initBlockServer(t) defer ts.Close() - //invalid block id - _, statusCode := httpGet(t, ts.URL+"/blocks/"+invalidBytes32) - assert.Equal(t, http.StatusBadRequest, statusCode) - //invalid block number - _, statusCode = httpGet(t, ts.URL+"/blocks/"+invalidNumberRevision) + + testBadQueryParams(t) + testInvalidBlockId(t) + testInvalidBlockNumber(t) + testGetBlockById(t) + testGetExpandedBlockById(t) + testGetBlockByHeight(t) + testGetBestBlock(t) + testGetFinalizedBlock(t) + testGetBlockWithRevisionNumberTooHigh(t) +} + +func testBadQueryParams(t *testing.T) { + badQueryParams := "?expanded=1" + res, statusCode := httpGet(t, ts.URL+"/blocks/best"+badQueryParams) + assert.Equal(t, http.StatusBadRequest, statusCode) + assert.Equal(t, "expanded: should be boolean", strings.TrimSpace(string(res))) +} - res, statusCode := httpGet(t, ts.URL+"/blocks/"+blk.Header().ID().String()) - rb := new(JSONCollapsedBlock) - if err := json.Unmarshal(res, rb); err != nil { +func testGetBestBlock(t *testing.T) { + res, statusCode := httpGet(t, ts.URL+"/blocks/best") + rb := new(blocks.JSONCollapsedBlock) + if err := json.Unmarshal(res, &rb); err != nil { t.Fatal(err) } - checkBlock(t, blk, rb) + checkCollapsedBlock(t, blk, rb) assert.Equal(t, http.StatusOK, statusCode) +} - res, statusCode = httpGet(t, ts.URL+"/blocks/1") +func testGetBlockByHeight(t *testing.T) { + res, statusCode := httpGet(t, ts.URL+"/blocks/1") + rb := new(blocks.JSONCollapsedBlock) if err := json.Unmarshal(res, &rb); err != nil { t.Fatal(err) } - checkBlock(t, blk, rb) + checkCollapsedBlock(t, blk, rb) assert.Equal(t, http.StatusOK, statusCode) +} - res, statusCode = httpGet(t, ts.URL+"/blocks/best") +func testGetFinalizedBlock(t *testing.T) { + res, statusCode := httpGet(t, ts.URL+"/blocks/finalized") + rb := new(blocks.JSONCollapsedBlock) if err := json.Unmarshal(res, &rb); err != nil { t.Fatal(err) } - checkBlock(t, blk, rb) + assert.Equal(t, http.StatusOK, statusCode) + assert.True(t, rb.IsFinalized) + assert.Equal(t, genesisBlock.Header().ID(), rb.ID) + assert.Equal(t, uint32(0), rb.Number) +} + +func testGetBlockById(t *testing.T) { + res, statusCode := httpGet(t, ts.URL+"/blocks/"+blk.Header().ID().String()) + rb := new(blocks.JSONCollapsedBlock) + if err := json.Unmarshal(res, rb); err != nil { + t.Fatal(err) + } + checkCollapsedBlock(t, blk, rb) + assert.Equal(t, http.StatusOK, statusCode) +} + +func testGetExpandedBlockById(t *testing.T) { + res, statusCode := httpGet(t, ts.URL+"/blocks/"+blk.Header().ID().String()+"?expanded=true") + rb := new(blocks.JSONExpandedBlock) + if err := json.Unmarshal(res, rb); err != nil { + t.Fatal(err) + } + checkExpandedBlock(t, blk, rb) + assert.Equal(t, http.StatusOK, statusCode) +} + +func testInvalidBlockNumber(t *testing.T) { + invalidNumberRevision := "4294967296" //invalid block number + _, statusCode := httpGet(t, ts.URL+"/blocks/"+invalidNumberRevision) + assert.Equal(t, http.StatusBadRequest, statusCode) +} + +func testInvalidBlockId(t *testing.T) { + _, statusCode := httpGet(t, ts.URL+"/blocks/"+invalidBytes32) + assert.Equal(t, http.StatusBadRequest, statusCode) +} + +func testGetBlockWithRevisionNumberTooHigh(t *testing.T) { + revisionNumberTooHigh := strconv.FormatUint(math.MaxUint64, 10) + res, statusCode := httpGet(t, ts.URL+"/blocks/"+revisionNumberTooHigh) + + assert.Equal(t, http.StatusBadRequest, statusCode) + assert.Equal(t, "revision: block number out of max uint32", strings.TrimSpace(string(res))) } func initBlockServer(t *testing.T) { @@ -76,6 +142,8 @@ func initBlockServer(t *testing.T) { if err != nil { t.Fatal(err) } + genesisBlock = b + repo, _ := chain.NewRepository(db, b) addr := thor.BytesToAddress([]byte("to")) cla := tx.NewClause(&addr).WithValue(big.NewInt(10000)) @@ -118,12 +186,13 @@ func initBlockServer(t *testing.T) { t.Fatal(err) } router := mux.NewRouter() - New(repo, &solo.BFTEngine{}).Mount(router, "/blocks") + bftEngine := solo.NewBFTEngine(repo) + blocks.New(repo, bftEngine).Mount(router, "/blocks") ts = httptest.NewServer(router) blk = block } -func checkBlock(t *testing.T, expBl *block.Block, actBl *JSONCollapsedBlock) { +func checkCollapsedBlock(t *testing.T, expBl *block.Block, actBl *blocks.JSONCollapsedBlock) { header := expBl.Header() assert.Equal(t, header.Number(), actBl.Number, "Number should be equal") assert.Equal(t, header.ID(), actBl.ID, "Hash should be equal") @@ -141,6 +210,24 @@ func checkBlock(t *testing.T, expBl *block.Block, actBl *JSONCollapsedBlock) { } } +func checkExpandedBlock(t *testing.T, expBl *block.Block, actBl *blocks.JSONExpandedBlock) { + header := expBl.Header() + assert.Equal(t, header.Number(), actBl.Number, "Number should be equal") + assert.Equal(t, header.ID(), actBl.ID, "Hash should be equal") + assert.Equal(t, header.ParentID(), actBl.ParentID, "ParentID should be equal") + assert.Equal(t, header.Timestamp(), actBl.Timestamp, "Timestamp should be equal") + assert.Equal(t, header.TotalScore(), actBl.TotalScore, "TotalScore should be equal") + assert.Equal(t, header.GasLimit(), actBl.GasLimit, "GasLimit should be equal") + assert.Equal(t, header.GasUsed(), actBl.GasUsed, "GasUsed should be equal") + assert.Equal(t, header.Beneficiary(), actBl.Beneficiary, "Beneficiary should be equal") + assert.Equal(t, header.TxsRoot(), actBl.TxsRoot, "TxsRoot should be equal") + assert.Equal(t, header.StateRoot(), actBl.StateRoot, "StateRoot should be equal") + assert.Equal(t, header.ReceiptsRoot(), actBl.ReceiptsRoot, "ReceiptsRoot should be equal") + for i, tx := range expBl.Transactions() { + assert.Equal(t, tx.ID(), actBl.Transactions[i].ID, "txid should be equal") + } +} + func httpGet(t *testing.T, url string) ([]byte, int) { res, err := http.Get(url) if err != nil { diff --git a/api/events/events.go b/api/events/events.go index c5f5dddd5..7fbab98fc 100644 --- a/api/events/events.go +++ b/api/events/events.go @@ -61,5 +61,5 @@ func (e *Events) handleFilter(w http.ResponseWriter, req *http.Request) error { func (e *Events) Mount(root *mux.Router, pathPrefix string) { sub := root.PathPrefix(pathPrefix).Subrouter() - sub.Path("").Methods("POST").HandlerFunc(utils.WrapHandlerFunc(e.handleFilter)) + sub.Path("").Methods(http.MethodPost).HandlerFunc(utils.WrapHandlerFunc(e.handleFilter)) } diff --git a/api/events/events_test.go b/api/events/events_test.go index 6750ffef1..7b4e2e4d0 100644 --- a/api/events/events_test.go +++ b/api/events/events_test.go @@ -5,4 +5,208 @@ package events_test -// TODO +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/vechain/thor/v2/api/events" + "github.com/vechain/thor/v2/block" + "github.com/vechain/thor/v2/chain" + "github.com/vechain/thor/v2/genesis" + "github.com/vechain/thor/v2/logdb" + "github.com/vechain/thor/v2/muxdb" + "github.com/vechain/thor/v2/state" + "github.com/vechain/thor/v2/thor" + "github.com/vechain/thor/v2/tx" +) + +var ts *httptest.Server + +var ( + addr = thor.BytesToAddress([]byte("address")) + topic = thor.BytesToBytes32([]byte("topic")) +) + +func TestEmptyEvents(t *testing.T) { + db := createDb(t) + initEventServer(t, db) + defer ts.Close() + + testEventsBadRequest(t) + testEventWithEmptyDb(t) +} + +func TestEvents(t *testing.T) { + db := createDb(t) + initEventServer(t, db) + defer ts.Close() + + blocksToInsert := 5 + insertBlocks(t, db, blocksToInsert) + + testEventWithBlocks(t, blocksToInsert) +} + +// Test functions +func testEventsBadRequest(t *testing.T) { + badBody := []byte{0x00, 0x01, 0x02} + + res, err := http.Post(ts.URL+"/events", "application/x-www-form-urlencoded", bytes.NewReader(badBody)) + + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) +} + +func testEventWithEmptyDb(t *testing.T) { + emptyFilter := events.EventFilter{ + CriteriaSet: make([]*events.EventCriteria, 0), + Range: nil, + Options: nil, + Order: logdb.DESC, + } + + res, statusCode := httpPost(t, ts.URL+"/events", emptyFilter) + var tLogs []*events.FilteredEvent + if err := json.Unmarshal(res, &tLogs); err != nil { + t.Fatal(err) + } + + assert.Equal(t, http.StatusOK, statusCode) + assert.Empty(t, tLogs) +} + +func testEventWithBlocks(t *testing.T, expectedBlocks int) { + emptyFilter := events.EventFilter{ + CriteriaSet: make([]*events.EventCriteria, 0), + Range: nil, + Options: nil, + Order: logdb.DESC, + } + + res, statusCode := httpPost(t, ts.URL+"/events", emptyFilter) + var tLogs []*events.FilteredEvent + if err := json.Unmarshal(res, &tLogs); err != nil { + t.Fatal(err) + } + + assert.Equal(t, http.StatusOK, statusCode) + assert.Equal(t, expectedBlocks, len(tLogs)) + for _, tLog := range tLogs { + assert.NotEmpty(t, tLog) + } + + // Test with matching filter + matchingFilter := events.EventFilter{ + CriteriaSet: []*events.EventCriteria{{ + Address: &addr, + TopicSet: events.TopicSet{ + &topic, + &topic, + &topic, + &topic, + &topic, + }, + }}, + } + + res, statusCode = httpPost(t, ts.URL+"/events", matchingFilter) + if err := json.Unmarshal(res, &tLogs); err != nil { + t.Fatal(err) + } + + assert.Equal(t, http.StatusOK, statusCode) + assert.Equal(t, expectedBlocks, len(tLogs)) + for _, tLog := range tLogs { + assert.NotEmpty(t, tLog) + } +} + +// Init functions +func initEventServer(t *testing.T, logDb *logdb.LogDB) { + router := mux.NewRouter() + + muxDb := muxdb.NewMem() + stater := state.NewStater(muxDb) + gene := genesis.NewDevnet() + + b, _, _, err := gene.Build(stater) + if err != nil { + t.Fatal(err) + } + + repo, _ := chain.NewRepository(muxDb, b) + + events.New(repo, logDb).Mount(router, "/events") + ts = httptest.NewServer(router) +} + +func createDb(t *testing.T) *logdb.LogDB { + logDb, err := logdb.NewMem() + if err != nil { + t.Fatal(err) + } + return logDb +} + +// Utilities functions +func httpPost(t *testing.T, url string, body interface{}) ([]byte, int) { + data, err := json.Marshal(body) + if err != nil { + t.Fatal(err) + } + res, err := http.Post(url, "application/x-www-form-urlencoded", bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + r, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + return r, res.StatusCode +} + +func insertBlocks(t *testing.T, db *logdb.LogDB, n int) { + b := new(block.Builder).Build() + for i := 0; i < n; i++ { + b = new(block.Builder). + ParentID(b.Header().ID()). + Build() + receipts := tx.Receipts{newReceipt()} + + w := db.NewWriter() + if err := w.Write(b, receipts); err != nil { + t.Fatal(err) + } + + if err := w.Commit(); err != nil { + t.Fatal(err) + } + } +} + +func newReceipt() *tx.Receipt { + return &tx.Receipt{ + Outputs: []*tx.Output{ + { + Events: tx.Events{{ + Address: addr, + Topics: []thor.Bytes32{ + topic, + topic, + topic, + topic, + topic, + }, + Data: []byte("0x0"), + }}, + }, + }, + } +} diff --git a/api/events/types_test.go b/api/events/types_test.go new file mode 100644 index 000000000..7f71b806e --- /dev/null +++ b/api/events/types_test.go @@ -0,0 +1,120 @@ +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package events_test + +import ( + "math" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vechain/thor/v2/api/events" + "github.com/vechain/thor/v2/chain" + "github.com/vechain/thor/v2/genesis" + "github.com/vechain/thor/v2/logdb" + "github.com/vechain/thor/v2/muxdb" + "github.com/vechain/thor/v2/state" +) + +func TestEventsTypes(t *testing.T) { + chain := initChain(t) + + testConvertRangeWithBlockRangeType(t, chain) + testConvertRangeWithTimeRangeTypeLessThenGenesis(t, chain) + testConvertRangeWithTimeRangeType(t, chain) + testConvertRangeWithFromGreaterThanGenesis(t, chain) +} + +func testConvertRangeWithBlockRangeType(t *testing.T, chain *chain.Chain) { + rng := &events.Range{ + Unit: events.BlockRangeType, + From: 1, + To: 2, + } + + convertedRng, err := events.ConvertRange(chain, rng) + + assert.NoError(t, err) + assert.Equal(t, uint32(rng.From), convertedRng.From) + assert.Equal(t, uint32(rng.To), convertedRng.To) +} + +func testConvertRangeWithTimeRangeTypeLessThenGenesis(t *testing.T, chain *chain.Chain) { + rng := &events.Range{ + Unit: events.TimeRangeType, + From: 1, + To: 2, + } + expectedEmptyRange := &logdb.Range{ + From: math.MaxUint32, + To: math.MaxUint32, + } + + convRng, err := events.ConvertRange(chain, rng) + + assert.NoError(t, err) + assert.Equal(t, expectedEmptyRange, convRng) +} + +func testConvertRangeWithTimeRangeType(t *testing.T, chain *chain.Chain) { + genesis, err := chain.GetBlockHeader(0) + if err != nil { + t.Fatal(err) + } + rng := &events.Range{ + Unit: events.TimeRangeType, + From: 1, + To: genesis.Timestamp(), + } + expectedZeroRange := &logdb.Range{ + From: 0, + To: 0, + } + + convRng, err := events.ConvertRange(chain, rng) + + assert.NoError(t, err) + assert.Equal(t, expectedZeroRange, convRng) +} + +func testConvertRangeWithFromGreaterThanGenesis(t *testing.T, chain *chain.Chain) { + genesis, err := chain.GetBlockHeader(0) + if err != nil { + t.Fatal(err) + } + rng := &events.Range{ + Unit: events.TimeRangeType, + From: genesis.Timestamp() + 1_000, + To: genesis.Timestamp() + 10_000, + } + expectedEmptyRange := &logdb.Range{ + From: math.MaxUint32, + To: math.MaxUint32, + } + + convRng, err := events.ConvertRange(chain, rng) + + assert.NoError(t, err) + assert.Equal(t, expectedEmptyRange, convRng) +} + +// Init functions +func initChain(t *testing.T) *chain.Chain { + muxDb := muxdb.NewMem() + stater := state.NewStater(muxDb) + gene := genesis.NewDevnet() + + b, _, _, err := gene.Build(stater) + if err != nil { + t.Fatal(err) + } + + repo, err := chain.NewRepository(muxDb, b) + if err != nil { + t.Fatal(err) + } + + return repo.NewBestChain() +} diff --git a/api/node/node.go b/api/node/node.go index 6e685c7b9..871288f26 100644 --- a/api/node/node.go +++ b/api/node/node.go @@ -33,5 +33,5 @@ func (n *Node) handleNetwork(w http.ResponseWriter, req *http.Request) error { func (n *Node) Mount(root *mux.Router, pathPrefix string) { sub := root.PathPrefix(pathPrefix).Subrouter() - sub.Path("/network/peers").Methods("Get").HandlerFunc(utils.WrapHandlerFunc(n.handleNetwork)) + sub.Path("/network/peers").Methods(http.MethodGet).HandlerFunc(utils.WrapHandlerFunc(n.handleNetwork)) } diff --git a/api/node/types_test.go b/api/node/types_test.go new file mode 100644 index 000000000..a0bbc36be --- /dev/null +++ b/api/node/types_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package node_test + +import ( + "crypto/rand" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/vechain/thor/v2/api/node" + "github.com/vechain/thor/v2/comm" + "github.com/vechain/thor/v2/thor" +) + +func TestConvertPeersStats(t *testing.T) { + // Test case 1: Empty input slice + ss := []*comm.PeerStats{} + expected := []*node.PeerStats(nil) + assert.Equal(t, expected, node.ConvertPeersStats(ss)) + + // Test case 2: Non-empty input slice + bestBlock1 := randomBytes32() + bestBlock2 := randomBytes32() + ss = []*comm.PeerStats{ + { + Name: "peer1", + BestBlockID: bestBlock1, + TotalScore: 100, + PeerID: "peerID1", + NetAddr: "netAddr1", + Inbound: true, + Duration: 10, + }, + { + Name: "peer2", + BestBlockID: bestBlock2, + TotalScore: 200, + PeerID: "peerID2", + NetAddr: "netAddr2", + Inbound: false, + Duration: 20, + }, + } + expected = []*node.PeerStats{ + { + Name: "peer1", + BestBlockID: bestBlock1, + TotalScore: 100, + PeerID: "peerID1", + NetAddr: "netAddr1", + Inbound: true, + Duration: 10, + }, + { + Name: "peer2", + BestBlockID: bestBlock2, + TotalScore: 200, + PeerID: "peerID2", + NetAddr: "netAddr2", + Inbound: false, + Duration: 20, + }, + } + assert.Equal(t, expected, node.ConvertPeersStats(ss)) +} + +func randomBytes32() thor.Bytes32 { + var b32 thor.Bytes32 + + rand.Read(b32[:]) + return b32 +} diff --git a/api/transactions/transactions.go b/api/transactions/transactions.go index dc9acda75..365aa6c7c 100644 --- a/api/transactions/transactions.go +++ b/api/transactions/transactions.go @@ -144,6 +144,7 @@ func (t *Transactions) handleGetTransactionByID(w http.ResponseWriter, req *http if err != nil { return utils.BadRequest(errors.WithMessage(err, "id")) } + head, err := t.parseHead(req.URL.Query().Get("head")) if err != nil { return utils.BadRequest(errors.WithMessage(err, "head")) @@ -183,6 +184,7 @@ func (t *Transactions) handleGetTransactionReceiptByID(w http.ResponseWriter, re if err != nil { return utils.BadRequest(errors.WithMessage(err, "id")) } + head, err := t.parseHead(req.URL.Query().Get("head")) if err != nil { return utils.BadRequest(errors.WithMessage(err, "head")) @@ -215,7 +217,7 @@ func (t *Transactions) parseHead(head string) (thor.Bytes32, error) { func (t *Transactions) Mount(root *mux.Router, pathPrefix string) { sub := root.PathPrefix(pathPrefix).Subrouter() - sub.Path("").Methods("POST").HandlerFunc(utils.WrapHandlerFunc(t.handleSendTransaction)) - sub.Path("/{id}").Methods("GET").HandlerFunc(utils.WrapHandlerFunc(t.handleGetTransactionByID)) - sub.Path("/{id}/receipt").Methods("GET").HandlerFunc(utils.WrapHandlerFunc(t.handleGetTransactionReceiptByID)) + sub.Path("").Methods(http.MethodPost).HandlerFunc(utils.WrapHandlerFunc(t.handleSendTransaction)) + sub.Path("/{id}").Methods(http.MethodGet).HandlerFunc(utils.WrapHandlerFunc(t.handleGetTransactionByID)) + sub.Path("/{id}/receipt").Methods(http.MethodGet).HandlerFunc(utils.WrapHandlerFunc(t.handleGetTransactionReceiptByID)) } diff --git a/api/transactions/transactions_test.go b/api/transactions/transactions_test.go index c11f4c1bb..a96f94585 100644 --- a/api/transactions/transactions_test.go +++ b/api/transactions/transactions_test.go @@ -8,10 +8,12 @@ package transactions_test import ( "bytes" "encoding/json" + "fmt" "io" "math/big" "net/http" "net/http/httptest" + "strings" "testing" "time" @@ -19,6 +21,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/rlp" "github.com/gorilla/mux" + "github.com/inconshreveable/log15" "github.com/stretchr/testify/assert" "github.com/vechain/thor/v2/api/transactions" "github.com/vechain/thor/v2/chain" @@ -31,27 +34,51 @@ import ( "github.com/vechain/thor/v2/txpool" ) +func init() { + log15.Root().SetHandler(log15.DiscardHandler()) +} + var repo *chain.Repository var ts *httptest.Server var transaction *tx.Transaction +var mempoolTx *tx.Transaction func TestTransaction(t *testing.T) { initTransactionServer(t) defer ts.Close() + + // Send tx + sendTx(t) + sendTxWithBadFormat(t) + sendTxThatCannotBeAcceptedInLocalMempool(t) + + // Get tx getTx(t) + getTxWithBadId(t) + txWithBadHeader(t) + getNonExistingRawTransactionWhenTxStillInMempool(t) + getNonPendingRawTransactionWhenTxStillInMempool(t) + getRawTransactionWhenTxStillInMempool(t) + getTransactionByIDTxNotFound(t) + getTransactionByIDPendingTxNotFound(t) + handleGetTransactionByIDWithBadQueryParams(t) + handleGetTransactionByIDWithNonExistingHead(t) + + // Get tx receipt getTxReceipt(t) - senTx(t) + getReceiptWithBadId(t) + handleGetTransactionReceiptByIDWithNonExistingHead(t) } func getTx(t *testing.T) { - res := httpGet(t, ts.URL+"/transactions/"+transaction.ID().String()) + res := httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+transaction.ID().String(), 200) var rtx *transactions.Transaction if err := json.Unmarshal(res, &rtx); err != nil { t.Fatal(err) } - checkTx(t, transaction, rtx) + checkMatchingTx(t, transaction, rtx) - res = httpGet(t, ts.URL+"/transactions/"+transaction.ID().String()+"?raw=true") + res = httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+transaction.ID().String()+"?raw=true", 200) var rawTx map[string]interface{} if err := json.Unmarshal(res, &rawTx); err != nil { t.Fatal(err) @@ -64,15 +91,15 @@ func getTx(t *testing.T) { } func getTxReceipt(t *testing.T) { - r := httpGet(t, ts.URL+"/transactions/"+transaction.ID().String()+"/receipt") + r := httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+transaction.ID().String()+"/receipt", 200) var receipt *transactions.Receipt if err := json.Unmarshal(r, &receipt); err != nil { t.Fatal(err) } - assert.Equal(t, receipt.GasUsed, transaction.Gas(), "gas should be equal") + assert.Equal(t, receipt.GasUsed, transaction.Gas(), "receipt gas used not equal to transaction gas") } -func senTx(t *testing.T) { +func sendTx(t *testing.T) { var blockRef = tx.NewBlockRef(0) var chainTag = repo.ChainTag() var expiration = uint32(10) @@ -94,7 +121,7 @@ func senTx(t *testing.T) { t.Fatal(err) } - res := httpPost(t, ts.URL+"/transactions", transactions.RawTx{Raw: hexutil.Encode(rlpTx)}) + res := httpPostAndCheckResponseStatus(t, ts.URL+"/transactions", transactions.RawTx{Raw: hexutil.Encode(rlpTx)}, 200) var txObj map[string]string if err = json.Unmarshal(res, &txObj); err != nil { t.Fatal(err) @@ -102,20 +129,142 @@ func senTx(t *testing.T) { assert.Equal(t, tx.ID().String(), txObj["id"], "should be the same transaction id") } -func httpPost(t *testing.T, url string, obj interface{}) []byte { - data, err := json.Marshal(obj) +func getTxWithBadId(t *testing.T) { + txBadId := "0x123" + + res := httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+txBadId, 400) + + assert.Contains(t, string(res), "invalid length") +} + +func txWithBadHeader(t *testing.T) { + badHeaderURL := []string{ + ts.URL + "/transactions/" + transaction.ID().String() + "?head=badHead", + ts.URL + "/transactions/" + transaction.ID().String() + "/receipt?head=badHead", + } + + for _, url := range badHeaderURL { + res := httpGetAndCheckResponseStatus(t, url, 400) + assert.Contains(t, string(res), "invalid length") + } +} + +func getReceiptWithBadId(t *testing.T) { + txBadId := "0x123" + + httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+txBadId+"/receipt", 400) +} + +func getNonExistingRawTransactionWhenTxStillInMempool(t *testing.T) { + nonExistingTxId := "0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" + queryParams := []string{ + "?raw=true", + "?raw=true&pending=true", + } + + for _, queryParam := range queryParams { + res := httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+nonExistingTxId+queryParam, 200) + + assert.Equal(t, "null\n", string(res)) + } +} + +func getNonPendingRawTransactionWhenTxStillInMempool(t *testing.T) { + res := httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+mempoolTx.ID().String()+"?raw=true", 200) + var rawTx map[string]interface{} + if err := json.Unmarshal(res, &rawTx); err != nil { + t.Fatal(err) + } + + assert.Empty(t, rawTx) +} + +func getRawTransactionWhenTxStillInMempool(t *testing.T) { + res := httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+mempoolTx.ID().String()+"?raw=true&pending=true", 200) + var rawTx map[string]interface{} + if err := json.Unmarshal(res, &rawTx); err != nil { + t.Fatal(err) + } + rlpTx, err := rlp.EncodeToBytes(mempoolTx) if err != nil { t.Fatal(err) } - res, err := http.Post(url, "application/x-www-form-urlencoded", bytes.NewReader(data)) + + assert.NotEmpty(t, rawTx) + assert.Equal(t, hexutil.Encode(rlpTx), rawTx["raw"], "should be equal raw") +} + +func getTransactionByIDTxNotFound(t *testing.T) { + res := httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+mempoolTx.ID().String(), 200) + + assert.Equal(t, "null\n", string(res)) +} + +func getTransactionByIDPendingTxNotFound(t *testing.T) { + res := httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+mempoolTx.ID().String()+"?pending=true", 200) + var rtx *transactions.Transaction + if err := json.Unmarshal(res, &rtx); err != nil { + t.Fatal(err) + } + + checkMatchingTx(t, mempoolTx, rtx) +} + +func sendTxWithBadFormat(t *testing.T) { + badRawTx := transactions.RawTx{Raw: "badRawTx"} + + res := httpPostAndCheckResponseStatus(t, ts.URL+"/transactions", badRawTx, 400) + + assert.Contains(t, string(res), hexutil.ErrMissingPrefix.Error()) +} + +func sendTxThatCannotBeAcceptedInLocalMempool(t *testing.T) { + tx := new(tx.Builder).Build() + rlpTx, err := rlp.EncodeToBytes(tx) if err != nil { t.Fatal(err) } - r, err := io.ReadAll(res.Body) - res.Body.Close() + duplicatedRawTx := transactions.RawTx{Raw: hexutil.Encode(rlpTx)} + + res := httpPostAndCheckResponseStatus(t, ts.URL+"/transactions", duplicatedRawTx, 400) + + assert.Contains(t, string(res), "bad tx: chain tag mismatch") +} + +func handleGetTransactionByIDWithBadQueryParams(t *testing.T) { + badQueryParams := []string{ + "?pending=badPending", + "?pending=true&raw=badRaw", + } + + for _, badQueryParam := range badQueryParams { + res := httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+transaction.ID().String()+badQueryParam, 400) + assert.Contains(t, string(res), "should be boolean") + } +} + +func handleGetTransactionByIDWithNonExistingHead(t *testing.T) { + res := httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+transaction.ID().String()+"?head=0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 400) + assert.Equal(t, "head: leveldb: not found", strings.TrimSpace(string(res))) +} + +func handleGetTransactionReceiptByIDWithNonExistingHead(t *testing.T) { + res := httpGetAndCheckResponseStatus(t, ts.URL+"/transactions/"+transaction.ID().String()+"/receipt?head=0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", 400) + assert.Equal(t, "head: leveldb: not found", strings.TrimSpace(string(res))) +} + +func httpPostAndCheckResponseStatus(t *testing.T, url string, obj interface{}, responseStatusCode int) []byte { + data, err := json.Marshal(obj) if err != nil { t.Fatal(err) } + res, err := http.Post(url, "application/x-www-form-urlencoded", bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + assert.Equal(t, responseStatusCode, res.StatusCode, fmt.Sprintf("status code should be %d", responseStatusCode)) + r := parseBytesBody(t, res.Body) + res.Body.Close() return r } @@ -141,11 +290,26 @@ func initTransactionServer(t *testing.T) { BlockRef(tx.NewBlockRef(0)). Build() + mempoolTx = new(tx.Builder). + ChainTag(repo.ChainTag()). + Expiration(10). + Gas(21000). + Nonce(1). + Build() + sig, err := crypto.Sign(transaction.SigningHash().Bytes(), genesis.DevAccounts()[0].PrivateKey) if err != nil { t.Fatal(err) } + + sig2, err := crypto.Sign(mempoolTx.SigningHash().Bytes(), genesis.DevAccounts()[0].PrivateKey) + if err != nil { + t.Fatal(err) + } + transaction = transaction.WithSignature(sig) + mempoolTx = mempoolTx.WithSignature(sig2) + packer := packer.New(repo, stater, genesis.DevAccounts()[0].Address, &genesis.DevAccounts()[0].Address, thor.NoFork) sum, _ := repo.GetBlockSummary(b.Header().ID()) flow, err := packer.Schedule(sum, uint64(time.Now().Unix())) @@ -170,11 +334,20 @@ func initTransactionServer(t *testing.T) { t.Fatal(err) } router := mux.NewRouter() - transactions.New(repo, txpool.New(repo, stater, txpool.Options{Limit: 10000, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute})).Mount(router, "/transactions") + + // Add a tx to the mempool to have both pending and non-pending transactions + mempool := txpool.New(repo, stater, txpool.Options{Limit: 10000, LimitPerAccount: 16, MaxLifetime: 10 * time.Minute}) + e := mempool.Add(mempoolTx) + if e != nil { + t.Fatal(e) + } + + transactions.New(repo, mempool).Mount(router, "/transactions") + ts = httptest.NewServer(router) } -func checkTx(t *testing.T, expectedTx *tx.Transaction, actualTx *transactions.Transaction) { +func checkMatchingTx(t *testing.T, expectedTx *tx.Transaction, actualTx *transactions.Transaction) { origin, err := expectedTx.Origin() if err != nil { t.Fatal(err) @@ -190,11 +363,12 @@ func checkTx(t *testing.T, expectedTx *tx.Transaction, actualTx *transactions.Tr } } -func httpGet(t *testing.T, url string) []byte { +func httpGetAndCheckResponseStatus(t *testing.T, url string, responseStatusCode int) []byte { res, err := http.Get(url) if err != nil { t.Fatal(err) } + assert.Equal(t, responseStatusCode, res.StatusCode, fmt.Sprintf("status code should be %d", responseStatusCode)) r, err := io.ReadAll(res.Body) res.Body.Close() if err != nil { @@ -202,3 +376,11 @@ func httpGet(t *testing.T, url string) []byte { } return r } + +func parseBytesBody(t *testing.T, body io.ReadCloser) []byte { + r, err := io.ReadAll(body) + if err != nil { + t.Fatal(err) + } + return r +} diff --git a/api/transactions/types_test.go b/api/transactions/types_test.go new file mode 100644 index 000000000..b5e6c5eb7 --- /dev/null +++ b/api/transactions/types_test.go @@ -0,0 +1,116 @@ +// Copyright (c) 2018 The VeChainThor developers + +// Distributed under the GNU Lesser General Public License v3.0 software license, see the accompanying +// file LICENSE or + +package transactions + +import ( + "crypto/rand" + "math/big" + "testing" + + "github.com/ethereum/go-ethereum/common/hexutil" + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/secp256k1" + "github.com/stretchr/testify/assert" + "github.com/vechain/thor/v2/block" + "github.com/vechain/thor/v2/thor" + "github.com/vechain/thor/v2/tx" +) + +func TestErrorWhileRetrievingTxOriginInConvertReceipt(t *testing.T) { + tr := &tx.Transaction{} + header := &block.Header{} + receipt := &tx.Receipt{ + Reward: big.NewInt(100), + Paid: big.NewInt(10), + } + + convRec, err := convertReceipt(receipt, header, tr) + + assert.Error(t, err) + assert.Equal(t, err, secp256k1.ErrInvalidSignatureLen) + assert.Nil(t, convRec) +} + +func TestConvertReceiptWhenTxHasNoClauseTo(t *testing.T) { + value := big.NewInt(100) + tr := newTx(tx.NewClause(nil).WithValue(value)) + b := new(block.Builder).Build() + header := b.Header() + receipt := newReceipt() + expectedOutputAddress := thor.CreateContractAddress(tr.ID(), uint32(0), 0) + + convRec, err := convertReceipt(receipt, header, tr) + + assert.NoError(t, err) + assert.Equal(t, 1, len(convRec.Outputs)) + assert.Equal(t, &expectedOutputAddress, convRec.Outputs[0].ContractAddress) +} + +func TestConvertReceipt(t *testing.T) { + value := big.NewInt(100) + addr := randAddress() + tr := newTx(tx.NewClause(&addr).WithValue(value)) + b := new(block.Builder).Build() + header := b.Header() + receipt := newReceipt() + + convRec, err := convertReceipt(receipt, header, tr) + + assert.NoError(t, err) + assert.Equal(t, 1, len(convRec.Outputs)) + assert.Equal(t, 1, len(convRec.Outputs[0].Events)) + assert.Equal(t, 1, len(convRec.Outputs[0].Transfers)) + assert.Nil(t, convRec.Outputs[0].ContractAddress) + assert.Equal(t, receipt.Outputs[0].Events[0].Address, convRec.Outputs[0].Events[0].Address) + assert.Equal(t, hexutil.Encode(receipt.Outputs[0].Events[0].Data), convRec.Outputs[0].Events[0].Data) + assert.Equal(t, receipt.Outputs[0].Transfers[0].Sender, convRec.Outputs[0].Transfers[0].Sender) + assert.Equal(t, receipt.Outputs[0].Transfers[0].Recipient, convRec.Outputs[0].Transfers[0].Recipient) + assert.Equal(t, (*math.HexOrDecimal256)(receipt.Outputs[0].Transfers[0].Amount), convRec.Outputs[0].Transfers[0].Amount) +} + +// Utilities functions +func randAddress() (addr thor.Address) { + rand.Read(addr[:]) + return +} + +func newReceipt() *tx.Receipt { + return &tx.Receipt{ + Outputs: []*tx.Output{ + { + Events: tx.Events{{ + Address: randAddress(), + Topics: []thor.Bytes32{randomBytes32()}, + Data: randomBytes32().Bytes(), + }}, + Transfers: tx.Transfers{{ + Sender: randAddress(), + Recipient: randAddress(), + Amount: new(big.Int).SetBytes(randAddress().Bytes()), + }}, + }, + }, + Reward: big.NewInt(100), + Paid: big.NewInt(10), + } +} + +func newTx(clause *tx.Clause) *tx.Transaction { + tx := new(tx.Builder). + Clause(clause). + Build() + pk, _ := crypto.GenerateKey() + sig, _ := crypto.Sign(tx.SigningHash().Bytes(), pk) + return tx.WithSignature(sig) +} + +func randomBytes32() thor.Bytes32 { + var b32 thor.Bytes32 + + rand.Read(b32[:]) + return b32 +} diff --git a/api/transfers/transfers.go b/api/transfers/transfers.go index bf4fb936f..175aad8d3 100644 --- a/api/transfers/transfers.go +++ b/api/transfers/transfers.go @@ -67,5 +67,5 @@ func (t *Transfers) handleFilterTransferLogs(w http.ResponseWriter, req *http.Re func (t *Transfers) Mount(root *mux.Router, pathPrefix string) { sub := root.PathPrefix(pathPrefix).Subrouter() - sub.Path("").Methods("POST").HandlerFunc(utils.WrapHandlerFunc(t.handleFilterTransferLogs)) + sub.Path("").Methods(http.MethodPost).HandlerFunc(utils.WrapHandlerFunc(t.handleFilterTransferLogs)) } diff --git a/api/transfers/transfers_test.go b/api/transfers/transfers_test.go index 9e36d1bd6..7c24d6504 100644 --- a/api/transfers/transfers_test.go +++ b/api/transfers/transfers_test.go @@ -5,4 +5,179 @@ package transfers_test -// TODO +import ( + "bytes" + "crypto/rand" + "encoding/json" + "io" + "math/big" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gorilla/mux" + "github.com/stretchr/testify/assert" + "github.com/vechain/thor/v2/api/transfers" + "github.com/vechain/thor/v2/block" + "github.com/vechain/thor/v2/chain" + "github.com/vechain/thor/v2/genesis" + "github.com/vechain/thor/v2/logdb" + "github.com/vechain/thor/v2/muxdb" + "github.com/vechain/thor/v2/state" + "github.com/vechain/thor/v2/thor" + "github.com/vechain/thor/v2/tx" +) + +var ts *httptest.Server + +func TestEmptyTransfers(t *testing.T) { + db := createDb(t) + initTransferServer(t, db) + defer ts.Close() + + testTransferBadRequest(t) + testTransferWithEmptyDb(t) +} + +func TestTransfers(t *testing.T) { + db := createDb(t) + initTransferServer(t, db) + defer ts.Close() + + blocksToInsert := 5 + insertBlocks(t, db, blocksToInsert) + + testTransferWithBlocks(t, blocksToInsert) +} + +// Test functions +func testTransferBadRequest(t *testing.T) { + badBody := []byte{0x00, 0x01, 0x02} + + res, err := http.Post(ts.URL+"/transfers", "application/x-www-form-urlencoded", bytes.NewReader(badBody)) + + assert.NoError(t, err) + assert.Equal(t, http.StatusBadRequest, res.StatusCode) +} + +func testTransferWithEmptyDb(t *testing.T) { + emptyFilter := transfers.TransferFilter{ + CriteriaSet: make([]*logdb.TransferCriteria, 0), + Range: nil, + Options: nil, + Order: logdb.DESC, + } + + res, statusCode := httpPost(t, ts.URL+"/transfers", emptyFilter) + var tLogs []*transfers.FilteredTransfer + if err := json.Unmarshal(res, &tLogs); err != nil { + t.Fatal(err) + } + + assert.Equal(t, http.StatusOK, statusCode) + assert.Empty(t, tLogs) +} + +func testTransferWithBlocks(t *testing.T, expectedBlocks int) { + emptyFilter := transfers.TransferFilter{ + CriteriaSet: make([]*logdb.TransferCriteria, 0), + Range: nil, + Options: nil, + Order: logdb.DESC, + } + + res, statusCode := httpPost(t, ts.URL+"/transfers", emptyFilter) + var tLogs []*transfers.FilteredTransfer + if err := json.Unmarshal(res, &tLogs); err != nil { + t.Fatal(err) + } + + assert.Equal(t, http.StatusOK, statusCode) + assert.Equal(t, expectedBlocks, len(tLogs)) + for _, tLog := range tLogs { + assert.NotEmpty(t, tLog) + } +} + +// Init functions +func insertBlocks(t *testing.T, db *logdb.LogDB, n int) { + b := new(block.Builder).Build() + for i := 0; i < n; i++ { + b = new(block.Builder). + ParentID(b.Header().ID()). + Build() + receipts := tx.Receipts{newReceipt()} + + w := db.NewWriter() + if err := w.Write(b, receipts); err != nil { + t.Fatal(err) + } + + if err := w.Commit(); err != nil { + t.Fatal(err) + } + } +} + +func initTransferServer(t *testing.T, logDb *logdb.LogDB) { + router := mux.NewRouter() + + muxDb := muxdb.NewMem() + stater := state.NewStater(muxDb) + gene := genesis.NewDevnet() + + b, _, _, err := gene.Build(stater) + if err != nil { + t.Fatal(err) + } + + repo, _ := chain.NewRepository(muxDb, b) + + transfers.New(repo, logDb).Mount(router, "/transfers") + ts = httptest.NewServer(router) +} + +func createDb(t *testing.T) *logdb.LogDB { + logDb, err := logdb.NewMem() + if err != nil { + t.Fatal(err) + } + return logDb +} + +// Utilities functions +func randAddress() (addr thor.Address) { + rand.Read(addr[:]) + return +} + +func newReceipt() *tx.Receipt { + return &tx.Receipt{ + Outputs: []*tx.Output{ + { + Transfers: tx.Transfers{{ + Sender: randAddress(), + Recipient: randAddress(), + Amount: new(big.Int).SetBytes(randAddress().Bytes()), + }}, + }, + }, + } +} + +func httpPost(t *testing.T, url string, body interface{}) ([]byte, int) { + data, err := json.Marshal(body) + if err != nil { + t.Fatal(err) + } + res, err := http.Post(url, "application/x-www-form-urlencoded", bytes.NewReader(data)) + if err != nil { + t.Fatal(err) + } + r, err := io.ReadAll(res.Body) + res.Body.Close() + if err != nil { + t.Fatal(err) + } + return r, res.StatusCode +}