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