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