| package client |
| |
| import ( |
| "crypto/tls" |
| "net/http" |
| "net/http/cookiejar" |
| "runtime" |
| "strings" |
| "testing" |
| "time" |
| |
| cerrdefs "github.com/containerd/errdefs" |
| "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"), defaultUserAgent())) |
| 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 TestWithHTTPHeaders(t *testing.T) { |
| t.Run("duplicates", func(t *testing.T) { |
| c, err := New( |
| WithHTTPHeaders(map[string]string{ |
| "custom-header": "custom-value-A", |
| "Custom-Header": "custom-value-B", |
| "CUSTOM-HEADER": "custom-value-C", |
| }), |
| ) |
| assert.Check(t, is.ErrorIs(err, cerrdefs.ErrInvalidArgument)) |
| assert.Check(t, is.Error(err, "duplicate custom HTTP header (Custom-Header)")) |
| assert.Check(t, is.Nil(c)) |
| }) |
| t.Run("multiple", func(t *testing.T) { |
| c, err := New( |
| WithHTTPHeaders(map[string]string{"custom-header-1": "hello"}), |
| WithHTTPHeaders(map[string]string{"custom-header-2": "custom-value-A"}), |
| WithHTTPHeaders(map[string]string{"Custom-Header-2": "custom-value-B"}), |
| WithHTTPHeaders(map[string]string{"CUSTOM-HEADER-2": "custom-value-C"}), |
| WithBaseMockClient(func(req *http.Request) (*http.Response, error) { |
| assert.Check(t, is.Equal(req.Header.Get("Custom-Header-1"), ""), "custom-header-1 should've been replaced") |
| assert.Check(t, is.Equal(req.Header.Get("Custom-Header-2"), "custom-value-C")) |
| 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{})) |
| } |
| |
| func TestWithResponseHook(t *testing.T) { |
| const hdrKey = "X-Test-Header" |
| const hdrVal = "hello-world" |
| |
| t.Run("single hook", func(t *testing.T) { |
| var got string |
| c, err := New( |
| WithResponseHook(func(resp *http.Response) { |
| got = resp.Header.Get(hdrKey) |
| }), |
| WithBaseMockClient(func(req *http.Request) (*http.Response, error) { |
| resp := &http.Response{ |
| StatusCode: http.StatusOK, |
| Header: make(http.Header), |
| } |
| resp.Header.Set(hdrKey, hdrVal) |
| return resp, nil |
| }), |
| ) |
| assert.NilError(t, err) |
| |
| _, err = c.Ping(t.Context(), PingOptions{}) |
| assert.NilError(t, err) |
| assert.Check(t, is.Equal(got, hdrVal)) |
| |
| assert.NilError(t, c.Close()) |
| }) |
| |
| t.Run("invalid hook", func(t *testing.T) { |
| _, err := New(WithResponseHook(nil)) |
| assert.Error(t, err, "invalid response hook: hook is nil") |
| }) |
| |
| t.Run("multiple hooks", func(t *testing.T) { |
| var triggered []string |
| |
| c, err := New( |
| WithResponseHook(func(*http.Response) { |
| triggered = append(triggered, "hook 1: "+hdrVal) |
| }), |
| WithResponseHook(func(*http.Response) { |
| triggered = append(triggered, "hook 2: "+hdrVal) |
| }), |
| WithBaseMockClient(func(req *http.Request) (*http.Response, error) { |
| resp := &http.Response{ |
| StatusCode: http.StatusOK, |
| Header: make(http.Header), |
| } |
| resp.Header.Set(hdrKey, hdrVal) |
| return resp, nil |
| }), |
| ) |
| assert.NilError(t, err) |
| |
| _, err = c.Ping(t.Context(), PingOptions{}) |
| assert.NilError(t, err) |
| assert.Check(t, is.DeepEqual(triggered, []string{"hook 1: " + hdrVal, "hook 2: " + hdrVal})) |
| |
| assert.NilError(t, c.Close()) |
| }) |
| } |