Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

perf(go): improve go core library test validators to improve test validation #79

Merged
merged 8 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions https/callBuilder.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,10 +662,11 @@ func (cb *defaultCallBuilder) CallAsJson() (*json.Decoder, *http.Response, error

if result.Response != nil {
if result.Response.Body == http.NoBody {
err = fmt.Errorf("response body empty")
return nil, result.Response, fmt.Errorf("response body empty")
}

return json.NewDecoder(result.Response.Body), result.Response, err
bodyBytes, err := io.ReadAll(result.Response.Body)
result.Response.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
return json.NewDecoder(io.NopCloser(bytes.NewBuffer(bodyBytes))), result.Response, err
}
return nil, result.Response, err
}
Expand All @@ -683,12 +684,12 @@ func (cb *defaultCallBuilder) CallAsText() (string, *http.Response, error) {
return "", result.Response, fmt.Errorf("response body empty")
}

body, err := io.ReadAll(result.Response.Body)
bodyBytes, err := io.ReadAll(result.Response.Body)
if err != nil {
return "", result.Response, fmt.Errorf("error reading Response body: %v", err.Error())
}

return string(body), result.Response, err
result.Response.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
return string(bodyBytes), result.Response, err
}
return "", result.Response, err
}
Expand Down
83 changes: 54 additions & 29 deletions https/formData.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ type formParam struct {
arraySerializationOption ArraySerializationOption
}

func (fp *formParam) clone(key string, value any) formParam {
return formParam{
key: key,
value: value,
headers: fp.headers,
arraySerializationOption: fp.arraySerializationOption,
}
}

type formParams []formParam

