Browse code

client: improve mocking responses

Make the mocked responses match the API closer;

- Add headers as returned by the daemon's VersionMiddleware
- By default handle "/_ping" requests to allow the client to
perform API-version negotiation as part of tests.

Signed-off-by: Sebastiaan van Stijn <github@gone.nl>

Sebastiaan van Stijn authored on 2025/11/13 09:25:15
Showing 4 changed files
... ...
@@ -5,9 +5,11 @@ import (
5 5
 	"fmt"
6 6
 	"io"
7 7
 	"net/http"
8
+	"runtime"
8 9
 	"strconv"
9 10
 	"strings"
10 11
 
12
+	"github.com/moby/moby/api/types/build"
11 13
 	"github.com/moby/moby/api/types/common"
12 14
 	"github.com/moby/moby/api/types/swarm"
13 15
 )
... ...
@@ -60,9 +62,71 @@ func ensureBody(f func(req *http.Request) (*http.Response, error)) testRoundTrip
60 60
 	}
61 61
 }
62 62
 
63
-// WithMockClient is a test helper that allows you to inject a mock client for testing.
63
+// makeTestRoundTripper makes sure the response has a Body, using [http.NoBody] if
64
+// none is present, and returns it as a testRoundTripper. If withDefaults is set,
65
+// it also mocks the "/_ping" endpoint and sets default headers as returned
66
+// by the daemon.
67
+func makeTestRoundTripper(f func(req *http.Request) (*http.Response, error)) testRoundTripper {
68
+	return func(req *http.Request) (*http.Response, error) {
69
+		if req.URL.Path == "/_ping" {
70
+			return mockPingResponse(http.StatusOK, PingResult{
71
+				APIVersion:     MaxAPIVersion,
72
+				OSType:         runtime.GOOS,
73
+				Experimental:   true,
74
+				BuilderVersion: build.BuilderBuildKit,
75
+				SwarmStatus: &SwarmStatus{
76
+					NodeState:        swarm.LocalNodeStateActive,
77
+					ControlAvailable: true,
78
+				},
79
+			})(req)
80
+		}
81
+		resp, err := f(req)
82
+		if resp != nil {
83
+			if resp.Body == nil {
84
+				resp.Body = http.NoBody
85
+			}
86
+			if resp.Request == nil {
87
+				resp.Request = req
88
+			}
89
+		}
90
+		applyDefaultHeaders(resp)
91
+		return resp, err
92
+	}
93
+}
94
+
95
+// applyDefaultHeaders mocks the headers set by the daemon's VersionMiddleware.
96
+func applyDefaultHeaders(resp *http.Response) {
97
+	if resp == nil {
98
+		return
99
+	}
100
+	if resp.Header == nil {
101
+		resp.Header = make(http.Header)
102
+	}
103
+	if resp.Header.Get("Server") == "" {
104
+		resp.Header.Set("Server", fmt.Sprintf("Docker/%s (%s)", "v99.99.99", runtime.GOOS))
105
+	}
106
+	if resp.Header.Get("Api-Version") == "" {
107
+		resp.Header.Set("Api-Version", MaxAPIVersion)
108
+	}
109
+	if resp.Header.Get("Ostype") == "" {
110
+		resp.Header.Set("Ostype", runtime.GOOS)
111
+	}
112
+}
113
+
114
+// WithMockClient is a test helper that allows you to inject a mock client for
115
+// testing. By default, it mocks the "/_ping" endpoint, so allow the client
116
+// to perform API-version negotiation. Other endpoints are handled by "doer".
64 117
 func WithMockClient(doer func(*http.Request) (*http.Response, error)) Opt {
65 118
 	return WithHTTPClient(&http.Client{
119
+		Transport: makeTestRoundTripper(doer),
120
+	})
121
+}
122
+
123
+// WithBaseMockClient is a test helper that allows you to inject a mock client
124
+// for testing. It is identical to [WithMockClient], but does not mock the "/_ping"
125
+// endpoint, and doesn't set the default headers.
126
+func WithBaseMockClient(doer func(*http.Request) (*http.Response, error)) Opt {
127
+	return WithHTTPClient(&http.Client{
66 128
 		Transport: ensureBody(doer),
67 129
 	})
68 130
 }
... ...
@@ -258,7 +258,7 @@ func TestNegotiateAPIVersionEmpty(t *testing.T) {
258 258
 
259 259
 	client, err := New(FromEnv,
260 260
 		WithAPIVersionNegotiation(),
261
-		WithMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: expected})),
261
+		WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: expected})),
262 262
 	)
263 263
 	assert.NilError(t, err)
264 264
 
... ...
@@ -331,7 +331,7 @@ func TestNegotiateAPIVersion(t *testing.T) {
331 331
 			opts := []Opt{
332 332
 				FromEnv,
333 333
 				WithAPIVersionNegotiation(),
334
-				WithMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: tc.pingVersion})),
334
+				WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: tc.pingVersion})),
335 335
 			}
