mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 23:16:45 +00:00
* Initial implementation of HTTP mock testing framework Convert to VCR testing server. Segregate live testing via build tags. Converted Binance to VCR server Convert Bitstamp to VCR mocking tests Added VCR mock testing for localbitcoins * Add server generation for concurrent testing * Fix linter issues * Fix linter issue * fix race - potentially * revert auto assigning of host vals * Fix requested changes * Adds mock testing for ANX Switch to using TestMain functionality Added cron job usage for travis-ci to live testing Added appveyor scheduled build check for live testing * WOOPS * silly correction * Fixes fantastic linter issues * fixed another whoopsie * WOOO! * Adds gemini mock testing with additional fixes * Add docs and sharedvalue * Added tls using httptest package * Fixed issues * added explicit mock recording reference to error * Fix requested changes * strip port from mock files as they are not needed on tls server * Change incorrect names * fix requested changes * lbank update * Fix another issue * Updated readme
440 lines
10 KiB
Go
440 lines
10 KiB
Go
package mock
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"log"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/thrasher-corp/gocryptotrader/common"
|
|
)
|
|
|
|
// HTTPResponse defines expected response from the end point including request
|
|
// data for pathing on the VCR server
|
|
type HTTPResponse struct {
|
|
Data json.RawMessage `json:"data"`
|
|
QueryString string `json:"queryString"`
|
|
BodyParams string `json:"bodyParams"`
|
|
Headers map[string][]string `json:"headers"`
|
|
}
|
|
|
|
// HTTPRecord will record the request and response to a default JSON file for
|
|
// mocking purposes
|
|
func HTTPRecord(res *http.Response, service string, respContents []byte) error {
|
|
if res == nil {
|
|
return errors.New("http.Response cannot be nil")
|
|
}
|
|
|
|
if res.Request == nil {
|
|
return errors.New("http.Request cannot be nil")
|
|
}
|
|
|
|
if res.Request.Method == "" {
|
|
return errors.New("request method not supplied")
|
|
}
|
|
|
|
if service == "" {
|
|
return errors.New("service not supplied cannot access correct mock file")
|
|
}
|
|
service = strings.ToLower(service)
|
|
|
|
fileout := filepath.Join(DefaultDirectory, service, service+".json")
|
|
|
|
contents, err := common.ReadFile(fileout)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var m VCRMock
|
|
err = json.Unmarshal(contents, &m)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if m.Routes == nil {
|
|
m.Routes = make(map[string]map[string][]HTTPResponse)
|
|
}
|
|
|
|
var httpResponse HTTPResponse
|
|
cleanedContents, err := CheckResponsePayload(respContents)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = json.Unmarshal(cleanedContents, &httpResponse.Data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var body string
|
|
if res.Request.GetBody != nil {
|
|
bodycopy, bodyErr := res.Request.GetBody()
|
|
if bodyErr != nil {
|
|
return bodyErr
|
|
}
|
|
payload, bodyErr := ioutil.ReadAll(bodycopy)
|
|
if bodyErr != nil {
|
|
return bodyErr
|
|
}
|
|
body = string(payload)
|
|
}
|
|
|
|
switch res.Request.Header.Get(contentType) {
|
|
case applicationURLEncoded:
|
|
vals, urlErr := url.ParseQuery(body)
|
|
if urlErr != nil {
|
|
return urlErr
|
|
}
|
|
|
|
httpResponse.BodyParams, urlErr = GetFilteredURLVals(vals)
|
|
if urlErr != nil {
|
|
return urlErr
|
|
}
|
|
|
|
case textPlain:
|
|
payload := res.Request.Header.Get("X-Gemini-Payload")
|
|
j, dErr := common.Base64Decode(payload)
|
|
if dErr != nil {
|
|
return dErr
|
|
}
|
|
|
|
httpResponse.BodyParams = string(j)
|
|
|
|
default:
|
|
httpResponse.BodyParams = body
|
|
}
|
|
|
|
httpResponse.Headers, err = GetFilteredHeader(res)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
httpResponse.QueryString, err = GetFilteredURLVals(res.Request.URL.Query())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, ok := m.Routes[res.Request.URL.Path]
|
|
if !ok {
|
|
m.Routes[res.Request.URL.Path] = make(map[string][]HTTPResponse)
|
|
m.Routes[res.Request.URL.Path][res.Request.Method] = []HTTPResponse{httpResponse}
|
|
} else {
|
|
mockResponses, ok := m.Routes[res.Request.URL.Path][res.Request.Method]
|
|
if !ok {
|
|
m.Routes[res.Request.URL.Path][res.Request.Method] = []HTTPResponse{httpResponse}
|
|
} else {
|
|
switch res.Request.Method { // Based off method - check add or replace
|
|
case http.MethodGet:
|
|
for i := range mockResponses {
|
|
mockQuery, urlErr := url.ParseQuery(mockResponses[i].QueryString)
|
|
if urlErr != nil {
|
|
return urlErr
|
|
}
|
|
|
|
if MatchURLVals(mockQuery, res.Request.URL.Query()) {
|
|
mockResponses = append(mockResponses[:i], mockResponses[i+1:]...) // Delete Old
|
|
break
|
|
}
|
|
}
|
|
|
|
case http.MethodPost:
|
|
for i := range mockResponses {
|
|
cType, ok := mockResponses[i].Headers[contentType]
|
|
if !ok {
|
|
return errors.New("cannot find content type within mock responses")
|
|
}
|
|
|
|
jCType := strings.Join(cType, "")
|
|
var found bool
|
|
switch jCType {
|
|
case applicationURLEncoded:
|
|
respQueryVals, urlErr := url.ParseQuery(body)
|
|
if urlErr != nil {
|
|
return urlErr
|
|
}
|
|
|
|
mockRespVals, urlErr := url.ParseQuery(mockResponses[i].BodyParams)
|
|
if urlErr != nil {
|
|
log.Fatal(urlErr)
|
|
}
|
|
|
|
if MatchURLVals(respQueryVals, mockRespVals) {
|
|
// if found will delete instance and overwrite with new
|
|
// data
|
|
mockResponses = append(mockResponses[:i], mockResponses[i+1:]...)
|
|
found = true
|
|
}
|
|
|
|
case applicationJSON, textPlain:
|
|
reqVals, jErr := DeriveURLValsFromJSONMap([]byte(body))
|
|
if jErr != nil {
|
|
return jErr
|
|
}
|
|
|
|
mockVals, jErr := DeriveURLValsFromJSONMap([]byte(mockResponses[i].BodyParams))
|
|
if jErr != nil {
|
|
return jErr
|
|
}
|
|
|
|
if MatchURLVals(reqVals, mockVals) {
|
|
// if found will delete instance and overwrite with new
|
|
// data
|
|
mockResponses = append(mockResponses[:i], mockResponses[i+1:]...)
|
|
found = true
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("unhandled content type %s", jCType)
|
|
}
|
|
if found {
|
|
break
|
|
}
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("unhandled request method %s", res.Request.Method)
|
|
}
|
|
|
|
m.Routes[res.Request.URL.Path][res.Request.Method] = append(mockResponses, httpResponse)
|
|
}
|
|
}
|
|
|
|
payload, err := json.MarshalIndent(m, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return common.WriteFile(fileout, payload)
|
|
}
|
|
|
|
// GetFilteredHeader filters excluded http headers for insertion into a mock
|
|
// test file
|
|
func GetFilteredHeader(res *http.Response) (http.Header, error) {
|
|
items, err := GetExcludedItems()
|
|
if err != nil {
|
|
return res.Header, err
|
|
}
|
|
|
|
for i := range items.Headers {
|
|
if res.Request.Header.Get(items.Headers[i]) != "" {
|
|
res.Request.Header.Set(items.Headers[i], "")
|
|
}
|
|
}
|
|
|
|
return res.Request.Header, nil
|
|
}
|
|
|
|
// GetFilteredURLVals filters excluded url value variables for insertion into a
|
|
// mock test file
|
|
func GetFilteredURLVals(vals url.Values) (string, error) {
|
|
items, err := GetExcludedItems()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
for key, val := range vals {
|
|
for i := range items.Variables {
|
|
if strings.EqualFold(items.Variables[i], val[0]) {
|
|
vals.Set(key, "")
|
|
}
|
|
}
|
|
}
|
|
return vals.Encode(), nil
|
|
}
|
|
|
|
// CheckResponsePayload checks to see if there are any response body variables
|
|
// that should not be there.
|
|
func CheckResponsePayload(data []byte) ([]byte, error) {
|
|
items, err := GetExcludedItems()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var intermediary interface{}
|
|
err = json.Unmarshal(data, &intermediary)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
payload, err := CheckJSON(intermediary, &items)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return json.MarshalIndent(payload, "", " ")
|
|
}
|
|
|
|
// Reflection consts
|
|
const (
|
|
Int64 = "int64"
|
|
Float64 = "float64"
|
|
Slice = "slice"
|
|
String = "string"
|
|
Bool = "bool"
|
|
Invalid = "invalid"
|
|
)
|
|
|
|
// CheckJSON recursively parses json data to retract keywords, quite intensive.
|
|
func CheckJSON(data interface{}, excluded *Exclusion) (interface{}, error) {
|
|
var context map[string]interface{}
|
|
if reflect.TypeOf(data).String() == "[]interface {}" {
|
|
var sData []interface{}
|
|
for i := range data.([]interface{}) {
|
|
checkedData, err := CheckJSON(data.([]interface{})[i], excluded)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
sData = append(sData, checkedData)
|
|
}
|
|
return sData, nil
|
|
}
|
|
|
|
conv, err := json.Marshal(data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = json.Unmarshal(conv, &context)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(context) == 0 {
|
|
// Nil for some reason, should error out before in json.Unmarshal
|
|
return nil, nil
|
|
}
|
|
|
|
for key, val := range context {
|
|
switch reflect.ValueOf(val).Kind().String() {
|
|
case String:
|
|
if IsExcluded(key, excluded.Variables) {
|
|
context[key] = "" // Zero val string
|
|
}
|
|
case Int64:
|
|
if IsExcluded(key, excluded.Variables) {
|
|
context[key] = 0 // Zero val int
|
|
}
|
|
case Float64:
|
|
if IsExcluded(key, excluded.Variables) {
|
|
context[key] = 0.0 // Zero val float
|
|
}
|
|
case Slice:
|
|
slice := val.([]interface{})
|
|
if len(slice) < 1 {
|
|
// Empty slice found
|
|
context[key] = slice
|
|
} else {
|
|
if _, ok := slice[0].(map[string]interface{}); ok {
|
|
var cleanSlice []interface{}
|
|
for i := range slice {
|
|
cleanMap, sErr := CheckJSON(slice[i], excluded)
|
|
if sErr != nil {
|
|
return nil, sErr
|
|
}
|
|
cleanSlice = append(cleanSlice, cleanMap)
|
|
}
|
|
context[key] = cleanSlice
|
|
} else if IsExcluded(key, excluded.Variables) {
|
|
context[key] = nil // Zero val slice
|
|
}
|
|
}
|
|
|
|
case Bool, Invalid: // Skip these bad boys for now
|
|
default:
|
|
// Recursively check map data
|
|
contextValue, err := CheckJSON(val, excluded)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
context[key] = contextValue
|
|
}
|
|
}
|
|
|
|
return context, nil
|
|
}
|
|
|
|
// IsExcluded cross references the key with the excluded variables
|
|
func IsExcluded(key string, excludedVars []string) bool {
|
|
for i := range excludedVars {
|
|
if strings.EqualFold(key, excludedVars[i]) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
var excludedList Exclusion
|
|
var m sync.Mutex
|
|
var set bool
|
|
var exclusionFile = DefaultDirectory + "exclusion.json"
|
|
|
|
var defaultExcludedHeaders = []string{"Key",
|
|
"X-Mbx-Apikey",
|
|
"Rest-Key",
|
|
"Apiauth-Key"}
|
|
var defaultExcludedVariables = []string{"bsb",
|
|
"user",
|
|
"name",
|
|
"real_name",
|
|
"receiver_name",
|
|
"account_number",
|
|
"username"}
|
|
|
|
// Exclusion defines a list of items to be excluded from the main mock output
|
|
// this attempts a catch all approach and needs to be updated per exchange basis
|
|
type Exclusion struct {
|
|
Headers []string `json:"headers"`
|
|
Variables []string `json:"variables"`
|
|
}
|
|
|
|
// GetExcludedItems checks to see if the variable is in the exclusion list as to
|
|
// not display secure items in mock file generator output
|
|
func GetExcludedItems() (Exclusion, error) {
|
|
m.Lock()
|
|
defer m.Unlock()
|
|
if !set {
|
|
file, err := ioutil.ReadFile(exclusionFile)
|
|
if err != nil {
|
|
if !strings.Contains(err.Error(), "no such file or directory") {
|
|
return excludedList, err
|
|
}
|
|
|
|
excludedList.Headers = defaultExcludedHeaders
|
|
excludedList.Variables = defaultExcludedVariables
|
|
|
|
data, mErr := json.MarshalIndent(excludedList, "", " ")
|
|
if mErr != nil {
|
|
return excludedList, mErr
|
|
}
|
|
|
|
mErr = ioutil.WriteFile(exclusionFile, data, os.ModePerm)
|
|
if mErr != nil {
|
|
return excludedList, mErr
|
|
}
|
|
|
|
} else {
|
|
err = json.Unmarshal(file, &excludedList)
|
|
if err != nil {
|
|
return excludedList, err
|
|
}
|
|
|
|
if len(excludedList.Headers) == 0 || len(excludedList.Variables) == 0 {
|
|
return excludedList, errors.New("exclusion list does not have names")
|
|
}
|
|
}
|
|
|
|
set = true
|
|
}
|
|
|
|
return excludedList, nil
|
|
}
|