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