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