Skip to content

Commit

Permalink
feat: support for waiting for response headers
Browse files Browse the repository at this point in the history
  • Loading branch information
mdelapenya committed Mar 13, 2024
1 parent fe0d3a8 commit a723e93
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 22 deletions.
8 changes: 8 additions & 0 deletions docs/features/wait/http.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ The HTTP wait strategy will check the result of an HTTP(S) request against the c
- the HTTP request body to be sent.
- the HTTP status code matcher as a function.
- the HTTP response matcher as a function.
- the HTTP headers to be used.
- the HTTP response headers matcher as a function.
- the TLS config to be used for HTTPS.
- the startup timeout to be used in seconds, default is 60 seconds.
- the poll interval to be used in milliseconds, default is 100 milliseconds.
Expand Down Expand Up @@ -41,3 +43,9 @@ Variations on the HTTP wait strategy are supported, including:
<!--codeinclude-->
[Waiting for an HTTP endpoint matching an HTTP status code](../../../wait/http_test.go) inside_block:waitForHTTPStatusCode
<!--/codeinclude-->

## Match for HTTP response headers

<!--codeinclude-->
[Waiting for an HTTP endpoint matching an HTTP response header](../../../wait/http_test.go) inside_block:waitForHTTPHeaders
<!--/codeinclude-->
67 changes: 45 additions & 22 deletions wait/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,33 +28,37 @@ type HTTPStrategy struct {
timeout *time.Duration

// additional properties
Port nat.Port
Path string
StatusCodeMatcher func(status int) bool
ResponseMatcher func(body io.Reader) bool
UseTLS bool
AllowInsecure bool
TLSConfig *tls.Config // TLS config for HTTPS
Method string // http method
Body io.Reader // http request body
PollInterval time.Duration
UserInfo *url.Userinfo
ForceIPv4LocalHost bool
Port nat.Port
Path string
StatusCodeMatcher func(status int) bool
ResponseMatcher func(body io.Reader) bool
UseTLS bool
AllowInsecure bool
TLSConfig *tls.Config // TLS config for HTTPS
Method string // http method
Body io.Reader // http request body
Headers map[string]string
ResponseHeadersMatcher func(headers http.Header) bool
PollInterval time.Duration
UserInfo *url.Userinfo
ForceIPv4LocalHost bool
}

// NewHTTPStrategy constructs a HTTP strategy waiting on port 80 and status code 200
func NewHTTPStrategy(path string) *HTTPStrategy {
return &HTTPStrategy{
Port: "",
Path: path,
StatusCodeMatcher: defaultStatusCodeMatcher,
ResponseMatcher: func(body io.Reader) bool { return true },
UseTLS: false,
TLSConfig: nil,
Method: http.MethodGet,
Body: nil,
PollInterval: defaultPollInterval(),
UserInfo: nil,
Port: "",
Path: path,
StatusCodeMatcher: defaultStatusCodeMatcher,
ResponseMatcher: func(body io.Reader) bool { return true },
UseTLS: false,
TLSConfig: nil,
Method: http.MethodGet,
Body: nil,
Headers: map[string]string{},
ResponseHeadersMatcher: func(headers http.Header) bool { return true },
PollInterval: defaultPollInterval(),
UserInfo: nil,
}
}

Expand Down Expand Up @@ -110,6 +114,16 @@ func (ws *HTTPStrategy) WithBody(reqdata io.Reader) *HTTPStrategy {
return ws
}

func (ws *HTTPStrategy) WithHeaders(headers map[string]string) *HTTPStrategy {
ws.Headers = headers
return ws
}

func (ws *HTTPStrategy) WithResponseHeadersMatcher(matcher func(http.Header) bool) *HTTPStrategy {
ws.ResponseHeadersMatcher = matcher
return ws
}

func (ws *HTTPStrategy) WithBasicAuth(username, password string) *HTTPStrategy {
ws.UserInfo = url.UserPassword(username, password)
return ws
Expand Down Expand Up @@ -281,6 +295,11 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
if err != nil {
return err
}

for k, v := range ws.Headers {
req.Header.Set(k, v)
}

resp, err := client.Do(req)
if err != nil {
continue
Expand All @@ -293,6 +312,10 @@ func (ws *HTTPStrategy) WaitUntilReady(ctx context.Context, target StrategyTarge
_ = resp.Body.Close()
continue
}
if ws.ResponseHeadersMatcher != nil && !ws.ResponseHeadersMatcher(resp.Header) {
_ = resp.Body.Close()
continue
}
if err := resp.Body.Close(); err != nil {
continue
}
Expand Down
57 changes: 57 additions & 0 deletions wait/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,63 @@ func ExampleHTTPStrategy() {
// true
}

func ExampleHTTPStrategy_WithHeaders() {
capath := filepath.Join("testdata", "root.pem")
cafile, err := os.ReadFile(capath)
if err != nil {
log.Fatalf("can't load ca file: %v", err)
}

certpool := x509.NewCertPool()
if !certpool.AppendCertsFromPEM(cafile) {
log.Fatalf("the ca file isn't valid")
}

ctx := context.Background()

// waitForHTTPHeaders {
tlsconfig := &tls.Config{RootCAs: certpool, ServerName: "testcontainer.go.test"}
req := testcontainers.ContainerRequest{
FromDockerfile: testcontainers.FromDockerfile{
Context: "testdata",
},
ExposedPorts: []string{"6443/tcp"},
WaitingFor: wait.ForHTTP("/headers").
WithTLS(true, tlsconfig).
WithPort("6443/tcp").
WithHeaders(map[string]string{"X-request-header": "value"}).
WithResponseHeadersMatcher(func(headers http.Header) bool {
return headers.Get("X-response-header") == "value"
},
),
}
// }

c, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
log.Fatalf("failed to start container: %s", err)
}

defer func() {
if err := c.Terminate(ctx); err != nil {
log.Fatalf("failed to terminate container: %s", err)
}
}()

state, err := c.State(ctx)
if err != nil {
log.Fatalf("failed to get container state: %s", err) // nolint:gocritic
}

fmt.Println(state.Running)

// Output:
// true

}
func ExampleHTTPStrategy_WithPort() {
// waitForHTTPWithPort {
ctx := context.Background()
Expand Down
11 changes: 11 additions & 0 deletions wait/testdata/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ func main() {
w.WriteHeader(http.StatusUnauthorized)
})

mux.HandleFunc("/headers", func(w http.ResponseWriter, req *http.Request) {
h := req.Header.Get("X-request-header")
if h != "" {
w.Header().Add("X-response-header", h)
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("headers"))
} else {
w.WriteHeader(http.StatusBadRequest)
}
})

mux.HandleFunc("/ping", func(w http.ResponseWriter, req *http.Request) {
data, _ := io.ReadAll(req.Body)
if bytes.Equal(data, []byte("ping")) {
Expand Down

0 comments on commit a723e93

Please sign in to comment.