moby/client/client_test.go
Boaz Shuster eb36d60216 Return error on client redirect
From Go 1.8 HTTP client redirect behaviour is changed:
When status code is 301, 307 or 308, the client
automatically converts it to a new HTTP request.

This behaviour change manifests in the client in that
before the 301 was not followed and the client did not generate
an error, but now results in an error message:
"Error response from daemon: page not found."

To fix that a new redirect policy is forced by setting
HTTP Client's CheckRedirect.
That policy is to return an error for any 301, 307 or 308
in the response's status code to a non-GET request.
The error message specifies that the daemon could not
process the request and it is probably due to bad
arguments that were provided by the user.

Signed-off-by: Boaz Shuster <ripcurld.github@gmail.com>
2017-05-11 15:39:41 +03:00

334 lines
7.8 KiB
Go

package client
import (
"bytes"
"encoding/json"
"io/ioutil"
"net/http"
"net/url"
"os"
"runtime"
"strings"
"testing"
"github.com/docker/docker/api"
"github.com/docker/docker/api/types"
"github.com/stretchr/testify/assert"
"golang.org/x/net/context"
)
func TestNewEnvClient(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("skipping unix only test for windows")
}
cases := []struct {
envs map[string]string
expectedError string
expectedVersion string
}{
{
envs: map[string]string{},
expectedVersion: api.DefaultVersion,
},
{
envs: map[string]string{
"DOCKER_CERT_PATH": "invalid/path",
},
expectedError: "Could not load X509 key pair: open invalid/path/cert.pem: no such file or directory",
},
{
envs: map[string]string{
"DOCKER_CERT_PATH": "testdata/",
},
expectedVersion: api.DefaultVersion,
},
{
envs: map[string]string{
"DOCKER_CERT_PATH": "testdata/",
"DOCKER_TLS_VERIFY": "1",
},
expectedVersion: api.DefaultVersion,
},
{
envs: map[string]string{
"DOCKER_CERT_PATH": "testdata/",
"DOCKER_HOST": "https://notaunixsocket",
},
expectedVersion: api.DefaultVersion,
},
{
envs: map[string]string{
"DOCKER_HOST": "host",
},
expectedError: "unable to parse docker host `host`",
},
{
envs: map[string]string{
"DOCKER_HOST": "invalid://url",
},
expectedVersion: api.DefaultVersion,
},
{
envs: map[string]string{
"DOCKER_API_VERSION": "anything",
},
expectedVersion: "anything",
},
{
envs: map[string]string{
"DOCKER_API_VERSION": "1.22",
},
expectedVersion: "1.22",
},
}
for _, c := range cases {
recoverEnvs := setupEnvs(t, c.envs)
apiclient, err := NewEnvClient()
if c.expectedError != "" {
if err == nil {
t.Errorf("expected an error for %v", c)
} else if err.Error() != c.expectedError {
t.Errorf("expected an error %s, got %s, for %v", c.expectedError, err.Error(), c)
}
} else {
if err != nil {
t.Error(err)
}
version := apiclient.ClientVersion()
if version != c.expectedVersion {
t.Errorf("expected %s, got %s, for %v", c.expectedVersion, version, c)
}
}
if c.envs["DOCKER_TLS_VERIFY"] != "" {
// pedantic checking that this is handled correctly
tr := apiclient.client.Transport.(*http.Transport)
if tr.TLSClientConfig == nil {
t.Error("no TLS config found when DOCKER_TLS_VERIFY enabled")
}
if tr.TLSClientConfig.InsecureSkipVerify {
t.Error("TLS verification should be enabled")
}
}
recoverEnvs(t)
}
}
func setupEnvs(t *testing.T, envs map[string]string) func(*testing.T) {
oldEnvs := map[string]string{}
for key, value := range envs {
oldEnv := os.Getenv(key)
oldEnvs[key] = oldEnv
err := os.Setenv(key, value)
if err != nil {
t.Error(err)
}
}
return func(t *testing.T) {
for key, value := range oldEnvs {
err := os.Setenv(key, value)
if err != nil {
t.Error(err)
}
}
}
}
func TestGetAPIPath(t *testing.T) {
cases := []struct {
v string
p string
q url.Values
e string
}{
{"", "/containers/json", nil, "/containers/json"},
{"", "/containers/json", url.Values{}, "/containers/json"},
{"", "/containers/json", url.Values{"s": []string{"c"}}, "/containers/json?s=c"},
{"1.22", "/containers/json", nil, "/v1.22/containers/json"},
{"1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
{"1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
{"v1.22", "/containers/json", nil, "/v1.22/containers/json"},
{"v1.22", "/containers/json", url.Values{}, "/v1.22/containers/json"},
{"v1.22", "/containers/json", url.Values{"s": []string{"c"}}, "/v1.22/containers/json?s=c"},
{"v1.22", "/networks/kiwl$%^", nil, "/v1.22/networks/kiwl$%25%5E"},
}
for _, cs := range cases {
c, err := NewClient("unix:///var/run/docker.sock", cs.v, nil, nil)
if err != nil {
t.Fatal(err)
}
g := c.getAPIPath(cs.p, cs.q)
if g != cs.e {
t.Fatalf("Expected %s, got %s", cs.e, g)
}
err = c.Close()
if nil != err {
t.Fatalf("close client failed, error message: %s", err)
}
}
}
func TestParseHost(t *testing.T) {
cases := []struct {
host string
proto string
addr string
base string
err bool
}{
{"", "", "", "", true},
{"foobar", "", "", "", true},
{"foo://bar", "foo", "bar", "", false},
{"tcp://localhost:2476", "tcp", "localhost:2476", "", false},
{"tcp://localhost:2476/path", "tcp", "localhost:2476", "/path", false},
}
for _, cs := range cases {
p, a, b, e := ParseHost(cs.host)
if cs.err && e == nil {
t.Fatalf("expected error, got nil")
}
if !cs.err && e != nil {
t.Fatal(e)
}
if cs.proto != p {
t.Fatalf("expected proto %s, got %s", cs.proto, p)
}
if cs.addr != a {
t.Fatalf("expected addr %s, got %s", cs.addr, a)
}
if cs.base != b {
t.Fatalf("expected base %s, got %s", cs.base, b)
}
}
}
func TestUpdateClientVersion(t *testing.T) {
client := &Client{
client: newMockClient(func(req *http.Request) (*http.Response, error) {
splitQuery := strings.Split(req.URL.Path, "/")
queryVersion := splitQuery[1]
b, err := json.Marshal(types.Version{
APIVersion: queryVersion,
})
if err != nil {
return nil, err
}
return &http.Response{
StatusCode: http.StatusOK,
Body: ioutil.NopCloser(bytes.NewReader(b)),
}, nil
}),
}
cases := []struct {
v string
}{
{"1.20"},
{"v1.21"},
{"1.22"},
{"v1.22"},
}
for _, cs := range cases {
client.UpdateClientVersion(cs.v)
r, err := client.ServerVersion(context.Background())
if err != nil {
t.Fatal(err)
}
if strings.TrimPrefix(r.APIVersion, "v") != strings.TrimPrefix(cs.v, "v") {
t.Fatalf("Expected %s, got %s", cs.v, r.APIVersion)
}
}
}
func TestNewEnvClientSetsDefaultVersion(t *testing.T) {
// Unset environment variables
envVarKeys := []string{
"DOCKER_HOST",
"DOCKER_API_VERSION",
"DOCKER_TLS_VERIFY",
"DOCKER_CERT_PATH",
}
envVarValues := make(map[string]string)
for _, key := range envVarKeys {
envVarValues[key] = os.Getenv(key)
os.Setenv(key, "")
}
client, err := NewEnvClient()
if err != nil {
t.Fatal(err)
}
if client.version != api.DefaultVersion {
t.Fatalf("Expected %s, got %s", api.DefaultVersion, client.version)
}
expected := "1.22"
os.Setenv("DOCKER_API_VERSION", expected)
client, err = NewEnvClient()
if err != nil {
t.Fatal(err)
}
if client.version != expected {
t.Fatalf("Expected %s, got %s", expected, client.version)
}
// Restore environment variables
for _, key := range envVarKeys {
os.Setenv(key, envVarValues[key])
}
}
type roundTripFunc func(*http.Request) (*http.Response, error)
func (rtf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
return rtf(req)
}
type bytesBufferClose struct {
*bytes.Buffer
}
func (bbc bytesBufferClose) Close() error {
return nil
}
func TestClientRedirect(t *testing.T) {
client := &http.Client{
CheckRedirect: CheckRedirect,
Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
if req.URL.String() == "/bla" {
return &http.Response{StatusCode: 404}, nil
}
return &http.Response{
StatusCode: 301,
Header: map[string][]string{"Location": {"/bla"}},
Body: bytesBufferClose{bytes.NewBuffer(nil)},
}, nil
}),
}
cases := []struct {
httpMethod string
expectedErr error
statusCode int
}{
{http.MethodGet, nil, 301},
{http.MethodPost, &url.Error{Op: "Post", URL: "/bla", Err: ErrRedirect}, 301},
{http.MethodPut, &url.Error{Op: "Put", URL: "/bla", Err: ErrRedirect}, 301},
{http.MethodDelete, &url.Error{Op: "Delete", URL: "/bla", Err: ErrRedirect}, 301},
}
for _, tc := range cases {
req, err := http.NewRequest(tc.httpMethod, "/redirectme", nil)
assert.NoError(t, err)
resp, err := client.Do(req)
assert.Equal(t, tc.expectedErr, err)
assert.Equal(t, tc.statusCode, resp.StatusCode)
}
}