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 .", }, { doc: "no minor", version: "1", expError: "invalid API version (1): must be formatted .", }, { doc: "too many digits", version: "1.2.3", expError: "invalid API version (1.2.3): invalid minor version: must be formatted .", }, { doc: "embedded whitespace", version: "v 1.0", expError: "invalid API version (v 1.0): invalid major version: must be formatted .", }, } 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 .", }, { doc: "no minor", version: "1", expError: "invalid API version (1): must be formatted .", }, { doc: "too many digits", version: "1.2.3", expError: "invalid API version (1.2.3): invalid minor version: must be formatted .", }, { doc: "embedded whitespace", version: "v 1.0", expError: "invalid API version (v 1.0): invalid major version: must be formatted .", }, } 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), WithMockClient(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"}), WithMockClient(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"}), WithMockClient(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"}), WithMockClient(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"}), WithMockClient(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{})) }