2019-12-08 04:10:22 +00:00
|
|
|
package httpc
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"compress/gzip"
|
|
|
|
"context"
|
|
|
|
"encoding/gob"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"sort"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
|
|
|
func TestClient(t *testing.T) {
|
|
|
|
newClient := func(t *testing.T, addr string, opts ...ClientOptFn) *Client {
|
|
|
|
t.Helper()
|
|
|
|
client, err := New(append(opts, WithAddr(addr))...)
|
|
|
|
require.NoError(t, err)
|
|
|
|
return client
|
|
|
|
}
|
|
|
|
|
|
|
|
type (
|
|
|
|
respFn func(status int, req *http.Request) (resp *http.Response, err error)
|
|
|
|
|
|
|
|
authFn func(status int, respFn respFn, opts ...ClientOptFn) (*Client, *fakeDoer)
|
|
|
|
|
|
|
|
newReqFn func(*Client, string, reqBody) *Req
|
|
|
|
|
|
|
|
testCase struct {
|
|
|
|
method string
|
|
|
|
status int
|
|
|
|
clientOpts []ClientOptFn
|
|
|
|
reqFn newReqFn
|
|
|
|
queryParams [][2]string
|
|
|
|
reqBody reqBody
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
tokenAuthClient := func(status int, respFn respFn, opts ...ClientOptFn) (*Client, *fakeDoer) {
|
|
|
|
const token = "secrettoken"
|
|
|
|
fakeDoer := &fakeDoer{
|
|
|
|
doFn: func(r *http.Request) (*http.Response, error) {
|
|
|
|
if r.Header.Get("Authorization") != "Token "+token {
|
|
|
|
return nil, errors.New("unauthed token")
|
|
|
|
}
|
|
|
|
return respFn(status, r)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
client := newClient(t, "http://example.com", append(opts, WithAuthToken(token))...)
|
|
|
|
client.doer = fakeDoer
|
|
|
|
return client, fakeDoer
|
|
|
|
}
|
|
|
|
|
|
|
|
noAuthClient := func(status int, respFn respFn, opts ...ClientOptFn) (*Client, *fakeDoer) {
|
|
|
|
fakeDoer := &fakeDoer{
|
|
|
|
doFn: func(r *http.Request) (*http.Response, error) {
|
|
|
|
return respFn(status, r)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
client := newClient(t, "http://example.com", opts...)
|
|
|
|
client.doer = fakeDoer
|
|
|
|
return client, fakeDoer
|
|
|
|
}
|
|
|
|
|
|
|
|
authTests := []struct {
|
|
|
|
name string
|
|
|
|
clientFn authFn
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "no auth",
|
|
|
|
clientFn: noAuthClient,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "token auth",
|
|
|
|
clientFn: tokenAuthClient,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
encodingTests := []struct {
|
|
|
|
name string
|
|
|
|
respFn respFn
|
|
|
|
decodeFn func(v interface{}) func(r *Req) *Req
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "json response",
|
|
|
|
respFn: stubRespNJSONBody,
|
|
|
|
decodeFn: func(v interface{}) func(r *Req) *Req {
|
|
|
|
return func(r *Req) *Req { return r.DecodeJSON(v) }
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "gzipped json response",
|
|
|
|
respFn: stubRespNGZippedJSON,
|
|
|
|
decodeFn: func(v interface{}) func(r *Req) *Req {
|
|
|
|
return func(r *Req) *Req { return r.DecodeJSON(v) }
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "gob response",
|
|
|
|
respFn: stubRespNGobBody,
|
|
|
|
decodeFn: func(v interface{}) func(r *Req) *Req {
|
|
|
|
return func(r *Req) *Req { return r.DecodeGob(v) }
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
testWithRespBody := func(tt testCase) func(t *testing.T) {
|
|
|
|
return func(t *testing.T) {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
for _, encTest := range encodingTests {
|
|
|
|
t.Run(encTest.name, func(t *testing.T) {
|
2019-12-12 03:26:02 +00:00
|
|
|
t.Helper()
|
|
|
|
|
2019-12-08 04:10:22 +00:00
|
|
|
for _, authTest := range authTests {
|
|
|
|
fn := func(t *testing.T) {
|
2019-12-12 03:26:02 +00:00
|
|
|
t.Helper()
|
2019-12-08 04:10:22 +00:00
|
|
|
client, fakeDoer := authTest.clientFn(tt.status, encTest.respFn, tt.clientOpts...)
|
|
|
|
|
|
|
|
req := tt.reqFn(client, "/new/path/heres", tt.reqBody).
|
|
|
|
Accept("application/json").
|
|
|
|
Header("X-Code", "Code").
|
|
|
|
QueryParams(tt.queryParams...).
|
|
|
|
StatusFn(StatusIn(tt.status))
|
|
|
|
|
|
|
|
var actual echoResp
|
|
|
|
req = encTest.decodeFn(&actual)(req)
|
|
|
|
|
|
|
|
err := req.Do(context.TODO())
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
expectedResp := echoResp{
|
|
|
|
Method: tt.method,
|
|
|
|
Scheme: "http",
|
|
|
|
Host: "example.com",
|
|
|
|
Path: "/new/path/heres",
|
|
|
|
Queries: tt.queryParams,
|
|
|
|
ReqBody: tt.reqBody,
|
|
|
|
}
|
|
|
|
assert.Equal(t, expectedResp, actual)
|
|
|
|
require.Len(t, fakeDoer.args, 1)
|
|
|
|
assert.Equal(t, "application/json", fakeDoer.args[0].Header.Get("Accept"))
|
|
|
|
assert.Equal(t, "Code", fakeDoer.args[0].Header.Get("X-Code"))
|
|
|
|
}
|
|
|
|
t.Run(authTest.name, fn)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
newGet := func(client *Client, urlPath string, _ reqBody) *Req {
|
|
|
|
return client.Get(urlPath)
|
|
|
|
}
|
|
|
|
|
|
|
|
t.Run("Delete", func(t *testing.T) {
|
|
|
|
for _, authTest := range authTests {
|
|
|
|
fn := func(t *testing.T) {
|
|
|
|
client, fakeDoer := authTest.clientFn(204, stubResp)
|
|
|
|
|
|
|
|
err := client.Delete("/new/path/heres").
|
|
|
|
Header("X-Code", "Code").
|
|
|
|
StatusFn(StatusIn(204)).
|
|
|
|
Do(context.TODO())
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.Len(t, fakeDoer.args, 1)
|
|
|
|
assert.Equal(t, "Code", fakeDoer.args[0].Header.Get("X-Code"))
|
|
|
|
}
|
|
|
|
t.Run(authTest.name, fn)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("Get", func(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
testCase
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "handles basic call",
|
|
|
|
testCase: testCase{
|
|
|
|
status: 200,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "handles query values",
|
|
|
|
testCase: testCase{
|
|
|
|
queryParams: [][2]string{{"q1", "v1"}, {"q2", "v2"}},
|
|
|
|
status: 202,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
tt.method = "GET"
|
|
|
|
tt.reqFn = newGet
|
|
|
|
|
|
|
|
t.Run(tt.name, testWithRespBody(tt.testCase))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("Patch Post Put with request bodies", func(t *testing.T) {
|
|
|
|
methods := []struct {
|
|
|
|
name string
|
|
|
|
methodCallFn func(client *Client, urlPath string, bFn BodyFn) *Req
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "PATCH",
|
|
|
|
methodCallFn: func(client *Client, urlPath string, bFn BodyFn) *Req {
|
|
|
|
return client.Patch(bFn, urlPath)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "POST",
|
|
|
|
methodCallFn: func(client *Client, urlPath string, bFn BodyFn) *Req {
|
|
|
|
return client.Post(bFn, urlPath)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "PUT",
|
|
|
|
methodCallFn: func(client *Client, urlPath string, bFn BodyFn) *Req {
|
|
|
|
return client.Put(bFn, urlPath)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, method := range methods {
|
|
|
|
t.Run(method.name, func(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
testCase
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "handles json req body",
|
|
|
|
testCase: testCase{
|
|
|
|
status: 200,
|
|
|
|
reqFn: func(client *Client, urlPath string, body reqBody) *Req {
|
|
|
|
return method.methodCallFn(client, urlPath, BodyJSON(body))
|
|
|
|
},
|
|
|
|
reqBody: reqBody{
|
|
|
|
Foo: "foo 1",
|
|
|
|
Bar: 31,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "handles gob req body",
|
|
|
|
testCase: testCase{
|
|
|
|
status: 201,
|
|
|
|
reqFn: func(client *Client, urlPath string, body reqBody) *Req {
|
|
|
|
return method.methodCallFn(client, urlPath, BodyGob(body))
|
|
|
|
},
|
|
|
|
reqBody: reqBody{
|
|
|
|
Foo: "foo 1",
|
|
|
|
Bar: 31,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "handles gzipped json req body",
|
|
|
|
testCase: testCase{
|
|
|
|
status: 201,
|
|
|
|
clientOpts: []ClientOptFn{WithWriterGZIP()},
|
|
|
|
reqFn: func(client *Client, urlPath string, body reqBody) *Req {
|
|
|
|
return method.methodCallFn(client, urlPath, BodyJSON(body))
|
|
|
|
},
|
|
|
|
reqBody: reqBody{
|
|
|
|
Foo: "foo",
|
|
|
|
Bar: 31,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
2019-12-12 03:26:02 +00:00
|
|
|
tt.method = method.name
|
|
|
|
|
|
|
|
t.Run(tt.name, testWithRespBody(tt.testCase))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
t.Run("PatchJSON PostJSON PutJSON with request bodies", func(t *testing.T) {
|
|
|
|
methods := []struct {
|
|
|
|
name string
|
|
|
|
methodCallFn func(client *Client, urlPath string, v interface{}) *Req
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "PATCH",
|
|
|
|
methodCallFn: func(client *Client, urlPath string, v interface{}) *Req {
|
|
|
|
return client.PatchJSON(v, urlPath)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "POST",
|
|
|
|
methodCallFn: func(client *Client, urlPath string, v interface{}) *Req {
|
|
|
|
return client.PostJSON(v, urlPath)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: "PUT",
|
|
|
|
methodCallFn: func(client *Client, urlPath string, v interface{}) *Req {
|
|
|
|
return client.PutJSON(v, urlPath)
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, method := range methods {
|
|
|
|
t.Run(method.name, func(t *testing.T) {
|
|
|
|
tests := []struct {
|
|
|
|
name string
|
|
|
|
testCase
|
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "handles json req body",
|
|
|
|
testCase: testCase{
|
|
|
|
status: 200,
|
|
|
|
reqFn: func(client *Client, urlPath string, body reqBody) *Req {
|
|
|
|
return method.methodCallFn(client, urlPath, body)
|
|
|
|
},
|
|
|
|
reqBody: reqBody{
|
|
|
|
Foo: "foo 1",
|
|
|
|
Bar: 31,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
2019-12-08 04:10:22 +00:00
|
|
|
tt.method = method.name
|
|
|
|
|
|
|
|
t.Run(tt.name, testWithRespBody(tt.testCase))
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
type fakeDoer struct {
|
|
|
|
doFn func(*http.Request) (*http.Response, error)
|
|
|
|
args []*http.Request
|
|
|
|
callCount int
|
|
|
|
}
|
|
|
|
|
|
|
|
func (f *fakeDoer) Do(r *http.Request) (*http.Response, error) {
|
|
|
|
f.callCount++
|
|
|
|
f.args = append(f.args, r)
|
|
|
|
return f.doFn(r)
|
|
|
|
}
|
|
|
|
|
|
|
|
func stubResp(status int, _ *http.Request) (*http.Response, error) {
|
|
|
|
return &http.Response{
|
|
|
|
StatusCode: status,
|
|
|
|
Body: ioutil.NopCloser(new(bytes.Buffer)),
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func stubRespNGZippedJSON(status int, r *http.Request) (*http.Response, error) {
|
|
|
|
e, err := decodeFromContentType(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
w := gzip.NewWriter(&buf)
|
|
|
|
defer w.Close()
|
|
|
|
if err := json.NewEncoder(w).Encode(e); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if err := w.Flush(); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &http.Response{
|
|
|
|
StatusCode: status,
|
|
|
|
Body: ioutil.NopCloser(&buf),
|
|
|
|
Header: http.Header{
|
|
|
|
"Content-Encoding": []string{"gzip"},
|
|
|
|
headerContentType: []string{"application/json"},
|
|
|
|
},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func stubRespNJSONBody(status int, r *http.Request) (*http.Response, error) {
|
|
|
|
e, err := decodeFromContentType(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if err := json.NewEncoder(&buf).Encode(e); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return &http.Response{
|
|
|
|
StatusCode: status,
|
|
|
|
Body: ioutil.NopCloser(&buf),
|
|
|
|
Header: http.Header{headerContentType: []string{"application/json"}},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func stubRespNGobBody(status int, r *http.Request) (*http.Response, error) {
|
|
|
|
e, err := decodeFromContentType(r)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
if err := gob.NewEncoder(&buf).Encode(e); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return &http.Response{
|
|
|
|
StatusCode: status,
|
|
|
|
Body: ioutil.NopCloser(&buf),
|
|
|
|
Header: http.Header{headerContentEncoding: []string{"application/gob"}},
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type (
|
|
|
|
reqBody struct {
|
|
|
|
Foo string
|
|
|
|
Bar int
|
|
|
|
}
|
|
|
|
|
|
|
|
echoResp struct {
|
|
|
|
Method string
|
|
|
|
Scheme string
|
|
|
|
Host string
|
|
|
|
Path string
|
|
|
|
Queries [][2]string
|
|
|
|
|
|
|
|
ReqBody reqBody
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
func decodeFromContentType(r *http.Request) (echoResp, error) {
|
|
|
|
e := echoResp{
|
|
|
|
Method: r.Method,
|
|
|
|
Scheme: r.URL.Scheme,
|
|
|
|
Host: r.URL.Host,
|
|
|
|
Path: r.URL.Path,
|
|
|
|
}
|
|
|
|
for key, vals := range r.URL.Query() {
|
|
|
|
for _, v := range vals {
|
|
|
|
e.Queries = append(e.Queries, [2]string{key, v})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
sort.Slice(e.Queries, func(i, j int) bool {
|
|
|
|
qi, qj := e.Queries[i], e.Queries[j]
|
|
|
|
if qi[0] == qj[0] {
|
|
|
|
return qi[1] < qj[1]
|
|
|
|
}
|
|
|
|
return qi[0] < qj[0]
|
|
|
|
})
|
|
|
|
|
|
|
|
var reader io.Reader = r.Body
|
|
|
|
if r.Header.Get(headerContentEncoding) == "gzip" {
|
|
|
|
gr, err := gzip.NewReader(reader)
|
|
|
|
if err != nil {
|
|
|
|
return echoResp{}, err
|
|
|
|
}
|
|
|
|
reader = gr
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Header.Get(headerContentEncoding) == "application/gob" {
|
|
|
|
return e, gob.NewDecoder(reader).Decode(&e.ReqBody)
|
|
|
|
}
|
|
|
|
|
|
|
|
if r.Header.Get(headerContentType) == "application/json" {
|
|
|
|
return e, json.NewDecoder(reader).Decode(&e.ReqBody)
|
|
|
|
}
|
|
|
|
|
|
|
|
return e, nil
|
|
|
|
}
|