arnaucube
arnaucube/hermez-nodePublic Self-hosted repository
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"
 11	"math/big"
 12	"net/http"
 13	"os"
 14	"sort"
 15	"strconv"
 16	"testing"
 17	"time"
 18
 19	ethCommon "github.com/ethereum/go-ethereum/common"
 20	swagger "github.com/getkin/kin-openapi/openapi3filter"
 21	"github.com/gin-gonic/gin"
 22	"github.com/hermeznetwork/hermez-node/common"
 23	dbUtils "github.com/hermeznetwork/hermez-node/db"
 24	"github.com/hermeznetwork/hermez-node/db/historydb"
 25	"github.com/hermeznetwork/hermez-node/db/l2db"
 26	"github.com/hermeznetwork/hermez-node/db/statedb"
 27	"github.com/hermeznetwork/hermez-node/log"
 28	"github.com/hermeznetwork/hermez-node/test"
 29	"github.com/iden3/go-iden3-crypto/babyjub"
 30	"github.com/mitchellh/copystructure"
 31	"github.com/stretchr/testify/assert"
 32	"github.com/stretchr/testify/require"
 33)
 34
 35const apiPort = ":4010"
 36const apiURL = "http://localhost" + apiPort + "/"
 37
 38type testCommon struct {
 39	blocks  []common.Block
 40	tokens  []common.Token
 41	batches []common.Batch
 42	usrAddr string
 43	usrBjj  string
 44	accs    []common.Account
 45	usrTxs  historyTxAPIs
 46	othrTxs historyTxAPIs
 47	allTxs  historyTxAPIs
 48	router  *swagger.Router
 49}
 50
 51type historyTxAPIs []historyTxAPI
 52
 53func (h historyTxAPIs) Len() int      { return len(h) }
 54func (h historyTxAPIs) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
 55func (h historyTxAPIs) Less(i, j int) bool {
 56	// i not forged yet
 57	if h[i].BatchNum == nil {
 58		if h[j].BatchNum != nil { // j is already forged
 59			return false
 60		}
 61		// Both aren't forged, is i in a smaller position?
 62		return h[i].Position < h[j].Position
 63	}
 64	// i is forged
 65	if h[j].BatchNum == nil {
 66		return true // j is not forged
 67	}
 68	// Both are forged
 69	if *h[i].BatchNum == *h[j].BatchNum {
 70		// At the same batch,  is i in a smaller position?
 71		return h[i].Position < h[j].Position
 72	}
 73	// At different batches, is i in a smaller batch?
 74	return *h[i].BatchNum < *h[j].BatchNum
 75}
 76
 77var tc testCommon
 78
 79func TestMain(m *testing.M) {
 80	// Init swagger
 81	router := swagger.NewRouter().WithSwaggerFromFile("./swagger.yml")
 82	// Init DBs
 83	// HistoryDB
 84	pass := os.Getenv("POSTGRES_PASS")
 85	db, err := dbUtils.InitSQLDB(5432, "localhost", "hermez", pass, "hermez")
 86	if err != nil {
 87		panic(err)
 88	}
 89	hdb := historydb.NewHistoryDB(db)
 90	err = hdb.Reorg(-1)
 91	if err != nil {
 92		panic(err)
 93	}
 94	// StateDB
 95	dir, err := ioutil.TempDir("", "tmpdb")
 96	if err != nil {
 97		panic(err)
 98	}
 99	sdb, err := statedb.NewStateDB(dir, false, 0)
100	if err != nil {
101		panic(err)
102	}
103	// L2DB
104	l2DB := l2db.NewL2DB(db, 10, 100, 24*time.Hour)
105	test.CleanL2DB(l2DB.DB())
106
107	// Init API
108	api := gin.Default()
109	if err := SetAPIEndpoints(
110		true,
111		true,
112		api,
113		hdb,
114		sdb,
115		l2DB,
116	); err != nil {
117		panic(err)
118	}
119	// Start server
120	server := &http.Server{Addr: apiPort, Handler: api}
121	go func() {
122		if err := server.ListenAndServe(); err != nil &&
123			err != http.ErrServerClosed {
124			panic(err)
125		}
126	}()
127
128	// Populate DBs
129	// Clean DB
130	err = h.Reorg(0)
131	if err != nil {
132		panic(err)
133	}
134	// Gen blocks and add them to DB
135	const nBlocks = 5
136	blocks := test.GenBlocks(1, nBlocks+1)
137	err = h.AddBlocks(blocks)
138	if err != nil {
139		panic(err)
140	}
141	// Gen tokens and add them to DB
142	const nTokens = 10
143	tokens := test.GenTokens(nTokens, blocks)
144	err = h.AddTokens(tokens)
145	if err != nil {
146		panic(err)
147	}
148	// Gen batches and add them to DB
149	const nBatches = 10
150	batches := test.GenBatches(nBatches, blocks)
151	err = h.AddBatches(batches)
152	if err != nil {
153		panic(err)
154	}
155	// Gen accounts and add them to DB
156	const totalAccounts = 40
157	const userAccounts = 4
158	usrAddr := ethCommon.BigToAddress(big.NewInt(4896847))
159	privK := babyjub.NewRandPrivKey()
160	usrBjj := privK.Public()
161	accs := test.GenAccounts(totalAccounts, userAccounts, tokens, &usrAddr, usrBjj, batches)
162	err = h.AddAccounts(accs)
163	if err != nil {
164		panic(err)
165	}
166	// Gen L1Txs and add them to DB
167	const totalL1Txs = 40
168	const userL1Txs = 4
169	usrL1Txs, othrL1Txs := test.GenL1Txs(256, totalL1Txs, userL1Txs, &usrAddr, accs, tokens, blocks, batches)
170	var l1Txs []common.L1Tx
171	l1Txs = append(l1Txs, usrL1Txs...)
172	l1Txs = append(l1Txs, othrL1Txs...)
173	err = h.AddL1Txs(l1Txs)
174	if err != nil {
175		panic(err)
176	}
177	// Gen L2Txs and add them to DB
178	const totalL2Txs = 20
179	const userL2Txs = 4
180	usrL2Txs, othrL2Txs := test.GenL2Txs(256+totalL1Txs, totalL2Txs, userL2Txs, &usrAddr, accs, tokens, blocks, batches)
181	var l2Txs []common.L2Tx
182	l2Txs = append(l2Txs, usrL2Txs...)
183	l2Txs = append(l2Txs, othrL2Txs...)
184	err = h.AddL2Txs(l2Txs)
185	if err != nil {
186		panic(err)
187	}
188
189	// Set test commons
190	txsToAPITxs := func(l1Txs []common.L1Tx, l2Txs []common.L2Tx, blocks []common.Block, tokens []common.Token) historyTxAPIs {
191		// Transform L1Txs and L2Txs to generic Txs
192		genericTxs := []*common.Tx{}
193		for _, l1tx := range l1Txs {
194			genericTxs = append(genericTxs, l1tx.Tx())
195		}
196		for _, l2tx := range l2Txs {
197			genericTxs = append(genericTxs, l2tx.Tx())
198		}
199		// Transform generic Txs to HistoryTx
200		historyTxs := []*historydb.HistoryTx{}
201		for _, genericTx := range genericTxs {
202			// find timestamp
203			var timestamp time.Time
204			for i := 0; i < len(blocks); i++ {
205				if blocks[i].EthBlockNum == genericTx.EthBlockNum {
206					timestamp = blocks[i].Timestamp
207					break
208				}
209			}
210			// find token
211			var token common.Token
212			if genericTx.IsL1 {
213				tokenID := genericTx.TokenID
214				found := false
215				for i := 0; i < len(tokens); i++ {
216					if tokens[i].TokenID == tokenID {
217						token = tokens[i]
218						found = true
219						break
220					}
221				}
222				if !found {
223					panic("Token not found")
224				}
225			} else {
226				token = test.GetToken(genericTx.FromIdx, accs, tokens)
227			}
228			var usd, loadUSD, feeUSD *float64
229			if token.USD != nil {
230				noDecimalsUSD := *token.USD / math.Pow(10, float64(token.Decimals))
231				usd = new(float64)
232				*usd = noDecimalsUSD * genericTx.AmountFloat
233				if genericTx.IsL1 {
234					loadUSD = new(float64)
235					*loadUSD = noDecimalsUSD * *genericTx.LoadAmountFloat
236				} else {
237					feeUSD = new(float64)
238					*feeUSD = *usd * genericTx.Fee.Percentage()
239				}
240			}
241			historyTxs = append(historyTxs, &historydb.HistoryTx{
242				IsL1:                  genericTx.IsL1,
243				TxID:                  genericTx.TxID,
244				Type:                  genericTx.Type,
245				Position:              genericTx.Position,
246				FromIdx:               genericTx.FromIdx,
247				ToIdx:                 genericTx.ToIdx,
248				Amount:                genericTx.Amount,
249				AmountFloat:           genericTx.AmountFloat,
250				HistoricUSD:           usd,
251				BatchNum:              genericTx.BatchNum,
252				EthBlockNum:           genericTx.EthBlockNum,
253				ToForgeL1TxsNum:       genericTx.ToForgeL1TxsNum,
254				UserOrigin:            genericTx.UserOrigin,
255				FromEthAddr:           genericTx.FromEthAddr,
256				FromBJJ:               genericTx.FromBJJ,
257				LoadAmount:            genericTx.LoadAmount,
258				LoadAmountFloat:       genericTx.LoadAmountFloat,
259				HistoricLoadAmountUSD: loadUSD,
260				Fee:                   genericTx.Fee,
261				HistoricFeeUSD:        feeUSD,
262				Nonce:                 genericTx.Nonce,
263				Timestamp:             timestamp,
264				TokenID:               token.TokenID,
265				TokenEthBlockNum:      token.EthBlockNum,
266				TokenEthAddr:          token.EthAddr,
267				TokenName:             token.Name,
268				TokenSymbol:           token.Symbol,
269				TokenDecimals:         token.Decimals,
270				TokenUSD:              token.USD,
271				TokenUSDUpdate:        token.USDUpdate,
272			})
273		}
274		return historyTxAPIs(historyTxsToAPI(historyTxs))
275	}
276	usrTxs := txsToAPITxs(usrL1Txs, usrL2Txs, blocks, tokens)
277	sort.Sort(usrTxs)
278	othrTxs := txsToAPITxs(othrL1Txs, othrL2Txs, blocks, tokens)
279	sort.Sort(othrTxs)
280	allTxs := append(usrTxs, othrTxs...)
281	sort.Sort(allTxs)
282	tc = testCommon{
283		blocks:  blocks,
284		tokens:  tokens,
285		batches: batches,
286		usrAddr: "hez:" + usrAddr.String(),
287		usrBjj:  bjjToString(usrBjj),
288		accs:    accs,
289		usrTxs:  usrTxs,
290		othrTxs: othrTxs,
291		allTxs:  allTxs,
292		router:  router,
293	}
294	// Run tests
295	result := m.Run()
296	// Stop server
297	if err := server.Shutdown(context.Background()); err != nil {
298		panic(err)
299	}
300	if err := db.Close(); err != nil {
301		panic(err)
302	}
303	os.Exit(result)
304}
305
306func TestGetHistoryTxs(t *testing.T) {
307	endpoint := apiURL + "transactions-history"
308	fetchedTxs := historyTxAPIs{}
309	appendIter := func(intr interface{}) {
310		for i := 0; i < len(intr.(*historyTxsAPI).Txs); i++ {
311			tmp, err := copystructure.Copy(intr.(*historyTxsAPI).Txs[i])
312			if err != nil {
313				panic(err)
314			}
315			fetchedTxs = append(fetchedTxs, tmp.(historyTxAPI))
316		}
317	}
318	// Get all (no filters)
319	limit := 8
320	path := fmt.Sprintf("%s?limit=%d&offset=", endpoint, limit)
321	err := doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
322	assert.NoError(t, err)
323	assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
324	// Get by ethAddr
325	fetchedTxs = historyTxAPIs{}
326	limit = 7
327	path = fmt.Sprintf(
328		"%s?hermezEthereumAddress=%s&limit=%d&offset=",
329		endpoint, tc.usrAddr, limit,
330	)
331	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
332	assert.NoError(t, err)
333	assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
334	// Get by bjj
335	fetchedTxs = historyTxAPIs{}
336	limit = 6
337	path = fmt.Sprintf(
338		"%s?BJJ=%s&limit=%d&offset=",
339		endpoint, tc.usrBjj, limit,
340	)
341	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
342	assert.NoError(t, err)
343	assertHistoryTxAPIs(t, tc.usrTxs, fetchedTxs)
344	// Get by tokenID
345	fetchedTxs = historyTxAPIs{}
346	limit = 5
347	tokenID := tc.allTxs[0].Token.TokenID
348	path = fmt.Sprintf(
349		"%s?tokenId=%d&limit=%d&offset=",
350		endpoint, tokenID, limit,
351	)
352	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
353	assert.NoError(t, err)
354	tokenIDTxs := historyTxAPIs{}
355	for i := 0; i < len(tc.allTxs); i++ {
356		if tc.allTxs[i].Token.TokenID == tokenID {
357			tokenIDTxs = append(tokenIDTxs, tc.allTxs[i])
358		}
359	}
360	assertHistoryTxAPIs(t, tokenIDTxs, fetchedTxs)
361	// idx
362	fetchedTxs = historyTxAPIs{}
363	limit = 4
364	idx := tc.allTxs[0].FromIdx
365	path = fmt.Sprintf(
366		"%s?accountIndex=%s&limit=%d&offset=",
367		endpoint, idx, limit,
368	)
369	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
370	assert.NoError(t, err)
371	idxTxs := historyTxAPIs{}
372	for i := 0; i < len(tc.allTxs); i++ {
373		if tc.allTxs[i].FromIdx[6:] == idx[6:] {
374			idxTxs = append(idxTxs, tc.allTxs[i])
375		}
376	}
377	assertHistoryTxAPIs(t, idxTxs, fetchedTxs)
378	// batchNum
379	fetchedTxs = historyTxAPIs{}
380	limit = 3
381	batchNum := tc.allTxs[0].BatchNum
382	path = fmt.Sprintf(
383		"%s?batchNum=%d&limit=%d&offset=",
384		endpoint, *batchNum, limit,
385	)
386	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
387	assert.NoError(t, err)
388	batchNumTxs := historyTxAPIs{}
389	for i := 0; i < len(tc.allTxs); i++ {
390		if tc.allTxs[i].BatchNum != nil &&
391			*tc.allTxs[i].BatchNum == *batchNum {
392			batchNumTxs = append(batchNumTxs, tc.allTxs[i])
393		}
394	}
395	assertHistoryTxAPIs(t, batchNumTxs, fetchedTxs)
396	// type
397	txTypes := []common.TxType{
398		common.TxTypeExit,
399		common.TxTypeWithdrawn,
400		common.TxTypeTransfer,
401		common.TxTypeDeposit,
402		common.TxTypeCreateAccountDeposit,
403		common.TxTypeCreateAccountDepositTransfer,
404		common.TxTypeDepositTransfer,
405		common.TxTypeForceTransfer,
406		common.TxTypeForceExit,
407		common.TxTypeTransferToEthAddr,
408		common.TxTypeTransferToBJJ,
409	}
410	for _, txType := range txTypes {
411		fetchedTxs = historyTxAPIs{}
412		limit = 2
413		path = fmt.Sprintf(
414			"%s?type=%s&limit=%d&offset=",
415			endpoint, txType, limit,
416		)
417		err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
418		assert.NoError(t, err)
419		txTypeTxs := historyTxAPIs{}
420		for i := 0; i < len(tc.allTxs); i++ {
421			if tc.allTxs[i].Type == txType {
422				txTypeTxs = append(txTypeTxs, tc.allTxs[i])
423			}
424		}
425		assertHistoryTxAPIs(t, txTypeTxs, fetchedTxs)
426	}
427	// Multiple filters
428	fetchedTxs = historyTxAPIs{}
429	limit = 1
430	path = fmt.Sprintf(
431		"%s?batchNum=%d&tokeId=%d&limit=%d&offset=",
432		endpoint, *batchNum, tokenID, limit,
433	)
434	err = doGoodReqPaginated(path, &historyTxsAPI{}, appendIter)
435	assert.NoError(t, err)
436	mixedTxs := historyTxAPIs{}
437	for i := 0; i < len(tc.allTxs); i++ {
438		if tc.allTxs[i].BatchNum != nil {
439			if *tc.allTxs[i].BatchNum == *batchNum && tc.allTxs[i].Token.TokenID == tokenID {
440				mixedTxs = append(mixedTxs, tc.allTxs[i])
441			}
442		}
443	}
444	assertHistoryTxAPIs(t, mixedTxs, fetchedTxs)
445	// All, in reverse order
446	fetchedTxs = historyTxAPIs{}
447	limit = 5
448	path = fmt.Sprintf("%s?", endpoint)
449	appendIterRev := func(intr interface{}) {
450		tmpAll := historyTxAPIs{}
451		for i := 0; i < len(intr.(*historyTxsAPI).Txs); i++ {
452			tmp, err := copystructure.Copy(intr.(*historyTxsAPI).Txs[i])
453			if err != nil {
454				panic(err)
455			}
456			tmpAll = append(tmpAll, tmp.(historyTxAPI))
457		}
458		fetchedTxs = append(tmpAll, fetchedTxs...)
459	}
460	err = doGoodReqPaginatedReverse(path, &historyTxsAPI{}, appendIterRev, limit)
461	assert.NoError(t, err)
462	assertHistoryTxAPIs(t, tc.allTxs, fetchedTxs)
463	// 400
464	path = fmt.Sprintf(
465		"%s?accountIndex=%s&hermezEthereumAddress=%s",
466		endpoint, idx, tc.usrAddr,
467	)
468	err = doBadReq("GET", path, nil, 400)
469	assert.NoError(t, err)
470	path = fmt.Sprintf("%s?tokenId=X", endpoint)
471	err = doBadReq("GET", path, nil, 400)
472	assert.NoError(t, err)
473	// 404
474	path = fmt.Sprintf("%s?batchNum=999999", endpoint)
475	err = doBadReq("GET", path, nil, 404)
476	assert.NoError(t, err)
477	path = fmt.Sprintf("%s?limit=1000&offset=1000", endpoint)
478	err = doBadReq("GET", path, nil, 404)
479	assert.NoError(t, err)
480}
481
482func assertHistoryTxAPIs(t *testing.T, expected, actual historyTxAPIs) {
483	require.Equal(t, len(expected), len(actual))
484	for i := 0; i < len(actual); i++ { //nolint len(actual) won't change within the loop
485		assert.Equal(t, expected[i].Timestamp.Unix(), actual[i].Timestamp.Unix())
486		expected[i].Timestamp = actual[i].Timestamp
487		if expected[i].Token.USDUpdate == nil {
488			assert.Equal(t, expected[i].Token.USDUpdate, actual[i].Token.USDUpdate)
489		} else {
490			assert.Equal(t, expected[i].Token.USDUpdate.Unix(), actual[i].Token.USDUpdate.Unix())
491			expected[i].Token.USDUpdate = actual[i].Token.USDUpdate
492		}
493		test.AssertUSD(t, expected[i].HistoricUSD, actual[i].HistoricUSD)
494		if expected[i].L2Info != nil {
495			test.AssertUSD(t, expected[i].L2Info.HistoricFeeUSD, actual[i].L2Info.HistoricFeeUSD)
496		} else {
497			test.AssertUSD(t, expected[i].L1Info.HistoricLoadAmountUSD, actual[i].L1Info.HistoricLoadAmountUSD)
498		}
499		assert.Equal(t, expected[i], actual[i])
500	}
501}
502
503func doGoodReqPaginated(
504	path string,
505	iterStruct paginationer,
506	appendIter func(res interface{}),
507) error {
508	next := 0
509	for {
510		// Call API to get this iteration items
511		if err := doGoodReq("GET", path+strconv.Itoa(next), nil, iterStruct); err != nil {
512			return err
513		}
514		appendIter(iterStruct)
515		// Keep iterating?
516		pag := iterStruct.GetPagination()
517		if pag.LastReturnedItem == pag.TotalItems-1 { // No
518			break
519		} else { // Yes
520			next = int(pag.LastReturnedItem + 1)
521		}
522	}
523	return nil
524}
525
526func doGoodReqPaginatedReverse(
527	path string,
528	iterStruct paginationer,
529	appendIter func(res interface{}),
530	limit int,
531) error {
532	next := 0
533	first := true
534	for {
535		// Call API to get this iteration items
536		if first {
537			first = false
538			pagQuery := fmt.Sprintf("last=true&limit=%d", limit)
539			if err := doGoodReq("GET", path+pagQuery, nil, iterStruct); err != nil {
540				return err
541			}
542		} else {
543			pagQuery := fmt.Sprintf("offset=%d&limit=%d", next, limit)
544			if err := doGoodReq("GET", path+pagQuery, nil, iterStruct); err != nil {
545				return err
546			}
547		}
548		appendIter(iterStruct)
549		// Keep iterating?
550		pag := iterStruct.GetPagination()
551		if iterStruct.Len() == pag.TotalItems || pag.LastReturnedItem-iterStruct.Len() == -1 { // No
552			break
553		} else { // Yes
554			prevOffset := next
555			next = pag.LastReturnedItem - iterStruct.Len() - limit + 1
556			if next < 0 {
557				next = 0
558				limit = prevOffset
559			}
560		}
561	}
562	return nil
563}
564
565func doGoodReq(method, path string, reqBody io.Reader, returnStruct interface{}) error {
566	ctx := context.Background()
567	client := &http.Client{}
568	httpReq, _ := http.NewRequest(method, path, reqBody)
569	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
570	if err != nil {
571		return err
572	}
573	// Validate request against swagger spec
574	requestValidationInput := &swagger.RequestValidationInput{
575		Request:    httpReq,
576		PathParams: pathParams,
577		Route:      route,
578	}
579	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
580		return err
581	}
582	// Do API call
583	resp, err := client.Do(httpReq)
584	if err != nil {
585		return err
586	}
587	if resp.Body == nil {
588		return errors.New("Nil body")
589	}
590	//nolint
591	defer resp.Body.Close()
592	body, err := ioutil.ReadAll(resp.Body)
593	if err != nil {
594		return err
595	}
596	if resp.StatusCode != 200 {
597		return fmt.Errorf("%d response: %s", resp.StatusCode, string(body))
598	}
599	// Unmarshal body into return struct
600	if err := json.Unmarshal(body, returnStruct); err != nil {
601		return err
602	}
603	// Validate response against swagger spec
604	responseValidationInput := &swagger.ResponseValidationInput{
605		RequestValidationInput: requestValidationInput,
606		Status:                 resp.StatusCode,
607		Header:                 resp.Header,
608	}
609	responseValidationInput = responseValidationInput.SetBodyBytes(body)
610	return swagger.ValidateResponse(ctx, responseValidationInput)
611}
612
613func doBadReq(method, path string, reqBody io.Reader, expectedResponseCode int) error {
614	ctx := context.Background()
615	client := &http.Client{}
616	httpReq, _ := http.NewRequest(method, path, reqBody)
617	route, pathParams, err := tc.router.FindRoute(httpReq.Method, httpReq.URL)
618	if err != nil {
619		return err
620	}
621	// Validate request against swagger spec
622	requestValidationInput := &swagger.RequestValidationInput{
623		Request:    httpReq,
624		PathParams: pathParams,
625		Route:      route,
626	}
627	if err := swagger.ValidateRequest(ctx, requestValidationInput); err != nil {
628		if expectedResponseCode != 400 {
629			return err
630		}
631		log.Warn("The request does not match the API spec")
632	}
633	// Do API call
634	resp, err := client.Do(httpReq)
635	if err != nil {
636		return err
637	}
638	if resp.Body == nil {
639		return errors.New("Nil body")
640	}
641	//nolint
642	defer resp.Body.Close()
643	body, err := ioutil.ReadAll(resp.Body)
644	if err != nil {
645		return err
646	}
647	if resp.StatusCode != expectedResponseCode {
648		return fmt.Errorf("Unexpected response code: %d", resp.StatusCode)
649	}
650	// Validate response against swagger spec
651	responseValidationInput := &swagger.ResponseValidationInput{
652		RequestValidationInput: requestValidationInput,
653		Status:                 resp.StatusCode,
654		Header:                 resp.Header,
655	}
656	responseValidationInput = responseValidationInput.SetBodyBytes(body)
657	return swagger.ValidateResponse(ctx, responseValidationInput)
658}