520 lines
13 KiB
Go
520 lines
13 KiB
Go
|
package httpmock
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"io/ioutil" //nolint: staticcheck
|
||
|
"net/http"
|
||
|
"runtime"
|
||
|
"strings"
|
||
|
"sync/atomic"
|
||
|
|
||
|
"github.com/jarcoal/httpmock/internal"
|
||
|
)
|
||
|
|
||
|
var ignorePackages = map[string]bool{}
|
||
|
|
||
|
func init() {
|
||
|
IgnoreMatcherHelper()
|
||
|
}
|
||
|
|
||
|
// IgnoreMatcherHelper should be called by external helpers building
|
||
|
// [Matcher], typically in an init() function, to avoid they appear in
|
||
|
// the autogenerated [Matcher] names.
|
||
|
func IgnoreMatcherHelper(skip ...int) {
|
||
|
sk := 2
|
||
|
if len(skip) > 0 {
|
||
|
sk += skip[0]
|
||
|
}
|
||
|
if pkg := getPackage(sk); pkg != "" {
|
||
|
ignorePackages[pkg] = true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Copied from github.com/maxatome/go-testdeep/internal/trace.getPackage.
|
||
|
func getPackage(skip int) string {
|
||
|
if pc, _, _, ok := runtime.Caller(skip); ok {
|
||
|
if fn := runtime.FuncForPC(pc); fn != nil {
|
||
|
return extractPackage(fn.Name())
|
||
|
}
|
||
|
}
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
// extractPackage extracts package part from a fully qualified function name:
|
||
|
//
|
||
|
// "foo/bar/test.fn" → "foo/bar/test"
|
||
|
// "foo/bar/test.X.fn" → "foo/bar/test"
|
||
|
// "foo/bar/test.(*X).fn" → "foo/bar/test"
|
||
|
// "foo/bar/test.(*X).fn.func1" → "foo/bar/test"
|
||
|
// "weird" → ""
|
||
|
//
|
||
|
// Derived from github.com/maxatome/go-testdeep/internal/trace.SplitPackageFunc.
|
||
|
func extractPackage(fn string) string {
|
||
|
sp := strings.LastIndexByte(fn, '/')
|
||
|
if sp < 0 {
|
||
|
sp = 0 // std package
|
||
|
}
|
||
|
|
||
|
dp := strings.IndexByte(fn[sp:], '.')
|
||
|
if dp < 0 {
|
||
|
return ""
|
||
|
}
|
||
|
|
||
|
return fn[:sp+dp]
|
||
|
}
|
||
|
|
||
|
// calledFrom returns a string like "@PKG.FUNC() FILE:LINE".
|
||
|
func calledFrom(skip int) string {
|
||
|
pc := make([]uintptr, 128)
|
||
|
npc := runtime.Callers(skip+1, pc)
|
||
|
pc = pc[:npc]
|
||
|
|
||
|
frames := runtime.CallersFrames(pc)
|
||
|
|
||
|
var lastFrame runtime.Frame
|
||
|
|
||
|
for {
|
||
|
frame, more := frames.Next()
|
||
|
|
||
|
// If testing package is encountered, it is too late
|
||
|
if strings.HasPrefix(frame.Function, "testing.") {
|
||
|
break
|
||
|
}
|
||
|
lastFrame = frame
|
||
|
// Stop if httpmock is not the caller
|
||
|
if !ignorePackages[extractPackage(frame.Function)] || !more {
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if lastFrame.Line == 0 {
|
||
|
return ""
|
||
|
}
|
||
|
return fmt.Sprintf(" @%s() %s:%d",
|
||
|
lastFrame.Function, lastFrame.File, lastFrame.Line)
|
||
|
}
|
||
|
|
||
|
// MatcherFunc type is the function to use to check a [Matcher]
|
||
|
// matches an incoming request. When httpmock calls a function of this
|
||
|
// type, it is guaranteed req.Body is never nil. If req.Body is nil in
|
||
|
// the original request, it is temporarily replaced by an instance
|
||
|
// returning always [io.EOF] for each Read() call, during the call.
|
||
|
type MatcherFunc func(req *http.Request) bool
|
||
|
|
||
|
func matcherFuncOr(mfs []MatcherFunc) MatcherFunc {
|
||
|
return func(req *http.Request) bool {
|
||
|
for _, mf := range mfs {
|
||
|
rearmBody(req)
|
||
|
if mf(req) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func matcherFuncAnd(mfs []MatcherFunc) MatcherFunc {
|
||
|
if len(mfs) == 0 {
|
||
|
return nil
|
||
|
}
|
||
|
return func(req *http.Request) bool {
|
||
|
for _, mf := range mfs {
|
||
|
rearmBody(req)
|
||
|
if !mf(req) {
|
||
|
return false
|
||
|
}
|
||
|
}
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Check returns true if mf is nil, otherwise it returns mf(req).
|
||
|
func (mf MatcherFunc) Check(req *http.Request) bool {
|
||
|
return mf == nil || mf(req)
|
||
|
}
|
||
|
|
||
|
// Or combines mf and all mfs in a new [MatcherFunc]. This new
|
||
|
// [MatcherFunc] succeeds if one of mf or mfs succeeds. Note that as a
|
||
|
// a nil [MatcherFunc] is considered succeeding, if mf or one of mfs
|
||
|
// items is nil, nil is returned.
|
||
|
func (mf MatcherFunc) Or(mfs ...MatcherFunc) MatcherFunc {
|
||
|
if len(mfs) == 0 || mf == nil {
|
||
|
return mf
|
||
|
}
|
||
|
cmfs := make([]MatcherFunc, len(mfs)+1)
|
||
|
cmfs[0] = mf
|
||
|
for i, cur := range mfs {
|
||
|
if cur == nil {
|
||
|
return nil
|
||
|
}
|
||
|
cmfs[i+1] = cur
|
||
|
}
|
||
|
return matcherFuncOr(cmfs)
|
||
|
}
|
||
|
|
||
|
// And combines mf and all mfs in a new [MatcherFunc]. This new
|
||
|
// [MatcherFunc] succeeds if all of mf and mfs succeed. Note that a
|
||
|
// [MatcherFunc] also succeeds if it is nil, so if mf and all mfs
|
||
|
// items are nil, nil is returned.
|
||
|
func (mf MatcherFunc) And(mfs ...MatcherFunc) MatcherFunc {
|
||
|
if len(mfs) == 0 {
|
||
|
return mf
|
||
|
}
|
||
|
cmfs := make([]MatcherFunc, 0, len(mfs)+1)
|
||
|
if mf != nil {
|
||
|
cmfs = append(cmfs, mf)
|
||
|
}
|
||
|
for _, cur := range mfs {
|
||
|
if cur != nil {
|
||
|
cmfs = append(cmfs, cur)
|
||
|
}
|
||
|
}
|
||
|
return matcherFuncAnd(cmfs)
|
||
|
}
|
||
|
|
||
|
// Matcher type defines a match case. The zero Matcher{} corresponds
|
||
|
// to the default case. Otherwise, use [NewMatcher] or any helper
|
||
|
// building a [Matcher] like [BodyContainsBytes], [BodyContainsBytes],
|
||
|
// [HeaderExists], [HeaderIs], [HeaderContains] or any of
|
||
|
// [github.com/maxatome/tdhttpmock] functions.
|
||
|
type Matcher struct {
|
||
|
name string
|
||
|
fn MatcherFunc // can be nil → means always true
|
||
|
}
|
||
|
|
||
|
var matcherID int64
|
||
|
|
||
|
// NewMatcher returns a [Matcher]. If name is empty and fn is non-nil,
|
||
|
// a name is automatically generated. When fn is nil, it is a default
|
||
|
// [Matcher]: its name can be empty.
|
||
|
//
|
||
|
// Automatically generated names have the form:
|
||
|
//
|
||
|
// ~HEXANUMBER@PKG.FUNC() FILE:LINE
|
||
|
//
|
||
|
// Legend:
|
||
|
// - HEXANUMBER is a unique 10 digit hexadecimal number, always increasing;
|
||
|
// - PKG is the NewMatcher caller package (except if
|
||
|
// [IgnoreMatcherHelper] has been previously called, in this case it
|
||
|
// is the caller of the caller package and so on);
|
||
|
// - FUNC is the function name of the caller in the previous PKG package;
|
||
|
// - FILE and LINE are the location of the call in FUNC function.
|
||
|
func NewMatcher(name string, fn MatcherFunc) Matcher {
|
||
|
if name == "" && fn != nil {
|
||
|
// Auto-name the matcher
|
||
|
name = fmt.Sprintf("~%010x%s", atomic.AddInt64(&matcherID, 1), calledFrom(1))
|
||
|
}
|
||
|
return Matcher{
|
||
|
name: name,
|
||
|
fn: fn,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// BodyContainsBytes returns a [Matcher] checking that request body
|
||
|
// contains subslice.
|
||
|
//
|
||
|
// The name of the returned [Matcher] is auto-generated (see [NewMatcher]).
|
||
|
// To name it explicitly, use [Matcher.WithName] as in:
|
||
|
//
|
||
|
// BodyContainsBytes([]byte("foo")).WithName("10-body-contains-foo")
|
||
|
//
|
||
|
// See also [github.com/maxatome/tdhttpmock.Body],
|
||
|
// [github.com/maxatome/tdhttpmock.JSONBody] and
|
||
|
// [github.com/maxatome/tdhttpmock.XMLBody] for powerful body testing.
|
||
|
func BodyContainsBytes(subslice []byte) Matcher {
|
||
|
return NewMatcher("",
|
||
|
func(req *http.Request) bool {
|
||
|
rearmBody(req)
|
||
|
b, err := ioutil.ReadAll(req.Body)
|
||
|
return err == nil && bytes.Contains(b, subslice)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// BodyContainsString returns a [Matcher] checking that request body
|
||
|
// contains substr.
|
||
|
//
|
||
|
// The name of the returned [Matcher] is auto-generated (see [NewMatcher]).
|
||
|
// To name it explicitly, use [Matcher.WithName] as in:
|
||
|
//
|
||
|
// BodyContainsString("foo").WithName("10-body-contains-foo")
|
||
|
//
|
||
|
// See also [github.com/maxatome/tdhttpmock.Body],
|
||
|
// [github.com/maxatome/tdhttpmock.JSONBody] and
|
||
|
// [github.com/maxatome/tdhttpmock.XMLBody] for powerful body testing.
|
||
|
func BodyContainsString(substr string) Matcher {
|
||
|
return NewMatcher("",
|
||
|
func(req *http.Request) bool {
|
||
|
rearmBody(req)
|
||
|
b, err := ioutil.ReadAll(req.Body)
|
||
|
return err == nil && bytes.Contains(b, []byte(substr))
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// HeaderExists returns a [Matcher] checking that request contains
|
||
|
// key header.
|
||
|
//
|
||
|
// The name of the returned [Matcher] is auto-generated (see [NewMatcher]).
|
||
|
// To name it explicitly, use [Matcher.WithName] as in:
|
||
|
//
|
||
|
// HeaderExists("X-Custom").WithName("10-custom-exists")
|
||
|
//
|
||
|
// See also [github.com/maxatome/tdhttpmock.Header] for powerful
|
||
|
// header testing.
|
||
|
func HeaderExists(key string) Matcher {
|
||
|
return NewMatcher("",
|
||
|
func(req *http.Request) bool {
|
||
|
_, ok := req.Header[key]
|
||
|
return ok
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// HeaderIs returns a [Matcher] checking that request contains
|
||
|
// key header set to value.
|
||
|
//
|
||
|
// The name of the returned [Matcher] is auto-generated (see [NewMatcher]).
|
||
|
// To name it explicitly, use [Matcher.WithName] as in:
|
||
|
//
|
||
|
// HeaderIs("X-Custom", "VALUE").WithName("10-custom-is-value")
|
||
|
//
|
||
|
// See also [github.com/maxatome/tdhttpmock.Header] for powerful
|
||
|
// header testing.
|
||
|
func HeaderIs(key, value string) Matcher {
|
||
|
return NewMatcher("",
|
||
|
func(req *http.Request) bool {
|
||
|
return req.Header.Get(key) == value
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// HeaderContains returns a [Matcher] checking that request contains key
|
||
|
// header itself containing substr.
|
||
|
//
|
||
|
// The name of the returned [Matcher] is auto-generated (see [NewMatcher]).
|
||
|
// To name it explicitly, use [Matcher.WithName] as in:
|
||
|
//
|
||
|
// HeaderContains("X-Custom", "VALUE").WithName("10-custom-contains-value")
|
||
|
//
|
||
|
// See also [github.com/maxatome/tdhttpmock.Header] for powerful
|
||
|
// header testing.
|
||
|
func HeaderContains(key, substr string) Matcher {
|
||
|
return NewMatcher("",
|
||
|
func(req *http.Request) bool {
|
||
|
return strings.Contains(req.Header.Get(key), substr)
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// Name returns the m's name.
|
||
|
func (m Matcher) Name() string {
|
||
|
return m.name
|
||
|
}
|
||
|
|
||
|
// WithName returns a new [Matcher] based on m with name name.
|
||
|
func (m Matcher) WithName(name string) Matcher {
|
||
|
return NewMatcher(name, m.fn)
|
||
|
}
|
||
|
|
||
|
// Check returns true if req is matched by m.
|
||
|
func (m Matcher) Check(req *http.Request) bool {
|
||
|
return m.fn.Check(req)
|
||
|
}
|
||
|
|
||
|
// Or combines m and all ms in a new [Matcher]. This new [Matcher]
|
||
|
// succeeds if one of m or ms succeeds. Note that as a [Matcher]
|
||
|
// succeeds if internal fn is nil, if m's internal fn or any of ms
|
||
|
// item's internal fn is nil, the returned [Matcher] always
|
||
|
// succeeds. The name of returned [Matcher] is m's one.
|
||
|
func (m Matcher) Or(ms ...Matcher) Matcher {
|
||
|
if len(ms) == 0 || m.fn == nil {
|
||
|
return m
|
||
|
}
|
||
|
mfs := make([]MatcherFunc, 1, len(ms)+1)
|
||
|
mfs[0] = m.fn
|
||
|
for _, cur := range ms {
|
||
|
if cur.fn == nil {
|
||
|
return Matcher{}
|
||
|
}
|
||
|
mfs = append(mfs, cur.fn)
|
||
|
}
|
||
|
m.fn = matcherFuncOr(mfs)
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
// And combines m and all ms in a new [Matcher]. This new [Matcher]
|
||
|
// succeeds if all of m and ms succeed. Note that a [Matcher] also
|
||
|
// succeeds if [Matcher] [MatcherFunc] is nil. The name of returned
|
||
|
// [Matcher] is m's one if the empty/default [Matcher] is returned.
|
||
|
func (m Matcher) And(ms ...Matcher) Matcher {
|
||
|
if len(ms) == 0 {
|
||
|
return m
|
||
|
}
|
||
|
mfs := make([]MatcherFunc, 0, len(ms)+1)
|
||
|
if m.fn != nil {
|
||
|
mfs = append(mfs, m.fn)
|
||
|
}
|
||
|
for _, cur := range ms {
|
||
|
if cur.fn != nil {
|
||
|
mfs = append(mfs, cur.fn)
|
||
|
}
|
||
|
}
|
||
|
m.fn = matcherFuncAnd(mfs)
|
||
|
if m.fn != nil {
|
||
|
return m
|
||
|
}
|
||
|
return Matcher{}
|
||
|
}
|
||
|
|
||
|
type matchResponder struct {
|
||
|
matcher Matcher
|
||
|
responder Responder
|
||
|
}
|
||
|
|
||
|
type matchResponders []matchResponder
|
||
|
|
||
|
// add adds or replaces a matchResponder.
|
||
|
func (mrs matchResponders) add(mr matchResponder) matchResponders {
|
||
|
// default is always at end
|
||
|
if mr.matcher.fn == nil {
|
||
|
if len(mrs) > 0 && (mrs)[len(mrs)-1].matcher.fn == nil {
|
||
|
mrs[len(mrs)-1] = mr
|
||
|
return mrs
|
||
|
}
|
||
|
return append(mrs, mr)
|
||
|
}
|
||
|
|
||
|
for i, cur := range mrs {
|
||
|
if cur.matcher.name == mr.matcher.name {
|
||
|
mrs[i] = mr
|
||
|
return mrs
|
||
|
}
|
||
|
}
|
||
|
|
||
|
for i, cur := range mrs {
|
||
|
if cur.matcher.fn == nil || cur.matcher.name > mr.matcher.name {
|
||
|
mrs = append(mrs, matchResponder{})
|
||
|
copy(mrs[i+1:], mrs[i:len(mrs)-1])
|
||
|
mrs[i] = mr
|
||
|
return mrs
|
||
|
}
|
||
|
}
|
||
|
return append(mrs, mr)
|
||
|
}
|
||
|
|
||
|
func (mrs matchResponders) checkEmptiness() matchResponders {
|
||
|
if len(mrs) == 0 {
|
||
|
return nil
|
||
|
}
|
||
|
return mrs
|
||
|
}
|
||
|
|
||
|
func (mrs matchResponders) shrink() matchResponders {
|
||
|
mrs[len(mrs)-1] = matchResponder{}
|
||
|
mrs = mrs[:len(mrs)-1]
|
||
|
return mrs.checkEmptiness()
|
||
|
}
|
||
|
|
||
|
func (mrs matchResponders) remove(name string) matchResponders {
|
||
|
// Special case, even if default has been renamed, we consider ""
|
||
|
// matching this default
|
||
|
if name == "" {
|
||
|
// default is always at end
|
||
|
if len(mrs) > 0 && mrs[len(mrs)-1].matcher.fn == nil {
|
||
|
return mrs.shrink()
|
||
|
}
|
||
|
return mrs.checkEmptiness()
|
||
|
}
|
||
|
|
||
|
for i, cur := range mrs {
|
||
|
if cur.matcher.name == name {
|
||
|
copy(mrs[i:], mrs[i+1:])
|
||
|
return mrs.shrink()
|
||
|
}
|
||
|
}
|
||
|
return mrs.checkEmptiness()
|
||
|
}
|
||
|
|
||
|
func (mrs matchResponders) findMatchResponder(req *http.Request) *matchResponder {
|
||
|
if len(mrs) == 0 {
|
||
|
return nil
|
||
|
}
|
||
|
if mrs[0].matcher.fn == nil { // nil match is always the last
|
||
|
return &mrs[0]
|
||
|
}
|
||
|
|
||
|
copyBody := &bodyCopyOnRead{body: req.Body}
|
||
|
req.Body = copyBody
|
||
|
defer func() {
|
||
|
copyBody.rearm()
|
||
|
req.Body = copyBody.body
|
||
|
}()
|
||
|
|
||
|
for _, mr := range mrs {
|
||
|
copyBody.rearm()
|
||
|
if mr.matcher.Check(req) {
|
||
|
return &mr
|
||
|
}
|
||
|
}
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
type matchRouteKey struct {
|
||
|
internal.RouteKey
|
||
|
name string
|
||
|
}
|
||
|
|
||
|
func (m matchRouteKey) String() string {
|
||
|
if m.name == "" {
|
||
|
return m.RouteKey.String()
|
||
|
}
|
||
|
return m.RouteKey.String() + " <" + m.name + ">"
|
||
|
}
|
||
|
|
||
|
func rearmBody(req *http.Request) {
|
||
|
if req != nil {
|
||
|
if body, ok := req.Body.(interface{ rearm() }); ok {
|
||
|
body.rearm()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
type buffer struct {
|
||
|
*bytes.Reader
|
||
|
}
|
||
|
|
||
|
func (b buffer) Close() error {
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
// bodyCopyOnRead mutates body into a buffer on first Read(), except
|
||
|
// if body is nil or http.NoBody. In this case, EOF is returned for
|
||
|
// each Read() and body stays untouched.
|
||
|
type bodyCopyOnRead struct {
|
||
|
body io.ReadCloser
|
||
|
}
|
||
|
|
||
|
func (b *bodyCopyOnRead) rearm() {
|
||
|
if buf, ok := b.body.(buffer); ok {
|
||
|
buf.Seek(0, io.SeekStart) //nolint:errcheck
|
||
|
} // else b.body contains the original body, so don't touch
|
||
|
}
|
||
|
|
||
|
func (b *bodyCopyOnRead) copy() {
|
||
|
if _, ok := b.body.(buffer); !ok && b.body != nil && b.body != http.NoBody {
|
||
|
buf, _ := ioutil.ReadAll(b.body)
|
||
|
b.body.Close()
|
||
|
b.body = buffer{bytes.NewReader(buf)}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (b *bodyCopyOnRead) Read(p []byte) (n int, err error) {
|
||
|
b.copy()
|
||
|
if b.body == nil {
|
||
|
return 0, io.EOF
|
||
|
}
|
||
|
return b.body.Read(p)
|
||
|
}
|
||
|
|
||
|
func (b *bodyCopyOnRead) Close() error {
|
||
|
return nil
|
||
|
}
|