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