Adding a simpler interface for the HTTP request library. (#8862)
This commit is contained in:
parent
79a311d3be
commit
12c9f00931
18 changed files with 954 additions and 102 deletions
1
test/e2e/framework/httpexpect/README.md
Normal file
1
test/e2e/framework/httpexpect/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
This module is based in the deprecated library `github.com/gavv/httpexpect`, and contains slightly adaptations.
|
||||
36
test/e2e/framework/httpexpect/array.go
Normal file
36
test/e2e/framework/httpexpect/array.go
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package httpexpect
|
||||
|
||||
// Array provides methods to inspect attached []interface{} object
|
||||
// (Go representation of JSON array).
|
||||
type Array struct {
|
||||
chain chain
|
||||
value []interface{}
|
||||
}
|
||||
|
||||
// Iter returns a new slice of Values attached to array elements.
|
||||
func (a *Array) Iter() []Value {
|
||||
if a.chain.failed() {
|
||||
return []Value{}
|
||||
}
|
||||
ret := []Value{}
|
||||
for n := range a.value {
|
||||
ret = append(ret, Value{a.chain, a.value[n]})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
54
test/e2e/framework/httpexpect/chain.go
Normal file
54
test/e2e/framework/httpexpect/chain.go
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package httpexpect
|
||||
|
||||
type chain struct {
|
||||
reporter Reporter
|
||||
failbit bool
|
||||
}
|
||||
|
||||
func makeChain(reporter Reporter) chain {
|
||||
return chain{reporter, false}
|
||||
}
|
||||
|
||||
func (c *chain) failed() bool {
|
||||
return c.failbit
|
||||
}
|
||||
|
||||
func (c *chain) fail(message string, args ...interface{}) {
|
||||
if c.failbit {
|
||||
return
|
||||
}
|
||||
c.failbit = true
|
||||
c.reporter.Errorf(message, args...)
|
||||
}
|
||||
|
||||
func (c *chain) reset() {
|
||||
c.failbit = false
|
||||
}
|
||||
|
||||
func (c *chain) assertFailed(r Reporter) {
|
||||
if !c.failbit {
|
||||
r.Errorf("expected chain is failed, but it's ok")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *chain) assertOK(r Reporter) {
|
||||
if c.failbit {
|
||||
r.Errorf("expected chain is ok, but it's failed")
|
||||
}
|
||||
}
|
||||
29
test/e2e/framework/httpexpect/cookie.go
Normal file
29
test/e2e/framework/httpexpect/cookie.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package httpexpect
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Cookie provides methods to inspect attached http.Cookie value.
|
||||
type Cookie struct {
|
||||
chain chain
|
||||
value *http.Cookie
|
||||
}
|
||||
|
||||
func (c *Cookie) Raw() *http.Cookie {
|
||||
return c.value
|
||||
}
|
||||
37
test/e2e/framework/httpexpect/match.go
Normal file
37
test/e2e/framework/httpexpect/match.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package httpexpect
|
||||
|
||||
// Match provides methods to inspect attached regexp match results.
|
||||
type Match struct {
|
||||
chain chain
|
||||
submatches []string
|
||||
names map[string]int
|
||||
}
|
||||
|
||||
func makeMatch(chain chain, submatches []string, names []string) *Match {
|
||||
if submatches == nil {
|
||||
submatches = []string{}
|
||||
}
|
||||
namemap := map[string]int{}
|
||||
for n, name := range names {
|
||||
if name != "" {
|
||||
namemap[name] = n
|
||||
}
|
||||
}
|
||||
return &Match{chain, submatches, namemap}
|
||||
}
|
||||
111
test/e2e/framework/httpexpect/object.go
Normal file
111
test/e2e/framework/httpexpect/object.go
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package httpexpect
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/yudai/gojsondiff"
|
||||
"github.com/yudai/gojsondiff/formatter"
|
||||
)
|
||||
|
||||
// Object provides methods to inspect attached map[string]interface{} object
|
||||
// (Go representation of JSON object).
|
||||
type Object struct {
|
||||
chain chain
|
||||
value map[string]interface{}
|
||||
}
|
||||
|
||||
func (o *Object) ValueEqual(key string, value interface{}) *Object {
|
||||
if !o.containsKey(key) {
|
||||
o.chain.fail("\nexpected object containing key '%s', but got:\n%s",
|
||||
key, dumpValue(o.value))
|
||||
return o
|
||||
}
|
||||
expected, ok := canonValue(&o.chain, value)
|
||||
if !ok {
|
||||
return o
|
||||
}
|
||||
if !reflect.DeepEqual(expected, o.value[key]) {
|
||||
o.chain.fail("\nexpected value for key '%s' equal to:\n%s\n\nbut got:\n%s\n\ndiff:\n%s",
|
||||
key,
|
||||
dumpValue(expected),
|
||||
dumpValue(o.value[key]),
|
||||
diffValues(expected, o.value[key]))
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *Object) ContainsKey(key string) *Object {
|
||||
if !o.containsKey(key) {
|
||||
o.chain.fail("\nexpected object containing key '%s', but got:\n%s",
|
||||
key,
|
||||
dumpValue(o.value))
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *Object) NotContainsKey(key string) *Object {
|
||||
if o.containsKey(key) {
|
||||
o.chain.fail("\nexpected object not containing key '%s', but got:\n%s",
|
||||
key, dumpValue(o.value))
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *Object) containsKey(key string) bool {
|
||||
for k := range o.value {
|
||||
if k == key {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func diffValues(expected, actual interface{}) string {
|
||||
differ := gojsondiff.New()
|
||||
|
||||
var diff gojsondiff.Diff
|
||||
|
||||
if ve, ok := expected.(map[string]interface{}); ok {
|
||||
if va, ok := actual.(map[string]interface{}); ok {
|
||||
diff = differ.CompareObjects(ve, va)
|
||||
} else {
|
||||
return " (unavailable)"
|
||||
}
|
||||
} else if ve, ok := expected.([]interface{}); ok {
|
||||
if va, ok := actual.([]interface{}); ok {
|
||||
diff = differ.CompareArrays(ve, va)
|
||||
} else {
|
||||
return " (unavailable)"
|
||||
}
|
||||
} else {
|
||||
return " (unavailable)"
|
||||
}
|
||||
|
||||
config := formatter.AsciiFormatterConfig{
|
||||
ShowArrayIndex: true,
|
||||
}
|
||||
f := formatter.NewAsciiFormatter(expected, config)
|
||||
|
||||
str, err := f.Format(diff)
|
||||
if err != nil {
|
||||
return " (unavailable)"
|
||||
}
|
||||
|
||||
return "--- expected\n+++ actual\n" + str
|
||||
}
|
||||
48
test/e2e/framework/httpexpect/reporter.go
Normal file
48
test/e2e/framework/httpexpect/reporter.go
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package httpexpect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/onsi/ginkgo/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// Reporter is used to report failures.
|
||||
// testing.TB, AssertReporter, and RequireReporter implement this interface.
|
||||
type Reporter interface {
|
||||
// Errorf reports failure.
|
||||
// Allowed to return normally or terminate test using t.FailNow().
|
||||
Errorf(message string, args ...interface{})
|
||||
}
|
||||
|
||||
// AssertReporter implements Reporter interface using `testify/assert'
|
||||
// package. Failures are non-fatal with this reporter.
|
||||
type AssertReporter struct {
|
||||
backend *assert.Assertions
|
||||
}
|
||||
|
||||
// NewAssertReporter returns a new AssertReporter object.
|
||||
func NewAssertReporter() *AssertReporter {
|
||||
return &AssertReporter{assert.New(ginkgo.GinkgoT())}
|
||||
}
|
||||
|
||||
// Errorf implements Reporter.Errorf.
|
||||
func (r *AssertReporter) Errorf(message string, args ...interface{}) {
|
||||
r.backend.Fail(fmt.Sprintf(message, args...))
|
||||
}
|
||||
176
test/e2e/framework/httpexpect/request.go
Normal file
176
test/e2e/framework/httpexpect/request.go
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package httpexpect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
|
||||
type HTTPRequest struct {
|
||||
chain chain
|
||||
reporter Reporter
|
||||
baseURL string
|
||||
client *http.Client
|
||||
query url.Values
|
||||
Request *http.Request
|
||||
HTTPResponse *HTTPResponse
|
||||
}
|
||||
|
||||
// NewRequest returns an HTTPRequest object.
|
||||
func NewRequest(baseURL string, client *http.Client, reporter Reporter) *HTTPRequest {
|
||||
response := NewResponse(reporter)
|
||||
return &HTTPRequest{
|
||||
baseURL: baseURL,
|
||||
client: client,
|
||||
reporter: reporter,
|
||||
chain: makeChain(reporter),
|
||||
HTTPResponse: response,
|
||||
}
|
||||
}
|
||||
|
||||
// GET creates a new HTTP request with GET method.
|
||||
func (h *HTTPRequest) GET(rpath string) *HTTPRequest {
|
||||
if h.chain.failed() {
|
||||
return h
|
||||
}
|
||||
return h.DoRequest("GET", rpath)
|
||||
}
|
||||
|
||||
// DoRequest creates a new HTTP request object.
|
||||
func (h *HTTPRequest) DoRequest(method, rpath string) *HTTPRequest {
|
||||
uri, err := url.Parse(h.baseURL)
|
||||
if err != nil {
|
||||
h.chain.fail(err.Error())
|
||||
}
|
||||
|
||||
var request *http.Request
|
||||
uri.Path = path.Join(uri.Path, rpath)
|
||||
if request, err = http.NewRequest(method, uri.String(), nil); err != nil {
|
||||
h.chain.fail(err.Error())
|
||||
}
|
||||
|
||||
h.Request = request
|
||||
return h
|
||||
}
|
||||
|
||||
// Expect executes the request and returns an HTTP response.
|
||||
func (h *HTTPRequest) Expect() *HTTPResponse {
|
||||
if h.query != nil {
|
||||
h.Request.URL.RawQuery = h.query.Encode()
|
||||
}
|
||||
|
||||
response, err := h.client.Do(h.Request)
|
||||
if err != nil {
|
||||
h.chain.fail(err.Error())
|
||||
}
|
||||
|
||||
h.HTTPResponse.Response = response // set the HTTP response
|
||||
|
||||
var content []byte
|
||||
if content, err = getContent(response); err != nil {
|
||||
h.chain.fail(err.Error())
|
||||
}
|
||||
// set content and cookies from HTTPResponse
|
||||
h.HTTPResponse.content = content
|
||||
h.HTTPResponse.cookies = h.HTTPResponse.Response.Cookies()
|
||||
return h.HTTPResponse
|
||||
}
|
||||
|
||||
// WithURL sets the request URL appending paths when already exist.
|
||||
func (h *HTTPRequest) WithURL(urlStr string) *HTTPRequest {
|
||||
if h.chain.failed() {
|
||||
return h
|
||||
}
|
||||
if u, err := url.Parse(urlStr); err != nil {
|
||||
h.chain.fail(err.Error())
|
||||
} else {
|
||||
u.Path = path.Join(h.Request.URL.Path, u.Path)
|
||||
h.Request.URL = u
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// WithHeader adds given header to request.
|
||||
func (h *HTTPRequest) WithHeader(key, value string) *HTTPRequest {
|
||||
if h.chain.failed() {
|
||||
return h
|
||||
}
|
||||
switch http.CanonicalHeaderKey(key) {
|
||||
case "Host":
|
||||
h.Request.Host = value
|
||||
default:
|
||||
h.Request.Header.Add(key, value)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// WithCookies adds given cookies to request.
|
||||
func (h *HTTPRequest) WithCookies(cookies map[string]string) *HTTPRequest {
|
||||
if h.chain.failed() {
|
||||
return h
|
||||
}
|
||||
for k, v := range cookies {
|
||||
h.WithCookie(k, v)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// WithCookie adds given single cookie to request.
|
||||
func (h *HTTPRequest) WithCookie(k, v string) *HTTPRequest {
|
||||
if h.chain.failed() {
|
||||
return h
|
||||
}
|
||||
h.Request.AddCookie(&http.Cookie{Name: k, Value: v})
|
||||
return h
|
||||
}
|
||||
|
||||
// WithBasicAuth sets the request's Authorization header to use HTTP
|
||||
// Basic Authentication with the provided username and password.
|
||||
//
|
||||
// With HTTP Basic Authentication the provided username and password
|
||||
// are not encrypted.
|
||||
func (h *HTTPRequest) WithBasicAuth(username, password string) *HTTPRequest {
|
||||
if h.chain.failed() {
|
||||
return h
|
||||
}
|
||||
h.Request.SetBasicAuth(username, password)
|
||||
return h
|
||||
}
|
||||
|
||||
// WithQuery adds query parameter to request URL.
|
||||
func (h *HTTPRequest) WithQuery(key string, value interface{}) *HTTPRequest {
|
||||
if h.chain.failed() {
|
||||
return h
|
||||
}
|
||||
if h.query == nil {
|
||||
h.query = make(url.Values)
|
||||
}
|
||||
h.query.Add(key, fmt.Sprint(value))
|
||||
return h
|
||||
}
|
||||
|
||||
// getContent returns the content from the body response.
|
||||
func getContent(resp *http.Response) ([]byte, error) {
|
||||
if resp.Body == nil {
|
||||
return []byte{}, nil
|
||||
}
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
273
test/e2e/framework/httpexpect/response.go
Normal file
273
test/e2e/framework/httpexpect/response.go
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package httpexpect
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"mime"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// StatusRange is enum for response status ranges.
|
||||
type StatusRange int
|
||||
|
||||
const (
|
||||
// Status1xx defines "Informational" status codes.
|
||||
Status1xx StatusRange = 100
|
||||
|
||||
// Status2xx defines "Success" status codes.
|
||||
Status2xx StatusRange = 200
|
||||
|
||||
// Status3xx defines "Redirection" status codes.
|
||||
Status3xx StatusRange = 300
|
||||
|
||||
// Status4xx defines "Client Error" status codes.
|
||||
Status4xx StatusRange = 400
|
||||
|
||||
// Status5xx defines "Server Error" status codes.
|
||||
Status5xx StatusRange = 500
|
||||
)
|
||||
|
||||
type HTTPResponse struct {
|
||||
chain chain
|
||||
content []byte
|
||||
cookies []*http.Cookie
|
||||
Response *http.Response
|
||||
}
|
||||
|
||||
// NewResponse returns an empty HTTPResponse object.
|
||||
func NewResponse(reporter Reporter) *HTTPResponse {
|
||||
return &HTTPResponse{
|
||||
chain: makeChain(reporter),
|
||||
}
|
||||
}
|
||||
|
||||
// Body returns the body of the response.
|
||||
func (r *HTTPResponse) Body() *String {
|
||||
return &String{value: string(r.content)}
|
||||
}
|
||||
|
||||
// Raw returns the raw http response.
|
||||
func (r *HTTPResponse) Raw() *http.Response {
|
||||
return r.Response
|
||||
}
|
||||
|
||||
// Status compare the actual http response with the expected one raising and error
|
||||
// if they don't match.
|
||||
func (r *HTTPResponse) Status(status int) *HTTPResponse {
|
||||
if r.chain.failed() {
|
||||
return r
|
||||
}
|
||||
r.checkEqual("status", statusCodeText(status), statusCodeText(r.Response.StatusCode))
|
||||
return r
|
||||
}
|
||||
|
||||
// ContentEncoding succeeds if response has exactly given Content-Encoding
|
||||
func (r *HTTPResponse) ContentEncoding(encoding ...string) *HTTPResponse {
|
||||
if r.chain.failed() {
|
||||
return r
|
||||
}
|
||||
r.checkEqual("\"Content-Encoding\" header", encoding, r.Response.Header["Content-Encoding"])
|
||||
return r
|
||||
}
|
||||
|
||||
// ContentType succeeds if response contains Content-Type header with given
|
||||
// media type and charset.
|
||||
func (r *HTTPResponse) ContentType(mediaType string, charset ...string) *HTTPResponse {
|
||||
r.checkContentType(mediaType, charset...)
|
||||
return r
|
||||
}
|
||||
|
||||
// Cookies returns a new Array object with all cookie names set by this response.
|
||||
// Returned Array contains a String value for every cookie name.
|
||||
func (r *HTTPResponse) Cookies() *Array {
|
||||
if r.chain.failed() {
|
||||
return &Array{r.chain, nil}
|
||||
}
|
||||
names := []interface{}{}
|
||||
for _, c := range r.cookies {
|
||||
names = append(names, c.Name)
|
||||
}
|
||||
return &Array{r.chain, names}
|
||||
}
|
||||
|
||||
// Cookie returns a new Cookie object that may be used to inspect given cookie
|
||||
// set by this response.
|
||||
func (r *HTTPResponse) Cookie(name string) *Cookie {
|
||||
if r.chain.failed() {
|
||||
return &Cookie{r.chain, nil}
|
||||
}
|
||||
names := []string{}
|
||||
for _, c := range r.cookies {
|
||||
if c.Name == name {
|
||||
return &Cookie{r.chain, c}
|
||||
}
|
||||
names = append(names, c.Name)
|
||||
}
|
||||
r.chain.fail("\nexpected response with cookie:\n %q\n\nbut got only cookies:\n%s", name, dumpValue(names))
|
||||
return &Cookie{r.chain, nil}
|
||||
}
|
||||
|
||||
// Headers returns a new Object that may be used to inspect header map.
|
||||
func (r *HTTPResponse) Headers() *Object {
|
||||
var value map[string]interface{}
|
||||
if !r.chain.failed() {
|
||||
value, _ = canonMap(&r.chain, r.Response.Header)
|
||||
}
|
||||
return &Object{r.chain, value}
|
||||
}
|
||||
|
||||
// Header returns a new String object that may be used to inspect given header.
|
||||
func (r *HTTPResponse) Header(header string) *String {
|
||||
return &String{chain: r.chain, value: r.Response.Header.Get(header)}
|
||||
}
|
||||
|
||||
func canonMap(chain *chain, in interface{}) (map[string]interface{}, bool) {
|
||||
var out map[string]interface{}
|
||||
data, ok := canonValue(chain, in)
|
||||
if ok {
|
||||
out, ok = data.(map[string]interface{})
|
||||
if !ok {
|
||||
chain.fail("expected map, got %v", out)
|
||||
}
|
||||
}
|
||||
return out, ok
|
||||
}
|
||||
|
||||
func canonValue(chain *chain, in interface{}) (interface{}, bool) {
|
||||
b, err := json.Marshal(in)
|
||||
if err != nil {
|
||||
chain.fail(err.Error())
|
||||
return nil, false
|
||||
}
|
||||
|
||||
var out interface{}
|
||||
if err := json.Unmarshal(b, &out); err != nil {
|
||||
chain.fail(err.Error())
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return out, true
|
||||
}
|
||||
|
||||
// StatusRange succeeds if response status belongs to given range.
|
||||
func (r *HTTPResponse) StatusRange(rn StatusRange) *HTTPResponse {
|
||||
if r.chain.failed() {
|
||||
return r
|
||||
}
|
||||
status := statusCodeText(r.Response.StatusCode)
|
||||
|
||||
actual := statusRangeText(r.Response.StatusCode)
|
||||
expected := statusRangeText(int(rn))
|
||||
|
||||
if actual == "" || actual != expected {
|
||||
if actual == "" {
|
||||
r.chain.fail("\nexpected status from range:\n %q\n\nbut got:\n %q",
|
||||
expected, status)
|
||||
} else {
|
||||
r.chain.fail("\nexpected status from range:\n %q\n\nbut got:\n %q (%q)",
|
||||
expected, actual, status)
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func statusCodeText(code int) string {
|
||||
if s := http.StatusText(code); s != "" {
|
||||
return strconv.Itoa(code) + " " + s
|
||||
}
|
||||
return strconv.Itoa(code)
|
||||
}
|
||||
|
||||
func statusRangeText(code int) string {
|
||||
switch {
|
||||
case code >= 100 && code < 200:
|
||||
return "1xx Informational"
|
||||
case code >= 200 && code < 300:
|
||||
return "2xx Success"
|
||||
case code >= 300 && code < 400:
|
||||
return "3xx Redirection"
|
||||
case code >= 400 && code < 500:
|
||||
return "4xx Client Error"
|
||||
case code >= 500 && code < 600:
|
||||
return "5xx Server Error"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (r *HTTPResponse) checkContentType(expectedType string, expectedCharset ...string) bool {
|
||||
if r.chain.failed() {
|
||||
return false
|
||||
}
|
||||
|
||||
contentType := r.Response.Header.Get("Content-Type")
|
||||
|
||||
if expectedType == "" && len(expectedCharset) == 0 {
|
||||
if contentType == "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
mediaType, params, err := mime.ParseMediaType(contentType)
|
||||
if err != nil {
|
||||
r.chain.fail("\ngot invalid \"Content-Type\" header %q", contentType)
|
||||
return false
|
||||
}
|
||||
|
||||
if mediaType != expectedType {
|
||||
r.chain.fail("\nexpected \"Content-Type\" header with %q media type,"+
|
||||
"\nbut got %q", expectedType, mediaType)
|
||||
return false
|
||||
}
|
||||
|
||||
charset := params["charset"]
|
||||
|
||||
if len(expectedCharset) == 0 {
|
||||
if charset != "" && !strings.EqualFold(charset, "utf-8") {
|
||||
r.chain.fail("\nexpected \"Content-Type\" header with \"utf-8\" or empty charset,"+
|
||||
"\nbut got %q", charset)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
if !strings.EqualFold(charset, expectedCharset[0]) {
|
||||
r.chain.fail("\nexpected \"Content-Type\" header with %q charset,"+
|
||||
"\nbut got %q", expectedCharset[0], charset)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *HTTPResponse) checkEqual(what string, expected, actual interface{}) {
|
||||
if !reflect.DeepEqual(expected, actual) {
|
||||
r.chain.fail("\nexpected %s equal to:\n%s\n\nbut got:\n%s",
|
||||
what, dumpValue(expected), dumpValue(actual))
|
||||
}
|
||||
}
|
||||
|
||||
func dumpValue(value interface{}) string {
|
||||
b, err := json.MarshalIndent(value, " ", " ")
|
||||
if err != nil {
|
||||
return " " + fmt.Sprintf("%#v", value)
|
||||
}
|
||||
return " " + string(b)
|
||||
}
|
||||
120
test/e2e/framework/httpexpect/string.go
Normal file
120
test/e2e/framework/httpexpect/string.go
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package httpexpect
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// String provides methods to inspect attached string value
|
||||
// (Go representation of JSON string).
|
||||
type String struct {
|
||||
chain chain
|
||||
value string
|
||||
}
|
||||
|
||||
// Raw returns underlying value attached to String.
|
||||
// This is the value originally passed to NewString.
|
||||
func (s *String) Raw() string {
|
||||
return s.value
|
||||
}
|
||||
|
||||
// Empty succeeds if string is empty.
|
||||
func (s *String) Empty() *String {
|
||||
return s.Equal("")
|
||||
}
|
||||
|
||||
// NotEmpty succeeds if string is non-empty.
|
||||
func (s *String) NotEmpty() *String {
|
||||
return s.NotEqual("")
|
||||
}
|
||||
|
||||
// Equal succeeds if string is equal to given Go string.
|
||||
func (s *String) Equal(value string) *String {
|
||||
if !(s.value == value) {
|
||||
s.chain.fail("\nexpected string equal to:\n %q\n\nbut got:\n %q", value, s.value)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// NotEqual succeeds if string is not equal to given Go string.
|
||||
func (s *String) NotEqual(value string) *String {
|
||||
if !(s.value != value) {
|
||||
s.chain.fail("\nexpected string not equal to:\n %q", value)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Contains succeeds if string contains given Go string as a substring.
|
||||
func (s *String) Contains(value string) *String {
|
||||
if !strings.Contains(s.value, value) {
|
||||
s.chain.fail(
|
||||
"\nexpected string containing substring:\n %q\n\nbut got:\n %q",
|
||||
value, s.value)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// NotContains succeeds if string doesn't contain Go string as a substring.
|
||||
func (s *String) NotContains(value string) *String {
|
||||
if strings.Contains(s.value, value) {
|
||||
s.chain.fail("\nexpected string not containing substring:\n %q\n\nbut got:\n %q", value, s.value)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// ContainsFold succeeds if string contains given Go string as a substring after
|
||||
// applying Unicode case-folding (so it's a case-insensitive match).
|
||||
func (s *String) ContainsFold(value string) *String {
|
||||
if !strings.Contains(strings.ToLower(s.value), strings.ToLower(value)) {
|
||||
s.chain.fail("\nexpected string containing substring (case-insensitive):\n %q"+"\n\nbut got:\n %q", value, s.value)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// NotContainsFold succeeds if string doesn't contain given Go string as a substring
|
||||
// after applying Unicode case-folding (so it's a case-insensitive match).
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// str := NewString(t, "Hello")
|
||||
// str.NotContainsFold("BYE")
|
||||
func (s *String) NotContainsFold(value string) *String {
|
||||
if strings.Contains(strings.ToLower(s.value), strings.ToLower(value)) {
|
||||
s.chain.fail("\nexpected string not containing substring (case-insensitive):\n %q"+"\n\nbut got:\n %q", value, s.value)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Match matches the string with given regexp and returns a new Match object
|
||||
// with found submatches.
|
||||
func (s *String) Match(re string) *Match {
|
||||
r, err := regexp.Compile(re)
|
||||
if err != nil {
|
||||
s.chain.fail(err.Error())
|
||||
return makeMatch(s.chain, nil, nil)
|
||||
}
|
||||
|
||||
m := r.FindStringSubmatch(s.value)
|
||||
if m == nil {
|
||||
s.chain.fail("\nexpected string matching regexp:\n `%s`\n\nbut got:\n %q", re, s.value)
|
||||
return makeMatch(s.chain, nil, nil)
|
||||
}
|
||||
|
||||
return makeMatch(s.chain, m, r.SubexpNames())
|
||||
}
|
||||
33
test/e2e/framework/httpexpect/value.go
Normal file
33
test/e2e/framework/httpexpect/value.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
Copyright 2022 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package httpexpect
|
||||
|
||||
// Value provides methods to inspect attached interface{} object
|
||||
// (Go representation of arbitrary JSON value) and cast it to
|
||||
// concrete type.
|
||||
type Value struct {
|
||||
chain chain
|
||||
value interface{}
|
||||
}
|
||||
|
||||
func (v *Value) String() *String {
|
||||
data, ok := v.value.(string)
|
||||
if !ok {
|
||||
v.chain.fail("\nexpected string value, but got:\n%s", dumpValue(v.value))
|
||||
}
|
||||
return &String{v.chain, data}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue