757 lines
23 KiB
Go
757 lines
23 KiB
Go
|
package httpmock
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"context"
|
||
|
"encoding/json"
|
||
|
"encoding/xml"
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"io"
|
||
|
"net/http"
|
||
|
"reflect"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
"time"
|
||
|
|
||
|
"github.com/jarcoal/httpmock/internal"
|
||
|
)
|
||
|
|
||
|
// fromThenKeyType is used by Then().
|
||
|
type fromThenKeyType struct{}
|
||
|
|
||
|
var fromThenKey = fromThenKeyType{}
|
||
|
|
||
|
type suggestedInfo struct {
|
||
|
kind string
|
||
|
suggested string
|
||
|
}
|
||
|
|
||
|
// suggestedMethodKeyType is used by NewNotFoundResponder().
|
||
|
type suggestedKeyType struct{}
|
||
|
|
||
|
var suggestedKey = suggestedKeyType{}
|
||
|
|
||
|
// Responder is a callback that receives an [*http.Request] and returns
|
||
|
// a mocked response.
|
||
|
type Responder func(*http.Request) (*http.Response, error)
|
||
|
|
||
|
func (r Responder) times(name string, n int, fn ...func(...any)) Responder {
|
||
|
count := 0
|
||
|
return func(req *http.Request) (*http.Response, error) {
|
||
|
count++
|
||
|
if count > n {
|
||
|
err := internal.StackTracer{
|
||
|
Err: fmt.Errorf("Responder not found for %s %s (coz %s and already called %d times)", req.Method, req.URL, name, count),
|
||
|
}
|
||
|
if len(fn) > 0 {
|
||
|
err.CustomFn = fn[0]
|
||
|
}
|
||
|
return nil, err
|
||
|
}
|
||
|
return r(req)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Times returns a [Responder] callable n times before returning an
|
||
|
// error. If the [Responder] is called more than n times and fn is
|
||
|
// passed and non-nil, it acts as the fn parameter of
|
||
|
// [NewNotFoundResponder], allowing to dump the stack trace to
|
||
|
// localize the origin of the call.
|
||
|
//
|
||
|
// import (
|
||
|
// "testing"
|
||
|
// "github.com/jarcoal/httpmock"
|
||
|
// )
|
||
|
// ...
|
||
|
// func TestMyApp(t *testing.T) {
|
||
|
// ...
|
||
|
// // This responder is callable 3 times, then an error is returned and
|
||
|
// // the stacktrace of the call logged using t.Log()
|
||
|
// httpmock.RegisterResponder("GET", "/foo/bar",
|
||
|
// httpmock.NewStringResponder(200, "{}").Times(3, t.Log),
|
||
|
// )
|
||
|
func (r Responder) Times(n int, fn ...func(...any)) Responder {
|
||
|
return r.times("Times", n, fn...)
|
||
|
}
|
||
|
|
||
|
// Once returns a new [Responder] callable once before returning an
|
||
|
// error. If the [Responder] is called 2 or more times and fn is passed
|
||
|
// and non-nil, it acts as the fn parameter of [NewNotFoundResponder],
|
||
|
// allowing to dump the stack trace to localize the origin of the
|
||
|
// call.
|
||
|
//
|
||
|
// import (
|
||
|
// "testing"
|
||
|
// "github.com/jarcoal/httpmock"
|
||
|
// )
|
||
|
// ...
|
||
|
// func TestMyApp(t *testing.T) {
|
||
|
// ...
|
||
|
// // This responder is callable only once, then an error is returned and
|
||
|
// // the stacktrace of the call logged using t.Log()
|
||
|
// httpmock.RegisterResponder("GET", "/foo/bar",
|
||
|
// httpmock.NewStringResponder(200, "{}").Once(t.Log),
|
||
|
// )
|
||
|
func (r Responder) Once(fn ...func(...any)) Responder {
|
||
|
return r.times("Once", 1, fn...)
|
||
|
}
|
||
|
|
||
|
// Trace returns a new [Responder] that allows to easily trace the calls
|
||
|
// of the original [Responder] using fn. It can be used in conjunction
|
||
|
// with the testing package as in the example below with the help of
|
||
|
// [*testing.T.Log] method:
|
||
|
//
|
||
|
// import (
|
||
|
// "testing"
|
||
|
// "github.com/jarcoal/httpmock"
|
||
|
// )
|
||
|
// ...
|
||
|
// func TestMyApp(t *testing.T) {
|
||
|
// ...
|
||
|
// httpmock.RegisterResponder("GET", "/foo/bar",
|
||
|
// httpmock.NewStringResponder(200, "{}").Trace(t.Log),
|
||
|
// )
|
||
|
func (r Responder) Trace(fn func(...any)) Responder {
|
||
|
return func(req *http.Request) (*http.Response, error) {
|
||
|
resp, err := r(req)
|
||
|
return resp, internal.StackTracer{
|
||
|
CustomFn: fn,
|
||
|
Err: err,
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Delay returns a new [Responder] that calls the original r Responder
|
||
|
// after a delay of d.
|
||
|
//
|
||
|
// import (
|
||
|
// "testing"
|
||
|
// "time"
|
||
|
// "github.com/jarcoal/httpmock"
|
||
|
// )
|
||
|
// ...
|
||
|
// func TestMyApp(t *testing.T) {
|
||
|
// ...
|
||
|
// httpmock.RegisterResponder("GET", "/foo/bar",
|
||
|
// httpmock.NewStringResponder(200, "{}").Delay(100*time.Millisecond),
|
||
|
// )
|
||
|
func (r Responder) Delay(d time.Duration) Responder {
|
||
|
return func(req *http.Request) (*http.Response, error) {
|
||
|
time.Sleep(d)
|
||
|
return r(req)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
var errThenDone = errors.New("ThenDone")
|
||
|
|
||
|
// similar is simple but a bit tricky. Here we consider two Responder
|
||
|
// are similar if they share the same function, but not necessarily
|
||
|
// the same environment. It is only used by Then below.
|
||
|
func (r Responder) similar(other Responder) bool {
|
||
|
return reflect.ValueOf(r).Pointer() == reflect.ValueOf(other).Pointer()
|
||
|
}
|
||
|
|
||
|
// Then returns a new [Responder] that calls r on first invocation, then
|
||
|
// next on following ones, except when Then is chained, in this case
|
||
|
// next is called only once:
|
||
|
//
|
||
|
// A := httpmock.NewStringResponder(200, "A")
|
||
|
// B := httpmock.NewStringResponder(200, "B")
|
||
|
// C := httpmock.NewStringResponder(200, "C")
|
||
|
//
|
||
|
// httpmock.RegisterResponder("GET", "/pipo", A.Then(B).Then(C))
|
||
|
//
|
||
|
// http.Get("http://foo.bar/pipo") // A is called
|
||
|
// http.Get("http://foo.bar/pipo") // B is called
|
||
|
// http.Get("http://foo.bar/pipo") // C is called
|
||
|
// http.Get("http://foo.bar/pipo") // C is called, and so on
|
||
|
//
|
||
|
// A panic occurs if next is the result of another Then call (because
|
||
|
// allowing it could cause inextricable problems at runtime). Then
|
||
|
// calls can be chained, but cannot call each other by
|
||
|
// parameter. Example:
|
||
|
//
|
||
|
// A.Then(B).Then(C) // is OK
|
||
|
// A.Then(B.Then(C)) // panics as A.Then() parameter is another Then() call
|
||
|
//
|
||
|
// See also [ResponderFromMultipleResponses].
|
||
|
func (r Responder) Then(next Responder) (x Responder) {
|
||
|
var done int
|
||
|
var mu sync.Mutex
|
||
|
x = func(req *http.Request) (*http.Response, error) {
|
||
|
mu.Lock()
|
||
|
defer mu.Unlock()
|
||
|
|
||
|
ctx := req.Context()
|
||
|
thenCalledUs, _ := ctx.Value(fromThenKey).(bool)
|
||
|
if !thenCalledUs {
|
||
|
req = req.WithContext(context.WithValue(ctx, fromThenKey, true))
|
||
|
}
|
||
|
|
||
|
switch done {
|
||
|
case 0:
|
||
|
resp, err := r(req)
|
||
|
if err != errThenDone {
|
||
|
if !x.similar(r) { // r is NOT a Then
|
||
|
done = 1
|
||
|
}
|
||
|
return resp, err
|
||
|
}
|
||
|
fallthrough
|
||
|
|
||
|
case 1:
|
||
|
done = 2 // next is NEVER a Then, as it is forbidden by design
|
||
|
return next(req)
|
||
|
}
|
||
|
if thenCalledUs {
|
||
|
return nil, errThenDone
|
||
|
}
|
||
|
return next(req)
|
||
|
}
|
||
|
|
||
|
if next.similar(x) {
|
||
|
panic("Then() does not accept another Then() Responder as parameter")
|
||
|
}
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// SetContentLength returns a new [Responder] based on r that ensures
|
||
|
// the returned [*http.Response] ContentLength field and
|
||
|
// Content-Length header are set to the right value.
|
||
|
//
|
||
|
// If r returns an [*http.Response] with a nil Body or equal to
|
||
|
// [http.NoBody], the length is always set to 0.
|
||
|
//
|
||
|
// If r returned response.Body implements:
|
||
|
//
|
||
|
// Len() int
|
||
|
//
|
||
|
// then the length is set to the Body.Len() returned value. All
|
||
|
// httpmock generated bodies implement this method. Beware that
|
||
|
// [strings.Builder], [strings.Reader], [bytes.Buffer] and
|
||
|
// [bytes.Reader] types used with [io.NopCloser] do not implement
|
||
|
// Len() anymore.
|
||
|
//
|
||
|
// Otherwise, r returned response.Body is entirely copied into an
|
||
|
// internal buffer to get its length, then it is closed. The Body of
|
||
|
// the [*http.Response] returned by the [Responder] returned by
|
||
|
// SetContentLength can then be read again to return its content as
|
||
|
// usual. But keep in mind that each time this [Responder] is called,
|
||
|
// r is called first. So this one has to carefully handle its body: it
|
||
|
// is highly recommended to use [NewRespBodyFromString] or
|
||
|
// [NewRespBodyFromBytes] to set the body once (as
|
||
|
// [NewStringResponder] and [NewBytesResponder] do behind the scene),
|
||
|
// or to build the body each time r is called.
|
||
|
//
|
||
|
// The following calls are all correct:
|
||
|
//
|
||
|
// responder = httpmock.NewStringResponder(200, "BODY").SetContentLength()
|
||
|
// responder = httpmock.NewBytesResponder(200, []byte("BODY")).SetContentLength()
|
||
|
// responder = ResponderFromResponse(&http.Response{
|
||
|
// // build a body once, but httpmock knows how to "rearm" it once read
|
||
|
// Body: NewRespBodyFromString("BODY"),
|
||
|
// StatusCode: 200,
|
||
|
// }).SetContentLength()
|
||
|
// responder = httpmock.Responder(func(req *http.Request) (*http.Response, error) {
|
||
|
// // build a new body for each call
|
||
|
// return &http.Response{
|
||
|
// StatusCode: 200,
|
||
|
// Body: io.NopCloser(strings.NewReader("BODY")),
|
||
|
// }, nil
|
||
|
// }).SetContentLength()
|
||
|
//
|
||
|
// But the following is not correct:
|
||
|
//
|
||
|
// responder = httpmock.ResponderFromResponse(&http.Response{
|
||
|
// StatusCode: 200,
|
||
|
// Body: io.NopCloser(strings.NewReader("BODY")),
|
||
|
// }).SetContentLength()
|
||
|
//
|
||
|
// it will only succeed for the first responder call. The following
|
||
|
// calls will deliver responses with an empty body, as it will already
|
||
|
// been read by the first call.
|
||
|
func (r Responder) SetContentLength() Responder {
|
||
|
return func(req *http.Request) (*http.Response, error) {
|
||
|
resp, err := r(req)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
nr := *resp
|
||
|
switch nr.Body {
|
||
|
case nil:
|
||
|
nr.Body = http.NoBody
|
||
|
fallthrough
|
||
|
case http.NoBody:
|
||
|
nr.ContentLength = 0
|
||
|
default:
|
||
|
bl, ok := nr.Body.(interface{ Len() int })
|
||
|
if !ok {
|
||
|
copyBody := &dummyReadCloser{orig: nr.Body}
|
||
|
bl, nr.Body = copyBody, copyBody
|
||
|
}
|
||
|
nr.ContentLength = int64(bl.Len())
|
||
|
}
|
||
|
if nr.Header == nil {
|
||
|
nr.Header = http.Header{}
|
||
|
}
|
||
|
nr.Header = nr.Header.Clone()
|
||
|
nr.Header.Set("Content-Length", strconv.FormatInt(nr.ContentLength, 10))
|
||
|
return &nr, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// HeaderAdd returns a new [Responder] based on r that ensures the
|
||
|
// returned [*http.Response] includes h header. It adds each h entry
|
||
|
// to the header. It appends to any existing values associated with
|
||
|
// each h key. Each key is case insensitive; it is canonicalized by
|
||
|
// [http.CanonicalHeaderKey].
|
||
|
//
|
||
|
// See also [Responder.HeaderSet] and [Responder.SetContentLength].
|
||
|
func (r Responder) HeaderAdd(h http.Header) Responder {
|
||
|
return func(req *http.Request) (*http.Response, error) {
|
||
|
resp, err := r(req)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
nr := *resp
|
||
|
if nr.Header == nil {
|
||
|
nr.Header = make(http.Header, len(h))
|
||
|
}
|
||
|
nr.Header = nr.Header.Clone()
|
||
|
for k, v := range h {
|
||
|
k = http.CanonicalHeaderKey(k)
|
||
|
if v == nil {
|
||
|
if _, ok := nr.Header[k]; !ok {
|
||
|
nr.Header[k] = nil
|
||
|
}
|
||
|
continue
|
||
|
}
|
||
|
nr.Header[k] = append(nr.Header[k], v...)
|
||
|
}
|
||
|
return &nr, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// HeaderSet returns a new [Responder] based on r that ensures the
|
||
|
// returned [*http.Response] includes h header. It sets the header
|
||
|
// entries associated with each h key. It replaces any existing values
|
||
|
// associated each h key. Each key is case insensitive; it is
|
||
|
// canonicalized by [http.CanonicalHeaderKey].
|
||
|
//
|
||
|
// See also [Responder.HeaderAdd] and [Responder.SetContentLength].
|
||
|
func (r Responder) HeaderSet(h http.Header) Responder {
|
||
|
return func(req *http.Request) (*http.Response, error) {
|
||
|
resp, err := r(req)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
nr := *resp
|
||
|
if nr.Header == nil {
|
||
|
nr.Header = make(http.Header, len(h))
|
||
|
}
|
||
|
nr.Header = nr.Header.Clone()
|
||
|
for k, v := range h {
|
||
|
k = http.CanonicalHeaderKey(k)
|
||
|
if v == nil {
|
||
|
nr.Header[k] = nil
|
||
|
continue
|
||
|
}
|
||
|
nr.Header[k] = append([]string(nil), v...)
|
||
|
}
|
||
|
return &nr, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ResponderFromResponse wraps an [*http.Response] in a [Responder].
|
||
|
//
|
||
|
// Be careful, except for responses generated by httpmock
|
||
|
// ([NewStringResponse] and [NewBytesResponse] functions) for which
|
||
|
// there is no problems, it is the caller responsibility to ensure the
|
||
|
// response body can be read several times and concurrently if needed,
|
||
|
// as it is shared among all [Responder] returned responses.
|
||
|
//
|
||
|
// For home-made responses, [NewRespBodyFromString] and
|
||
|
// [NewRespBodyFromBytes] functions can be used to produce response
|
||
|
// bodies that can be read several times and concurrently.
|
||
|
func ResponderFromResponse(resp *http.Response) Responder {
|
||
|
return func(req *http.Request) (*http.Response, error) {
|
||
|
res := *resp
|
||
|
|
||
|
// Our stuff: generate a new io.ReadCloser instance sharing the same buffer
|
||
|
if body, ok := resp.Body.(*dummyReadCloser); ok {
|
||
|
res.Body = body.copy()
|
||
|
}
|
||
|
|
||
|
res.Request = req
|
||
|
return &res, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ResponderFromMultipleResponses wraps an [*http.Response] list in a
|
||
|
// [Responder].
|
||
|
//
|
||
|
// Each response will be returned in the order of the provided list.
|
||
|
// If the [Responder] is called more than the size of the provided
|
||
|
// list, an error will be thrown.
|
||
|
//
|
||
|
// Be careful, except for responses generated by httpmock
|
||
|
// ([NewStringResponse] and [NewBytesResponse] functions) for which
|
||
|
// there is no problems, it is the caller responsibility to ensure the
|
||
|
// response body can be read several times and concurrently if needed,
|
||
|
// as it is shared among all [Responder] returned responses.
|
||
|
//
|
||
|
// For home-made responses, [NewRespBodyFromString] and
|
||
|
// [NewRespBodyFromBytes] functions can be used to produce response
|
||
|
// bodies that can be read several times and concurrently.
|
||
|
//
|
||
|
// If all responses have been returned and fn is passed and non-nil,
|
||
|
// it acts as the fn parameter of [NewNotFoundResponder], allowing to
|
||
|
// dump the stack trace to localize the origin of the call.
|
||
|
//
|
||
|
// import (
|
||
|
// "github.com/jarcoal/httpmock"
|
||
|
// "testing"
|
||
|
// )
|
||
|
// ...
|
||
|
// func TestMyApp(t *testing.T) {
|
||
|
// ...
|
||
|
// // This responder is callable only once, then an error is returned and
|
||
|
// // the stacktrace of the call logged using t.Log()
|
||
|
// httpmock.RegisterResponder("GET", "/foo/bar",
|
||
|
// httpmock.ResponderFromMultipleResponses(
|
||
|
// []*http.Response{
|
||
|
// httpmock.NewStringResponse(200, `{"name":"bar"}`),
|
||
|
// httpmock.NewStringResponse(404, `{"mesg":"Not found"}`),
|
||
|
// },
|
||
|
// t.Log),
|
||
|
// )
|
||
|
// }
|
||
|
//
|
||
|
// See also [Responder.Then].
|
||
|
func ResponderFromMultipleResponses(responses []*http.Response, fn ...func(...any)) Responder {
|
||
|
responseIndex := 0
|
||
|
mutex := sync.Mutex{}
|
||
|
return func(req *http.Request) (*http.Response, error) {
|
||
|
mutex.Lock()
|
||
|
defer mutex.Unlock()
|
||
|
defer func() { responseIndex++ }()
|
||
|
if responseIndex >= len(responses) {
|
||
|
err := internal.StackTracer{
|
||
|
Err: fmt.Errorf("not enough responses provided: responder called %d time(s) but %d response(s) provided", responseIndex+1, len(responses)),
|
||
|
}
|
||
|
if len(fn) > 0 {
|
||
|
err.CustomFn = fn[0]
|
||
|
}
|
||
|
return nil, err
|
||
|
}
|
||
|
res := *responses[responseIndex]
|
||
|
// Our stuff: generate a new io.ReadCloser instance sharing the same buffer
|
||
|
if body, ok := responses[responseIndex].Body.(*dummyReadCloser); ok {
|
||
|
res.Body = body.copy()
|
||
|
}
|
||
|
|
||
|
res.Request = req
|
||
|
return &res, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewErrorResponder creates a [Responder] that returns an empty request and the
|
||
|
// given error. This can be used to e.g. imitate more deep http errors for the
|
||
|
// client.
|
||
|
func NewErrorResponder(err error) Responder {
|
||
|
return func(req *http.Request) (*http.Response, error) {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewNotFoundResponder creates a [Responder] typically used in
|
||
|
// conjunction with [RegisterNoResponder] function and [testing]
|
||
|
// package, to be proactive when a [Responder] is not found. fn is
|
||
|
// called with a unique string parameter containing the name of the
|
||
|
// missing route and the stack trace to localize the origin of the
|
||
|
// call. If fn returns (= if it does not panic), the [Responder] returns
|
||
|
// an error of the form: "Responder not found for GET http://foo.bar/path".
|
||
|
// Note that fn can be nil.
|
||
|
//
|
||
|
// It is useful when writing tests to ensure that all routes have been
|
||
|
// mocked.
|
||
|
//
|
||
|
// Example of use:
|
||
|
//
|
||
|
// import (
|
||
|
// "testing"
|
||
|
// "github.com/jarcoal/httpmock"
|
||
|
// )
|
||
|
// ...
|
||
|
// func TestMyApp(t *testing.T) {
|
||
|
// ...
|
||
|
// // Calls testing.Fatal with the name of Responder-less route and
|
||
|
// // the stack trace of the call.
|
||
|
// httpmock.RegisterNoResponder(httpmock.NewNotFoundResponder(t.Fatal))
|
||
|
//
|
||
|
// Will abort the current test and print something like:
|
||
|
//
|
||
|
// transport_test.go:735: Called from net/http.Get()
|
||
|
// at /go/src/github.com/jarcoal/httpmock/transport_test.go:714
|
||
|
// github.com/jarcoal/httpmock.TestCheckStackTracer()
|
||
|
// at /go/src/testing/testing.go:865
|
||
|
// testing.tRunner()
|
||
|
// at /go/src/runtime/asm_amd64.s:1337
|
||
|
func NewNotFoundResponder(fn func(...any)) Responder {
|
||
|
return func(req *http.Request) (*http.Response, error) {
|
||
|
var extra string
|
||
|
suggested, _ := req.Context().Value(suggestedKey).(*suggestedInfo)
|
||
|
if suggested != nil {
|
||
|
if suggested.kind == "matcher" {
|
||
|
extra = fmt.Sprintf(` despite %s`, suggested.suggested)
|
||
|
} else {
|
||
|
extra = fmt.Sprintf(`, but one matches %s %q`, suggested.kind, suggested.suggested)
|
||
|
}
|
||
|
}
|
||
|
return nil, internal.StackTracer{
|
||
|
CustomFn: fn,
|
||
|
Err: fmt.Errorf("Responder not found for %s %s%s", req.Method, req.URL, extra),
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewStringResponse creates an [*http.Response] with a body based on
|
||
|
// the given string. Also accepts an HTTP status code.
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewStringResponse(200, httpmock.File("body.txt").String())
|
||
|
func NewStringResponse(status int, body string) *http.Response {
|
||
|
return &http.Response{
|
||
|
Status: strconv.Itoa(status),
|
||
|
StatusCode: status,
|
||
|
Body: NewRespBodyFromString(body),
|
||
|
Header: http.Header{},
|
||
|
ContentLength: -1,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewStringResponder creates a [Responder] from a given body (as a
|
||
|
// string) and status code.
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewStringResponder(200, httpmock.File("body.txt").String())
|
||
|
func NewStringResponder(status int, body string) Responder {
|
||
|
return ResponderFromResponse(NewStringResponse(status, body))
|
||
|
}
|
||
|
|
||
|
// NewBytesResponse creates an [*http.Response] with a body based on the
|
||
|
// given bytes. Also accepts an HTTP status code.
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewBytesResponse(200, httpmock.File("body.raw").Bytes())
|
||
|
func NewBytesResponse(status int, body []byte) *http.Response {
|
||
|
return &http.Response{
|
||
|
Status: strconv.Itoa(status),
|
||
|
StatusCode: status,
|
||
|
Body: NewRespBodyFromBytes(body),
|
||
|
Header: http.Header{},
|
||
|
ContentLength: -1,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// NewBytesResponder creates a [Responder] from a given body (as a byte
|
||
|
// slice) and status code.
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewBytesResponder(200, httpmock.File("body.raw").Bytes())
|
||
|
func NewBytesResponder(status int, body []byte) Responder {
|
||
|
return ResponderFromResponse(NewBytesResponse(status, body))
|
||
|
}
|
||
|
|
||
|
// NewJsonResponse creates an [*http.Response] with a body that is a
|
||
|
// JSON encoded representation of the given any. Also accepts
|
||
|
// an HTTP status code.
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewJsonResponse(200, httpmock.File("body.json"))
|
||
|
func NewJsonResponse(status int, body any) (*http.Response, error) { // nolint: revive
|
||
|
encoded, err := json.Marshal(body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
response := NewBytesResponse(status, encoded)
|
||
|
response.Header.Set("Content-Type", "application/json")
|
||
|
return response, nil
|
||
|
}
|
||
|
|
||
|
// NewJsonResponder creates a [Responder] from a given body (as an
|
||
|
// any that is encoded to JSON) and status code.
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewJsonResponder(200, httpmock.File("body.json"))
|
||
|
func NewJsonResponder(status int, body any) (Responder, error) { // nolint: revive
|
||
|
resp, err := NewJsonResponse(status, body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return ResponderFromResponse(resp), nil
|
||
|
}
|
||
|
|
||
|
// NewJsonResponderOrPanic is like [NewJsonResponder] but panics in
|
||
|
// case of error.
|
||
|
//
|
||
|
// It simplifies the call of [RegisterResponder], avoiding the use of a
|
||
|
// temporary variable and an error check, and so can be used as
|
||
|
// [NewStringResponder] or [NewBytesResponder] in such context:
|
||
|
//
|
||
|
// httpmock.RegisterResponder(
|
||
|
// "GET",
|
||
|
// "/test/path",
|
||
|
// httpmock.NewJsonResponderOrPanic(200, &MyBody),
|
||
|
// )
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewJsonResponderOrPanic(200, httpmock.File("body.json"))
|
||
|
func NewJsonResponderOrPanic(status int, body any) Responder { // nolint: revive
|
||
|
responder, err := NewJsonResponder(status, body)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
return responder
|
||
|
}
|
||
|
|
||
|
// NewXmlResponse creates an [*http.Response] with a body that is an
|
||
|
// XML encoded representation of the given any. Also accepts an HTTP
|
||
|
// status code.
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewXmlResponse(200, httpmock.File("body.xml"))
|
||
|
func NewXmlResponse(status int, body any) (*http.Response, error) { // nolint: revive
|
||
|
var (
|
||
|
encoded []byte
|
||
|
err error
|
||
|
)
|
||
|
if f, ok := body.(File); ok {
|
||
|
encoded, err = f.bytes()
|
||
|
} else {
|
||
|
encoded, err = xml.Marshal(body)
|
||
|
}
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
response := NewBytesResponse(status, encoded)
|
||
|
response.Header.Set("Content-Type", "application/xml")
|
||
|
return response, nil
|
||
|
}
|
||
|
|
||
|
// NewXmlResponder creates a [Responder] from a given body (as an
|
||
|
// any that is encoded to XML) and status code.
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewXmlResponder(200, httpmock.File("body.xml"))
|
||
|
func NewXmlResponder(status int, body any) (Responder, error) { // nolint: revive
|
||
|
resp, err := NewXmlResponse(status, body)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return ResponderFromResponse(resp), nil
|
||
|
}
|
||
|
|
||
|
// NewXmlResponderOrPanic is like [NewXmlResponder] but panics in case
|
||
|
// of error.
|
||
|
//
|
||
|
// It simplifies the call of [RegisterResponder], avoiding the use of a
|
||
|
// temporary variable and an error check, and so can be used as
|
||
|
// [NewStringResponder] or [NewBytesResponder] in such context:
|
||
|
//
|
||
|
// httpmock.RegisterResponder(
|
||
|
// "GET",
|
||
|
// "/test/path",
|
||
|
// httpmock.NewXmlResponderOrPanic(200, &MyBody),
|
||
|
// )
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewXmlResponderOrPanic(200, httpmock.File("body.xml"))
|
||
|
func NewXmlResponderOrPanic(status int, body any) Responder { // nolint: revive
|
||
|
responder, err := NewXmlResponder(status, body)
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
return responder
|
||
|
}
|
||
|
|
||
|
// NewRespBodyFromString creates an [io.ReadCloser] from a string that
|
||
|
// is suitable for use as an HTTP response body.
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewRespBodyFromString(httpmock.File("body.txt").String())
|
||
|
func NewRespBodyFromString(body string) io.ReadCloser {
|
||
|
return &dummyReadCloser{orig: body}
|
||
|
}
|
||
|
|
||
|
// NewRespBodyFromBytes creates an [io.ReadCloser] from a byte slice
|
||
|
// that is suitable for use as an HTTP response body.
|
||
|
//
|
||
|
// To pass the content of an existing file as body use [File] as in:
|
||
|
//
|
||
|
// httpmock.NewRespBodyFromBytes(httpmock.File("body.txt").Bytes())
|
||
|
func NewRespBodyFromBytes(body []byte) io.ReadCloser {
|
||
|
return &dummyReadCloser{orig: body}
|
||
|
}
|
||
|
|
||
|
type lenReadSeeker interface {
|
||
|
io.ReadSeeker
|
||
|
Len() int
|
||
|
}
|
||
|
|
||
|
type dummyReadCloser struct {
|
||
|
orig any // string or []byte
|
||
|
body lenReadSeeker // instanciated on demand from orig
|
||
|
}
|
||
|
|
||
|
// copy returns a new instance resetting d.body to nil.
|
||
|
func (d *dummyReadCloser) copy() *dummyReadCloser {
|
||
|
return &dummyReadCloser{orig: d.orig}
|
||
|
}
|
||
|
|
||
|
// setup ensures d.body is correctly initialized.
|
||
|
func (d *dummyReadCloser) setup() {
|
||
|
if d.body == nil {
|
||
|
switch body := d.orig.(type) {
|
||
|
case string:
|
||
|
d.body = strings.NewReader(body)
|
||
|
case []byte:
|
||
|
d.body = bytes.NewReader(body)
|
||
|
case io.ReadCloser:
|
||
|
var buf bytes.Buffer
|
||
|
io.Copy(&buf, body) //nolint: errcheck
|
||
|
body.Close()
|
||
|
d.body = bytes.NewReader(buf.Bytes())
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (d *dummyReadCloser) Read(p []byte) (n int, err error) {
|
||
|
d.setup()
|
||
|
return d.body.Read(p)
|
||
|
}
|
||
|
|
||
|
func (d *dummyReadCloser) Close() error {
|
||
|
d.setup()
|
||
|
d.body.Seek(0, io.SeekEnd) // nolint: errcheck
|
||
|
return nil
|
||
|
}
|
||
|
|
||
|
func (d *dummyReadCloser) Len() int {
|
||
|
d.setup()
|
||
|
return d.body.Len()
|
||
|
}
|