Files
moby/client/client_options_test.go
Sebastiaan van Stijn 9f2faa5cec client: fix TestWithUserAgent
This test was testing the /_ping endpoint, which is intercepted by
the default MockClient.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
2026-01-09 13:58:04 +01:00

393 lines
12 KiB
Go

package client
import (
"crypto/tls"
"net/http"
"net/http/cookiejar"
"runtime"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp/cmpopts"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestOptionWithHostFromEnv(t *testing.T) {
c, err := New(WithHostFromEnv())
assert.NilError(t, err)
assert.Check(t, c.client != nil)
assert.Check(t, is.Equal(c.basePath, ""))
if runtime.GOOS == "windows" {
assert.Check(t, is.Equal(c.host, "npipe:////./pipe/docker_engine"))
assert.Check(t, is.Equal(c.proto, "npipe"))
assert.Check(t, is.Equal(c.addr, "//./pipe/docker_engine"))
} else {
assert.Check(t, is.Equal(c.host, "unix:///var/run/docker.sock"))
assert.Check(t, is.Equal(c.proto, "unix"))
assert.Check(t, is.Equal(c.addr, "/var/run/docker.sock"))
}
t.Setenv("DOCKER_HOST", "tcp://foo.example.com:2376/test/")
c, err = New(WithHostFromEnv())
assert.NilError(t, err)
assert.Check(t, c.client != nil)
assert.Check(t, is.Equal(c.basePath, "/test/"))
assert.Check(t, is.Equal(c.host, "tcp://foo.example.com:2376/test/"))
assert.Check(t, is.Equal(c.proto, "tcp"))
assert.Check(t, is.Equal(c.addr, "foo.example.com:2376"))
}
func TestOptionWithTimeout(t *testing.T) {
timeout := 10 * time.Second
c, err := New(WithTimeout(timeout))
assert.NilError(t, err)
assert.Check(t, c.client != nil)
assert.Check(t, is.Equal(c.client.Timeout, timeout))
}
func TestOptionWithAPIVersion(t *testing.T) {
tests := []struct {
doc string
version string
expected string
expError string
}{
{
doc: "empty version",
version: "",
expected: MaxAPIVersion,
},
{
doc: "custom lower version with whitespace, no v-prefix",
version: " 1.50 ",
expected: "1.50",
},
{
// We currently allow downgrading the client to an unsupported lower version for testing.
doc: "downgrade unsupported version, no v-prefix",
version: "1.0",
expected: "1.0",
},
{
doc: "custom lower version, no v-prefix",
version: "1.50",
expected: "1.50",
},
{
// We currently allow upgrading the client to an unsupported higher version for testing.
doc: "upgrade version, no v-prefix",
version: "9.99",
expected: "9.99",
},
{
doc: "empty version, with v-prefix",
version: "v",
expected: MaxAPIVersion,
},
{
doc: "whitespace, with v-prefix",
version: " v1.0 ",
expected: "1.0",
},
{
doc: "downgrade unsupported version, with v-prefix",
version: "v1.0",
expected: "1.0",
},
{
doc: "custom lower version with whitespace and v-prefix",
version: " v1.50 ",
expected: "1.50",
},
{
doc: "custom lower version, with v-prefix",
version: "v1.50",
expected: "1.50",
},
{
doc: "upgrade version, with v-prefix",
version: "v9.99",
expected: "9.99",
},
{
doc: "malformed version",
version: "something-weird",
expError: "invalid API version (something-weird): must be formatted <major>.<minor>",
},
{
doc: "no minor",
version: "1",
expError: "invalid API version (1): must be formatted <major>.<minor>",
},
{
doc: "too many digits",
version: "1.2.3",
expError: "invalid API version (1.2.3): invalid minor version: must be formatted <major>.<minor>",
},
{
doc: "embedded whitespace",
version: "v 1.0",
expError: "invalid API version (v 1.0): invalid major version: must be formatted <major>.<minor>",
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
client, err := New(WithAPIVersion(tc.version))
if tc.expError != "" {
assert.Check(t, is.ErrorContains(err, tc.expError))
assert.Check(t, client == nil)
} else {
assert.NilError(t, err)
assert.Check(t, client != nil)
assert.Check(t, is.Equal(client.ClientVersion(), tc.expected))
isNoOp := strings.TrimPrefix(strings.TrimSpace(tc.version), "v") == ""
assert.Check(t, is.Equal(client.negotiated.Load(), !isNoOp))
}
})
}
}
func TestOptionWithAPIVersionFromEnv(t *testing.T) {
tests := []struct {
doc string
version string
expected string
expError string
}{
{
doc: "empty version",
version: "",
expected: MaxAPIVersion,
},
{
doc: "custom lower version with whitespace, no v-prefix",
version: " 1.50 ",
expected: "1.50",
},
{
// We currently allow downgrading the client to an unsupported lower version for testing.
doc: "downgrade unsupported version, no v-prefix",
version: "1.0",
expected: "1.0",
},
{
doc: "custom lower version, no v-prefix",
version: "1.50",
expected: "1.50",
},
{
// We currently allow upgrading the client to an unsupported higher version for testing.
doc: "upgrade version, no v-prefix",
version: "9.99",
expected: "9.99",
},
{
doc: "empty version, with v-prefix",
version: "v",
expected: MaxAPIVersion,
},
{
doc: "whitespace, with v-prefix",
version: " v1.0 ",
expected: "1.0",
},
{
doc: "downgrade unsupported version, with v-prefix",
version: "v1.0",
expected: "1.0",
},
{
doc: "custom lower version with whitespace and v-prefix",
version: " v1.50 ",
expected: "1.50",
},
{
doc: "custom lower version, with v-prefix",
version: "v1.50",
expected: "1.50",
},
{
doc: "upgrade version, with v-prefix",
version: "v9.99",
expected: "9.99",
},
{
doc: "malformed version",
version: "something-weird",
expError: "invalid API version (something-weird): must be formatted <major>.<minor>",
},
{
doc: "no minor",
version: "1",
expError: "invalid API version (1): must be formatted <major>.<minor>",
},
{
doc: "too many digits",
version: "1.2.3",
expError: "invalid API version (1.2.3): invalid minor version: must be formatted <major>.<minor>",
},
{
doc: "embedded whitespace",
version: "v 1.0",
expError: "invalid API version (v 1.0): invalid major version: must be formatted <major>.<minor>",
},
}
for _, tc := range tests {
t.Run(tc.doc, func(t *testing.T) {
t.Setenv(EnvOverrideAPIVersion, tc.version)
client, err := New(WithAPIVersionFromEnv())
if tc.expError != "" {
assert.Check(t, is.ErrorContains(err, tc.expError))
assert.Check(t, client == nil)
} else {
assert.NilError(t, err)
assert.Check(t, client != nil)
assert.Check(t, is.Equal(client.ClientVersion(), tc.expected))
isNoOp := strings.TrimPrefix(strings.TrimSpace(tc.version), "v") == ""
assert.Check(t, is.Equal(client.negotiated.Load(), !isNoOp))
}
})
}
}
// TestOptionOverridePriority validates that overriding the API version through
// [WithAPIVersionFromEnv] takes precedence over other manual options, regardless
// the order in which they're passed.
func TestOptionOverridePriority(t *testing.T) {
t.Run("no env-var set", func(t *testing.T) {
client, err := New(WithAPIVersionFromEnv(), WithAPIVersion("1.50"))
assert.NilError(t, err)
assert.Check(t, is.Equal(client.ClientVersion(), "1.50"))
assert.Check(t, is.Equal(client.negotiated.Load(), true))
})
const expected = "1.51"
t.Setenv(EnvOverrideAPIVersion, expected)
t.Run("WithAPIVersionFromEnv first", func(t *testing.T) {
client, err := New(WithAPIVersionFromEnv(), WithAPIVersion("1.50"))
assert.NilError(t, err)
assert.Check(t, is.Equal(client.ClientVersion(), expected))
assert.Check(t, is.Equal(client.negotiated.Load(), true))
})
t.Run("WithAPIVersionFromEnv last", func(t *testing.T) {
client, err := New(WithAPIVersion("1.50"), WithAPIVersionFromEnv())
assert.NilError(t, err)
assert.Check(t, is.Equal(client.ClientVersion(), expected))
assert.Check(t, is.Equal(client.negotiated.Load(), true))
})
t.Run("FromEnv first", func(t *testing.T) {
client, err := New(FromEnv, WithAPIVersion("1.50"))
assert.NilError(t, err)
assert.Check(t, is.Equal(client.ClientVersion(), expected))
assert.Check(t, is.Equal(client.negotiated.Load(), true))
})
t.Run("FromEnv last", func(t *testing.T) {
client, err := New(WithAPIVersion("1.50"), FromEnv)
assert.NilError(t, err)
assert.Check(t, is.Equal(client.ClientVersion(), expected))
assert.Check(t, is.Equal(client.negotiated.Load(), true))
})
}
func TestWithUserAgent(t *testing.T) {
const userAgent = "Magic-Client/v1.2.3"
t.Run("user-agent", func(t *testing.T) {
c, err := New(
WithUserAgent(userAgent),
WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
assert.Check(t, is.Equal(req.Header.Get("User-Agent"), userAgent))
return &http.Response{StatusCode: http.StatusOK}, nil
}),
)
assert.NilError(t, err)
_, err = c.Ping(t.Context(), PingOptions{})
assert.NilError(t, err)
assert.NilError(t, c.Close())
})
t.Run("user-agent and custom headers", func(t *testing.T) {
c, err := New(
WithUserAgent(userAgent),
WithHTTPHeaders(map[string]string{"User-Agent": "should-be-ignored/1.0.0", "Other-Header": "hello-world"}),
WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
assert.Check(t, is.Equal(req.Header.Get("User-Agent"), userAgent))
assert.Check(t, is.Equal(req.Header.Get("Other-Header"), "hello-world"))
return &http.Response{StatusCode: http.StatusOK}, nil
}),
)
assert.NilError(t, err)
_, err = c.Ping(t.Context(), PingOptions{})
assert.NilError(t, err)
assert.NilError(t, c.Close())
})
t.Run("custom headers", func(t *testing.T) {
c, err := New(
WithHTTPHeaders(map[string]string{"User-Agent": "from-custom-headers/1.0.0", "Other-Header": "hello-world"}),
WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
assert.Check(t, is.Equal(req.Header.Get("User-Agent"), "from-custom-headers/1.0.0"))
assert.Check(t, is.Equal(req.Header.Get("Other-Header"), "hello-world"))
return &http.Response{StatusCode: http.StatusOK}, nil
}),
)
assert.NilError(t, err)
_, err = c.Ping(t.Context(), PingOptions{})
assert.NilError(t, err)
assert.NilError(t, c.Close())
})
t.Run("no user-agent set", func(t *testing.T) {
c, err := New(
WithHTTPHeaders(map[string]string{"Other-Header": "hello-world"}),
WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
assert.Check(t, is.Equal(req.Header.Get("User-Agent"), ""))
assert.Check(t, is.Equal(req.Header.Get("Other-Header"), "hello-world"))
return &http.Response{StatusCode: http.StatusOK}, nil
}),
)
assert.NilError(t, err)
_, err = c.Ping(t.Context(), PingOptions{})
assert.NilError(t, err)
assert.NilError(t, c.Close())
})
t.Run("reset custom user-agent", func(t *testing.T) {
c, err := New(
WithUserAgent(""),
WithHTTPHeaders(map[string]string{"User-Agent": "from-custom-headers/1.0.0", "Other-Header": "hello-world"}),
WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
assert.Check(t, is.Equal(req.Header.Get("User-Agent"), ""))
assert.Check(t, is.Equal(req.Header.Get("Other-Header"), "hello-world"))
return &http.Response{StatusCode: http.StatusOK}, nil
}),
)
assert.NilError(t, err)
_, err = c.Ping(t.Context(), PingOptions{})
assert.NilError(t, err)
assert.NilError(t, c.Close())
})
}
func TestWithHTTPClient(t *testing.T) {
cookieJar, err := cookiejar.New(nil)
assert.NilError(t, err)
pristineHTTPClient := func() *http.Client {
return &http.Client{
Timeout: 42 * time.Second,
Jar: cookieJar,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{ServerName: "example.com", MinVersion: tls.VersionTLS12},
},
}
}
hc := pristineHTTPClient()
_, err = New(WithHTTPClient(hc), WithHost("tcp://example.com:443"))
assert.NilError(t, err)
assert.DeepEqual(t, hc, pristineHTTPClient(),
cmpopts.IgnoreUnexported(http.Transport{}, tls.Config{}),
cmpopts.EquateComparable(&cookiejar.Jar{}))
}