Skip to content

Commit 644f5bd

Browse files
gossijarlah
andauthored
Add HttpWaitStrategy (#237)
* Add `HttpWaitStrategy` * a test case * with retry * Update lib/wait_strategy/http_wait_strategy.ex Co-authored-by: Jarl André Hübenthal <jarlah@protonmail.com> --------- Co-authored-by: Jarl André Hübenthal <jarlah@protonmail.com>
1 parent cd7c70d commit 644f5bd

File tree

2 files changed

+164
-0
lines changed

2 files changed

+164
-0
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
defmodule Testcontainers.HttpWaitStrategy do
2+
@moduledoc """
3+
Considers the container as ready when a http request is successful.
4+
"""
5+
6+
@timeout 5000
7+
@max_retries 3
8+
9+
@typedoc """
10+
The HttpWaitStrategy struct
11+
12+
## Options
13+
14+
- `:endpoint` - The endpoint to request
15+
16+
- `:port` - The exposed port of your container
17+
18+
Verification Options:
19+
20+
- `:status_code` - Check if the request responds with the given status code
21+
22+
- `:match` - Run your custom matcher on the given response. A 1-arity function
23+
taking a response as first parameter and must return a boolean
24+
25+
Request Options:
26+
27+
- `:protocol` - which protocol to use, defaults to `http`
28+
29+
- `:method` - the request method, one of [`:head`, `:get`, `:delete`, `:trace`, `:options`, `:post`, `:put`, `:patch`]
30+
31+
- `:timeout` - The timeout of the request (in milliseconds), defaults to `5000`
32+
33+
- `:headers` - Apply any headers to your request
34+
"""
35+
@type t() :: %__MODULE__{
36+
endpoint: String.t(),
37+
port: integer(),
38+
protocol: String.t(),
39+
method: :get | :post | :patch | :put | :delete | :head | :options | :trace,
40+
timeout: integer(),
41+
headers: [{binary(), binary()}],
42+
status_code: integer(),
43+
match: (map() -> boolean())
44+
}
45+
46+
defstruct [
47+
:endpoint,
48+
:port,
49+
# request options
50+
protocol: "http",
51+
method: :get,
52+
headers: [],
53+
timeout: @timeout,
54+
max_retries: @max_retries,
55+
# verification options
56+
status_code: nil,
57+
match: nil
58+
]
59+
60+
# Public interface
61+
62+
@doc """
63+
Creates a new HttpWaitStrategy to wait until a http requests succeeds.
64+
"""
65+
def new(endpoint, port, options \\ []) do
66+
struct(%__MODULE__{endpoint: endpoint, port: port}, options)
67+
end
68+
69+
# Private functions and implementations
70+
71+
defimpl Testcontainers.WaitStrategy do
72+
alias Testcontainers.HttpWaitStrategy
73+
alias Testcontainers.Container
74+
75+
@impl true
76+
def wait_until_container_is_ready(wait_strategy, container, _conn) do
77+
client = build_request(wait_strategy, container)
78+
79+
raw_response =
80+
Tesla.request(client,
81+
url: wait_strategy.endpoint,
82+
method: wait_strategy.method,
83+
headers: wait_strategy.headers
84+
)
85+
86+
with response <- validate_response(raw_response),
87+
:ok <- verify_status_code(wait_strategy, response),
88+
:ok <- verify_match(wait_strategy, response) do
89+
:ok
90+
else
91+
{:error, reason} ->
92+
{:error, reason, wait_strategy}
93+
end
94+
end
95+
96+
# Response evaluation
97+
98+
defp validate_response({:ok, response}), do: response
99+
defp validate_response({:error, reason}), do: {:error, reason}
100+
101+
defp verify_status_code(wait_strategy, %{status: status_code})
102+
when not is_nil(wait_strategy.status_code) and
103+
status_code == wait_strategy.status_code,
104+
do: :ok
105+
106+
defp verify_status_code(wait_strategy, response) when not is_nil(wait_strategy.status_code),
107+
do:
108+
{:error,
109+
"Status Code does not match. Expected: #{wait_strategy.status_code} Received: #{response.status}"}
110+
111+
defp verify_status_code(wait_strategy, _) when is_nil(wait_strategy.status_code), do: :ok
112+
113+
defp verify_match(wait_strategy, response)
114+
when not is_nil(wait_strategy.match) and is_function(wait_strategy.match) do
115+
case wait_strategy.match.(response) do
116+
true -> :ok
117+
false -> {:error, "Matcher function failed"}
118+
end
119+
end
120+
121+
defp verify_match(_, _), do: :ok
122+
123+
# Request composition
124+
125+
defp build_request(wait_strategy, container) do
126+
base_url = get_base_url(wait_strategy, container)
127+
request_timeout = round(wait_strategy.timeout / wait_strategy.max_retries)
128+
129+
Tesla.client([
130+
{Tesla.Middleware.BaseUrl, base_url: base_url},
131+
{Tesla.Middleware.Timeout, timeout: request_timeout},
132+
{Tesla.Middleware.Retry,
133+
delay: 1,
134+
max_retries: wait_strategy.max_retries,
135+
max_delay: 10,
136+
should_retry: fn
137+
_, _env, _context -> true
138+
end}
139+
])
140+
end
141+
142+
defp get_base_url(%HttpWaitStrategy{} = wait_strategy, %Container{} = container) do
143+
port = Container.mapped_port(container, wait_strategy.port)
144+
145+
"#{wait_strategy.protocol}://#{Testcontainers.get_host()}:#{port}/"
146+
end
147+
end
148+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
defmodule Testcontainers.HttpWaitStrategyTest do
2+
alias Testcontainers.HttpWaitStrategy
3+
use ExUnit.Case, async: true
4+
5+
test "can wait a http request" do
6+
port = 80
7+
8+
config =
9+
%Testcontainers.Container{image: "nginx:alpine"}
10+
|> Testcontainers.Container.with_exposed_port(port)
11+
|> Testcontainers.Container.with_waiting_strategy(HttpWaitStrategy.new("/", port))
12+
13+
assert {:ok, container} = Testcontainers.start_container(config)
14+
assert :ok = Testcontainers.stop_container(container.container_id)
15+
end
16+
end

0 commit comments

Comments
 (0)