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	auctionVars := common.AuctionVariables{
288		BootCoordinator:    ethCommon.HexToAddress("0x1111111111111111111111111111111111111111"),
289		ClosedAuctionSlots: uint16(2),
290		OpenAuctionSlots:   uint16(5),
291	}
292
293	rollupVars := common.RollupVariables{
294		WithdrawalDelay: uint64(3000),
295	}
296
297	wdelayerVars := common.WDelayerVariables{
298		WithdrawalDelay: uint64(3000),
299	}
300
301	err = api.h.AddAuctionVars(&auctionVars)
302	if err != nil {
303		panic(err)
304	}
305
306	const nSlots = 20
307
308	// Set testCommon
309	usrTxs, allTxs := genTestTxs(sortedTxs, usrIdxs, accs, tokensUSD, blocks)
310	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.
311	testBatches, fullBatches := genTestBatches(blocks, batches, allTxs)
312	usrExits, allExits := genTestExits(exits, tokensUSD, accs, usrIdxs)
313	tc = testCommon{
314		blocks:           blocks,
315		tokens:           tokensUSD,
316		batches:          testBatches,
317		fullBatches:      fullBatches,
318		coordinators:     coordinators,
319		accounts:         genTestAccounts(accs, tokensUSD),
320		usrAddr:          ethAddrToHez(usrAddr),
321		usrBjj:           bjjToString(usrBjj),
322		accs:             accs,
323		usrTxs:           usrTxs,
324		allTxs:           allTxs,
325		exits:            allExits,
326		usrExits:         usrExits,
327		poolTxsToSend:    poolTxsToSend,
328		poolTxsToReceive: poolTxsToReceive,
329		auths:            genTestAuths(test.GenAuths(5)),
330		router:           router,
331		bids:             testBids,
332		slots:            api.genTestSlots(nSlots, lastBlockNum, testBids, auctionVars),
333		auctionVars:      auctionVars,
334		rollupVars:       rollupVars,
335		wdelayerVars:     wdelayerVars,
336	}
337
338	// Fake server
339	if os.Getenv("FAKE_SERVER") == "yes" {
340		for {
341			log.Info("Running fake server at " + apiURL + " until ^C is received")
342			time.Sleep(30 * time.Second)
343		}
344	}
345	// Run tests
346	result := m.Run()
347	// Stop server
348	if err := server.Shutdown(context.Background()); err != nil {
349		panic(err)
350	}
351	if err := database.Close(); err != nil {
352		panic(err)
353	}
354	if err := os.RemoveAll(dir); err != nil {
355		panic(err)
356	}
357	os.Exit(result)
358}
359
360func doGoodReqPaginated(
361	path, order string,
362	iterStruct Pendinger,
363	appendIter func(res interface{}),
364) error {
365	var next uint64
366	firstIte := true
367	expectedTotal := 0
368	totalReceived := 0
369	for {
370		// Calculate fromItem
371		iterPath := path
372		if firstIte {
373			if order == historydb.OrderDesc {
374				// Fetch first item in reverse order
375				iterPath += "99999" // Asumption that for testing there won't be any itemID > 99999
376			} else {
377				iterPath += "0"
378			}
379		} else {
380			iterPath += strconv.Itoa(int(next))
381		}
382		// Call API to get this iteration items
383		if err := doGoodReq("GET", iterPath+"&order="+order, nil, iterStruct); err != nil {
384			return err
385		}
386		appendIter(iterStruct)
387		// Keep iterating?
388		remaining, lastID := iterStruct.GetPending()
389		if remaining == 0 {
390			break
391		}
392		if order == historydb.OrderDesc {
393			next = lastID - 1
394		} else {
395			next = lastID + 1
396		}
397		// Check that the expected amount of items is consistent across iterations
398		totalReceived += iterStruct.Len()
399		if firstIte {
400			firstIte = false
401			expectedTotal = totalReceived + int(remaining)
402		}
403		if expectedTotal != totalReceived+int(remaining) {
404			panic(fmt.Sprintf(
405				"pagination error, totalReceived + remaining should be %d, but is %d",
406				expectedTotal, totalReceived+int(remaining),
407			))
408		}
409	}
410	return nil
411}
412
413func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
414	ctx := context.Background()
415	client := &http.Client{}
416	httpReq, err := http.NewRequest(method, path, reqBody)
417	if err != nil {
418		return err
419	}
420	if reqBody != nil {
421		httpReq.Header.Add("Content-Type", "application/json")
422	}
423	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
424	if err != nil {
425		return err
426	}
427	// Validate request against swagger spec
428	requestValidationInput := &swagger.RequestValidationInput{
429		Request:    httpReq,
430		PathParams: pathParams,
431		Route:      route,
432	}
433	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
434		return err
435	}
436	// Do API call
437	resp, err := client.Do(httpReq)
438	if err != nil {
439		return err
440	}
441	if resp.Body == nil && returnStruct != nil {
442		return errors.New("Nil body")
443	}
444	//nolint
445	defer resp.Body.Close()
446	body, err := ioutil.ReadAll(resp.Body)
447	if err != nil {
448		return err
449	}
450	if resp.StatusCode != 200 {
451		return fmt.Errorf("%d response. Body: %s", resp.StatusCode, string(body))
452	}
453	if returnStruct == nil {
454		return nil
455	}
456	// Unmarshal body into return struct
457	if err := json.Unmarshal(body, returnStruct); err != nil {
458		log.Error("invalid json: " + string(body))
459		return err
460	}
461	// Validate response against swagger spec
462	responseValidationInput := &swagger.ResponseValidationInput{
463		RequestValidationInput: requestValidationInput,
464		Status:                 resp.StatusCode,
465		Header:                 resp.Header,
466	}
467	responseValidationInput = responseValidationInput.SetBodyBytes(body)
468	return swagger.ValidateResponse(ctx, responseValidationInput)
469}
470
471func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
472	ctx := context.Background()
473	client := &http.Client{}
474	httpReq, _ := http.NewRequest(method, path, reqBody)
475	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
476	if err != nil {
477		return err
478	}
479	// Validate request against swagger spec
480	requestValidationInput := &swagger.RequestValidationInput{
481		Request:    httpReq,
482		PathParams: pathParams,
483		Route:      route,
484	}
485	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
486		if expectedResponseCode != 400 {
487			return err
488		}
489		log.Warn("The request does not match the API spec")
490	}
491	// Do API call
492	resp, err := client.Do(httpReq)
493	if err != nil {
494		return err
495	}
496	if resp.Body == nil {
497		return errors.New("Nil body")
498	}
499	//nolint
500	defer resp.Body.Close()
501	body, err := ioutil.ReadAll(resp.Body)
502	if err != nil {
503		return err
504	}
505	if resp.StatusCode != expectedResponseCode {
506		return fmt.Errorf("Unexpected response code: %d. Body: %s", resp.StatusCode, string(body))
507	}
508	// Validate response against swagger spec
509	responseValidationInput := &swagger.ResponseValidationInput{
510		RequestValidationInput: requestValidationInput,
511		Status:                 resp.StatusCode,
512		Header:                 resp.Header,
513	}
514	responseValidationInput = responseValidationInput.SetBodyBytes(body)
515	return swagger.ValidateResponse(ctx, responseValidationInput)
516}
517
518// test helpers
519
520func getTimestamp(blockNum int64, blocks []common.Block) time.Time {
521	for i := 0; i < len(blocks); i++ {
522		if blocks[i].EthBlockNum == blockNum {
523			return blocks[i].Timestamp
524		}
525	}
526	panic("timesamp not found")
527}
528
529func getTokenByID(id common.TokenID, tokens []historydb.TokenWithUSD) historydb.TokenWithUSD {
530	for i := 0; i < len(tokens); i++ {
531		if tokens[i].TokenID == id {
532			return tokens[i]
533		}
534	}
535	panic("token not found")
536}
537
538func getTokenByIdx(idx common.Idx, tokens []historydb.TokenWithUSD, accs []common.Account) historydb.TokenWithUSD {
539	for _, acc := range accs {
540		if idx == acc.Idx {
541			return getTokenByID(acc.TokenID, tokens)
542		}
543	}
544	panic("token not found")
545}
546
547func getAccountByIdx(idx common.Idx, accs []common.Account) *common.Account {
548	for _, acc := range accs {
549		if acc.Idx == idx {
550			return &acc
551		}
552	}
553	panic("account not found")
554}
555
556func getBlockByNum(ethBlockNum int64, blocks []common.Block) common.Block {
557	for _, b := range blocks {
558		if b.EthBlockNum == ethBlockNum {
559			return b
560		}
561	}
562	panic("block not found")
563}
564
565func getCoordinatorByBidder(bidder ethCommon.Address, coordinators []historydb.CoordinatorAPI) historydb.CoordinatorAPI {
566	for _, c := range coordinators {
567		if c.Bidder == bidder {
568			return c
569		}
570	}
571	panic("coordinator not found")
572}