fix: implement TD-02.
This commit is contained in:
144
test/bds/ai/http_client_test.exs
Normal file
144
test/bds/ai/http_client_test.exs
Normal file
@@ -0,0 +1,144 @@
|
||||
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
|
||||
Reference in New Issue
Block a user