diff --git a/client/client_mock_test.go b/client/client_mock_test.go index 0f1129917b..af76b79856 100644 --- a/client/client_mock_test.go +++ b/client/client_mock_test.go @@ -5,9 +5,11 @@ import ( "fmt" "io" "net/http" + "runtime" "strconv" "strings" + "github.com/moby/moby/api/types/build" "github.com/moby/moby/api/types/common" "github.com/moby/moby/api/types/swarm" ) @@ -60,8 +62,70 @@ func ensureBody(f func(req *http.Request) (*http.Response, error)) testRoundTrip } } -// WithMockClient is a test helper that allows you to inject a mock client for testing. +// makeTestRoundTripper makes sure the response has a Body, using [http.NoBody] if +// none is present, and returns it as a testRoundTripper. If withDefaults is set, +// it also mocks the "/_ping" endpoint and sets default headers as returned +// by the daemon. +func makeTestRoundTripper(f func(req *http.Request) (*http.Response, error)) testRoundTripper { + return func(req *http.Request) (*http.Response, error) { + if req.URL.Path == "/_ping" { + return mockPingResponse(http.StatusOK, PingResult{ + APIVersion: MaxAPIVersion, + OSType: runtime.GOOS, + Experimental: true, + BuilderVersion: build.BuilderBuildKit, + SwarmStatus: &SwarmStatus{ + NodeState: swarm.LocalNodeStateActive, + ControlAvailable: true, + }, + })(req) + } + resp, err := f(req) + if resp != nil { + if resp.Body == nil { + resp.Body = http.NoBody + } + if resp.Request == nil { + resp.Request = req + } + } + applyDefaultHeaders(resp) + return resp, err + } +} + +// applyDefaultHeaders mocks the headers set by the daemon's VersionMiddleware. +func applyDefaultHeaders(resp *http.Response) { + if resp == nil { + return + } + if resp.Header == nil { + resp.Header = make(http.Header) + } + if resp.Header.Get("Server") == "" { + resp.Header.Set("Server", fmt.Sprintf("Docker/%s (%s)", "v99.99.99", runtime.GOOS)) + } + if resp.Header.Get("Api-Version") == "" { + resp.Header.Set("Api-Version", MaxAPIVersion) + } + if resp.Header.Get("Ostype") == "" { + resp.Header.Set("Ostype", runtime.GOOS) + } +} + +// WithMockClient is a test helper that allows you to inject a mock client for +// testing. By default, it mocks the "/_ping" endpoint, so allow the client +// to perform API-version negotiation. Other endpoints are handled by "doer". func WithMockClient(doer func(*http.Request) (*http.Response, error)) Opt { + return WithHTTPClient(&http.Client{ + Transport: makeTestRoundTripper(doer), + }) +} + +// WithBaseMockClient is a test helper that allows you to inject a mock client +// for testing. It is identical to [WithMockClient], but does not mock the "/_ping" +// endpoint, and doesn't set the default headers. +func WithBaseMockClient(doer func(*http.Request) (*http.Response, error)) Opt { return WithHTTPClient(&http.Client{ Transport: ensureBody(doer), }) diff --git a/client/client_test.go b/client/client_test.go index 2e6326c89b..248616fa34 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -258,7 +258,7 @@ func TestNegotiateAPIVersionEmpty(t *testing.T) { client, err := New(FromEnv, WithAPIVersionNegotiation(), - WithMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: expected})), + WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: expected})), ) assert.NilError(t, err) @@ -331,7 +331,7 @@ func TestNegotiateAPIVersion(t *testing.T) { opts := []Opt{ FromEnv, WithAPIVersionNegotiation(), - WithMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: tc.pingVersion})), + WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: tc.pingVersion})), } if tc.clientVersion != "" { @@ -363,7 +363,7 @@ func TestNegotiateAPIVersionOverride(t *testing.T) { client, err := New( FromEnv, - WithMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: "1.45"})), + WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: "1.45"})), ) assert.NilError(t, err) @@ -393,7 +393,7 @@ func TestNegotiateAPIVersionAutomatic(t *testing.T) { ctx := t.Context() client, err := New( - WithMockClient(func(req *http.Request) (*http.Response, error) { + WithBaseMockClient(func(req *http.Request) (*http.Response, error) { return mockPingResponse(http.StatusOK, PingResult{APIVersion: pingVersion})(req) }), WithAPIVersionNegotiation(), @@ -422,7 +422,7 @@ func TestNegotiateAPIVersionAutomatic(t *testing.T) { func TestNegotiateAPIVersionWithEmptyVersion(t *testing.T) { client, err := New( WithAPIVersion(""), - WithMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: "1.50"})), + WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: "1.50"})), ) assert.NilError(t, err) @@ -442,7 +442,7 @@ func TestNegotiateAPIVersionWithFixedVersion(t *testing.T) { ) client, err := New( WithAPIVersion(customVersion), - WithMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: pingVersion})), + WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: pingVersion})), ) assert.NilError(t, err) diff --git a/client/ping_test.go b/client/ping_test.go index 5fb20b01c0..e395ae9ccc 100644 --- a/client/ping_test.go +++ b/client/ping_test.go @@ -18,7 +18,7 @@ import ( // panics. func TestPingFail(t *testing.T) { var withHeader bool - client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) { + client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) { var hdr http.Header if withHeader { hdr = http.Header{} @@ -48,7 +48,7 @@ func TestPingFail(t *testing.T) { // TestPingWithError tests the case where there is a protocol error in the ping. // This test is mostly just testing that there are no panics in this code path. func TestPingWithError(t *testing.T) { - client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) { + client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) { return nil, errors.New("some connection error") })) assert.NilError(t, err) @@ -64,7 +64,7 @@ func TestPingWithError(t *testing.T) { // TestPingSuccess tests that we are able to get the expected API headers/ping // details on success. func TestPingSuccess(t *testing.T) { - client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) { + client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) { hdr := http.Header{} hdr.Set("Api-Version", "awesome") hdr.Set("Docker-Experimental", "true") @@ -110,7 +110,7 @@ func TestPingHeadFallback(t *testing.T) { for _, tc := range tests { t.Run(http.StatusText(tc.status), func(t *testing.T) { var reqs []string - client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) { + client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) { if !strings.HasPrefix(req.URL.Path, expectedPath) { return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedPath, req.URL.Path) } diff --git a/client/request_test.go b/client/request_test.go index 3294c44306..98def7b6cd 100644 --- a/client/request_test.go +++ b/client/request_test.go @@ -194,7 +194,7 @@ func TestResponseErrors(t *testing.T) { } for _, tc := range tests { t.Run(tc.doc, func(t *testing.T) { - client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) { + client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) { return mockResponse(http.StatusBadRequest, http.Header{"Content-Type": []string{tc.contentType}}, tc.response)(req) })) if tc.apiVersion != "" { @@ -210,7 +210,7 @@ func TestResponseErrors(t *testing.T) { func TestInfiniteError(t *testing.T) { infinitR := rand.New(rand.NewSource(42)) - client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) { + client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) { resp := &http.Response{ StatusCode: http.StatusInternalServerError, Header: http.Header{},