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 }