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