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