Add WithHTTPRequestHook to allow callers to inspect outgoing HTTP requests and modify request metadata before they are sent to the daemon. Request hooks receive a clone of the original request so that modifications made by hooks do not mutate the request passed to the client's RoundTripper. Changes made by hooks are preserved on the request passed to the underlying transport. Protect request bodies while hooks are running, preventing hooks from reading or closing the body while restoring the original body before the request is sent. Hooks are invoked in registration order and may return an error to abort the request. Signed-off-by: Sebastiaan van Stijn <github@gone.nl>
649 lines
19 KiB
Go
649 lines
19 KiB
Go
package client
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"errors"
|
|
"io"
|
|
"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 TestWithHTTPRequestHook(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(
|
|
WithHTTPRequestHook(func(req *http.Request) error {
|
|
req.Header.Set(hdrKey, hdrVal)
|
|
return nil
|
|
}),
|
|
WithHTTPResponseHook(func(resp *http.Response) {
|
|
got = resp.Request.Header.Get(hdrKey)
|
|
}),
|
|
WithMockClient(mockResponse(http.StatusOK, 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(WithHTTPRequestHook(nil))
|
|
assert.Error(t, err, "invalid request hook: hook is nil")
|
|
})
|
|
|
|
t.Run("failing hook", func(t *testing.T) {
|
|
c, err := New(WithHTTPRequestHook(func(req *http.Request) error {
|
|
return errors.New("hook error")
|
|
}))
|
|
assert.NilError(t, err)
|
|
|
|
_, err = c.Ping(t.Context(), PingOptions{})
|
|
assert.ErrorContains(t, err, "hook error")
|
|
})
|
|
|
|
t.Run("failing hook closes body", func(t *testing.T) {
|
|
body := &trackingBody{Reader: strings.NewReader("request body")}
|
|
|
|
c, err := New(
|
|
WithHTTPRequestHook(func(*http.Request) error {
|
|
return errors.New("hook error")
|
|
}),
|
|
WithBaseMockClient(func(*http.Request) (*http.Response, error) {
|
|
t.Fatal("transport must not be called")
|
|
return nil, nil
|
|
}),
|
|
)
|
|
assert.NilError(t, err)
|
|
|
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, c.scheme+"://"+c.addr+"/", body)
|
|
assert.NilError(t, err)
|
|
|
|
_, err = c.client.Do(req)
|
|
assert.ErrorContains(t, err, "hook error")
|
|
assert.Check(t, body.closed, "request body must be closed when hook returns an error")
|
|
|
|
assert.NilError(t, c.Close())
|
|
})
|
|
|
|
t.Run("multiple hooks", func(t *testing.T) {
|
|
var triggered []string
|
|
|
|
c, err := New(
|
|
WithHTTPRequestHook(func(*http.Request) error {
|
|
triggered = append(triggered, "hook 1: "+hdrVal)
|
|
return nil
|
|
}),
|
|
WithHTTPRequestHook(func(*http.Request) error {
|
|
triggered = append(triggered, "hook 2: "+hdrVal)
|
|
return nil
|
|
}),
|
|
WithMockClient(mockResponse(http.StatusOK, 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())
|
|
})
|
|
|
|
t.Run("cannot read or close body", func(t *testing.T) {
|
|
body := &trackingBody{Reader: strings.NewReader("request body")}
|
|
|
|
c, err := New(
|
|
WithHTTPRequestHook(func(req *http.Request) error {
|
|
_, err := io.ReadAll(req.Body)
|
|
assert.Error(t, err, "hooks must not read HTTP message body")
|
|
assert.NilError(t, req.Body.Close())
|
|
assert.Check(t, !body.closed, "request hook must not close request body")
|
|
return nil
|
|
}),
|
|
WithBaseMockClient(func(req *http.Request) (*http.Response, error) {
|
|
got, err := io.ReadAll(req.Body)
|
|
assert.NilError(t, err)
|
|
assert.Equal(t, string(got), "request body")
|
|
|
|
return mockResponse(http.StatusOK, nil, "")(req)
|
|
}),
|
|
)
|
|
assert.NilError(t, err)
|
|
|
|
req, err := http.NewRequestWithContext(t.Context(), http.MethodPost, c.scheme+"://"+c.addr+"/_ping", body)
|
|
assert.NilError(t, err)
|
|
|
|
_, err = c.client.Do(req)
|
|
assert.NilError(t, err)
|
|
assert.NilError(t, c.Close())
|
|
})
|
|
}
|
|
|
|
func TestWithHTTPResponseHook(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(
|
|
WithHTTPResponseHook(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(WithHTTPResponseHook(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(
|
|
WithHTTPResponseHook(func(*http.Response) {
|
|
triggered = append(triggered, "hook 1: "+hdrVal)
|
|
}),
|
|
WithHTTPResponseHook(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())
|
|
})
|
|
|
|
t.Run("cannot read or close body", func(t *testing.T) {
|
|
body := &trackingBody{Reader: strings.NewReader("response body")}
|
|
|
|
c, err := New(
|
|
WithHTTPResponseHook(func(resp *http.Response) {
|
|
_, err := io.ReadAll(resp.Body)
|
|
assert.Check(t, !body.read, "response hook must not read response body")
|
|
assert.Error(t, err, "hooks must not read HTTP message body")
|
|
assert.NilError(t, resp.Body.Close())
|
|
assert.Check(t, !body.closed, "response hook must not close response body")
|
|
}),
|
|
WithBaseMockClient(func(*http.Request) (*http.Response, error) {
|
|
return &http.Response{
|
|
StatusCode: http.StatusOK,
|
|
Header: make(http.Header),
|
|
Body: body,
|
|
}, nil
|
|
}),
|
|
)
|
|
assert.NilError(t, err)
|
|
|
|
_, err = c.Ping(t.Context(), PingOptions{})
|
|
assert.NilError(t, err)
|
|
assert.NilError(t, c.Close())
|
|
})
|
|
}
|
|
|
|
type trackingBody struct {
|
|
io.Reader
|
|
read bool
|
|
closed bool
|
|
}
|
|
|
|
func (b *trackingBody) Read(p []byte) (int, error) {
|
|
b.read = true
|
|
return b.Reader.Read(p)
|
|
}
|
|
|
|
func (b *trackingBody) Close() error {
|
|
b.closed = true
|
|
return nil
|
|
}
|