336 336
 
337 337
 			if tc.clientVersion != "" {
... ...
@@ -363,7 +363,7 @@ func TestNegotiateAPIVersionOverride(t *testing.T) {
363 363
 
364 364
 	client, err := New(
365 365
 		FromEnv,
366
-		WithMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: "1.45"})),
366
+		WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: "1.45"})),
367 367
 	)
368 368
 	assert.NilError(t, err)
369 369
 
... ...
@@ -393,7 +393,7 @@ func TestNegotiateAPIVersionAutomatic(t *testing.T) {
393 393
 
394 394
 	ctx := t.Context()
395 395
 	client, err := New(
396
-		WithMockClient(func(req *http.Request) (*http.Response, error) {
396
+		WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
397 397
 			return mockPingResponse(http.StatusOK, PingResult{APIVersion: pingVersion})(req)
398 398
 		}),
399 399
 		WithAPIVersionNegotiation(),
... ...
@@ -422,7 +422,7 @@ func TestNegotiateAPIVersionAutomatic(t *testing.T) {
422 422
 func TestNegotiateAPIVersionWithEmptyVersion(t *testing.T) {
423 423
 	client, err := New(
424 424
 		WithAPIVersion(""),
425
-		WithMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: "1.50"})),
425
+		WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: "1.50"})),
426 426
 	)
427 427
 	assert.NilError(t, err)
428 428
 
... ...
@@ -442,7 +442,7 @@ func TestNegotiateAPIVersionWithFixedVersion(t *testing.T) {
442 442
 	)
443 443
 	client, err := New(
444 444
 		WithAPIVersion(customVersion),
445
-		WithMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: pingVersion})),
445
+		WithBaseMockClient(mockPingResponse(http.StatusOK, PingResult{APIVersion: pingVersion})),
446 446
 	)
447 447
 	assert.NilError(t, err)
448 448
 
... ...
@@ -18,7 +18,7 @@ import (
18 18
 // panics.
19 19
 func TestPingFail(t *testing.T) {
20 20
 	var withHeader bool
21
-	client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) {
21
+	client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
22 22
 		var hdr http.Header
23 23
 		if withHeader {
24 24
 			hdr = http.Header{}
... ...
@@ -48,7 +48,7 @@ func TestPingFail(t *testing.T) {
48 48
 // TestPingWithError tests the case where there is a protocol error in the ping.
49 49
 // This test is mostly just testing that there are no panics in this code path.
50 50
 func TestPingWithError(t *testing.T) {
51
-	client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) {
51
+	client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
52 52
 		return nil, errors.New("some connection error")
53 53
 	}))
54 54
 	assert.NilError(t, err)
... ...
@@ -64,7 +64,7 @@ func TestPingWithError(t *testing.T) {
64 64
 // TestPingSuccess tests that we are able to get the expected API headers/ping
65 65
 // details on success.
66 66
 func TestPingSuccess(t *testing.T) {
67
-	client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) {
67
+	client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
68 68
 		hdr := http.Header{}
69 69
 		hdr.Set("Api-Version", "awesome")
70 70
 		hdr.Set("Docker-Experimental", "true")
... ...
@@ -110,7 +110,7 @@ func TestPingHeadFallback(t *testing.T) {
110 110
 	for _, tc := range tests {
111 111
 		t.Run(http.StatusText(tc.status), func(t *testing.T) {
112 112
 			var reqs []string
113
-			client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) {
113
+			client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
114 114
 				if !strings.HasPrefix(req.URL.Path, expectedPath) {
115 115
 					return nil, fmt.Errorf("expected URL '%s', got '%s'", expectedPath, req.URL.Path)
116 116
 				}
... ...
@@ -194,7 +194,7 @@ func TestResponseErrors(t *testing.T) {
194 194
 	}
195 195
 	for _, tc := range tests {
196 196
 		t.Run(tc.doc, func(t *testing.T) {
197
-			client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) {
197
+			client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
198 198
 				return mockResponse(http.StatusBadRequest, http.Header{"Content-Type": []string{tc.contentType}}, tc.response)(req)
199 199
 			}))
200 200
 			if tc.apiVersion != "" {
... ...
@@ -210,7 +210,7 @@ func TestResponseErrors(t *testing.T) {
210 210
 
211 211
 func TestInfiniteError(t *testing.T) {
212 212
 	infinitR := rand.New(rand.NewSource(42))
213
-	client, err := New(WithMockClient(func(req *http.Request) (*http.Response, error) {
213
+	client, err := New(WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
214 214
 		resp := &http.Response{
215 215
 			StatusCode: http.StatusInternalServerError,
216 216
 			Header:     http.Header{},