Skip to content

Commit

Permalink
Cleanup Response.Reader implementation and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
gavv committed Oct 6, 2023
1 parent b7b42ae commit a841610
Show file tree
Hide file tree
Showing 4 changed files with 283 additions and 232 deletions.
57 changes: 41 additions & 16 deletions e2e/chunked_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package e2e

import (
"bufio"
"io"
"net/http"
"net/http/httptest"
"strings"
Expand Down Expand Up @@ -101,30 +102,54 @@ func TestE2EChunked_BinderFast(t *testing.T) {

func TestE2EChunked_ResponseReader(t *testing.T) {
const chars = "abcdefghijklmnopqrstuvwxyz"

doneCh := make(chan struct{})

mux := http.NewServeMux()
mux.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) {
b := make([]byte, 1000000)
for i := range b {
b[i] = chars[i%26]
w.Header().Add("Content-Type", "text/plain")
for {
wb := make([]byte, len(chars)*10)
for i := range wb {
wb[i] = chars[i%26]
}
_, err := w.Write(wb)
if err != nil {
break
}
w.(http.Flusher).Flush()
}
w.Header().Add("Content-Type", "text/plain; charset=utf-8")
_, _ = w.Write(b)
close(doneCh)
})
e := httpexpect.WithConfig(httpexpect.Config{
BaseURL: "http://example.com",
Reporter: httpexpect.NewAssertReporter(t),
Client: &http.Client{
Transport: httpexpect.NewBinder(mux),
},
})
reader := e.GET("/test").Expect().Reader()

server := httptest.NewServer(mux)
defer server.Close()

e := httpexpect.Default(t, server.URL)

resp := e.GET("/test").Expect()

resp.Status(http.StatusOK).
HasContentType("text/plain").
HasTransferEncoding("chunked")

reader := resp.Reader()

rb := make([]byte, 1000000)
l, err := reader.Read(rb)
n, err := io.ReadFull(reader, rb)
assert.NoError(t, err)
assert.Equal(t, 1000000, l)
assert.Equal(t, chars, string(rb[0:26]))
assert.Equal(t, 1000000, n)

diff := 0
for i := range rb {
if rb[i] != chars[i%26] {
diff++
}
}
assert.Equal(t, 0, diff)

err = reader.Close()
assert.NoError(t, err)

<-doneCh
}
10 changes: 5 additions & 5 deletions printer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import (
"github.com/stretchr/testify/assert"
)

type errorReader struct{}
type failingReader struct{}

func (errorReader) Read(_ []byte) (n int, err error) {
func (failingReader) Read(_ []byte) (n int, err error) {
return 0, errors.New("error")
}

func (errorReader) Close() error {
func (failingReader) Close() error {
return errors.New("error")
}

Expand Down Expand Up @@ -70,13 +70,13 @@ func TestPrinter_Panics(t *testing.T) {

assert.Panics(t, func() {
curl.Request(&http.Request{
Body: errorReader{},
Body: failingReader{},
})
})

assert.Panics(t, func() {
curl.Response(&http.Response{
Body: errorReader{},
Body: failingReader{},
}, 0)
})
})
Expand Down
109 changes: 55 additions & 54 deletions response.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,28 +35,16 @@ type Response struct {
cookies []*http.Cookie
}

type ErrorReader struct {
err error
}

func (er *ErrorReader) Read(_ []byte) (int, error) {
return 0, er.err
}

func (er *ErrorReader) Close() error {
return er.err
}

func newErrorReader(err error) *ErrorReader {
return &ErrorReader{err}
}

type contentState int

const (
// We didn't try to retrieve response content yet
contentPending contentState = iota
// We successfully retrieved response content
contentRetreived
// We tried to retrieve response content and failed
contentFailed
// We transferred body reader to user and will not use it by ourselves
contentHijacked
)

Expand Down Expand Up @@ -583,6 +571,44 @@ func (r *Response) Websocket() *Websocket {
return newWebsocket(opChain, r.config, r.websocket)
}

// Reader returns the body reader from the response.
//
// This method is mutually exclusive with methods that read entire
// response body, like Text, Body, JSON, etc. It can be used when
// you need to parse body manually or retrieve infinite responses.
//
// Example:
//
// resp := NewResponse(t, response)
// reader := resp.Reader()
func (r *Response) Reader() io.ReadCloser {
opChain := r.chain.enter("Reader()")
defer opChain.leave()

if opChain.failed() {
return errBodyReader{errors.New("cannot read from failed Response")}
}

if r.contentState != contentPending {
opChain.fail(AssertionFailure{
Type: AssertUsage,
Errors: []error{
fmt.Errorf("cannot call Reader() because %s was already called",
r.contentMethod),
},
})
return errBodyReader{errors.New("cannot read from failed Response")}
}

if bw, _ := r.httpResp.Body.(*bodyWrapper); bw != nil {
bw.DisableRewinds()
}

r.contentState = contentHijacked

return r.httpResp.Body
}

// Body returns a new String instance with response body.
//
// Example:
Expand Down Expand Up @@ -916,7 +942,7 @@ func (r *Response) getJSON(
// resp.JSONP("myCallback").Array().ConsistsOf("foo", "bar")
// resp.JSONP("myCallback", ContentOpts{
// MediaType: "application/javascript",
// }).Array.ConsistsOf("foo", "bar")
// }).Array().ConsistsOf("foo", "bar")
func (r *Response) JSONP(callback string, options ...ContentOpts) *Value {
opChain := r.chain.enter("JSONP()")
defer opChain.leave()
Expand Down Expand Up @@ -991,43 +1017,6 @@ func (r *Response) getJSONP(
return value
}

// Reader returns the body reader from the response.
//
// Reader replaces the other methods for reading response body
// and disables rewinding when reading.
//
// Example:
//
// resp := NewResponse(t, response)
// reader := resp.Reader()
func (r *Response) Reader() io.ReadCloser {
opChain := r.chain.enter("Reader()")
defer opChain.leave()

if opChain.failed() {
return newErrorReader(errors.New("cannot call Reader() because chain has failed"))
}

if r.contentState != contentPending {
err := fmt.Errorf("cannot call Reader() because %s was already called", r.contentMethod)
opChain.fail(AssertionFailure{
Type: AssertUsage,
Errors: []error{err},
})
return newErrorReader(err)
}

resp := r.httpResp
bw, ok := resp.Body.(*bodyWrapper)
if ok && bw != nil {
bw.DisableRewinds()
}

r.contentState = contentHijacked

return resp.Body
}

func (r *Response) checkContentOptions(
opChain *chain, options []ContentOpts, expectedType string, expectedCharset ...string,
) bool {
Expand Down Expand Up @@ -1126,3 +1115,15 @@ func (r *Response) checkEqual(

return true
}

type errBodyReader struct {
err error
}

func (r errBodyReader) Read(_ []byte) (int, error) {
return 0, r.err
}

func (r errBodyReader) Close() error {
return r.err
}
Loading

0 comments on commit a841610

Please sign in to comment.