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 auctionVars := common.AuctionVariables{
288 BootCoordinator: ethCommon.HexToAddress("0x1111111111111111111111111111111111111111"),
289 ClosedAuctionSlots: uint16(2),
290 OpenAuctionSlots: uint16(5),
291 }
292
293 rollupVars := common.RollupVariables{
294 WithdrawalDelay: uint64(3000),
295 }
296
297 wdelayerVars := common.WDelayerVariables{
298 WithdrawalDelay: uint64(3000),
299 }
300
301 err = api.h.AddAuctionVars(&auctionVars)
302 if err != nil {
303 panic(err)
304 }
305
306 const nSlots = 20
307
308 // Set testCommon
309 usrTxs, allTxs := genTestTxs(sortedTxs, usrIdxs, accs, tokensUSD, blocks)
310 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.
311 testBatches, fullBatches := genTestBatches(blocks, batches, allTxs)
312 usrExits, allExits := genTestExits(exits, tokensUSD, accs, usrIdxs)
313 tc = testCommon{
314 blocks: blocks,
315 tokens: tokensUSD,
316 batches: testBatches,
317 fullBatches: fullBatches,
318 coordinators: coordinators,
319 accounts: genTestAccounts(accs, tokensUSD),
320 usrAddr: ethAddrToHez(usrAddr),
321 usrBjj: bjjToString(usrBjj),
322 accs: accs,
323 usrTxs: usrTxs,
324 allTxs: allTxs,
325 exits: allExits,
326 usrExits: usrExits,
327 poolTxsToSend: poolTxsToSend,
328 poolTxsToReceive: poolTxsToReceive,
329 auths: genTestAuths(test.GenAuths(5)),
330 router: router,
331 bids: testBids,
332 slots: api.genTestSlots(nSlots, lastBlockNum, testBids, auctionVars),
333 auctionVars: auctionVars,
334 rollupVars: rollupVars,
335 wdelayerVars: wdelayerVars,
336 }
337
338 // Fake server
339 if os.Getenv("FAKE_SERVER") == "yes" {
340 for {
341 log.Info("Running fake server at " + apiURL + " until ^C is received")
342 time.Sleep(30 * time.Second)
343 }
344 }
345 // Run tests
346 result := m.Run()
347 // Stop server
348 if err := server.Shutdown(context.Background()); err != nil {
349 panic(err)
350 }
351 if err := database.Close(); err != nil {
352 panic(err)
353 }
354 if err := os.RemoveAll(dir); err != nil {
355 panic(err)
356 }
357 os.Exit(result)
358}
359
360func doGoodReqPaginated(
361 path, order string,
362 iterStruct Pendinger,
363 appendIter func(res interface{}),
364) error {
365 var next uint64
366 firstIte := true
367 expectedTotal := 0
368 totalReceived := 0
369 for {
370 // Calculate fromItem
371 iterPath := path
372 if firstIte {
373 if order == historydb.OrderDesc {
374 // Fetch first item in reverse order
375 iterPath += "99999" // Asumption that for testing there won't be any itemID > 99999
376 } else {
377 iterPath += "0"
378 }
379 } else {
380 iterPath += strconv.Itoa(int(next))
381 }
382 // Call API to get this iteration items
383 if err := doGoodReq("GET", iterPath+"&order="+order, nil, iterStruct); err != nil {
384 return err
385 }
386 appendIter(iterStruct)
387 // Keep iterating?
388 remaining, lastID := iterStruct.GetPending()
389 if remaining == 0 {
390 break
391 }
392 if order == historydb.OrderDesc {
393 next = lastID - 1
394 } else {
395 next = lastID + 1
396 }
397 // Check that the expected amount of items is consistent across iterations
398 totalReceived += iterStruct.Len()
399 if firstIte {
400 firstIte = false
401 expectedTotal = totalReceived + int(remaining)
402 }
403 if expectedTotal != totalReceived+int(remaining) {
404 panic(fmt.Sprintf(
405 "pagination error, totalReceived + remaining should be %d, but is %d",
406 expectedTotal, totalReceived+int(remaining),
407 ))
408 }
409 }
410 return nil
411}
412
413func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
414 ctx := context.Background()
415 client := &http.Client{}
416 httpReq, err := http.NewRequest(method, path, reqBody)
417 if err != nil {
418 return err
419 }
420 if reqBody != nil {
421 httpReq.Header.Add("Content-Type", "application/json")
422 }
423 route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
424 if err != nil {
425 return err
426 }
427 // Validate request against swagger spec
428 requestValidationInput := &swagger.RequestValidationInput{
429 Request: httpReq,
430 PathParams: pathParams,
431 Route: route,
432 }
433 if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
434 return err
435 }
436 // Do API call
437 resp, err := client.Do(httpReq)
438 if err != nil {
439 return err
440 }
441 if resp.Body == nil && returnStruct != nil {
442 return errors.New("Nil body")
443 }
444 //nolint
445 defer resp.Body.Close()
446 body, err := ioutil.ReadAll(resp.Body)
447 if err != nil {
448 return err
449 }
450 if resp.StatusCode != 200 {
451 return fmt.Errorf("%d response. Body: %s", resp.StatusCode, string(body))
452 }
453 if returnStruct == nil {
454 return nil
455 }
456 // Unmarshal body into return struct
457 if err := json.Unmarshal(body, returnStruct); err != nil {
458 log.Error("invalid json: " + string(body))
459 return err
460 }
461 // Validate response against swagger spec
462 responseValidationInput := &swagger.ResponseValidationInput{
463 RequestValidationInput: requestValidationInput,
464 Status: resp.StatusCode,
465 Header: resp.Header,
466 }
467 responseValidationInput = responseValidationInput.SetBodyBytes(body)
468 return swagger.ValidateResponse(ctx, responseValidationInput)
469}
470
471func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
472 ctx := context.Background()
473 client := &http.Client{}
474 httpReq, _ := http.NewRequest(method, path, reqBody)
475 route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
476 if err != nil {
477 return err
478 }
479 // Validate request against swagger spec
480 requestValidationInput := &swagger.RequestValidationInput{
481 Request: httpReq,
482 PathParams: pathParams,
483 Route: route,
484 }
485 if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
486 if expectedResponseCode != 400 {
487 return err
488 }
489 log.Warn("The request does not match the API spec")
490 }
491 // Do API call
492 resp, err := client.Do(httpReq)
493 if err != nil {
494 return err
495 }
496 if resp.Body == nil {
497 return errors.New("Nil body")
498 }
499 //nolint
500 defer resp.Body.Close()
501 body, err := ioutil.ReadAll(resp.Body)
502 if err != nil {
503 return err
504 }
505 if resp.StatusCode != expectedResponseCode {
506 return fmt.Errorf("Unexpected response code: %d. Body: %s", resp.StatusCode, string(body))
507 }
508 // Validate response against swagger spec
509 responseValidationInput := &swagger.ResponseValidationInput{
510 RequestValidationInput: requestValidationInput,
511 Status: resp.StatusCode,
512 Header: resp.Header,
513 }
514 responseValidationInput = responseValidationInput.SetBodyBytes(body)
515 return swagger.ValidateResponse(ctx, responseValidationInput)
516}
517
518// test helpers
519
520func getTimestamp(blockNum int64, blocks []common.Block) time.Time {
521 for i := 0; i < len(blocks); i++ {
522 if blocks[i].EthBlockNum == blockNum {
523 return blocks[i].Timestamp
524 }
525 }
526 panic("timesamp not found")
527}
528
529func getTokenByID(id common.TokenID, tokens []historydb.TokenWithUSD) historydb.TokenWithUSD {
530 for i := 0; i < len(tokens); i++ {
531 if tokens[i].TokenID == id {
532 return tokens[i]
533 }
534 }
535 panic("token not found")
536}
537
538func getTokenByIdx(idx common.Idx, tokens []historydb.TokenWithUSD, accs []common.Account) historydb.TokenWithUSD {
539 for _, acc := range accs {
540 if idx == acc.Idx {
541 return getTokenByID(acc.TokenID, tokens)
542 }
543 }
544 panic("token not found")
545}
546
547func getAccountByIdx(idx common.Idx, accs []common.Account) *common.Account {
548 for _, acc := range accs {
549 if acc.Idx == idx {
550 return &acc
551 }
552 }
553 panic("account not found")
554}
555
556func getBlockByNum(ethBlockNum int64, blocks []common.Block) common.Block {
557 for _, b := range blocks {
558 if b.EthBlockNum == ethBlockNum {
559 return b
560 }
561 }
562 panic("block not found")
563}
564
565func getCoordinatorByBidder(bidder ethCommon.Address, coordinators []historydb.CoordinatorAPI) historydb.CoordinatorAPI {
566 for _, c := range coordinators {
567 if c.Bidder == bidder {
568 return c
569 }
570 }
571 panic("coordinator not found")
572}