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