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/historydb"
 23	"github.com/hermeznetwork/hermez-node/db/l2db"
 24	"github.com/hermeznetwork/hermez-node/db/statedb"
 25	"github.com/hermeznetwork/hermez-node/log"
 26	"github.com/hermeznetwork/hermez-node/test"
 27	"github.com/iden3/go-iden3-crypto/babyjub"
 28	"github.com/jinzhu/copier"
 29	"github.com/stretchr/testify/assert"
 30)
 31
 32const apiPort = ":4010"
 33const apiURL = "http://localhost" + apiPort + "/"
 34
 35type testCommon struct {
 36	blocks  []common.Block
 37	tokens  []common.Token
 38	batches []common.Batch
 39	usrAddr string
 40	usrBjj  string
 41	accs    []common.Account
 42	usrTxs  historyTxAPIs
 43	othrTxs historyTxAPIs
 44	allTxs  historyTxAPIs
 45	router  *swagger.Router
 46}
 47
 48type historyTxAPIs []historyTxAPI
 49
 50func (h historyTxAPIs) Len() int      { return len(h) }
 51func (h historyTxAPIs) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
 52func (h historyTxAPIs) Less(i, j int) bool {
 53	// i not forged yet
 54	if h[i].BatchNum == nil {
 55		if h[j].BatchNum != nil { // j is already forged
 56			return false
 57		}
 58		// Both aren't forged, is i in a smaller position?
 59		return h[i].Position < h[j].Position
 60	}
 61	// i is forged
 62	if h[j].BatchNum == nil {
 63		return true // j is not forged
 64	}
 65	// Both are forged
 66	if *h[i].BatchNum == *h[j].BatchNum {
 67		// At the same batch,  is i in a smaller position?
 68		return h[i].Position < h[j].Position
 69	}
 70	// At different batches, is i in a smaller batch?
 71	return *h[i].BatchNum < *h[j].BatchNum
 72}
 73
 74var tc testCommon
 75
 76func TestMain(m *testing.M) {
 77	// Init swagger
 78	router := swagger.NewRouter().WithSwaggerFromFile("./swagger.yml")
 79	// Init DBs
 80	pass := os.Getenv("POSTGRES_PASS")
 81	hdb, err := historydb.NewHistoryDB(5432, "localhost", "hermez", pass, "history")
 82	if err != nil {
 83		panic(err)
 84	}
 85	dir, err := ioutil.TempDir("", "tmpdb")
 86	if err != nil {
 87		panic(err)
 88	}
 89	sdb, err := statedb.NewStateDB(dir, false, 0)
 90	if err != nil {
 91		panic(err)
 92	}
 93	l2db, err := l2db.NewL2DB(5432, "localhost", "hermez", pass, "l2", 10, 512, 24*time.Hour)
 94	if err != nil {
 95		panic(err)
 96	}
 97	// Init API
 98	api := gin.Default()
 99	if err := SetAPIEndpoints(
100		true,
101		true,
102		api,
103		hdb,
104		sdb,
105		l2db,
106	); err != nil {
107		panic(err)
108	}
109	// Start server
110	server := &http.Server{Addr: apiPort, Handler: api}
111	go func() {
112		if err := server.ListenAndServe(); err != nil &&
113			err != http.ErrServerClosed {
114			panic(err)
115		}
116	}()
117	// Populate DBs
118	// Clean DB
119	err = h.Reorg(0)
120	if err != nil {
121		panic(err)
122	}
123	// Gen blocks and add them to DB
124	const nBlocks = 5
125	blocks := test.GenBlocks(1, nBlocks+1)
126	err = h.AddBlocks(blocks)
127	if err != nil {
128		panic(err)
129	}
130	// Gen tokens and add them to DB
131	const nTokens = 10
132	tokens := test.GenTokens(nTokens, blocks)
133	err = h.AddTokens(tokens)
134	if err != nil {
135		panic(err)
136	}
137	// Gen batches and add them to DB
138	const nBatches = 10
139	batches := test.GenBatches(nBatches, blocks)
140	err = h.AddBatches(batches)
141	if err != nil {
142		panic(err)
143	}
144	// Gen accounts and add them to DB
145	const totalAccounts = 40
146	const userAccounts = 4
147	usrAddr := ethCommon.BigToAddress(big.NewInt(4896847))
148	privK := babyjub.NewRandPrivKey()
149	usrBjj := privK.Public()
150	accs := test.GenAccounts(totalAccounts, userAccounts, tokens, &usrAddr, usrBjj, batches)
151	err = h.AddAccounts(accs)
152	if err != nil {
153		panic(err)
154	}
155	// Gen L1Txs and add them to DB
156	const totalL1Txs = 40
157	const userL1Txs = 4
158	usrL1Txs, othrL1Txs := test.GenL1Txs(0, totalL1Txs, userL1Txs, &usrAddr, accs, tokens, blocks, batches)
159	var l1Txs []common.L1Tx
160	l1Txs = append(l1Txs, usrL1Txs...)
161	l1Txs = append(l1Txs, othrL1Txs...)
162	err = h.AddL1Txs(l1Txs)
163	if err != nil {
164		panic(err)
165	}
166	// Gen L2Txs and add them to DB
167	const totalL2Txs = 20
168	const userL2Txs = 4
169	usrL2Txs, othrL2Txs := test.GenL2Txs(totalL1Txs, totalL2Txs, userL2Txs, &usrAddr, accs, tokens, blocks, batches)
170	var l2Txs []common.L2Tx
171	l2Txs = append(l2Txs, usrL2Txs...)
172	l2Txs = append(l2Txs, othrL2Txs...)
173	err = h.AddL2Txs(l2Txs)
174	if err != nil {
175		panic(err)
176	}
177
178	// Set test commons
179	txsToAPITxs := func(l1Txs []common.L1Tx, l2Txs []common.L2Tx, blocks []common.Block, tokens []common.Token) historyTxAPIs {
180		// Transform L1Txs and L2Txs to generic Txs
181		genericTxs := []*common.Tx{}
182		for _, l1tx := range l1Txs {
183			genericTxs = append(genericTxs, l1tx.Tx())
184		}
185		for _, l2tx := range l2Txs {
186			genericTxs = append(genericTxs, l2tx.Tx())
187		}
188		// Transform generic Txs to HistoryTx
189		historyTxs := []*historydb.HistoryTx{}
190		for _, genericTx := range genericTxs {
191			// find timestamp
192			var timestamp time.Time
193			for i := 0; i < len(blocks); i++ {
194				if blocks[i].EthBlockNum == genericTx.EthBlockNum {
195					timestamp = blocks[i].Timestamp
196					break
197				}
198			}
199			// find token
200			token := common.Token{}
201			for i := 0; i < len(tokens); i++ {
202				if tokens[i].TokenID == genericTx.TokenID {
203					token = tokens[i]
204					break
205				}
206			}
207			historyTxs = append(historyTxs, &historydb.HistoryTx{
208				IsL1:            genericTx.IsL1,
209				TxID:            genericTx.TxID,
210				Type:            genericTx.Type,
211				Position:        genericTx.Position,
212				FromIdx:         genericTx.FromIdx,
213				ToIdx:           genericTx.ToIdx,
214				Amount:          genericTx.Amount,
215				AmountFloat:     genericTx.AmountFloat,
216				TokenID:         genericTx.TokenID,
217				USD:             token.USD * genericTx.AmountFloat,
218				BatchNum:        genericTx.BatchNum,
219				EthBlockNum:     genericTx.EthBlockNum,
220				ToForgeL1TxsNum: genericTx.ToForgeL1TxsNum,
221				UserOrigin:      genericTx.UserOrigin,
222				FromEthAddr:     genericTx.FromEthAddr,
223				FromBJJ:         genericTx.FromBJJ,
224				LoadAmount:      genericTx.LoadAmount,
225				LoadAmountFloat: genericTx.LoadAmountFloat,
226				LoadAmountUSD:   token.USD * genericTx.LoadAmountFloat,
227				Fee:             genericTx.Fee,
228				FeeUSD:          genericTx.Fee.Percentage() * token.USD * genericTx.AmountFloat,
229				Nonce:           genericTx.Nonce,
230				Timestamp:       timestamp,
231				TokenSymbol:     token.Symbol,
232				CurrentUSD:      token.USD * genericTx.AmountFloat,
233				USDUpdate:       token.USDUpdate,
234			})
235		}
236		return historyTxAPIs(historyTxsToAPI(historyTxs))
237	}
238	usrTxs := txsToAPITxs(usrL1Txs, usrL2Txs, blocks, tokens)
239	sort.Sort(usrTxs)
240	othrTxs := txsToAPITxs(othrL1Txs, othrL2Txs, blocks, tokens)
241	sort.Sort(othrTxs)
242	allTxs := append(usrTxs, othrTxs...)
243	sort.Sort(allTxs)
244	tc = testCommon{
245		blocks:  blocks,
246		tokens:  tokens,
247		batches: batches,
248		usrAddr: "hez:" + usrAddr.String(),
249		usrBjj:  bjjToString(usrBjj),
250		accs:    accs,
251		usrTxs:  usrTxs,
252		othrTxs: othrTxs,
253		allTxs:  allTxs,
254		router:  router,
255	}
256	// Run tests
257	result := m.Run()
258	// Stop server
259	if err := server.Shutdown(context.Background()); err != nil {
260		panic(err)
261	}
262	if err := h.Close(); err != nil {
263		panic(err)
264	}
265	if err := l2.Close(); err != nil {
266		panic(err)
267	}
268	os.Exit(result)
269}
270
271func TestGetHistoryTxs(t *testing.T) {
272	endpoint := apiURL + "transactions-history"
273	fetchedTxs := historyTxAPIs{}
274	appendIter := func(intr interface{}) {
275		for i := 0; i < len(intr.(*historyTxsAPI).Txs); i++ {
276			tmp := &historyTxAPI{}
277			if err := copier.Copy(tmp, &intr.(*historyTxsAPI).Txs[i]); err != nil {
278				panic(err)
279			}
280			fetchedTxs = append(fetchedTxs, *tmp)
281		}
282	}
283	// Get all (no filters)
284	limit := 8
285	path := fmt.Sprintf("%s?limit=%d&offset=", endpoint, limit)
286	err := doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
287	assert.NoError(t, err)
288	assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
289	// Get by ethAddr
290	fetchedTxs = historyTxAPIs{}
291	limit = 7
292	path = fmt.Sprintf(
293		"%s?hermezEthereumAddress=%s&limit=%d&offset=",
294		endpoint, tc.usrAddr, limit,
295	)
296	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
297	assert.NoError(t, err)
298	assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
299	// Get by bjj
300	fetchedTxs = historyTxAPIs{}
301	limit = 6
302	path = fmt.Sprintf(
303		"%s?BJJ=%s&limit=%d&offset=",
304		endpoint, tc.usrBjj, limit,
305	)
306	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
307	assert.NoError(t, err)
308	assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
309	// Get by tokenID
310	fetchedTxs = historyTxAPIs{}
311	limit = 5
312	tokenID := tc.allTxs[0].TokenID
313	path = fmt.Sprintf(
314		"%s?tokenId=%d&limit=%d&offset=",
315		endpoint, tokenID, limit,
316	)
317	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
318	assert.NoError(t, err)
319	tokenIDTxs := historyTxAPIs{}
320	for i := 0; i < len(tc.allTxs); i++ {
321		if tc.allTxs[i].TokenID == tokenID {
322			tokenIDTxs = append(tokenIDTxs, tc.allTxs[i])
323		}
324	}
325	assertHistoryTxAPIs(t, tokenIDTxs, fetchedTxs)
326	// idx
327	fetchedTxs = historyTxAPIs{}
328	limit = 4
329	idx := tc.allTxs[0].FromIdx
330	path = fmt.Sprintf(
331		"%s?accountIndex=%s&limit=%d&offset=",
332		endpoint, idx, limit,
333	)
334	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
335	assert.NoError(t, err)
336	idxTxs := historyTxAPIs{}
337	for i := 0; i < len(tc.allTxs); i++ {
338		if tc.allTxs[i].FromIdx == idx {
339			idxTxs = append(idxTxs, tc.allTxs[i])
340		}
341	}
342	assertHistoryTxAPIs(t, idxTxs, fetchedTxs)
343	// batchNum
344	fetchedTxs = historyTxAPIs{}
345	limit = 3
346	batchNum := tc.allTxs[0].BatchNum
347	path = fmt.Sprintf(
348		"%s?batchNum=%d&limit=%d&offset=",
349		endpoint, *batchNum, limit,
350	)
351	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
352	assert.NoError(t, err)
353	batchNumTxs := historyTxAPIs{}
354	for i := 0; i < len(tc.allTxs); i++ {
355		if tc.allTxs[i].BatchNum != nil &&
356			*tc.allTxs[i].BatchNum == *batchNum {
357			batchNumTxs = append(batchNumTxs, tc.allTxs[i])
358		}
359	}
360	assertHistoryTxAPIs(t, batchNumTxs, fetchedTxs)
361	// type
362	txTypes := []common.TxType{
363		common.TxTypeExit,
364		common.TxTypeWithdrawn,
365		common.TxTypeTransfer,
366		common.TxTypeDeposit,
367		common.TxTypeCreateAccountDeposit,
368		common.TxTypeCreateAccountDepositTransfer,
369		common.TxTypeDepositTransfer,
370		common.TxTypeForceTransfer,
371		common.TxTypeForceExit,
372		common.TxTypeTransferToEthAddr,
373		common.TxTypeTransferToBJJ,
374	}
375	for _, txType := range txTypes {
376		fetchedTxs = historyTxAPIs{}
377		limit = 2
378		path = fmt.Sprintf(
379			"%s?type=%s&limit=%d&offset=",
380			endpoint, txType, limit,
381		)
382		err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
383		assert.NoError(t, err)
384		txTypeTxs := historyTxAPIs{}
385		for i := 0; i < len(tc.allTxs); i++ {
386			if tc.allTxs[i].Type == txType {
387				txTypeTxs = append(txTypeTxs, tc.allTxs[i])
388			}
389		}
390		assertHistoryTxAPIs(t, txTypeTxs, fetchedTxs)
391	}
392	// Multiple filters
393	fetchedTxs = historyTxAPIs{}
394	limit = 1
395	path = fmt.Sprintf(
396		"%s?batchNum=%d&tokeId=%d&limit=%d&offset=",
397		endpoint, *batchNum, tokenID, limit,
398	)
399	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
400	assert.NoError(t, err)
401	mixedTxs := historyTxAPIs{}
402	for i := 0; i < len(tc.allTxs); i++ {
403		if tc.allTxs[i].BatchNum != nil {
404			if *tc.allTxs[i].BatchNum == *batchNum && tc.allTxs[i].TokenID == tokenID {
405				mixedTxs = append(mixedTxs, tc.allTxs[i])
406			}
407		}
408	}
409	assertHistoryTxAPIs(t, mixedTxs, fetchedTxs)
410	// All, in reverse order
411	fetchedTxs = historyTxAPIs{}
412	limit = 5
413	path = fmt.Sprintf("%s?", endpoint)
414	appendIterRev := func(intr interface{}) {
415		tmpAll := historyTxAPIs{}
416		for i := 0; i < len(intr.(*historyTxsAPI).Txs); i++ {
417			tmpItem := &historyTxAPI{}
418			if err := copier.Copy(tmpItem, &intr.(*historyTxsAPI).Txs[i]); err != nil {
419				panic(err)
420			}
421			tmpAll = append(tmpAll, *tmpItem)
422		}
423		fetchedTxs = append(tmpAll, fetchedTxs...)
424	}
425	err = doGoodReqPaginatedReverse(path, &historyTxsAPI{}, appendIterRev, limit)
426	assert.NoError(t, err)
427	assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
428	// 400
429	path = fmt.Sprintf(
430		"%s?accountIndex=%s&hermezEthereumAddress=%s",
431		endpoint, idx, tc.usrAddr,
432	)
433	err = doBadReq("GET", path, nil, 400)
434	assert.NoError(t, err)
435	path = fmt.Sprintf("%s?tokenId=X", endpoint)
436	err = doBadReq("GET", path, nil, 400)
437	assert.NoError(t, err)
438	// 404
439	path = fmt.Sprintf("%s?batchNum=999999", endpoint)
440	err = doBadReq("GET", path, nil, 404)
441	assert.NoError(t, err)
442	path = fmt.Sprintf("%s?limit=1000&offset=1000", endpoint)
443	err = doBadReq("GET", path, nil, 404)
444	assert.NoError(t, err)
445}
446
447func assertHistoryTxAPIs(t *testing.T, expected, actual historyTxAPIs) {
448	assert.Equal(t, len(expected), len(actual))
449	for i := 0; i < len(actual); i++ { //nolint len(actual) won't change within the loop
450		assert.Equal(t, expected[i].Timestamp.Unix(), actual[i].Timestamp.Unix())
451		expected[i].Timestamp = actual[i].Timestamp
452		assert.Equal(t, expected[i].USDUpdate.Unix(), actual[i].USDUpdate.Unix())
453		expected[i].USDUpdate = actual[i].USDUpdate
454		if expected[i].L2Info != nil {
455			if expected[i].L2Info.FeeUSD > actual[i].L2Info.FeeUSD {
456				assert.Less(t, 0.999, actual[i].L2Info.FeeUSD/expected[i].L2Info.FeeUSD)
457			} else if expected[i].L2Info.FeeUSD < actual[i].L2Info.FeeUSD {
458				assert.Less(t, 0.999, expected[i].L2Info.FeeUSD/actual[i].L2Info.FeeUSD)
459			}
460			expected[i].L2Info.FeeUSD = actual[i].L2Info.FeeUSD
461		}
462		assert.Equal(t, expected[i], actual[i])
463	}
464}
465
466func doGoodReqPaginated(
467	path string,
468	iterStruct paginationer,
469	appendIter func(res interface{}),
470) error {
471	next := 0
472	for {
473		// Call API to get this iteration items
474		if err := doGoodReq("GET", path+strconv.Itoa(next), nil, iterStruct); err != nil {
475			return err
476		}
477		appendIter(iterStruct)
478		// Keep iterating?
479		pag := iterStruct.GetPagination()
480		if pag.LastReturnedItem == pag.TotalItems-1 { // No
481			break
482		} else { // Yes
483			next = int(pag.LastReturnedItem + 1)
484		}
485	}
486	return nil
487}
488
489func doGoodReqPaginatedReverse(
490	path string,
491	iterStruct paginationer,
492	appendIter func(res interface{}),
493	limit int,
494) error {
495	next := 0
496	first := true
497	for {
498		// Call API to get this iteration items
499		if first {
500			first = false
501			pagQuery := fmt.Sprintf("last=true&limit=%d", limit)
502			if err := doGoodReq("GET", path+pagQuery, nil, iterStruct); err != nil {
503				return err
504			}
505		} else {
506			pagQuery := fmt.Sprintf("offset=%d&limit=%d", next, limit)
507			if err := doGoodReq("GET", path+pagQuery, nil, iterStruct); err != nil {
508				return err
509			}
510		}
511		appendIter(iterStruct)
512		// Keep iterating?
513		pag := iterStruct.GetPagination()
514		if iterStruct.Len() == pag.TotalItems || pag.LastReturnedItem-iterStruct.Len() == -1 { // No
515			break
516		} else { // Yes
517			prevOffset := next
518			next = pag.LastReturnedItem - iterStruct.Len() - limit + 1
519			if next < 0 {
520				next = 0
521				limit = prevOffset
522			}
523		}
524	}
525	return nil
526}
527
528func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
529	ctx := context.Background()
530	client := &http.Client{}
531	httpReq, _ := http.NewRequest(method, path, reqBody)
532	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
533	if err != nil {
534		return err
535	}
536	// Validate request against swagger spec
537	requestValidationInput := &swagger.RequestValidationInput{
538		Request:    httpReq,
539		PathParams: pathParams,
540		Route:      route,
541	}
542	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
543		return err
544	}
545	// Do API call
546	resp, err := client.Do(httpReq)
547	if err != nil {
548		return err
549	}
550	if resp.Body == nil {
551		return errors.New("Nil body")
552	}
553	//nolint
554	defer resp.Body.Close()
555	body, err := ioutil.ReadAll(resp.Body)
556	if err != nil {
557		return err
558	}
559	if resp.StatusCode != 200 {
560		return fmt.Errorf("%d response: %s", resp.StatusCode, string(body))
561	}
562	// Unmarshal body into return struct
563	if err := json.Unmarshal(body, returnStruct); err != nil {
564		return err
565	}
566	// Validate response against swagger spec
567	responseValidationInput := &swagger.ResponseValidationInput{
568		RequestValidationInput: requestValidationInput,
569		Status:                 resp.StatusCode,
570		Header:                 resp.Header,
571	}
572	responseValidationInput = responseValidationInput.SetBodyBytes(body)
573	return swagger.ValidateResponse(ctx, responseValidationInput)
574}
575
576func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
577	ctx := context.Background()
578	client := &http.Client{}
579	httpReq, _ := http.NewRequest(method, path, reqBody)
580	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
581	if err != nil {
582		return err
583	}
584	// Validate request against swagger spec
585	requestValidationInput := &swagger.RequestValidationInput{
586		Request:    httpReq,
587		PathParams: pathParams,
588		Route:      route,
589	}
590	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
591		if expectedResponseCode != 400 {
592			return err
593		}
594		log.Warn("The request does not match the API spec")
595	}
596	// Do API call
597	resp, err := client.Do(httpReq)
598	if err != nil {
599		return err
600	}
601	if resp.Body == nil {
602		return errors.New("Nil body")
603	}
604	//nolint
605	defer resp.Body.Close()
606	body, err := ioutil.ReadAll(resp.Body)
607	if err != nil {
608		return err
609	}
610	if resp.StatusCode != expectedResponseCode {
611		return fmt.Errorf("Unexpected response code: %d", resp.StatusCode)
612	}
613	// Validate response against swagger spec
614	responseValidationInput := &swagger.ResponseValidationInput{
615		RequestValidationInput: requestValidationInput,
616		Status:                 resp.StatusCode,
617		Header:                 resp.Header,
618	}
619	responseValidationInput = responseValidationInput.SetBodyBytes(body)
620	return swagger.ValidateResponse(ctx, responseValidationInput)
621}