defmodule HiiveRisk.HTTPModelTest do use ExUnit.Case, async: false alias HiiveRisk.Model.HTTP setup do config = Application.get_env(:hiive_risk, HTTP, []) on_exit(fn -> Application.put_env(:hiive_risk, HTTP, config) end) :ok end test "posts JSON snapshots and bearer auth to the configured model endpoint" do {url, server} = endpoint(200, ~s({"p_review":0.9,"answer_mass":0.99})) Application.put_env(:hiive_risk, HTTP, decision_url: url, token: "test-token") assert {:ok, %{"p_review" => 0.9, "answer_mass" => 0.99}} = HTTP.decision(%{"case_id" => "test"}) {headers, body} = Task.await(server) assert headers =~ "POST /score HTTP/1.1" assert String.downcase(headers) =~ "authorization: bearer test-token" assert Jason.decode!(body) == %{"case_id" => "test"} end test "missing endpoints and HTTP errors return errors without following redirects" do Application.put_env(:hiive_risk, HTTP, []) assert {:error, :model_endpoint_not_configured} = HTTP.sft(%{}) for status <- [302, 503] do {url, server} = endpoint(status, "{}") Application.put_env(:hiive_risk, HTTP, sft_url: url) assert {:error, {:model_http_status, ^status}} = HTTP.sft(%{}) Task.await(server) end end # One real loopback HTTP exchange, with no external service or model calls. defp endpoint(status, response) do {:ok, listener} = :gen_tcp.listen(0, [:binary, active: false, ip: {127, 0, 0, 1}]) {:ok, {_, port}} = :inet.sockname(listener) server = Task.async(fn -> {:ok, socket} = :gen_tcp.accept(listener, 5_000) {headers, body} = read_request(socket, "") :ok = :gen_tcp.send(socket, [ "HTTP/1.1 #{status} Test\r\nContent-Type: application/json\r\n", "Location: /must-not-follow\r\nContent-Length: #{byte_size(response)}\r\nConnection: close\r\n\r\n", response ]) :gen_tcp.close(socket) :gen_tcp.close(listener) {headers, body} end) {"http://127.0.0.1:#{port}/score", server} end defp read_request(socket, data) do case String.split(data, "\r\n\r\n", parts: 2) do [headers, body] -> [_, size] = Regex.run(~r/content-length: (\d+)/i, headers) if byte_size(body) >= String.to_integer(size) do {headers, body} else {:ok, chunk} = :gen_tcp.recv(socket, 0, 5_000) read_request(socket, data <> chunk) end _ -> {:ok, chunk} = :gen_tcp.recv(socket, 0, 5_000) read_request(socket, data <> chunk) end end end