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