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