You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

513 lines
13 KiB

package api
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"math/big"
"net/http"
"os"
"sort"
"strconv"
"testing"
"time"
ethCommon "github.com/ethereum/go-ethereum/common"
swagger "github.com/getkin/kin-openapi/openapi3filter"
"github.com/gin-gonic/gin"
"github.com/hermeznetwork/hermez-node/common"
"github.com/hermeznetwork/hermez-node/db"
"github.com/hermeznetwork/hermez-node/db/historydb"
"github.com/hermeznetwork/hermez-node/db/l2db"
"github.com/hermeznetwork/hermez-node/db/statedb"
"github.com/hermeznetwork/hermez-node/log"
"github.com/hermeznetwork/hermez-node/test"
"github.com/iden3/go-iden3-crypto/babyjub"
)
const apiPort = ":4010"
const apiURL = "http://localhost" + apiPort + "/"
type testCommon struct {
blocks []common.Block
tokens []historydb.TokenWithUSD
batches []testBatch
fullBatches []testFullBatch
coordinators []historydb.CoordinatorAPI
usrAddr string
usrBjj string
accs []common.Account
usrTxs []testTx
allTxs []testTx
exits []testExit
usrExits []testExit
poolTxsToSend []testPoolTxSend
poolTxsToReceive []testPoolTxReceive
auths []testAuth
router *swagger.Router
bids []testBid
}
var tc testCommon
var config configAPI
// TestMain initializes the API server, and fill HistoryDB and StateDB with fake data,
// emulating the task of the synchronizer in order to have data to be returned
// by the API endpoints that will be tested
func TestMain(m *testing.M) {
// Initializations
// Swagger
router := swagger.NewRouter().WithSwaggerFromFile("./swagger.yml")
// HistoryDB
pass := os.Getenv("POSTGRES_PASS")
database, err := db.InitSQLDB(5432, "localhost", "hermez", pass, "hermez")
if err != nil {
panic(err)
}
hdb := historydb.NewHistoryDB(database)
if err != nil {
panic(err)
}
// StateDB
dir, err := ioutil.TempDir("", "tmpdb")
if err != nil {
panic(err)
}
defer func() {
if err := os.RemoveAll(dir); err != nil {
panic(err)
}
}()
sdb, err := statedb.NewStateDB(dir, statedb.TypeTxSelector, 0)
if err != nil {
panic(err)
}
// L2DB
l2DB := l2db.NewL2DB(database, 10, 100, 24*time.Hour)
test.WipeDB(l2DB.DB()) // this will clean HistoryDB and L2DB
// Config (smart contract constants)
config = getConfigTest()
// API
api := gin.Default()
if err := SetAPIEndpoints(
true,
true,
api,
hdb,
sdb,
l2DB,
&config,
); err != nil {
panic(err)
}
// Start server
server := &http.Server{Addr: apiPort, Handler: api}
go func() {
if err := server.ListenAndServe(); err != nil &&
err != http.ErrServerClosed {
panic(err)
}
}()
// Fill HistoryDB and StateDB with fake data
// Gen blocks and add them to DB
const nBlocks = 5
blocks := test.GenBlocks(1, nBlocks+1)
err = h.AddBlocks(blocks)
if err != nil {
panic(err)
}
// Gen tokens and add them to DB
const nTokens = 10
tokens, ethToken := test.GenTokens(nTokens, blocks)
err = h.AddTokens(tokens)
if err != nil {
panic(err)
}
tokens = append([]common.Token{ethToken}, tokens...)
// Set token value
tokensUSD := []historydb.TokenWithUSD{}
for i, tkn := range tokens {
token := historydb.TokenWithUSD{
TokenID: tkn.TokenID,
EthBlockNum: tkn.EthBlockNum,
EthAddr: tkn.EthAddr,
Name: tkn.Name,
Symbol: tkn.Symbol,
Decimals: tkn.Decimals,
}
// Set value of 50% of the tokens
if i%2 != 0 {
value := float64(i) * 1.234567
now := time.Now().UTC()
token.USD = &value
token.USDUpdate = &now
err = h.UpdateTokenValue(token.Symbol, value)
if err != nil {
panic(err)
}
}
tokensUSD = append(tokensUSD, token)
}
// Gen batches and add them to DB
const nBatches = 10
batches := test.GenBatches(nBatches, blocks)
err = h.AddBatches(batches)
if err != nil {
panic(err)
}
// Gen accounts and add them to HistoryDB and StateDB
const totalAccounts = 40
const userAccounts = 4
usrAddr := ethCommon.BigToAddress(big.NewInt(4896847))
privK := babyjub.NewRandPrivKey()
usrBjj := privK.Public()
accs := test.GenAccounts(totalAccounts, userAccounts, tokens, &usrAddr, usrBjj, batches)
err = h.AddAccounts(accs)
if err != nil {
panic(err)
}
for i := 0; i < len(accs); i++ {
if _, err := s.CreateAccount(accs[i].Idx, &accs[i]); err != nil {
panic(err)
}
}
// helper to vinculate user related resources
usrIdxs := []string{}
for _, acc := range accs {
if acc.EthAddr == usrAddr || acc.PublicKey == usrBjj {
for _, token := range tokens {
if token.TokenID == acc.TokenID {
usrIdxs = append(usrIdxs, idxToHez(acc.Idx, token.Symbol))
}
}
}
}
// Gen exits and add them to DB
const totalExits = 40
exits := test.GenExitTree(totalExits, batches, accs)
err = h.AddExitTree(exits)
if err != nil {
panic(err)
}
// L1 and L2 txs need to be sorted in a combined way
// Gen L1Txs
const totalL1Txs = 40
const userL1Txs = 4
usrL1Txs, othrL1Txs := test.GenL1Txs(256, totalL1Txs, userL1Txs, &usrAddr, accs, tokens, blocks, batches)
// Gen L2Txs
const totalL2Txs = 20
const userL2Txs = 4
usrL2Txs, othrL2Txs := test.GenL2Txs(256+totalL1Txs, totalL2Txs, userL2Txs, &usrAddr, accs, tokens, blocks, batches)
// Sort txs
sortedTxs := []txSortFielder{}
for i := 0; i < len(usrL1Txs); i++ {
wL1 := wrappedL1(usrL1Txs[i])
sortedTxs = append(sortedTxs, &wL1)
}
for i := 0; i < len(othrL1Txs); i++ {
wL1 := wrappedL1(othrL1Txs[i])
sortedTxs = append(sortedTxs, &wL1)
}
for i := 0; i < len(usrL2Txs); i++ {
wL2 := wrappedL2(usrL2Txs[i])
sortedTxs = append(sortedTxs, &wL2)
}
for i := 0; i < len(othrL2Txs); i++ {
wL2 := wrappedL2(othrL2Txs[i])
sortedTxs = append(sortedTxs, &wL2)
}
sort.Sort(txsSort(sortedTxs))
// Store txs to DB
for _, genericTx := range sortedTxs {
l1 := genericTx.L1()
l2 := genericTx.L2()
if l1 != nil {
err = h.AddL1Txs([]common.L1Tx{*l1})
if err != nil {
panic(err)
}
} else if l2 != nil {
err = h.AddL2Txs([]common.L2Tx{*l2})
if err != nil {
panic(err)
}
} else {
panic("should be l1 or l2")
}
}
// Coordinators
const nCoords = 10
coords := test.GenCoordinators(nCoords, blocks)
err = hdb.AddCoordinators(coords)
if err != nil {
panic(err)
}
fromItem := uint(0)
limit := uint(99999)
coordinators, _, err := hdb.GetCoordinatorsAPI(&fromItem, &limit, historydb.OrderAsc)
if err != nil {
panic(err)
}
// Bids
const nBids = 10
bids := test.GenBids(nBids, blocks, coords)
err = hdb.AddBids(bids)
if err != nil {
panic(err)
}
// Set testCommon
usrTxs, allTxs := genTestTxs(sortedTxs, usrIdxs, accs, tokensUSD, blocks)
poolTxsToSend, poolTxsToReceive := genTestPoolTx(accs, []babyjub.PrivateKey{privK}, tokensUSD) // NOTE: pool txs are not inserted to the DB here. In the test they will be posted and getted.
testBatches, fullBatches := genTestBatches(blocks, batches, allTxs)
usrExits, allExits := genTestExits(exits, tokensUSD, accs, usrIdxs)
tc = testCommon{
blocks: blocks,
tokens: tokensUSD,
batches: testBatches,
fullBatches: fullBatches,
coordinators: coordinators,
usrAddr: ethAddrToHez(usrAddr),
usrBjj: bjjToString(usrBjj),
accs: accs,
usrTxs: usrTxs,
allTxs: allTxs,
exits: allExits,
usrExits: usrExits,
poolTxsToSend: poolTxsToSend,
poolTxsToReceive: poolTxsToReceive,
auths: genTestAuths(test.GenAuths(5)),
router: router,
bids: genTestBids(blocks, coordinators, bids),
}
// Fake server
if os.Getenv("FAKE_SERVER") == "yes" {
for {
log.Info("Running fake server at " + apiURL + " until ^C is received")
time.Sleep(30 * time.Second)
}
}
// Run tests
result := m.Run()
// Stop server
if err := server.Shutdown(context.Background()); err != nil {
panic(err)
}
if err := database.Close(); err != nil {
panic(err)
}
if err := os.RemoveAll(dir); err != nil {
panic(err)
}
os.Exit(result)
}
func doGoodReqPaginated(
path, order string,
iterStruct db.Paginationer,
appendIter func(res interface{}),
) error {
next := 0
for {
// Call API to get this iteration items
iterPath := path
if next == 0 && order == historydb.OrderDesc {
// Fetch first item in reverse order
iterPath += "99999"
} else {
// Fetch from next item or 0 if it's ascending order
iterPath += strconv.Itoa(next)
}
if err := doGoodReq("GET", iterPath+"&order="+order, nil, iterStruct); err != nil {
return err
}
appendIter(iterStruct)
// Keep iterating?
pag := iterStruct.GetPagination()
if order == historydb.OrderAsc {
if pag.LastReturnedItem == pag.LastItem { // No
break
} else { // Yes
next = pag.LastReturnedItem + 1
}
} else {
if pag.FirstReturnedItem == pag.FirstItem { // No
break
} else { // Yes
next = pag.FirstReturnedItem - 1
}
}
}
return nil
}
func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
ctx := context.Background()
client := &http.Client{}
httpReq, err := http.NewRequest(method, path, reqBody)
if err != nil {
return err
}
if reqBody != nil {
httpReq.Header.Add("Content-Type", "application/json")
}
route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
if err != nil {
return err
}
// Validate request against swagger spec
requestValidationInput := &swagger.RequestValidationInput{
Request: httpReq,
PathParams: pathParams,
Route: route,
}
if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
return err
}
// Do API call
resp, err := client.Do(httpReq)
if err != nil {
return err
}
if resp.Body == nil && returnStruct != nil {
return errors.New("Nil body")
}
//nolint
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != 200 {
return fmt.Errorf("%d response. Body: %s", resp.StatusCode, string(body))
}
if returnStruct == nil {
return nil
}
// Unmarshal body into return struct
if err := json.Unmarshal(body, returnStruct); err != nil {
log.Error("invalid json: " + string(body))
return err
}
// Validate response against swagger spec
responseValidationInput := &swagger.ResponseValidationInput{
RequestValidationInput: requestValidationInput,
Status: resp.StatusCode,
Header: resp.Header,
}
responseValidationInput = responseValidationInput.SetBodyBytes(body)
return swagger.ValidateResponse(ctx, responseValidationInput)
}
func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
ctx := context.Background()
client := &http.Client{}
httpReq, _ := http.NewRequest(method, path, reqBody)
route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
if err != nil {
return err
}
// Validate request against swagger spec
requestValidationInput := &swagger.RequestValidationInput{
Request: httpReq,
PathParams: pathParams,
Route: route,
}
if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
if expectedResponseCode != 400 {
return err
}
log.Warn("The request does not match the API spec")
}
// Do API call
resp, err := client.Do(httpReq)
if err != nil {
return err
}
if resp.Body == nil {
return errors.New("Nil body")
}
//nolint
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
if resp.StatusCode != expectedResponseCode {
return fmt.Errorf("Unexpected response code: %d. Body: %s", resp.StatusCode, string(body))
}
// Validate response against swagger spec
responseValidationInput := &swagger.ResponseValidationInput{
RequestValidationInput: requestValidationInput,
Status: resp.StatusCode,
Header: resp.Header,
}
responseValidationInput = responseValidationInput.SetBodyBytes(body)
return swagger.ValidateResponse(ctx, responseValidationInput)
}
// test helpers
func getTimestamp(blockNum int64, blocks []common.Block) time.Time {
for i := 0; i < len(blocks); i++ {
if blocks[i].EthBlockNum == blockNum {
return blocks[i].Timestamp
}
}
panic("timesamp not found")
}
func getTokenByID(id common.TokenID, tokens []historydb.TokenWithUSD) historydb.TokenWithUSD {
for i := 0; i < len(tokens); i++ {
if tokens[i].TokenID == id {
return tokens[i]
}
}
panic("token not found")
}
func getTokenByIdx(idx common.Idx, tokens []historydb.TokenWithUSD, accs []common.Account) historydb.TokenWithUSD {
for _, acc := range accs {
if idx == acc.Idx {
return getTokenByID(acc.TokenID, tokens)
}
}
panic("token not found")
}
func getAccountByIdx(idx common.Idx, accs []common.Account) *common.Account {
for _, acc := range accs {
if acc.Idx == idx {
return &acc
}
}
panic("account not found")
}
func getBlockByNum(ethBlockNum int64, blocks []common.Block) common.Block {
for _, b := range blocks {
if b.EthBlockNum == ethBlockNum {
return b
}
}
panic("block not found")
}
func getCoordinatorByBidder(bidder ethCommon.Address, coordinators []historydb.CoordinatorAPI) historydb.CoordinatorAPI {
for _, c := range coordinators {
if c.Bidder == bidder {
return c
}
}
panic("coordinator not found")
}