arnaucube
arnaucube/hermez-nodePublic Self-hosted repository
hermez-node/api/api_test.go
api/api_test.goRaw
  1package api
  2
  3import (
  4	"context"
  5	"encoding/json"
  6	"errors"
  7	"fmt"
  8	"io"
  9	"io/ioutil"
 10	"math/big"
 11	"net/http"
 12	"os"
 13	"sort"
 14	"strconv"
 15	"testing"
 16	"time"
 17
 18	ethCommon "github.com/ethereum/go-ethereum/common"
 19	swagger "github.com/getkin/kin-openapi/openapi3filter"
 20	"github.com/gin-gonic/gin"
 21	"github.com/hermeznetwork/hermez-node/common"
 22	"github.com/hermeznetwork/hermez-node/db"
 23	"github.com/hermeznetwork/hermez-node/db/historydb"
 24	"github.com/hermeznetwork/hermez-node/db/l2db"
 25	"github.com/hermeznetwork/hermez-node/db/statedb"
 26	"github.com/hermeznetwork/hermez-node/log"
 27	"github.com/hermeznetwork/hermez-node/test"
 28	"github.com/iden3/go-iden3-crypto/babyjub"
 29)
 30
 31// Pendinger is an interface that allows getting last returned item ID and PendingItems to be used for building fromItem
 32// when testing paginated endpoints.
 33type Pendinger interface {
 34	GetPending() (pendingItems, lastItemID uint64)
 35	Len() int
 36}
 37
 38const apiPort = ":4010"
 39const apiURL = "http://localhost" + apiPort + "/"
 40
 41type testCommon struct {
 42	blocks           []common.Block
 43	tokens           []historydb.TokenWithUSD
 44	batches          []testBatch
 45	fullBatches      []testFullBatch
 46	coordinators     []historydb.CoordinatorAPI
 47	accounts         []testAccount
 48	usrAddr          string
 49	usrBjj           string
 50	accs             []common.Account
 51	usrTxs           []testTx
 52	allTxs           []testTx
 53	exits            []testExit
 54	usrExits         []testExit
 55	poolTxsToSend    []testPoolTxSend
 56	poolTxsToReceive []testPoolTxReceive
 57	auths            []testAuth
 58	router           *swagger.Router
 59	bids             []testBid
 60	slots            []testSlot
 61	auctionVars      common.AuctionVariables
 62	rollupVars       common.RollupVariables
 63	wdelayerVars     common.WDelayerVariables
 64}
 65
 66var tc testCommon
 67var config configAPI
 68var api *API
 69
 70// TestMain initializes the API server, and fill HistoryDB and StateDB with fake data,
 71// emulating the task of the synchronizer in order to have data to be returned
 72// by the API endpoints that will be tested
 73func TestMain(m *testing.M) {
 74	// Initializations
 75	// Swagger
 76	router := swagger.NewRouter().WithSwaggerFromFile("./swagger.yml")
 77	// HistoryDB
 78	pass := os.Getenv("POSTGRES_PASS")
 79
 80	database, err := db.InitSQLDB(5432, "localhost", "hermez", pass, "hermez")
 81	if err != nil {
 82		panic(err)
 83	}
 84	hdb := historydb.NewHistoryDB(database)
 85	if err != nil {
 86		panic(err)
 87	}
 88	// StateDB
 89	dir, err := ioutil.TempDir("", "tmpdb")
 90	if err != nil {
 91		panic(err)
 92	}
 93	defer func() {
 94		if err := os.RemoveAll(dir); err != nil {
 95			panic(err)
 96		}
 97	}()
 98	sdb, err := statedb.NewStateDB(dir, statedb.TypeTxSelector, 0)
 99	if err != nil {
100		panic(err)
101	}
102	// L2DB
103	l2DB := l2db.NewL2DB(database, 10, 100, 24*time.Hour)
104	test.WipeDB(l2DB.DB()) // this will clean HistoryDB and L2DB
105
106	// Config (smart contract constants)
107	config = getConfigTest()
108
109	// API
110	apiGin := gin.Default()
111	api, err = NewAPI(
112		true,
113		true,
114		apiGin,
115		hdb,
116		sdb,
117		l2DB,
118		&config,
119	)
120	if err != nil {
121		panic(err)
122	}
123	// Start server
124	server := &http.Server{Addr: apiPort, Handler: apiGin}
125	go func() {
126		if err := server.ListenAndServe(); err != nil &&
127			err != http.ErrServerClosed {
128			panic(err)
129		}
130	}()
131
132	// Fill HistoryDB and StateDB with fake data
133	// Gen blocks and add them to DB
134	const nBlocks = 5
135	blocks := test.GenBlocks(1, nBlocks+1)
136	err = api.h.AddBlocks(blocks)
137	if err != nil {
138		panic(err)
139	}
140	lastBlockNum := blocks[nBlocks-1].EthBlockNum
141
142	// Gen tokens and add them to DB
143	const nTokens = 10
144	tokens, ethToken := test.GenTokens(nTokens, blocks)
145	err = api.h.AddTokens(tokens)
146	if err != nil {
147		panic(err)
148	}
149	tokens = append([]common.Token{ethToken}, tokens...)
150	// Set token value
151	tokensUSD := []historydb.TokenWithUSD{}
152	for i, tkn := range tokens {
153		token := historydb.TokenWithUSD{
154			TokenID:     tkn.TokenID,
155			EthBlockNum: tkn.EthBlockNum,
156			EthAddr:     tkn.EthAddr,
157			Name:        tkn.Name,
158			Symbol:      tkn.Symbol,
159			Decimals:    tkn.Decimals,
160		}
161		// Set value of 50% of the tokens
162		if i%2 != 0 {
163			value := float64(i) * 1.234567
164			now := time.Now().UTC()
165			token.USD = &value
166			token.USDUpdate = &now
167			err = api.h.UpdateTokenValue(token.Symbol, value)
168			if err != nil {
169				panic(err)
170			}
171		}
172		tokensUSD = append(tokensUSD, token)
173	}
174	// Gen batches and add them to DB
175	const nBatches = 10
176	batches := test.GenBatches(nBatches, blocks)
177	err = api.h.AddBatches(batches)
178	if err != nil {
179		panic(err)
180	}
181	// Gen accounts and add them to HistoryDB and StateDB
182	const totalAccounts = 40
183	const userAccounts = 4
184	usrAddr := ethCommon.BigToAddress(big.NewInt(4896847))
185	privK := babyjub.NewRandPrivKey()
186	usrBjj := privK.Public()
187	accs := test.GenAccounts(totalAccounts, userAccounts, tokens, &usrAddr, usrBjj, batches)
188	err = api.h.AddAccounts(accs)
189	if err != nil {
190		panic(err)
191	}
192	for i := 0; i < len(accs); i++ {
193		if _, err := api.s.CreateAccount(accs[i].Idx, &accs[i]); err != nil {
194			panic(err)
195		}
196	}
197	// helper to vinculate user related resources
198	usrIdxs := []string{}
199	for _, acc := range accs {
200		if acc.EthAddr == usrAddr || acc.PublicKey == usrBjj {
201			for _, token := range tokens {
202				if token.TokenID == acc.TokenID {
203					usrIdxs = append(usrIdxs, idxToHez(acc.Idx, token.Symbol))
204				}
205			}
206		}
207	}
208	// Gen exits and add them to DB
209	const totalExits = 40
210	exits := test.GenExitTree(totalExits, batches, accs)
211	err = api.h.AddExitTree(exits)
212	if err != nil {
213		panic(err)
214	}
215
216	// L1 and L2 txs need to be sorted in a combined way
217	// Gen L1Txs
218	const totalL1Txs = 40
219	const userL1Txs = 4
220	usrL1Txs, othrL1Txs := test.GenL1Txs(256, totalL1Txs, userL1Txs, &usrAddr, accs, tokens, blocks, batches)
221	// Gen L2Txs
222	const totalL2Txs = 20
223	const userL2Txs = 4
224	usrL2Txs, othrL2Txs := test.GenL2Txs(256+totalL1Txs, totalL2Txs, userL2Txs, &usrAddr, accs, tokens, blocks, batches)
225	// Sort txs
226	sortedTxs := []txSortFielder{}
227	for i := 0; i < len(usrL1Txs); i++ {
228		wL1 := wrappedL1(usrL1Txs[i])
229		sortedTxs = append(sortedTxs, &wL1)
230	}
231	for i := 0; i < len(othrL1Txs); i++ {
232		wL1 := wrappedL1(othrL1Txs[i])
233		sortedTxs = append(sortedTxs, &wL1)
234	}
235	for i := 0; i < len(usrL2Txs); i++ {
236		wL2 := wrappedL2(usrL2Txs[i])
237		sortedTxs = append(sortedTxs, &wL2)
238	}
239	for i := 0; i < len(othrL2Txs); i++ {
240		wL2 := wrappedL2(othrL2Txs[i])
241		sortedTxs = append(sortedTxs, &wL2)
242	}
243	sort.Sort(txsSort(sortedTxs))
244	// Store txs to DB
245	for _, genericTx := range sortedTxs {
246		l1 := genericTx.L1()
247		l2 := genericTx.L2()
248		if l1 != nil {
249			err = api.h.AddL1Txs([]common.L1Tx{*l1})
250			if err != nil {
251				panic(err)
252			}
253		} else if l2 != nil {
254			err = api.h.AddL2Txs([]common.L2Tx{*l2})
255			if err != nil {
256				panic(err)
257			}
258		} else {
259			panic("should be l1 or l2")
260		}
261	}
262
263	// Coordinators
264	const nCoords = 10
265	coords := test.GenCoordinators(nCoords, blocks)
266	err = api.h.AddCoordinators(coords)
267	if err != nil {
268		panic(err)
269	}
270	fromItem := uint(0)
271	limit := uint(99999)
272	coordinators, _, err := api.h.GetCoordinatorsAPI(&fromItem, &limit, historydb.OrderAsc)
273	if err != nil {
274		panic(err)
275	}
276
277	// Bids
278	const nBids = 20
279	bids := test.GenBids(nBids, blocks, coords)
280	err = api.h.AddBids(bids)
281	if err != nil {
282		panic(err)
283	}
284	testBids := genTestBids(blocks, coordinators, bids)
285
286	// Vars
287	var defaultSlotSetBid [6]*big.Int = [6]*big.Int{big.NewInt(10), big.NewInt(10), big.NewInt(10), big.NewInt(10), big.NewInt(10), big.NewInt(10)}
288	auctionVars := common.AuctionVariables{
289		EthBlockNum:        int64(2),
290		DonationAddress:    ethCommon.HexToAddress("0x1111111111111111111111111111111111111111"),
291		DefaultSlotSetBid:  defaultSlotSetBid,
292		Outbidding:         uint16(1),
293		SlotDeadline:       uint8(20),
294		BootCoordinator:    ethCommon.HexToAddress("0x1111111111111111111111111111111111111111"),
295		ClosedAuctionSlots: uint16(2),
296		OpenAuctionSlots:   uint16(5),
297	}
298
299	rollupVars := common.RollupVariables{
300		EthBlockNum:           int64(3),
301		FeeAddToken:           big.NewInt(100),
302		ForgeL1L2BatchTimeout: int64(44),
303		WithdrawalDelay:       uint64(3000),
304	}
305
306	wdelayerVars := common.WDelayerVariables{
307		WithdrawalDelay: uint64(3000),
308	}
309
310	err = api.h.AddAuctionVars(&auctionVars)
311	if err != nil {
312		panic(err)
313	}
314
315	const nSlots = 20
316
317	// Set testCommon
318	usrTxs, allTxs := genTestTxs(sortedTxs, usrIdxs, accs, tokensUSD, blocks)
319	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.
320	testBatches, fullBatches := genTestBatches(blocks, batches, allTxs)
321	usrExits, allExits := genTestExits(exits, tokensUSD, accs, usrIdxs)
322	tc = testCommon{
323		blocks:           blocks,
324		tokens:           tokensUSD,
325		batches:          testBatches,
326		fullBatches:      fullBatches,
327		coordinators:     coordinators,
328		accounts:         genTestAccounts(accs, tokensUSD),
329		usrAddr:          ethAddrToHez(usrAddr),
330		usrBjj:           bjjToString(usrBjj),
331		accs:             accs,
332		usrTxs:           usrTxs,
333		allTxs:           allTxs,
334		exits:            allExits,
335		usrExits:         usrExits,
336		poolTxsToSend:    poolTxsToSend,
337		poolTxsToReceive: poolTxsToReceive,
338		auths:            genTestAuths(test.GenAuths(5)),
339		router:           router,
340		bids:             testBids,
341		slots:            api.genTestSlots(nSlots, lastBlockNum, testBids, auctionVars),
342		auctionVars:      auctionVars,
343		rollupVars:       rollupVars,
344		wdelayerVars:     wdelayerVars,
345	}
346
347	// Fake server
348	if os.Getenv("FAKE_SERVER") == "yes" {
349		for {
350			log.Info("Running fake server at " + apiURL + " until ^C is received")
351			time.Sleep(30 * time.Second)
352		}
353	}
354	// Run tests
355	result := m.Run()
356	// Stop server
357	if err := server.Shutdown(context.Background()); err != nil {
358		panic(err)
359	}
360	if err := database.Close(); err != nil {
361		panic(err)
362	}
363	if err := os.RemoveAll(dir); err != nil {
364		panic(err)
365	}
366	os.Exit(result)
367}
368
369func doGoodReqPaginated(
370	path, order string,
371	iterStruct Pendinger,
372	appendIter func(res interface{}),
373) error {
374	var next uint64
375	firstIte := true
376	expectedTotal := 0
377	totalReceived := 0
378	for {
379		// Calculate fromItem
380		iterPath := path
381		if firstIte {
382			if order == historydb.OrderDesc {
383				// Fetch first item in reverse order
384				iterPath += "99999" // Asumption that for testing there won't be any itemID > 99999
385			} else {
386				iterPath += "0"
387			}
388		} else {
389			iterPath += strconv.Itoa(int(next))
390		}
391		// Call API to get this iteration items
392		if err := doGoodReq("GET", iterPath+"&order="+order, nil, iterStruct); err != nil {
393			return err
394		}
395		appendIter(iterStruct)
396		// Keep iterating?
397		remaining, lastID := iterStruct.GetPending()
398		if remaining == 0 {
399			break
400		}
401		if order == historydb.OrderDesc {
402			next = lastID - 1
403		} else {
404			next = lastID + 1
405		}
406		// Check that the expected amount of items is consistent across iterations
407		totalReceived += iterStruct.Len()
408		if firstIte {
409			firstIte = false
410			expectedTotal = totalReceived + int(remaining)
411		}
412		if expectedTotal != totalReceived+int(remaining) {
413			panic(fmt.Sprintf(
414				"pagination error, totalReceived + remaining should be %d, but is %d",
415				expectedTotal, totalReceived+int(remaining),
416			))
417		}
418	}
419	return nil
420}
421
422func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
423	ctx := context.Background()
424	client := &http.Client{}
425	httpReq, err := http.NewRequest(method, path, reqBody)
426	if err != nil {
427		return err
428	}
429	if reqBody != nil {
430		httpReq.Header.Add("Content-Type", "application/json")
431	}
432	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
433	if err != nil {
434		return err
435	}
436	// Validate request against swagger spec
437	requestValidationInput := &swagger.RequestValidationInput{
438		Request:    httpReq,
439		PathParams: pathParams,
440		Route:      route,
441	}
442	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
443		return err
444	}
445	// Do API call
446	resp, err := client.Do(httpReq)
447	if err != nil {
448		return err
449	}
450	if resp.Body == nil && returnStruct != nil {
451		return errors.New("Nil body")
452	}
453	//nolint
454	defer resp.Body.Close()
455	body, err := ioutil.ReadAll(resp.Body)
456	if err != nil {
457		return err
458	}
459	if resp.StatusCode != 200 {
460		return fmt.Errorf("%d response. Body: %s", resp.StatusCode, string(body))
461	}
462	if returnStruct == nil {
463		return nil
464	}
465	// Unmarshal body into return struct
466	if err := json.Unmarshal(body, returnStruct); err != nil {
467		log.Error("invalid json: " + string(body))
468		return err
469	}
470	// Validate response against swagger spec
471	responseValidationInput := &swagger.ResponseValidationInput{
472		RequestValidationInput: requestValidationInput,
473		Status:                 resp.StatusCode,
474		Header:                 resp.Header,
475	}
476	responseValidationInput = responseValidationInput.SetBodyBytes(body)
477	return swagger.ValidateResponse(ctx, responseValidationInput)
478}
479
480func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
481	ctx := context.Background()
482	client := &http.Client{}
483	httpReq, _ := http.NewRequest(method, path, reqBody)
484	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
485	if err != nil {
486		return err
487	}
488	// Validate request against swagger spec
489	requestValidationInput := &swagger.RequestValidationInput{
490		Request:    httpReq,
491		PathParams: pathParams,
492		Route:      route,
493	}
494	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
495		if expectedResponseCode != 400 {
496			return err
497		}
498		log.Warn("The request does not match the API spec")
499	}
500	// Do API call
501	resp, err := client.Do(httpReq)
502	if err != nil {
503		return err
504	}
505	if resp.Body == nil {
506		return errors.New("Nil body")
507	}
508	//nolint
509	defer resp.Body.Close()
510	body, err := ioutil.ReadAll(resp.Body)
511	if err != nil {
512		return err
513	}
514	if resp.StatusCode != expectedResponseCode {
515		return fmt.Errorf("Unexpected response code: %d. Body: %s", resp.StatusCode, string(body))
516	}
517	// Validate response against swagger spec
518	responseValidationInput := &swagger.ResponseValidationInput{
519		RequestValidationInput: requestValidationInput,
520		Status:                 resp.StatusCode,
521		Header:                 resp.Header,
522	}
523	responseValidationInput = responseValidationInput.SetBodyBytes(body)
524	return swagger.ValidateResponse(ctx, responseValidationInput)
525}
526
527// test helpers
528
529func getTimestamp(blockNum int64, blocks []common.Block) time.Time {
530	for i := 0; i < len(blocks); i++ {
531		if blocks[i].EthBlockNum == blockNum {
532			return blocks[i].Timestamp
533		}
534	}
535	panic("timesamp not found")
536}
537
538func getTokenByID(id common.TokenID, tokens []historydb.TokenWithUSD) historydb.TokenWithUSD {
539	for i := 0; i < len(tokens); i++ {
540		if tokens[i].TokenID == id {
541			return tokens[i]
542		}
543	}
544	panic("token not found")
545}
546
547func getTokenByIdx(idx common.Idx, tokens []historydb.TokenWithUSD, accs []common.Account) historydb.TokenWithUSD {
548	for _, acc := range accs {
549		if idx == acc.Idx {
550			return getTokenByID(acc.TokenID, tokens)
551		}
552	}
553	panic("token not found")
554}
555
556func getAccountByIdx(idx common.Idx, accs []common.Account) *common.Account {
557	for _, acc := range accs {
558		if acc.Idx == idx {
559			return &acc
560		}
561	}
562	panic("account not found")
563}
564
565func getBlockByNum(ethBlockNum int64, blocks []common.Block) common.Block {
566	for _, b := range blocks {
567		if b.EthBlockNum == ethBlockNum {
568			return b
569		}
570	}
571	panic("block not found")
572}
573
574func getCoordinatorByBidder(bidder ethCommon.Address, coordinators []historydb.CoordinatorAPI) historydb.CoordinatorAPI {
575	for _, c := range coordinators {
576		if c.Bidder == bidder {
577			return c
578		}
579	}
580	panic("coordinator not found")
581}