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