Skip to content

Commit

Permalink
support header string + array for request & response
Browse files Browse the repository at this point in the history
Also, support ":control" in header for the flattened version.

see #73980
  • Loading branch information
martinrode committed Oct 31, 2024
1 parent 27423dc commit 66c865b
Show file tree
Hide file tree
Showing 15 changed files with 316 additions and 157 deletions.
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,14 +224,14 @@ Manifest is loaded as **template**, so you can use variables, Go **range** and *
"query_params_from_store": {
"format": "formatFromDatastore",
// If the datastore key starts with an ?, wo do not throw an error if the key could not be found, but just
// do not set the query param. If the key "a" is not found it datastore, the queryparameter test will not be set
// do not set the query param. If the key "a" is not found it datastore, the query parameter test will not be set
"test": "?a"
},

// Additional headers that should be added to the request
"header": {
"header1": "value",
"header2": "value"
"header2": ["value1", "value2"]
},

// Cookies can be added to the request
Expand Down Expand Up @@ -270,7 +270,7 @@ Manifest is loaded as **template**, so you can use variables, Go **range** and *
}
],

// With header_from_you set a header to the value of the dat astore field
// With header_from_store you set a header to the value of the datastore field
// In this example we set the "Content-Type" header to the value "application/json"
// As "application/json" is stored as string in the datastore on index "contentType"
"header_from_store": {
Expand Down Expand Up @@ -298,13 +298,22 @@ Manifest is loaded as **template**, so you can use variables, Go **range** and *
// Expected http status code. See api documentation vor the right ones
"statuscode": 200,

// If you expect certain response headers, you can define them here. A single key can have mulitble headers (as defined in rfc2616)
// If you expect certain response headers, you can define them here. A single key can have multiple headers (as defined in rfc2616)
"header": {
"key1": [
"val1",
"val2",
"val3"
],

// Headers sharing the same key are concatenated using ";", if the comparison value is a simple string,
// thus "key1" can also be checked like this:
"key1": "val1;val2;val3"

// :control in header is always applied to the flat format
"key1:control": {
// see below, this is not applied against the array
},
"x-easydb-token": [
"csdklmwerf8ßwji02kopwfjko2"
]
Expand Down Expand Up @@ -1931,10 +1940,7 @@ The datastore stores all responses in a list. We can retrieve the response (as a
{
"statuscode": 200,
"header": {
"foo": [
"bar",
"baz"
]
"foo": "bar;baz"
},
"body": "..."
}
Expand Down Expand Up @@ -2658,7 +2664,7 @@ The endpoint `bounce` returns the binary of the request body, as well as the req
"param1": "abc"
},
"header": {
"header1": 123
"header1": "123"
},
"body": {
"file": "@path/to/file.jpg"
Expand Down
11 changes: 5 additions & 6 deletions api_testcase.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,7 @@ type Case struct {
index int
dataStore *datastore.Datastore

standardHeader map[string]*string
headerFlat map[string]*string
standardHeader map[string]any // can be string or []string
standardHeaderFromStore map[string]string

ServerURL string `json:"server_url"`
Expand Down Expand Up @@ -490,7 +489,7 @@ func (testCase Case) responsesEqual(expected, got api.Response) (compare.Compare
if err != nil {
return compare.CompareResult{}, fmt.Errorf("error loading expected generic json: %s", err)
}
if len(expected.Body) == 0 {
if len(expected.Body) == 0 && len(expected.BodyControl) == 0 {
expected.Format.IgnoreBody = true
} else {
expected.Format.IgnoreBody = false
Expand Down Expand Up @@ -524,10 +523,10 @@ func (testCase Case) loadRequestSerialization() (api.Request, error) {
spec.ServerURL = testCase.ServerURL
}
if len(spec.Headers) == 0 {
spec.Headers = make(map[string]*string)
spec.Headers = make(map[string]any)
}
for k, v := range testCase.standardHeader {
if spec.Headers[k] == nil {
if _, exist := spec.Headers[k]; !exist {
spec.Headers[k] = v
}
}
Expand All @@ -536,7 +535,7 @@ func (testCase Case) loadRequestSerialization() (api.Request, error) {
spec.HeaderFromStore = make(map[string]string)
}
for k, v := range testCase.standardHeaderFromStore {
if spec.HeaderFromStore[k] == "" {
if _, exist := spec.HeaderFromStore[k]; !exist {
spec.HeaderFromStore[k] = v
}
}
Expand Down
6 changes: 2 additions & 4 deletions api_testsuite.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,8 @@ type Suite struct {
Tests []any `json:"tests"`
Store map[string]any `json:"store"`

StandardHeader map[string]*string `yaml:"header" json:"header"`
HeaderFlat map[string]*string `yaml:"header_flat" json:"header_flat"`
StandardHeaderFromStore map[string]string `yaml:"header_from_store" json:"header_from_store"`
StandardHeader map[string]any `yaml:"header" json:"header"`
StandardHeaderFromStore map[string]string `yaml:"header_from_store" json:"header_from_store"`

Config TestToolConfig
datastore *datastore.Datastore
Expand Down Expand Up @@ -399,7 +398,6 @@ func (ats *Suite) runLiteralTest(
test.index = index
test.dataStore = ats.datastore
test.standardHeader = ats.StandardHeader
test.headerFlat = ats.HeaderFlat
test.standardHeaderFromStore = ats.StandardHeaderFromStore
if test.LogNetwork == nil {
test.LogNetwork = &ats.Config.LogNetwork
Expand Down
31 changes: 22 additions & 9 deletions pkg/lib/api/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ type Request struct {
NoRedirect bool `yaml:"no_redirect" json:"no_redirect"`
QueryParams map[string]any `yaml:"query_params" json:"query_params"`
QueryParamsFromStore map[string]string `yaml:"query_params_from_store" json:"query_params_from_store"`
Headers map[string]*string `yaml:"header" json:"header"`
Headers map[string]any `yaml:"header" json:"header"`
HeaderFromStore map[string]string `yaml:"header_from_store" json:"header_from_store"`
Cookies map[string]*RequestCookie `yaml:"cookies" json:"cookies"`
SetCookies []*Cookie `yaml:"header-x-test-set-cookie" json:"header-x-test-set-cookie"`
Expand Down Expand Up @@ -202,12 +202,25 @@ func (request Request) buildHttpRequest() (req *http.Request, err error) {
}

for key, val := range request.Headers {
if *val == "" {
//Unset header explicit
req.Header.Del(key)
} else {
//ADD header
req.Header.Set(key, *val)
switch v := val.(type) {
case string:
if v == "" {
req.Header.Del(key)
} else {
req.Header.Set(key, v)
}
case []any:
vArr := []string{}
for _, vOne := range v {
vOneS, isString := vOne.(string)
if !isString {
return nil, fmt.Errorf("unsupported header %q value %T: %v", key, vOne, vOne)
}
vArr = append(vArr, vOneS)
}
req.Header[key] = vArr
default:
return nil, fmt.Errorf("unsupported header value %T: %v", val, val)
}
}

Expand Down Expand Up @@ -344,9 +357,9 @@ func (request Request) Send() (response Response, err error) {
if err != nil {
return response, err
}
response, err = NewResponse(httpResponse.StatusCode, header, nil, httpResponse.Cookies(), httpResponse.Body, ResponseFormat{})
response, err = NewResponse(httpResponse.StatusCode, header, httpResponse.Cookies(), httpResponse.Body, nil, ResponseFormat{})
if err != nil {
return response, fmt.Errorf("error constructing response from http response")
return response, fmt.Errorf("error constructing response from http response: %w", err)
}
response.ReqDur = elapsedTime
return response, err
Expand Down
129 changes: 81 additions & 48 deletions pkg/lib/api/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,18 @@ import (
)

type Response struct {
StatusCode int
Headers map[string]any
HeaderFlat map[string]any
Cookies []*http.Cookie
Body []byte
Format ResponseFormat
StatusCode int
Headers map[string]any
HeaderFlat map[string]any // ":control" is an object, so we must use "any" here
Cookies []*http.Cookie
Body []byte
BodyControl util.JsonObject
Format ResponseFormat

ReqDur time.Duration
BodyLoadDur time.Duration
}

func (res Response) SerializeHeaderFlat() (headers map[string]any, err error) {
headers = map[string]any{}
for k, h := range res.Headers {
switch v := h.(type) {
case string:
headers[k] = h
case nil:
headers[k] = ""
case []string:
headers[k] = strings.Join(v, "; ")
}
}
return headers, nil
}

func HttpHeaderToMap(header http.Header) (headers map[string]any, err error) {
headers = map[string]any{}
for k, h := range header {
Expand All @@ -66,12 +52,17 @@ type Cookie struct {
}

type ResponseSerialization struct {
StatusCode int `yaml:"statuscode" json:"statuscode"`
Headers map[string]any `yaml:"header" json:"header,omitempty"`
HeaderFlat map[string]any `yaml:"header_flat" json:"header_flat,omitempty"`
Cookies map[string]Cookie `yaml:"cookie" json:"cookie,omitempty"`
Body any `yaml:"body" json:"body,omitempty"`
Format ResponseFormat `yaml:"format" json:"format,omitempty"`
StatusCode int `yaml:"statuscode" json:"statuscode"`
Headers map[string]any `yaml:"header" json:"header,omitempty"`
Cookies map[string]Cookie `yaml:"cookie" json:"cookie,omitempty"`
Body any `yaml:"body" json:"body,omitempty"`
BodyControl util.JsonObject `yaml:"body:control" json:"body:control,omitempty"`
Format ResponseFormat `yaml:"format" json:"format,omitempty"`
}

type responseSerializationInternal struct {
ResponseSerialization
HeaderFlat map[string]any `json:"header_flat,omitemty"`
}

type ResponseFormat struct {
Expand All @@ -84,18 +75,55 @@ type ResponseFormat struct {
}

func NewResponse(statusCode int,
headers map[string]any,
headerFlat map[string]any,
headersAny map[string]any,
cookies []*http.Cookie,
body io.Reader,
bodyControl util.JsonObject,
bodyFormat ResponseFormat,
) (res Response, err error) {

headerFlat := map[string]any{}
headers := map[string]any{}

// parse headers and set HeaderFlat if the values are string
for key, value := range headersAny {
switch v := value.(type) {
case string:
headerFlat[key] = v
continue
case []any:
headerS := []string{}
for _, item := range v {
switch v2 := item.(type) {
case string:
headerS = append(headerS, v2)
continue
default:
return res, fmt.Errorf("unknown type %T in header %q", v2, key)
}
}
headers[key] = v
continue
case []string:
headers[key] = v
continue
case map[string]any: // check if that is a control
if strings.HasSuffix(key, ":control") {
headerFlat[key] = v
continue
}
}
// all valid cases continue above
return res, fmt.Errorf("unknown type %T in header %q", value, key)
}

res = Response{
StatusCode: statusCode,
Headers: headers,
HeaderFlat: headerFlat,
Cookies: cookies,
Format: bodyFormat,
StatusCode: statusCode,
Headers: headers,
BodyControl: bodyControl,
HeaderFlat: headerFlat,
Cookies: cookies,
Format: bodyFormat,
}
if body != nil {
start := time.Now()
Expand Down Expand Up @@ -141,7 +169,7 @@ func NewResponseFromSpec(spec ResponseSerialization) (res Response, err error) {
}
}

return NewResponse(spec.StatusCode, spec.Headers, spec.HeaderFlat, cookies, body, spec.Format)
return NewResponse(spec.StatusCode, spec.Headers, cookies, body, spec.BodyControl, spec.Format)
}

// ServerResponseToGenericJSON parse response from server. convert xml, csv, binary to json if necessary
Expand Down Expand Up @@ -211,15 +239,19 @@ func (response Response) ServerResponseToGenericJSON(responseFormat ResponseForm
return res, fmt.Errorf("Invalid response format '%s'", responseFormat.Type)
}

headerFlat, err := resp.SerializeHeaderFlat()
if err != nil {
return res, err
}
responseJSON := ResponseSerialization{
StatusCode: resp.StatusCode,
Headers: resp.Headers,
HeaderFlat: headerFlat,
headerFlat := map[string]any{}
headersAny := map[string]any{}
for key, value := range resp.Headers {
headersAny[key] = value
values := value.([]string) // this must be []string, if not this panics
headerFlat[key] = strings.Join(values, ";")
}

responseJSON := responseSerializationInternal{}
responseJSON.StatusCode = resp.StatusCode
responseJSON.Headers = headersAny
responseJSON.HeaderFlat = headerFlat

// Build cookies map from standard bag
if len(resp.Cookies) > 0 {
responseJSON.Cookies = make(map[string]Cookie)
Expand Down Expand Up @@ -282,11 +314,12 @@ func (response Response) ToGenericJSON() (any, error) {
}
}

responseJSON := ResponseSerialization{
StatusCode: response.StatusCode,
Headers: response.Headers,
HeaderFlat: response.HeaderFlat,
}
responseJSON := responseSerializationInternal{}

responseJSON.StatusCode = response.StatusCode
responseJSON.Headers = response.Headers
responseJSON.HeaderFlat = response.HeaderFlat
responseJSON.BodyControl = response.BodyControl

// Build cookies map from standard bag
if len(response.Cookies) > 0 {
Expand Down
6 changes: 3 additions & 3 deletions pkg/lib/api/response_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func TestResponse_NewResponseFromSpec_StatusCode_not_set(t *testing.T) {
}

func TestResponse_NewResponse(t *testing.T) {
response, err := NewResponse(200, nil, nil, nil, strings.NewReader("foo"), ResponseFormat{})
response, err := NewResponse(200, nil, nil, strings.NewReader("foo"), nil, ResponseFormat{})
go_test_utils.ExpectNoError(t, err, "unexpected error")
go_test_utils.AssertIntEquals(t, response.StatusCode, 200)
}
Expand All @@ -86,7 +86,7 @@ func TestResponse_String(t *testing.T) {
}
}`

response, err := NewResponse(200, nil, nil, nil, strings.NewReader(requestString), ResponseFormat{})
response, err := NewResponse(200, nil, nil, strings.NewReader(requestString), nil, ResponseFormat{})
go_test_utils.ExpectNoError(t, err, "error constructing response")

assertString := "200\n\n\n" + requestString
Expand Down Expand Up @@ -119,7 +119,7 @@ func TestResponse_Cookies(t *testing.T) {
if err != nil {
t.Fatal(err)
}
response, err := NewResponse(res.StatusCode, header, nil, res.Cookies(), res.Body, ResponseFormat{})
response, err := NewResponse(res.StatusCode, header, res.Cookies(), res.Body, nil, ResponseFormat{})
if err != nil {
t.Fatal(err)
}
Expand Down
Loading

0 comments on commit 66c865b

Please sign in to comment.