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