mirror of
https://github.com/d0zingcat/gocryptotrader.git
synced 2026-05-13 15:09:42 +00:00
* Adds new file.Move func to address a bug with Golang/Docker volumes when using os.Rename Also uses TempDir for tests instead of live directories and increases test coverage for file.Write * Goimport the imports * Make usage of file package name consistent so it no longer clashes with vars * Remove outputFile if io.Copy fails
439 lines
10 KiB
Go
439 lines
10 KiB
Go
package mock
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/thrasher-corp/gocryptotrader/common/crypto"
|
|
"github.com/thrasher-corp/gocryptotrader/common/file"
|
|
)
|
|
|
|
// 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 := ioutil.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 := crypto.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 {
|
|
return 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 file.Write(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
|
|
}
|