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