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