314 lines
11 KiB
Go
314 lines
11 KiB
Go
|
package ghttp
|
||
|
|
||
|
import (
|
||
|
"encoding/base64"
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"io/ioutil"
|
||
|
"net/http"
|
||
|
"net/url"
|
||
|
"reflect"
|
||
|
|
||
|
"github.com/golang/protobuf/proto"
|
||
|
. "github.com/onsi/gomega"
|
||
|
"github.com/onsi/gomega/types"
|
||
|
)
|
||
|
|
||
|
//CombineHandler takes variadic list of handlers and produces one handler
|
||
|
//that calls each handler in order.
|
||
|
func CombineHandlers(handlers ...http.HandlerFunc) http.HandlerFunc {
|
||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||
|
for _, handler := range handlers {
|
||
|
handler(w, req)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//VerifyRequest returns a handler that verifies that a request uses the specified method to connect to the specified path
|
||
|
//You may also pass in an optional rawQuery string which is tested against the request's `req.URL.RawQuery`
|
||
|
//
|
||
|
//For path, you may pass in a string, in which case strict equality will be applied
|
||
|
//Alternatively you can pass in a matcher (ContainSubstring("/foo") and MatchRegexp("/foo/[a-f0-9]+") for example)
|
||
|
func VerifyRequest(method string, path interface{}, rawQuery ...string) http.HandlerFunc {
|
||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||
|
Ω(req.Method).Should(Equal(method), "Method mismatch")
|
||
|
switch p := path.(type) {
|
||
|
case types.GomegaMatcher:
|
||
|
Ω(req.URL.Path).Should(p, "Path mismatch")
|
||
|
default:
|
||
|
Ω(req.URL.Path).Should(Equal(path), "Path mismatch")
|
||
|
}
|
||
|
if len(rawQuery) > 0 {
|
||
|
values, err := url.ParseQuery(rawQuery[0])
|
||
|
Ω(err).ShouldNot(HaveOccurred(), "Expected RawQuery is malformed")
|
||
|
|
||
|
Ω(req.URL.Query()).Should(Equal(values), "RawQuery mismatch")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//VerifyContentType returns a handler that verifies that a request has a Content-Type header set to the
|
||
|
//specified value
|
||
|
func VerifyContentType(contentType string) http.HandlerFunc {
|
||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||
|
Ω(req.Header.Get("Content-Type")).Should(Equal(contentType))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//VerifyBasicAuth returns a handler that verifies the request contains a BasicAuth Authorization header
|
||
|
//matching the passed in username and password
|
||
|
func VerifyBasicAuth(username string, password string) http.HandlerFunc {
|
||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||
|
auth := req.Header.Get("Authorization")
|
||
|
Ω(auth).ShouldNot(Equal(""), "Authorization header must be specified")
|
||
|
|
||
|
decoded, err := base64.StdEncoding.DecodeString(auth[6:])
|
||
|
Ω(err).ShouldNot(HaveOccurred())
|
||
|
|
||
|
Ω(string(decoded)).Should(Equal(fmt.Sprintf("%s:%s", username, password)), "Authorization mismatch")
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//VerifyHeader returns a handler that verifies the request contains the passed in headers.
|
||
|
//The passed in header keys are first canonicalized via http.CanonicalHeaderKey.
|
||
|
//
|
||
|
//The request must contain *all* the passed in headers, but it is allowed to have additional headers
|
||
|
//beyond the passed in set.
|
||
|
func VerifyHeader(header http.Header) http.HandlerFunc {
|
||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||
|
for key, values := range header {
|
||
|
key = http.CanonicalHeaderKey(key)
|
||
|
Ω(req.Header[key]).Should(Equal(values), "Header mismatch for key: %s", key)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//VerifyHeaderKV returns a handler that verifies the request contains a header matching the passed in key and values
|
||
|
//(recall that a `http.Header` is a mapping from string (key) to []string (values))
|
||
|
//It is a convenience wrapper around `VerifyHeader` that allows you to avoid having to create an `http.Header` object.
|
||
|
func VerifyHeaderKV(key string, values ...string) http.HandlerFunc {
|
||
|
return VerifyHeader(http.Header{key: values})
|
||
|
}
|
||
|
|
||
|
//VerifyBody returns a handler that verifies that the body of the request matches the passed in byte array.
|
||
|
//It does this using Equal().
|
||
|
func VerifyBody(expectedBody []byte) http.HandlerFunc {
|
||
|
return CombineHandlers(
|
||
|
func(w http.ResponseWriter, req *http.Request) {
|
||
|
body, err := ioutil.ReadAll(req.Body)
|
||
|
req.Body.Close()
|
||
|
Ω(err).ShouldNot(HaveOccurred())
|
||
|
Ω(body).Should(Equal(expectedBody), "Body Mismatch")
|
||
|
},
|
||
|
)
|
||
|
}
|
||
|
|
||
|
//VerifyJSON returns a handler that verifies that the body of the request is a valid JSON representation
|
||
|
//matching the passed in JSON string. It does this using Gomega's MatchJSON method
|
||
|
//
|
||
|
//VerifyJSON also verifies that the request's content type is application/json
|
||
|
func VerifyJSON(expectedJSON string) http.HandlerFunc {
|
||
|
return CombineHandlers(
|
||
|
VerifyContentType("application/json"),
|
||
|
func(w http.ResponseWriter, req *http.Request) {
|
||
|
body, err := ioutil.ReadAll(req.Body)
|
||
|
req.Body.Close()
|
||
|
Ω(err).ShouldNot(HaveOccurred())
|
||
|
Ω(body).Should(MatchJSON(expectedJSON), "JSON Mismatch")
|
||
|
},
|
||
|
)
|
||
|
}
|
||
|
|
||
|
//VerifyJSONRepresenting is similar to VerifyJSON. Instead of taking a JSON string, however, it
|
||
|
//takes an arbitrary JSON-encodable object and verifies that the requests's body is a JSON representation
|
||
|
//that matches the object
|
||
|
func VerifyJSONRepresenting(object interface{}) http.HandlerFunc {
|
||
|
data, err := json.Marshal(object)
|
||
|
Ω(err).ShouldNot(HaveOccurred())
|
||
|
return CombineHandlers(
|
||
|
VerifyContentType("application/json"),
|
||
|
VerifyJSON(string(data)),
|
||
|
)
|
||
|
}
|
||
|
|
||
|
//VerifyForm returns a handler that verifies a request contains the specified form values.
|
||
|
//
|
||
|
//The request must contain *all* of the specified values, but it is allowed to have additional
|
||
|
//form values beyond the passed in set.
|
||
|
func VerifyForm(values url.Values) http.HandlerFunc {
|
||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||
|
err := r.ParseForm()
|
||
|
Ω(err).ShouldNot(HaveOccurred())
|
||
|
for key, vals := range values {
|
||
|
Ω(r.Form[key]).Should(Equal(vals), "Form mismatch for key: %s", key)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//VerifyFormKV returns a handler that verifies a request contains a form key with the specified values.
|
||
|
//
|
||
|
//It is a convenience wrapper around `VerifyForm` that lets you avoid having to create a `url.Values` object.
|
||
|
func VerifyFormKV(key string, values ...string) http.HandlerFunc {
|
||
|
return VerifyForm(url.Values{key: values})
|
||
|
}
|
||
|
|
||
|
//VerifyProtoRepresenting returns a handler that verifies that the body of the request is a valid protobuf
|
||
|
//representation of the passed message.
|
||
|
//
|
||
|
//VerifyProtoRepresenting also verifies that the request's content type is application/x-protobuf
|
||
|
func VerifyProtoRepresenting(expected proto.Message) http.HandlerFunc {
|
||
|
return CombineHandlers(
|
||
|
VerifyContentType("application/x-protobuf"),
|
||
|
func(w http.ResponseWriter, req *http.Request) {
|
||
|
body, err := ioutil.ReadAll(req.Body)
|
||
|
Ω(err).ShouldNot(HaveOccurred())
|
||
|
req.Body.Close()
|
||
|
|
||
|
expectedType := reflect.TypeOf(expected)
|
||
|
actualValuePtr := reflect.New(expectedType.Elem())
|
||
|
|
||
|
actual, ok := actualValuePtr.Interface().(proto.Message)
|
||
|
Ω(ok).Should(BeTrue(), "Message value is not a proto.Message")
|
||
|
|
||
|
err = proto.Unmarshal(body, actual)
|
||
|
Ω(err).ShouldNot(HaveOccurred(), "Failed to unmarshal protobuf")
|
||
|
|
||
|
Ω(actual).Should(Equal(expected), "ProtoBuf Mismatch")
|
||
|
},
|
||
|
)
|
||
|
}
|
||
|
|
||
|
func copyHeader(src http.Header, dst http.Header) {
|
||
|
for key, value := range src {
|
||
|
dst[key] = value
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
RespondWith returns a handler that responds to a request with the specified status code and body
|
||
|
|
||
|
Body may be a string or []byte
|
||
|
|
||
|
Also, RespondWith can be given an optional http.Header. The headers defined therein will be added to the response headers.
|
||
|
*/
|
||
|
func RespondWith(statusCode int, body interface{}, optionalHeader ...http.Header) http.HandlerFunc {
|
||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||
|
if len(optionalHeader) == 1 {
|
||
|
copyHeader(optionalHeader[0], w.Header())
|
||
|
}
|
||
|
w.WriteHeader(statusCode)
|
||
|
switch x := body.(type) {
|
||
|
case string:
|
||
|
w.Write([]byte(x))
|
||
|
case []byte:
|
||
|
w.Write(x)
|
||
|
default:
|
||
|
Ω(body).Should(BeNil(), "Invalid type for body. Should be string or []byte.")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
RespondWithPtr returns a handler that responds to a request with the specified status code and body
|
||
|
|
||
|
Unlike RespondWith, you pass RepondWithPtr a pointer to the status code and body allowing different tests
|
||
|
to share the same setup but specify different status codes and bodies.
|
||
|
|
||
|
Also, RespondWithPtr can be given an optional http.Header. The headers defined therein will be added to the response headers.
|
||
|
Since the http.Header can be mutated after the fact you don't need to pass in a pointer.
|
||
|
*/
|
||
|
func RespondWithPtr(statusCode *int, body interface{}, optionalHeader ...http.Header) http.HandlerFunc {
|
||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||
|
if len(optionalHeader) == 1 {
|
||
|
copyHeader(optionalHeader[0], w.Header())
|
||
|
}
|
||
|
w.WriteHeader(*statusCode)
|
||
|
if body != nil {
|
||
|
switch x := (body).(type) {
|
||
|
case *string:
|
||
|
w.Write([]byte(*x))
|
||
|
case *[]byte:
|
||
|
w.Write(*x)
|
||
|
default:
|
||
|
Ω(body).Should(BeNil(), "Invalid type for body. Should be string or []byte.")
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
RespondWithJSONEncoded returns a handler that responds to a request with the specified status code and a body
|
||
|
containing the JSON-encoding of the passed in object
|
||
|
|
||
|
Also, RespondWithJSONEncoded can be given an optional http.Header. The headers defined therein will be added to the response headers.
|
||
|
*/
|
||
|
func RespondWithJSONEncoded(statusCode int, object interface{}, optionalHeader ...http.Header) http.HandlerFunc {
|
||
|
data, err := json.Marshal(object)
|
||
|
Ω(err).ShouldNot(HaveOccurred())
|
||
|
|
||
|
var headers http.Header
|
||
|
if len(optionalHeader) == 1 {
|
||
|
headers = optionalHeader[0]
|
||
|
} else {
|
||
|
headers = make(http.Header)
|
||
|
}
|
||
|
if _, found := headers["Content-Type"]; !found {
|
||
|
headers["Content-Type"] = []string{"application/json"}
|
||
|
}
|
||
|
return RespondWith(statusCode, string(data), headers)
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
RespondWithJSONEncodedPtr behaves like RespondWithJSONEncoded but takes a pointer
|
||
|
to a status code and object.
|
||
|
|
||
|
This allows different tests to share the same setup but specify different status codes and JSON-encoded
|
||
|
objects.
|
||
|
|
||
|
Also, RespondWithJSONEncodedPtr can be given an optional http.Header. The headers defined therein will be added to the response headers.
|
||
|
Since the http.Header can be mutated after the fact you don't need to pass in a pointer.
|
||
|
*/
|
||
|
func RespondWithJSONEncodedPtr(statusCode *int, object interface{}, optionalHeader ...http.Header) http.HandlerFunc {
|
||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||
|
data, err := json.Marshal(object)
|
||
|
Ω(err).ShouldNot(HaveOccurred())
|
||
|
var headers http.Header
|
||
|
if len(optionalHeader) == 1 {
|
||
|
headers = optionalHeader[0]
|
||
|
} else {
|
||
|
headers = make(http.Header)
|
||
|
}
|
||
|
if _, found := headers["Content-Type"]; !found {
|
||
|
headers["Content-Type"] = []string{"application/json"}
|
||
|
}
|
||
|
copyHeader(headers, w.Header())
|
||
|
w.WriteHeader(*statusCode)
|
||
|
w.Write(data)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
//RespondWithProto returns a handler that responds to a request with the specified status code and a body
|
||
|
//containing the protobuf serialization of the provided message.
|
||
|
//
|
||
|
//Also, RespondWithProto can be given an optional http.Header. The headers defined therein will be added to the response headers.
|
||
|
func RespondWithProto(statusCode int, message proto.Message, optionalHeader ...http.Header) http.HandlerFunc {
|
||
|
return func(w http.ResponseWriter, req *http.Request) {
|
||
|
data, err := proto.Marshal(message)
|
||
|
Ω(err).ShouldNot(HaveOccurred())
|
||
|
|
||
|
var headers http.Header
|
||
|
if len(optionalHeader) == 1 {
|
||
|
headers = optionalHeader[0]
|
||
|
} else {
|
||
|
headers = make(http.Header)
|
||
|
}
|
||
|
if _, found := headers["Content-Type"]; !found {
|
||
|
headers["Content-Type"] = []string{"application/x-protobuf"}
|
||
|
}
|
||
|
copyHeader(headers, w.Header())
|
||
|
|
||
|
w.WriteHeader(statusCode)
|
||
|
w.Write(data)
|
||
|
}
|
||
|
}
|