mirror of
https://github.com/alibaba/higress.git
synced 2026-03-15 22:30:47 +08:00
feat: e2e test support http body check (#733)
This commit is contained in:
@@ -61,7 +61,10 @@ spec:
|
||||
containers:
|
||||
- name: infra-backend-v1
|
||||
# From https://github.com/kubernetes-sigs/ingress-controller-conformance/tree/master/images/echoserver
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
# image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
|
||||
# From https://github.com/Uncle-Justice/echo-server
|
||||
image: 873292889/echo-server:1.3.0
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
@@ -107,7 +110,10 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: infra-backend-v2
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
# image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
|
||||
# From https://github.com/Uncle-Justice/echo-server
|
||||
image: 873292889/echo-server:1.3.0
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
@@ -153,7 +159,57 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: infra-backend-v3
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
# image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
|
||||
# From https://github.com/Uncle-Justice/echo-server
|
||||
image: 873292889/echo-server:1.3.0
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: NAMESPACE
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.namespace
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: infra-backend-echo-body-v1
|
||||
namespace: higress-conformance-infra
|
||||
spec:
|
||||
selector:
|
||||
app: infra-backend-echo-body-v1
|
||||
ports:
|
||||
- protocol: TCP
|
||||
port: 8080
|
||||
targetPort: 3000
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: infra-backend-echo-body-v1
|
||||
namespace: higress-conformance-infra
|
||||
labels:
|
||||
app: infra-backend-echo-body-v1
|
||||
spec:
|
||||
replicas: 2
|
||||
selector:
|
||||
matchLabels:
|
||||
app: infra-backend-echo-body-v1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: infra-backend-echo-body-v1
|
||||
spec:
|
||||
containers:
|
||||
- name: infra-backend-echo-body-v1
|
||||
# FROM https://github.com/higress-group/echo-body
|
||||
image: 873292889/echo-body:1.0.0
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
@@ -206,7 +262,10 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: app-backend-v1
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
# image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
|
||||
# From https://github.com/Uncle-Justice/echo-server
|
||||
image: 873292889/echo-server:1.3.0
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
@@ -252,7 +311,10 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: app-backend-v2
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
# image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
|
||||
# From https://github.com/Uncle-Justice/echo-server
|
||||
image: 873292889/echo-server:1.3.0
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
@@ -305,7 +367,10 @@ spec:
|
||||
spec:
|
||||
containers:
|
||||
- name: web-backend
|
||||
image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
# image: higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/echoserver:v20221109-7ee2f3e
|
||||
|
||||
# From https://github.com/Uncle-Justice/echo-server
|
||||
image: 873292889/echo-server:1.3.0
|
||||
env:
|
||||
- name: POD_NAME
|
||||
valueFrom:
|
||||
|
||||
@@ -86,6 +86,76 @@ var WasmPluginsRequestBlock = suite.ConformanceTest{
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// post blocked body
|
||||
Meta: http.AssertionMeta{
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo.com",
|
||||
Path: "/foo",
|
||||
Method: "POST",
|
||||
ContentType: http.ContentTypeTextPlain,
|
||||
Body: []byte(`hello world`),
|
||||
UnfollowRedirect: true,
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 403,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// check body echoed back in expected request(same as ActualRequest if not set)
|
||||
Meta: http.AssertionMeta{
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
CompareTarget: http.CompareTargetRequest,
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo.com",
|
||||
Path: "/foo",
|
||||
Method: "POST",
|
||||
ContentType: http.ContentTypeTextPlain,
|
||||
Body: []byte(`hello higress`),
|
||||
UnfollowRedirect: true,
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// check body echoed back in expected response
|
||||
Meta: http.AssertionMeta{
|
||||
TargetBackend: "infra-backend-echo-body-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
CompareTarget: http.CompareTargetResponse,
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo2.com",
|
||||
Path: "/foo",
|
||||
Method: "POST",
|
||||
ContentType: http.ContentTypeTextPlain,
|
||||
Body: []byte(`hello higress`),
|
||||
UnfollowRedirect: true,
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 200,
|
||||
ContentType: http.ContentTypeTextPlain,
|
||||
Body: []byte(`hello higress`),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Run("WasmPlugins request-block", func(t *testing.T) {
|
||||
for _, testcase := range testcases {
|
||||
|
||||
@@ -33,6 +33,27 @@ spec:
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/app-root: "/foo"
|
||||
name: httproute-app-root2
|
||||
namespace: higress-conformance-infra
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: "foo2.com"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: infra-backend-echo-body-v1
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
@@ -46,4 +67,6 @@ spec:
|
||||
- "/env.*"
|
||||
block_exact_urls:
|
||||
- "/web/info"
|
||||
block_bodies:
|
||||
- "hello world"
|
||||
url: file:///opt/plugins/wasm-go/extensions/request-block/plugin.wasm
|
||||
|
||||
@@ -35,7 +35,7 @@ var WasmPluginsTransformer = suite.ConformanceTest{
|
||||
testcases := []http.Assertion{
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 1: request transformer",
|
||||
TestCaseName: "case 1: request header&query transformer",
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
@@ -77,7 +77,7 @@ var WasmPluginsTransformer = suite.ConformanceTest{
|
||||
},
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 2: response transformer",
|
||||
TestCaseName: "case 2: response header&query transformer",
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
@@ -111,6 +111,85 @@ var WasmPluginsTransformer = suite.ConformanceTest{
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 4: request body transformer",
|
||||
TargetBackend: "infra-backend-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo4.com",
|
||||
Path: "/post",
|
||||
// TODO(Uncle-Justice) dedupe, replace的body插件逻辑有问题,暂跳过测试
|
||||
Method: "POST",
|
||||
Body: []byte(`
|
||||
{
|
||||
"X-removed":["v1", "v2"],
|
||||
"X-not-renamed":["v1"]
|
||||
}
|
||||
`),
|
||||
ContentType: http.ContentTypeApplicationJson,
|
||||
},
|
||||
ExpectedRequest: &http.ExpectedRequest{
|
||||
Request: http.Request{
|
||||
Host: "foo4.com",
|
||||
Path: "/post",
|
||||
// TODO(Uncle-Justice) dedupe, replace的body插件逻辑有问题,暂跳过测试
|
||||
Method: "POST",
|
||||
ContentType: http.ContentTypeApplicationJson,
|
||||
Body: []byte(`
|
||||
{
|
||||
"X-renamed":["v1"],
|
||||
"X-add-append":["add","append"],
|
||||
"X-map":["add","append"]
|
||||
}
|
||||
`),
|
||||
},
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 200,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Meta: http.AssertionMeta{
|
||||
TestCaseName: "case 5: response json body transformer",
|
||||
TargetBackend: "infra-backend-echo-body-v1",
|
||||
TargetNamespace: "higress-conformance-infra",
|
||||
CompareTarget: http.CompareTargetResponse,
|
||||
},
|
||||
Request: http.AssertionRequest{
|
||||
ActualRequest: http.Request{
|
||||
Host: "foo5.com",
|
||||
Path: "/post",
|
||||
// TODO(Uncle-Justice) dedupe, replace的body插件逻辑有问题,暂跳过测试
|
||||
Method: "POST",
|
||||
Body: []byte(`
|
||||
{
|
||||
"X-removed":["v1", "v2"],
|
||||
"X-not-renamed":["v1"]
|
||||
}
|
||||
`),
|
||||
ContentType: http.ContentTypeApplicationJson,
|
||||
},
|
||||
},
|
||||
Response: http.AssertionResponse{
|
||||
ExpectedResponse: http.Response{
|
||||
StatusCode: 200,
|
||||
ContentType: http.ContentTypeApplicationJson,
|
||||
Body: []byte(`
|
||||
{
|
||||
"X-renamed":["v1"],
|
||||
"X-add-append":["add","append"],
|
||||
"X-map":["add","append"]
|
||||
}
|
||||
`),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
t.Run("WasmPlugin transformer", func(t *testing.T) {
|
||||
for _, testcase := range testcases {
|
||||
|
||||
@@ -36,7 +36,7 @@ apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
name: wasmplugin-transform-response
|
||||
name: wasmplugin-transform-response-header-and-query
|
||||
namespace: higress-conformance-infra
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
@@ -52,6 +52,46 @@ spec:
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
name: wasmplugin-transform-request-body
|
||||
namespace: higress-conformance-infra
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: "foo4.com"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: infra-backend-v1
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
annotations:
|
||||
name: wasmplugin-transform-response-body
|
||||
namespace: higress-conformance-infra
|
||||
spec:
|
||||
ingressClassName: higress
|
||||
rules:
|
||||
- host: "foo5.com"
|
||||
http:
|
||||
paths:
|
||||
- pathType: Prefix
|
||||
path: "/"
|
||||
backend:
|
||||
service:
|
||||
name: infra-backend-echo-body-v1
|
||||
port:
|
||||
number: 8080
|
||||
---
|
||||
apiVersion: extensions.higress.io/v1alpha1
|
||||
kind: WasmPlugin
|
||||
metadata:
|
||||
@@ -118,9 +158,8 @@ spec:
|
||||
- key: k4
|
||||
value: RETAIN_FIRST
|
||||
|
||||
# response transformer
|
||||
- ingress:
|
||||
- higress-conformance-infra/wasmplugin-transform-response
|
||||
- higress-conformance-infra/wasmplugin-transform-response-header-and-query
|
||||
configDisable: false
|
||||
config:
|
||||
type: response
|
||||
@@ -151,5 +190,78 @@ spec:
|
||||
headers:
|
||||
- key: X-add-append
|
||||
value: X-map
|
||||
|
||||
- ingress:
|
||||
- higress-conformance-infra/wasmplugin-transform-request-body
|
||||
configDisable: false
|
||||
config:
|
||||
type: request
|
||||
rules:
|
||||
- operate: remove
|
||||
body:
|
||||
- key: X-removed
|
||||
- operate: rename
|
||||
body:
|
||||
- key: X-not-renamed
|
||||
value: X-renamed
|
||||
- operate: replace
|
||||
body:
|
||||
- key: X-replace
|
||||
value: replaced
|
||||
- operate: add
|
||||
body:
|
||||
- key: X-add-append
|
||||
value: add
|
||||
- operate: append
|
||||
body:
|
||||
- key: X-add-append
|
||||
value: append
|
||||
- operate: map
|
||||
body:
|
||||
- key: X-add-append
|
||||
value: X-map
|
||||
- operate: dedupe
|
||||
body:
|
||||
- key: X-dedupe-first
|
||||
value: RETAIN_FIRST
|
||||
- key: X-dedupe-last
|
||||
value: RETAIN_LAST
|
||||
- key: X-dedupe-unique
|
||||
value: RETAIN_UNIQUE
|
||||
- ingress:
|
||||
- higress-conformance-infra/wasmplugin-transform-response-body
|
||||
configDisable: false
|
||||
config:
|
||||
type: response
|
||||
rules:
|
||||
- operate: remove
|
||||
body:
|
||||
- key: X-removed
|
||||
- operate: rename
|
||||
body:
|
||||
- key: X-not-renamed
|
||||
value: X-renamed
|
||||
- operate: replace
|
||||
body:
|
||||
- key: X-replace
|
||||
value: replaced
|
||||
- operate: add
|
||||
body:
|
||||
- key: X-add-append
|
||||
value: add
|
||||
- operate: append
|
||||
body:
|
||||
- key: X-add-append
|
||||
value: append
|
||||
- operate: map
|
||||
body:
|
||||
- key: X-add-append
|
||||
value: X-map
|
||||
- operate: dedupe
|
||||
body:
|
||||
- key: X-dedupe-first
|
||||
value: RETAIN_FIRST
|
||||
- key: X-dedupe-last
|
||||
value: RETAIN_LAST
|
||||
- key: X-dedupe-unique
|
||||
value: RETAIN_UNIQUE
|
||||
url: file:///opt/plugins/wasm-go/extensions/transformer/plugin.wasm
|
||||
@@ -14,8 +14,14 @@ limitations under the License.
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -37,6 +43,8 @@ type AssertionMeta struct {
|
||||
TargetBackend string
|
||||
// TargetNamespace defines the target backend namespace
|
||||
TargetNamespace string
|
||||
// CompareTarget defines who's header&body to compare in test, either CompareTargetResponse or CompareTargetRequest
|
||||
CompareTarget string
|
||||
}
|
||||
|
||||
type AssertionRequest struct {
|
||||
@@ -62,6 +70,18 @@ type AssertionResponse struct {
|
||||
ExpectedResponseNoRequest bool
|
||||
}
|
||||
|
||||
const (
|
||||
ContentTypeApplicationJson string = "application/json"
|
||||
ContentTypeFormUrlencoded = "application/x-www-form-urlencoded"
|
||||
ContentTypeMultipartForm = "multipart/form-data"
|
||||
ContentTypeTextPlain = "text/plain"
|
||||
)
|
||||
|
||||
const (
|
||||
CompareTargetRequest = "Request"
|
||||
CompareTargetResponse = "Response"
|
||||
)
|
||||
|
||||
// Request can be used as both the request to make and a means to verify
|
||||
// that echoserver received the expected request. Note that multiple header
|
||||
// values can be provided, as a comma-separated value.
|
||||
@@ -70,6 +90,8 @@ type Request struct {
|
||||
Method string
|
||||
Path string
|
||||
Headers map[string]string
|
||||
Body []byte
|
||||
ContentType string
|
||||
UnfollowRedirect bool
|
||||
TLSConfig *TLSConfig
|
||||
}
|
||||
@@ -118,6 +140,8 @@ type ExpectedRequest struct {
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Headers map[string]string
|
||||
Body []byte
|
||||
ContentType string
|
||||
AbsentHeaders []string
|
||||
}
|
||||
|
||||
@@ -164,11 +188,22 @@ func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripp
|
||||
tlsConfig.SNI = expected.Request.ActualRequest.Host
|
||||
}
|
||||
}
|
||||
|
||||
if expected.Meta.CompareTarget == "" {
|
||||
expected.Meta.CompareTarget = CompareTargetRequest
|
||||
}
|
||||
if expected.Request.ActualRequest.Method == "" {
|
||||
expected.Request.ActualRequest.Method = "GET"
|
||||
}
|
||||
|
||||
if expected.Request.ActualRequest.Body != nil && len(expected.Request.ActualRequest.Body) > 0 {
|
||||
if len(expected.Request.ActualRequest.ContentType) == 0 {
|
||||
t.Error(`please set Content-Type in ActualRequest manually if you want to send a request with body.
|
||||
For example, \"ContentType: http.ContentTypeApplicationJson\"`)
|
||||
}
|
||||
}
|
||||
|
||||
expected.Request.ActualRequest.Method = strings.ToUpper(expected.Request.ActualRequest.Method)
|
||||
|
||||
if expected.Response.ExpectedResponse.StatusCode == 0 {
|
||||
expected.Response.ExpectedResponse.StatusCode = 200
|
||||
}
|
||||
@@ -183,6 +218,8 @@ func MakeRequestAndExpectEventuallyConsistentResponse(t *testing.T, r roundtripp
|
||||
URL: url.URL{Scheme: scheme, Host: gwAddr, Path: path, RawQuery: query},
|
||||
Protocol: protocol,
|
||||
Headers: map[string][]string{},
|
||||
Body: expected.Request.ActualRequest.Body,
|
||||
ContentType: expected.Request.ActualRequest.ContentType,
|
||||
UnfollowRedirect: expected.Request.ActualRequest.UnfollowRedirect,
|
||||
TLSConfig: tlsConfig,
|
||||
}
|
||||
@@ -252,9 +289,36 @@ func WaitForConsistentResponse(t *testing.T, r roundtripper.RoundTripper, req ro
|
||||
t.Logf("Request failed, not ready yet: %v (after %v)", err.Error(), elapsed)
|
||||
return false
|
||||
}
|
||||
// CompareTarget为Request(默认)时,ExpectedRequest中设置的所有断言均支持;除ExpectedResponse.Body外,ExpectedResponse中设置的所有断言均支持。目前支持echo-server作为backend
|
||||
// CompareTarget为Response时,不支持设定ExpectedRequest断言,ExpectedResponse中设置的所有断言均支持。支持任意backend,如echo-body
|
||||
if expected.Meta.CompareTarget == CompareTargetRequest {
|
||||
if expected.Response.ExpectedResponse.Body != nil {
|
||||
t.Logf(`detected CompareTarget is Request, but ExpectedResponse.Body is set.
|
||||
You can only choose one to compare between Response and Request.`)
|
||||
return false
|
||||
}
|
||||
|
||||
if err := CompareRequest(&req, cReq, cRes, expected); err != nil {
|
||||
t.Logf("Response expectation failed for request: %v not ready yet: %v (after %v)", req, err, elapsed)
|
||||
if cRes.StatusCode == 200 && !expected.Response.ExpectedResponseNoRequest && cReq.Host == "" && cReq.Path == "" && cReq.Headers == nil && cReq.Body == nil {
|
||||
t.Logf(`decoding client's response failed. Maybe you have chosen a wrong backend.
|
||||
Choose echo-server if you want to check expected request header&body instead of response header&body.`)
|
||||
return false
|
||||
}
|
||||
if err := CompareRequest(&req, cReq, cRes, expected); err != nil {
|
||||
t.Logf("request expectation failed for actual request: %v not ready yet: %v (after %v)", req, err, elapsed)
|
||||
return false
|
||||
}
|
||||
} else if expected.Meta.CompareTarget == CompareTargetResponse {
|
||||
if expected.Request.ExpectedRequest != nil {
|
||||
t.Logf(`detected CompareTarget is Response, but ExpectedRequest is set.
|
||||
You can only choose one to compare between Response and Request.`)
|
||||
return false
|
||||
}
|
||||
if err := CompareResponse(cRes, expected); err != nil {
|
||||
t.Logf("Response expectation failed for actual request: %v not ready yet: %v (after %v)", req, err, elapsed)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
t.Logf("invalid CompareTarget: %v please set it Request or Response", expected.Meta.CompareTarget, err, elapsed)
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -306,10 +370,85 @@ func CompareRequest(req *roundtripper.Request, cReq *roundtripper.CapturedReques
|
||||
} else if strings.Join(actualVal, ",") != expectedVal {
|
||||
return fmt.Errorf("expected %s header to be set to %s, got %s", name, expectedVal, strings.Join(actualVal, ","))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if expected.Request.ExpectedRequest.Body != nil && len(expected.Request.ExpectedRequest.Body) > 0 {
|
||||
// 对ExpectedRequest.Body做断言时,须手动指定ExpectedRequest.ContentType
|
||||
if len(expected.Request.ExpectedRequest.ContentType) == 0 {
|
||||
return fmt.Errorf("ExpectedRequest.ContentType should not be empty since ExpectedRequest.Body is set")
|
||||
}
|
||||
|
||||
if cReq.Headers["Content-Type"] == nil || len(cReq.Headers["Content-Type"]) == 0 {
|
||||
cReq.Headers["Content-Type"] = []string{expected.Request.ExpectedRequest.ContentType}
|
||||
}
|
||||
|
||||
eTyp, eParams, err := mime.ParseMediaType(expected.Request.ExpectedRequest.ContentType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ExpectedRequest Content-Type: %s failed to parse: %s", expected.Request.ExpectedRequest.ContentType, err.Error())
|
||||
}
|
||||
|
||||
cTyp := cReq.Headers["Content-Type"][0]
|
||||
|
||||
if eTyp != cTyp {
|
||||
return fmt.Errorf("expected %s Content-Type to be set, got %s", expected.Request.ExpectedRequest.ContentType, cReq.Headers["Content-Type"][0])
|
||||
}
|
||||
var ok bool
|
||||
switch cTyp {
|
||||
case ContentTypeTextPlain:
|
||||
if string(expected.Request.ExpectedRequest.Body) != cReq.Body.(string) {
|
||||
return fmt.Errorf("expected %s body to be %s, got %s##", cTyp, string(expected.Request.ExpectedRequest.Body), cReq.Body.(string))
|
||||
}
|
||||
case ContentTypeApplicationJson:
|
||||
var eReqBody map[string]interface{}
|
||||
var cReqBody map[string]interface{}
|
||||
|
||||
err := json.Unmarshal(expected.Request.ExpectedRequest.Body, &eReqBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshall ExpectedRequest body %s, %s", string(expected.Request.ExpectedRequest.Body), err.Error())
|
||||
}
|
||||
|
||||
if cReqBody, ok = cReq.Body.(map[string]interface{}); !ok {
|
||||
return fmt.Errorf("failed to parse CapturedRequest body %s, %s", string(cReq.Body.([]byte)), err.Error())
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(eReqBody, cReqBody) {
|
||||
b, _ := json.Marshal(cReqBody)
|
||||
return fmt.Errorf("expected %s body to be %s, got %s", cTyp, string(expected.Request.ExpectedRequest.Body), string(b))
|
||||
}
|
||||
case ContentTypeFormUrlencoded:
|
||||
var eReqBody map[string][]string
|
||||
var cReqBody map[string][]string
|
||||
eReqBody, err = ParseFormUrlencodedBody(expected.Request.ExpectedRequest.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse ExpectedRequest body %s, %s", string(expected.Request.ExpectedRequest.Body), err.Error())
|
||||
}
|
||||
|
||||
if cReqBody, ok = cReq.Body.(map[string][]string); !ok {
|
||||
return fmt.Errorf("failed to parse CapturedRequest body %s, %s", string(cReq.Body.([]byte)), err.Error())
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(eReqBody, cReqBody) {
|
||||
return fmt.Errorf("expected %s body to be %s, got %s", cTyp, string(expected.Request.ExpectedRequest.Body), string(cReq.Body.([]byte)))
|
||||
}
|
||||
case ContentTypeMultipartForm:
|
||||
var eReqBody map[string][]string
|
||||
var cReqBody map[string][]string
|
||||
|
||||
eReqBody, err = ParseMultipartFormBody(expected.Request.ExpectedRequest.Body, eParams["boundary"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse ExpectedRequest body %s, %s", string(expected.Request.ExpectedRequest.Body), err.Error())
|
||||
}
|
||||
if cReqBody, ok = cReq.Body.(map[string][]string); !ok {
|
||||
return fmt.Errorf("failed to parse CapturedRequest body %s, %s", string(cReq.Body.([]byte)), err.Error())
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(eReqBody, cReqBody) {
|
||||
return fmt.Errorf("expected %s body to be %s, got %s", cTyp, string(expected.Request.ExpectedRequest.Body), string(cReq.Body.([]byte)))
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Content-Type: %s invalid or not support.", cTyp)
|
||||
}
|
||||
}
|
||||
if expected.Response.ExpectedResponse.Headers != nil {
|
||||
if cRes.Headers == nil {
|
||||
return fmt.Errorf("no headers captured, expected %v", len(expected.Request.ExpectedRequest.Headers))
|
||||
@@ -359,6 +498,7 @@ func CompareRequest(req *roundtripper.Request, cReq *roundtripper.CapturedReques
|
||||
if !strings.HasPrefix(cReq.Pod, expected.Meta.TargetBackend) {
|
||||
return fmt.Errorf("expected pod name to start with %s, got %s", expected.Meta.TargetBackend, cReq.Pod)
|
||||
}
|
||||
|
||||
} else if roundtripper.IsRedirect(cRes.StatusCode) {
|
||||
if expected.Request.RedirectRequest == nil {
|
||||
return nil
|
||||
@@ -385,6 +525,169 @@ func CompareRequest(req *roundtripper.Request, cReq *roundtripper.CapturedReques
|
||||
return nil
|
||||
}
|
||||
|
||||
func CompareResponse(cRes *roundtripper.CapturedResponse, expected Assertion) error {
|
||||
if expected.Response.ExpectedResponse.StatusCode != cRes.StatusCode {
|
||||
return fmt.Errorf("expected status code to be %d, got %d", expected.Response.ExpectedResponse.StatusCode, cRes.StatusCode)
|
||||
}
|
||||
if cRes.StatusCode == 200 {
|
||||
if len(expected.Meta.TargetNamespace) > 0 {
|
||||
if cRes.Headers["Namespace"] == nil || len(cRes.Headers["Namespace"]) == 0 {
|
||||
return fmt.Errorf("expected namespace to be %s, field not found in CaptureResponse", expected.Meta.TargetNamespace)
|
||||
}
|
||||
if expected.Meta.TargetNamespace != cRes.Headers["Namespace"][0] {
|
||||
return fmt.Errorf("expected namespace to be %s, got %s", expected.Meta.TargetNamespace, cRes.Headers["Namespace"][0])
|
||||
}
|
||||
}
|
||||
|
||||
if len(expected.Meta.TargetBackend) > 0 {
|
||||
if cRes.Headers["Pod"] == nil || len(cRes.Headers["Pod"]) == 0 {
|
||||
return fmt.Errorf("expected pod to be %s, field not found in CaptureResponse", expected.Meta.TargetBackend)
|
||||
}
|
||||
if !strings.HasPrefix(cRes.Headers["Pod"][0], expected.Meta.TargetBackend) {
|
||||
return fmt.Errorf("expected pod to be %s, got %s", expected.Meta.TargetBackend, cRes.Headers["Pod"][0])
|
||||
}
|
||||
}
|
||||
|
||||
if expected.Response.ExpectedResponse.Headers != nil {
|
||||
if cRes.Headers == nil {
|
||||
return fmt.Errorf("no headers captured, expected %v", len(expected.Response.ExpectedResponse.Headers))
|
||||
}
|
||||
for name, val := range cRes.Headers {
|
||||
cRes.Headers[strings.ToLower(name)] = val
|
||||
}
|
||||
for name, expectedVal := range expected.Response.ExpectedResponse.Headers {
|
||||
actualVal, ok := cRes.Headers[strings.ToLower(name)]
|
||||
if !ok {
|
||||
return fmt.Errorf("expected %s header to be set, actual headers: %v", name, cRes.Headers)
|
||||
} else if strings.Join(actualVal, ",") != expectedVal {
|
||||
return fmt.Errorf("expected %s header to be set to %s, got %s", name, expectedVal, strings.Join(actualVal, ","))
|
||||
}
|
||||
}
|
||||
}
|
||||
if expected.Response.ExpectedResponse.Body != nil && len(expected.Response.ExpectedResponse.Body) > 0 {
|
||||
// 对ExpectedResponse.Body做断言时,必须指定ExpectedResponse.ContentType
|
||||
if len(expected.Response.ExpectedResponse.ContentType) == 0 {
|
||||
return fmt.Errorf("ExpectedResponse.ContentType should not be empty since ExpectedResponse.Body is set")
|
||||
}
|
||||
|
||||
if cRes.Headers["Content-Type"] == nil || len(cRes.Headers["Content-Type"]) == 0 {
|
||||
cRes.Headers["Content-Type"] = []string{expected.Response.ExpectedResponse.ContentType}
|
||||
}
|
||||
|
||||
eTyp, eParams, err := mime.ParseMediaType(expected.Response.ExpectedResponse.ContentType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ExpectedResponse Content-Type: %s failed to parse: %s", expected.Response.ExpectedResponse.ContentType, err.Error())
|
||||
}
|
||||
cTyp, cParams, err := mime.ParseMediaType(cRes.Headers["Content-Type"][0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("CapturedResponse Content-Type: %s failed to parse: %s", cRes.Headers["Content-Type"][0], err.Error())
|
||||
}
|
||||
|
||||
if eTyp != cTyp {
|
||||
return fmt.Errorf("expected %s Content-Type to be set, got %s", expected.Response.ExpectedResponse.ContentType, cRes.Headers["Content-Type"][0])
|
||||
}
|
||||
|
||||
switch cTyp {
|
||||
case ContentTypeTextPlain:
|
||||
if !bytes.Equal(expected.Response.ExpectedResponse.Body, cRes.Body) {
|
||||
return fmt.Errorf("expected %s body to be %s, got %s##", cTyp, string(expected.Response.ExpectedResponse.Body), string(cRes.Body))
|
||||
}
|
||||
case ContentTypeApplicationJson:
|
||||
eResBody := make(map[string]interface{})
|
||||
cResBody := make(map[string]interface{})
|
||||
err := json.Unmarshal(expected.Response.ExpectedResponse.Body, &eResBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshall ExpectedResponse body %s, %s", string(expected.Response.ExpectedResponse.Body), err.Error())
|
||||
}
|
||||
err = json.Unmarshal(cRes.Body, &cResBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshall CapturedResponse body %s, %s", string(cRes.Body), err.Error())
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(eResBody, cResBody) {
|
||||
return fmt.Errorf("expected %s body to be %s, got %s", cTyp, string(expected.Response.ExpectedResponse.Body), string(cRes.Body))
|
||||
}
|
||||
case ContentTypeFormUrlencoded:
|
||||
eResBody, err := ParseFormUrlencodedBody(expected.Response.ExpectedResponse.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse ExpectedResponse body %s, %s", string(expected.Response.ExpectedResponse.Body), err.Error())
|
||||
}
|
||||
cResBody, err := ParseFormUrlencodedBody(cRes.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse CapturedResponse body %s, %s", string(cRes.Body), err.Error())
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(eResBody, cResBody) {
|
||||
return fmt.Errorf("expected %s body to be %s, got %s", cTyp, string(expected.Response.ExpectedResponse.Body), string(cRes.Body))
|
||||
}
|
||||
case ContentTypeMultipartForm:
|
||||
eResBody, err := ParseMultipartFormBody(expected.Response.ExpectedResponse.Body, eParams["boundary"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse ExpectedResponse body %s, %s", string(expected.Response.ExpectedResponse.Body), err.Error())
|
||||
}
|
||||
cResBody, err := ParseMultipartFormBody(cRes.Body, cParams["boundary"])
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse CapturedResponse body %s, %s", string(cRes.Body), err.Error())
|
||||
}
|
||||
if !reflect.DeepEqual(eResBody, cResBody) {
|
||||
return fmt.Errorf("expected %s body to be %s, got %s", cTyp, string(expected.Response.ExpectedResponse.Body), string(cRes.Body))
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Content-Type: %s invalid or not support.", cTyp)
|
||||
}
|
||||
}
|
||||
if len(expected.Response.ExpectedResponse.AbsentHeaders) > 0 {
|
||||
for name, val := range cRes.Headers {
|
||||
cRes.Headers[strings.ToLower(name)] = val
|
||||
}
|
||||
|
||||
for _, name := range expected.Response.ExpectedResponse.AbsentHeaders {
|
||||
val, ok := cRes.Headers[strings.ToLower(name)]
|
||||
if ok {
|
||||
return fmt.Errorf("expected %s header to not be set, got %s", name, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func ParseFormUrlencodedBody(body []byte) (map[string][]string, error) {
|
||||
ret := make(map[string][]string)
|
||||
kvs, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for k, vs := range kvs {
|
||||
ret[k] = vs
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
func ParseMultipartFormBody(body []byte, boundary string) (map[string][]string, error) {
|
||||
ret := make(map[string][]string)
|
||||
mr := multipart.NewReader(bytes.NewReader(body), boundary)
|
||||
for {
|
||||
p, err := mr.NextPart()
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
formName := p.FormName()
|
||||
fileName := p.FileName()
|
||||
if formName == "" || fileName != "" {
|
||||
continue
|
||||
}
|
||||
formValue, err := io.ReadAll(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret[formName] = append(ret[formName], string(formValue))
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// Get User-defined test case name or generate from expected response to a given request.
|
||||
func (er *Assertion) GetTestCaseName(i int) string {
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ limitations under the License.
|
||||
package roundtripper
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
@@ -41,6 +42,8 @@ type Request struct {
|
||||
Protocol string
|
||||
Method string
|
||||
Headers map[string][]string
|
||||
Body []byte
|
||||
ContentType string
|
||||
UnfollowRedirect bool
|
||||
TLSConfig *TLSConfig
|
||||
}
|
||||
@@ -70,14 +73,14 @@ type ClientKeyPair struct {
|
||||
// CapturedRequest contains request metadata captured from an echoserver
|
||||
// response.
|
||||
type CapturedRequest struct {
|
||||
Path string `json:"path"`
|
||||
Host string `json:"host"`
|
||||
Method string `json:"method"`
|
||||
Protocol string `json:"proto"`
|
||||
Headers map[string][]string `json:"headers"`
|
||||
|
||||
Namespace string `json:"namespace"`
|
||||
Pod string `json:"pod"`
|
||||
Path string `json:"path"`
|
||||
Host string `json:"host"`
|
||||
Method string `json:"method"`
|
||||
Protocol string `json:"proto"`
|
||||
Headers map[string][]string `json:"headers"`
|
||||
Body interface{} `json:"body"`
|
||||
Namespace string `json:"namespace"`
|
||||
Pod string `json:"pod"`
|
||||
}
|
||||
|
||||
// RedirectRequest contains a follow up request metadata captured from a redirect
|
||||
@@ -95,6 +98,7 @@ type CapturedResponse struct {
|
||||
ContentLength int64
|
||||
Protocol string
|
||||
Headers map[string][]string
|
||||
Body []byte
|
||||
RedirectRequest *RedirectRequest
|
||||
}
|
||||
|
||||
@@ -112,7 +116,7 @@ type DefaultRoundTripper struct {
|
||||
func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedRequest, *CapturedResponse, error) {
|
||||
cReq := &CapturedRequest{}
|
||||
client := &http.Client{}
|
||||
|
||||
cRes := &CapturedResponse{}
|
||||
if request.UnfollowRedirect {
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
@@ -171,6 +175,11 @@ func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedReques
|
||||
}
|
||||
}
|
||||
|
||||
if request.Body != nil {
|
||||
req.Header.Add("Content-Type", string(request.ContentType))
|
||||
req.Body = io.NopCloser(bytes.NewReader(request.Body))
|
||||
}
|
||||
|
||||
if d.Debug {
|
||||
var dump []byte
|
||||
dump, err = httputil.DumpRequestOut(req, true)
|
||||
@@ -198,21 +207,25 @@ func (d *DefaultRoundTripper) CaptureRoundTrip(request Request) (*CapturedReques
|
||||
fmt.Printf("Received Response:\n%s\n\n", formatDump(dump, "< "))
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unexpected error reading response body: %w", err)
|
||||
}
|
||||
|
||||
// we cannot assume the response is JSON
|
||||
if resp.Header.Get("Content-type") == "application/json" {
|
||||
if resp.Header.Get("Content-Type") == "application/json" {
|
||||
err = json.Unmarshal(body, cReq)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unexpected error reading response: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
cRes := &CapturedResponse{
|
||||
cRes = &CapturedResponse{
|
||||
StatusCode: resp.StatusCode,
|
||||
ContentLength: resp.ContentLength,
|
||||
Protocol: resp.Proto,
|
||||
Headers: resp.Header,
|
||||
Body: body,
|
||||
}
|
||||
|
||||
if IsRedirect(resp.StatusCode) {
|
||||
|
||||
Reference in New Issue
Block a user