mirror of
https://github.com/moby/moby.git
synced 2026-01-11 10:41:43 +00:00
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>
191 lines
5.9 KiB
Go
191 lines
5.9 KiB
Go
package client
|
|
|
|
import (
|
|
"encoding/json"
|
|
"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"
|
|
)
|
|
|
|
// defaultAPIPath is the API path prefix for the default API version used.
|
|
const defaultAPIPath = "/v" + MaxAPIVersion
|
|
|
|
// assertRequest checks for the request method and path. If the expected
|
|
// path does not contain a version prefix, it is prefixed with the current API
|
|
// version.
|
|
func assertRequest(req *http.Request, expMethod string, expectedPath string) error {
|
|
if !strings.HasPrefix(expectedPath, "/v1.") {
|
|
expectedPath = defaultAPIPath + expectedPath
|
|
}
|
|
if !strings.HasPrefix(req.URL.Path, expectedPath) {
|
|
return fmt.Errorf("expected URL '%s', got '%s'", expectedPath, req.URL.Path)
|
|
}
|
|
if req.Method != expMethod {
|
|
return fmt.Errorf("expected %s method, got %s", expMethod, req.Method)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func assertRequestWithQuery(req *http.Request, expMethod string, expectedPath string, expectedQuery string) error {
|
|
if err := assertRequest(req, expMethod, expectedPath); err != nil {
|
|
return err
|
|
}
|
|
q := req.URL.Query().Encode()
|
|
if q != expectedQuery {
|
|
return fmt.Errorf("expected query '%s', got '%s'", expectedQuery, q)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ensureBody makes sure the response has a Body (using [http.NoBody] if
|
|
// none is present), and that the request is set on the response, then returns
|
|
// it as a testRoundTripper.
|
|
func ensureBody(f func(req *http.Request) (*http.Response, error)) testRoundTripper {
|
|
return func(req *http.Request) (*http.Response, error) {
|
|
resp, err := f(req)
|
|
if resp != nil {
|
|
if resp.Body == nil {
|
|
resp.Body = http.NoBody
|
|
}
|
|
if resp.Request == nil {
|
|
resp.Request = req
|
|
}
|
|
}
|
|
return resp, err
|
|
}
|
|
}
|
|
|
|
// 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),
|
|
})
|
|
}
|
|
|
|
func errorMock(statusCode int, message string) func(req *http.Request) (*http.Response, error) {
|
|
return mockJSONResponse(statusCode, nil, common.ErrorResponse{
|
|
Message: message,
|
|
})
|
|
}
|
|
|
|
func mockJSONResponse[T any](statusCode int, headers http.Header, resp T) func(req *http.Request) (*http.Response, error) {
|
|
respBody, err := json.Marshal(&resp)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
hdr := make(http.Header)
|
|
if headers != nil {
|
|
hdr = headers.Clone()
|
|
}
|
|
hdr.Set("Content-Type", "application/json")
|
|
return mockResponse(statusCode, hdr, string(respBody))
|
|
}
|
|
|
|
// mockPingResponse mocks the headers set for a "/_ping" response.
|
|
func mockPingResponse(statusCode int, ping PingResult) func(req *http.Request) (*http.Response, error) {
|
|
headers := http.Header{}
|
|
if s := ping.SwarmStatus; s != nil {
|
|
role := "worker"
|
|
if s.ControlAvailable {
|
|
role = "manager"
|
|
}
|
|
headers.Set("Swarm", fmt.Sprintf("%s/%s", string(swarm.LocalNodeStateActive), role))
|
|
}
|
|
headers.Set("Api-Version", ping.APIVersion)
|
|
headers.Set("Ostype", ping.OSType)
|
|
headers.Set("Docker-Experimental", strconv.FormatBool(ping.Experimental))
|
|
headers.Set("Builder-Version", string(ping.BuilderVersion))
|
|
|
|
headers.Set("Content-Type", "text/plain; charset=utf-8")
|
|
headers.Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
|
headers.Set("Pragma", "no-cache")
|
|
return mockResponse(statusCode, headers, "OK")
|
|
}
|
|
|
|
func mockResponse(statusCode int, headers http.Header, respBody string) func(req *http.Request) (*http.Response, error) {
|
|
return func(req *http.Request) (*http.Response, error) {
|
|
var body io.ReadCloser
|
|
if respBody == "" || req.Method == http.MethodHead {
|
|
body = http.NoBody
|
|
} else {
|
|
body = io.NopCloser(strings.NewReader(respBody))
|
|
}
|
|
return &http.Response{
|
|
Status: fmt.Sprintf("%d %s", statusCode, http.StatusText(statusCode)),
|
|
StatusCode: statusCode,
|
|
Header: headers.Clone(),
|
|
Body: body,
|
|
Request: req,
|
|
}, nil
|
|
}
|
|
}
|