Files
bDS2/test/bds/ai/http_client_test.exs
2026-06-11 16:18:09 +02:00

145 lines
4.6 KiB
Elixir

defmodule BDS.AI.HttpClientTest do
use ExUnit.Case, async: false
alias BDS.AI.HttpClient
defmodule TestPlug do
import Plug.Conn
def init(opts), do: opts
def call(%Plug.Conn{path_info: ["ok"]} = conn, _opts) do
conn
|> put_resp_header("x-test-header", "Value-1")
|> put_resp_content_type("application/json")
|> send_resp(200, ~s({"hello":"world"}))
end
def call(%Plug.Conn{path_info: ["echo"], method: "POST"} = conn, _opts) do
{:ok, body, conn} = read_body(conn)
send(test_pid(), {:echo_request, conn.req_headers, body})
send_resp(conn, 200, body)
end
def call(%Plug.Conn{path_info: ["slow"]} = conn, _opts) do
Process.sleep(1_000)
send_resp(conn, 200, "late")
end
def call(%Plug.Conn{path_info: ["flaky-get"]} = conn, _opts) do
send(test_pid(), :flaky_hit)
if :ets.update_counter(:bds_http_client_test, :flaky_get, 1) == 1 do
send_resp(conn, 500, "boom")
else
send_resp(conn, 200, "recovered")
end
end
def call(%Plug.Conn{path_info: ["fail-post"], method: "POST"} = conn, _opts) do
send(test_pid(), :post_hit)
send_resp(conn, 500, "boom")
end
defp test_pid, do: Application.get_env(:bds, :http_client_test_pid)
end
setup do
if :ets.whereis(:bds_http_client_test) == :undefined do
:ets.new(:bds_http_client_test, [:named_table, :public, :set])
end
:ets.insert(:bds_http_client_test, {:flaky_get, 0})
Application.put_env(:bds, :http_client_test_pid, self())
original_config = Application.fetch_env(:bds, HttpClient)
# Fast retries so retry tests don't slow the suite down.
Application.put_env(:bds, HttpClient, retry_delay_ms: 10)
on_exit(fn ->
case original_config do
{:ok, value} -> Application.put_env(:bds, HttpClient, value)
:error -> Application.delete_env(:bds, HttpClient)
end
end)
server = start_supervised!({Bandit, plug: TestPlug, port: 0, startup_log: false})
{:ok, {_address, port}} = ThousandIsland.listener_info(server)
{:ok, base_url: "http://127.0.0.1:#{port}"}
end
defp put_config(extra) do
Application.put_env(:bds, HttpClient, Keyword.merge([retry_delay_ms: 10], extra))
end
test "GET returns status, body, and downcased single-valued headers", %{base_url: base_url} do
assert {:ok, response} = HttpClient.get(base_url <> "/ok", %{"accept" => "application/json"})
assert response.status == 200
assert response.body == ~s({"hello":"world"})
assert response.headers["x-test-header"] == "Value-1"
assert response.headers["content-type"] =~ "application/json"
refute Map.has_key?(response.headers, "X-Test-Header")
end
test "POST sends the raw body and headers and returns the raw response body", %{
base_url: base_url
} do
payload = ~s({"model":"gpt-test"})
assert {:ok, response} =
HttpClient.post(
base_url <> "/echo",
%{"content-type" => "application/json", "authorization" => "Bearer sk-123"},
payload
)
assert response.status == 200
assert response.body == payload
assert_received {:echo_request, req_headers, ^payload}
assert {"authorization", "Bearer sk-123"} in req_headers
assert {"content-type", "application/json"} in req_headers
end
test "a hung endpoint times out within the configured budget", %{base_url: base_url} do
put_config(receive_timeout_ms: 100, get_max_retries: 0)
started_at = System.monotonic_time(:millisecond)
assert {:error, :timeout} = HttpClient.post(base_url <> "/slow", %{}, "{}")
assert {:error, :timeout} = HttpClient.get(base_url <> "/slow", %{})
elapsed = System.monotonic_time(:millisecond) - started_at
assert elapsed < 2_000
end
test "a refused connection returns an error instead of raising" do
put_config(get_max_retries: 0)
# An ephemeral port nothing listens on.
{:ok, socket} = :gen_tcp.listen(0, [])
{:ok, port} = :inet.port(socket)
:ok = :gen_tcp.close(socket)
assert {:error, :econnrefused} = HttpClient.get("http://127.0.0.1:#{port}/ok", %{})
end
test "GET retries transient server errors", %{base_url: base_url} do
assert {:ok, %{status: 200, body: "recovered"}} =
HttpClient.get(base_url <> "/flaky-get", %{})
assert_received :flaky_hit
assert_received :flaky_hit
end
test "POST is never retried, even on transient server errors", %{base_url: base_url} do
assert {:ok, %{status: 500, body: "boom"}} =
HttpClient.post(base_url <> "/fail-post", %{}, "{}")
assert_received :post_hit
refute_received :post_hit
end
end