// FormParams represents a collection of FormParam objects.
Expand All @@ -53,7 +62,7 @@ func (fp *formParams) prepareFormFields(form url.Values) error {
form = url.Values{}
}
for _, param := range *fp {
paramsMap, err := toMap(param.key, param.value, param.arraySerializationOption)
paramsMap, err := param.toMap()
if err != nil {
return err
}
Expand All @@ -80,7 +89,7 @@ func (fp *formParams) prepareMultipartFields() (bytes.Buffer, string, error) {
}
formParamWriter(writer, field.headers, mediaParam, fieldValue.File)
default:
paramsMap, err := toMap(field.key, field.value, field.arraySerializationOption)
paramsMap, err := field.toMap()
if err != nil {
return *body, writer.FormDataContentType(), err
}
Expand Down Expand Up @@ -119,49 +128,64 @@ func formParamWriter(
return nil
}

func toMap(keyPrefix string, param any, option ArraySerializationOption) (map[string][]string, error) {
if param == nil {
func (fp *formParam) IsMultipart() bool {
contentType := fp.headers.Get(CONTENT_TYPE_HEADER)
if contentType != "" {
return contentType != FORM_URLENCODED_CONTENT_TYPE
}
return false
}

func (fp *formParam) toMap() (map[string][]string, error) {
if fp.value == nil {
return map[string][]string{}, nil
}

switch reflect.TypeOf(param).Kind() {
if (fp.IsMultipart()){
return fp.processDefault()
}

switch reflect.TypeOf(fp.value).Kind() {
case reflect.Ptr:
return processStructAndPtr(keyPrefix, param, option)
return fp.processStructAndPtr()
case reflect.Struct:
return processStructAndPtr(keyPrefix, toStructPtr(param), option)
innerfp := fp.clone(fp.key, toStructPtr(fp.value))
return innerfp.processStructAndPtr()
case reflect.Map:
return processMap(keyPrefix, param, option)
return fp.processMap()
case reflect.Slice:
return processSlice(keyPrefix, param, option)
return fp.processSlice()
default:
return processDefault(keyPrefix, param)
return fp.processDefault()
}
}

func processStructAndPtr(keyPrefix string, param any, option ArraySerializationOption) (map[string][]string, error) {
innerData, err := structToAny(param)
func (fp *formParam) processStructAndPtr() (map[string][]string, error) {
innerData, err := structToAny(fp.value)
if err != nil { return nil, err }

return toMap(keyPrefix, innerData, option)
innerfp := fp.clone(fp.key, innerData)
return innerfp.toMap()
}

func processMap(keyPrefix string, param any, option ArraySerializationOption) (map[string][]string, error) {
iter := reflect.ValueOf(param).MapRange()
func (fp *formParam) processMap() (map[string][]string, error) {
iter := reflect.ValueOf(fp.value).MapRange()
result := make(map[string][]string)
for iter.Next() {
innerKey := option.joinKey(keyPrefix, iter.Key().Interface())
innerKey := fp.arraySerializationOption.joinKey(fp.key, iter.Key().Interface())
innerValue := iter.Value().Interface()
innerFlatMap, err := toMap(innerKey, innerValue, option)
innerfp := fp.clone(innerKey, innerValue)
innerFlatMap, err := innerfp.toMap()
if err != nil {
return nil, err
}
option.appendMap(result, innerFlatMap)
fp.arraySerializationOption.appendMap(result, innerFlatMap)
}
return result, nil
}

func processSlice(keyPrefix string, param any, option ArraySerializationOption) (map[string][]string, error) {
reflectValue := reflect.ValueOf(param)
func (fp *formParam) processSlice() (map[string][]string, error) {
reflectValue := reflect.ValueOf(fp.value)
result := make(map[string][]string)
for i := 0; i < reflectValue.Len(); i++ {
innerStruct := reflectValue.Index(i).Interface()
Expand All @@ -172,30 +196,31 @@ func processSlice(keyPrefix string, param any, option ArraySerializationOption)
default:
indexStr = fmt.Sprintf("%v", i)
}
innerKey := option.joinKey(keyPrefix, indexStr)
innerFlatMap, err := toMap(innerKey, innerStruct, option)
innerKey := fp.arraySerializationOption.joinKey(fp.key, indexStr)
innerfp := fp.clone(innerKey, innerStruct)
innerFlatMap, err := innerfp.toMap()
if err != nil {
return result, err
}
option.appendMap(result, innerFlatMap)
fp.arraySerializationOption.appendMap(result, innerFlatMap)
}
return result, nil
}

func processDefault(keyPrefix string, param any) (map[string][]string, error) {
func (fp *formParam) processDefault() (map[string][]string, error) {
var defaultValue string
switch reflect.TypeOf(param).Kind() {
switch reflect.TypeOf(fp.value).Kind() {
case reflect.String:
defaultValue = fmt.Sprintf("%v", param)
defaultValue = fmt.Sprintf("%v", fp.value)
default:
dataBytes, err := json.Marshal(param)
dataBytes, err := json.Marshal(fp.value)
if err == nil {
defaultValue = string(dataBytes)
} else {
defaultValue = fmt.Sprintf("%v", param)
defaultValue = fmt.Sprintf("%v", fp.value)
}
}
return map[string][]string{keyPrefix: {defaultValue}}, nil
return map[string][]string{fp.key: {defaultValue}}, nil
}

// structToAny converts a given data structure into an any type.
Expand Down
12 changes: 6 additions & 6 deletions https/formData_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ func TestStructToMapMarshallingError(t *testing.T) {
}

func TestToMapNilMap(t *testing.T) {
param := FormParam{"param", "value", nil}
result, _ := toMap(param.Key, param.Value, Indexed)
param := formParam{"param", "value", nil, Indexed}
result, _ := param.toMap()

expected := map[string][]string{
"param": {"value"},
Expand All @@ -53,8 +53,8 @@ func TestToMapNilMap(t *testing.T) {
}

func TestFormEncodeMapNilValue(t *testing.T) {
param := FormParam{"param", nil, nil}
result, _ := toMap(param.Key, param.Value, Indexed)
param := formParam{"param", nil, nil, Indexed}
result, _ := param.toMap()

expected := make(map[string][]string)

Expand All @@ -64,8 +64,8 @@ func TestFormEncodeMapNilValue(t *testing.T) {
}

func TestFormEncodeMapStructType(t *testing.T) {
param := FormParam{"param2", GetStruct(), nil}
result, _ := toMap(param.Key, param.Value, Indexed)
param := formParam{"param2", GetStruct(), nil, Indexed}
result, _ := param.toMap()

expected := FormParams{
{"param2[Name]", "Bisma", nil},
Expand Down
110 changes: 85 additions & 25 deletions testHelper/bodyMatchers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,115 @@ package testHelper
import (
"encoding/json"
"fmt"
"io"
"reflect"
"strings"
"testing"

"github.com/apimatic/go-core-runtime/https"
)

// RawBodyMatcher checks if the expectedBody is contained within the JSON response body.
func RawBodyMatcher[T any](test *testing.T, expectedBody string, responseObject T) {
responseBytes, _ := json.Marshal(&responseObject)
responseBody := string(responseBytes)
func setTestingError(test *testing.T, responseArg any, expectedArg any){
test.Errorf("got \n%v \nbut expected %v", responseArg, expectedArg)
}

func setTestResponseError(test *testing.T, responseErr error){
test.Errorf("Invalid response data: %v", responseErr)
}

if !strings.Contains(responseBody, expectedBody) {
test.Errorf("got \n%v \nbut expected %v", responseBody, expectedBody)
// RawBodyMatcher compares the response body with the expected body via simple string checking. In case of Binary response, byte-by-byte comparison is performed.
func RawBodyMatcher(test *testing.T, expectedBody string, responseBody io.ReadCloser) {
responseBytes, responseReadErr := io.ReadAll(responseBody)
if responseReadErr != nil {
setTestResponseError(test, responseReadErr)
}
response := string(responseBytes)

if !strings.Contains(response, expectedBody) {
setTestingError(test, responseBody, expectedBody)
}
}

// NativeBodyMatcher compares the JSON response body with the expected JSON body.
func NativeBodyMatcher[T any](test *testing.T, expectedBody string, responseObject T) {
responseBytes, _ := json.Marshal(&responseObject)
var expected, response any
expectedError := json.Unmarshal([]byte(expectedBody), &expected)
responseError := json.Unmarshal(responseBytes, &response)
// NativeBodyMatcher compares the response body as a primitive type(int, int64, float64, bool & time.Time) using a simple equality test.
// Response must match exactly except in case of arrays where array ordering and strictness can be controlled via other options.
func NativeBodyMatcher(test *testing.T, expectedBody string, responseBody io.ReadCloser, isArray, checkArrayCount bool) {

responseBytes, responseReadErr := io.ReadAll(responseBody)
if responseReadErr != nil {
setTestResponseError(test, responseReadErr)
}
expectedBytes := []byte(expectedBody)

if (isArray){
matchNativeArray(test, expectedBytes, responseBytes, checkArrayCount)
return
}
if !reflect.DeepEqual(responseBytes, expectedBytes) {
setTestingError(test, string(responseBytes), string(expectedBytes))
}
}

if expectedError != nil || responseError != nil {
func matchNativeArray(test *testing.T, expectedBytes, responseBytes []byte, checkArrayCount bool) {
var expected, response []any
expectedErr := json.Unmarshal(expectedBytes, &expected)
responseErr := json.Unmarshal(responseBytes, &response)
if expectedErr != nil || responseErr != nil {
test.Error("error while unmarshalling for comparison")
}

if (!checkArrayCount){
matchNativeArrayValues(test, response, expected)
return
}
if !reflect.DeepEqual(response, expected) {
test.Errorf("got \n%v \nbut expected \n%v", string(responseBytes), expectedBody)
setTestingError(test, response, expected)
}
}

func matchNativeArrayValues(test *testing.T, response, expected []any) {
containsFunc := func(slice []any, val any) bool {
for _, v := range slice {
if reflect.DeepEqual(v, val) {
return true
}
}
return false
}
for _, v := range response {
if !containsFunc(expected, v) {
setTestingError(test, response, expected)
break
}
}
}

// KeysBodyMatcher compares the JSON response body with the expected JSON body using keys only.
// The responseObject and expectedBody should have the same keys.
func KeysBodyMatcher[T any](test *testing.T, expectedBody string, responseObject T, checkArrayCount, checkArrayOrder bool) {
responseBytes, _ := json.Marshal(&responseObject)
// KeysBodyMatcher Checks whether the response body contains the same keys as those specified in the expected body.
// The keys provided can be a subset of the response being received. If any key is absent in the response body, the test fails.
// The test generated will perform deep checking which means if the response object contains nested objects, their keys will also be tested.
func KeysBodyMatcher(test *testing.T, expectedBody string, responseBody io.ReadCloser, checkArrayCount, checkArrayOrder bool) {
responseBytes, responseErr := io.ReadAll(responseBody)
if responseErr != nil {
setTestResponseError(test, responseErr)
}
expectedBytes := []byte(expectedBody)

if !matchKeysAndValues(responseBytes, []byte(expectedBody), checkArrayCount, checkArrayOrder, false) {
if !matchKeysAndValues(responseBytes, expectedBytes, checkArrayCount, checkArrayOrder, false) {
test.Errorf("got \n%v \nbut expected \n%v", string(responseBytes), expectedBody)
}
}

// KeysAndValuesBodyMatcher compares the JSON response body with the expected JSON body using keys and values.
// The responseObject and expectedBody should have the same keys and their corresponding values should be equal.
func KeysAndValuesBodyMatcher[T any](test *testing.T, expectedBody string, responseObject T, checkArrayCount, checkArrayOrder bool) {
responseBytes, _ := json.Marshal(&responseObject)
// KeysAndValuesBodyMatcher Checks whether the response body contains the same keys and values as those specified in the expected body.
// The keys and values provided can be a subset of the response being received. If any key or value is absent in the response body, the test fails.
// The test generated will perform deep checking which means if the response object contains nested objects, their keys and values will also be tested.
// In case of nested arrays, their ordering and strictness depends on the provided options.
func KeysAndValuesBodyMatcher(test *testing.T, expectedBody string, responseBody io.ReadCloser, checkArrayCount, checkArrayOrder bool) {

responseBytes, responseErr := io.ReadAll(responseBody)
if responseErr != nil {
setTestResponseError(test, responseErr)
}
expectedBytes := []byte(expectedBody)

if !matchKeysAndValues(responseBytes, []byte(expectedBody), checkArrayCount, checkArrayOrder, true) {
if !matchKeysAndValues(responseBytes, expectedBytes, checkArrayCount, checkArrayOrder, true) {
test.Errorf("got \n%v \nbut expected \n%v", string(responseBytes), expectedBody)
}
}
Expand Down
Loading
Loading