diff --git a/lib/towerops/preseem/client.ex b/lib/towerops/preseem/client.ex new file mode 100644 index 00000000..f8317d8b --- /dev/null +++ b/lib/towerops/preseem/client.ex @@ -0,0 +1,103 @@ +defmodule Towerops.Preseem.Client do + @moduledoc """ + HTTP client for the Preseem Model API. + + Uses Req with built-in test support via `Req.Test`. + """ + + require Logger + + @default_base_url "https://apidocs.preseem.com/model/v1" + + @doc """ + Tests the connection to the Preseem API by fetching account info. + + Returns `{:ok, body}` on success, `{:error, reason}` on failure. + """ + def test_connection(api_key, opts \\ []) do + base_url = Keyword.get(opts, :base_url, @default_base_url) + + case request(:get, "#{base_url}/account", api_key) do + {:ok, %{status: status, body: body}} when status in 200..299 -> + {:ok, body} + + {:ok, %{status: 401}} -> + {:error, :unauthorized} + + {:ok, %{status: 403}} -> + {:error, :forbidden} + + {:ok, %{status: status, body: body}} -> + Logger.warning("Preseem API unexpected status #{status}: #{inspect(body)}") + {:error, {:unexpected_status, status}} + + {:error, reason} -> + Logger.error("Preseem API connection error: #{inspect(reason)}") + {:error, reason} + end + end + + @doc """ + Lists all access points from the Preseem API. + + Returns `{:ok, [access_point]}` on success. + """ + def list_access_points(api_key, opts \\ []) do + base_url = Keyword.get(opts, :base_url, @default_base_url) + + case request(:get, "#{base_url}/access_points", api_key) do + {:ok, %{status: status, body: %{"data" => data}}} when status in 200..299 -> + {:ok, data} + + {:ok, %{status: 401}} -> + {:error, :unauthorized} + + {:ok, %{status: 403}} -> + {:error, :forbidden} + + {:ok, %{status: status, body: body}} -> + {:error, {:unexpected_status, status, body}} + + {:error, reason} -> + {:error, reason} + end + end + + @doc """ + Fetches metrics for a specific access point. + + Returns `{:ok, metrics_map}` on success. + """ + def get_access_point_metrics(api_key, ap_id, opts \\ []) do + base_url = Keyword.get(opts, :base_url, @default_base_url) + + case request(:get, "#{base_url}/access_points/#{ap_id}/metrics", api_key) do + {:ok, %{status: status, body: %{"data" => data}}} when status in 200..299 -> + {:ok, data} + + {:ok, %{status: 401}} -> + {:error, :unauthorized} + + {:ok, %{status: 403}} -> + {:error, :forbidden} + + {:ok, %{status: status, body: body}} -> + {:error, {:unexpected_status, status, body}} + + {:error, reason} -> + {:error, reason} + end + end + + defp request(method, url, api_key) do + Req.request( + method: method, + url: url, + headers: [{"authorization", "Bearer #{api_key}"}, {"accept", "application/json"}], + plug: {Req.Test, __MODULE__} + ) + rescue + exception -> + {:error, Exception.message(exception)} + end +end diff --git a/test/towerops/preseem/client_test.exs b/test/towerops/preseem/client_test.exs new file mode 100644 index 00000000..4c3a773a --- /dev/null +++ b/test/towerops/preseem/client_test.exs @@ -0,0 +1,78 @@ +defmodule Towerops.Preseem.ClientTest do + use ExUnit.Case, async: true + + alias Towerops.Preseem.Client + + describe "test_connection/1" do + test "returns ok when API responds with 200" do + Req.Test.stub(Client, fn conn -> + Req.Test.json(conn, %{"status" => "ok", "account" => "test-isp"}) + end) + + assert {:ok, %{"status" => "ok"}} = Client.test_connection("valid-api-key") + end + + test "returns error for 401 unauthorized" do + Req.Test.stub(Client, fn conn -> + conn + |> Plug.Conn.put_status(401) + |> Req.Test.json(%{"error" => "unauthorized"}) + end) + + assert {:error, :unauthorized} = Client.test_connection("bad-key") + end + + test "returns error for 403 forbidden" do + Req.Test.stub(Client, fn conn -> + conn + |> Plug.Conn.put_status(403) + |> Req.Test.json(%{"error" => "forbidden"}) + end) + + assert {:error, :forbidden} = Client.test_connection("restricted-key") + end + end + + describe "list_access_points/1" do + test "returns list of access points on success" do + Req.Test.stub(Client, fn conn -> + Req.Test.json(conn, %{ + "data" => [ + %{"id" => "ap-1", "name" => "Tower1-AP1", "ip" => "10.0.0.1"}, + %{"id" => "ap-2", "name" => "Tower1-AP2", "ip" => "10.0.0.2"} + ] + }) + end) + + assert {:ok, [ap1, ap2]} = Client.list_access_points("valid-key") + assert ap1["id"] == "ap-1" + assert ap2["id"] == "ap-2" + end + + test "returns error for 401" do + Req.Test.stub(Client, fn conn -> + conn |> Plug.Conn.put_status(401) |> Req.Test.json(%{"error" => "unauthorized"}) + end) + + assert {:error, :unauthorized} = Client.list_access_points("bad-key") + end + end + + describe "get_access_point_metrics/2" do + test "returns metrics for an AP" do + Req.Test.stub(Client, fn conn -> + Req.Test.json(conn, %{ + "data" => %{ + "qoe_score" => 85.5, + "capacity_score" => 72.0, + "subscriber_count" => 42, + "p95_latency" => 18.3 + } + }) + end) + + assert {:ok, metrics} = Client.get_access_point_metrics("valid-key", "ap-1") + assert metrics["qoe_score"] == 85.5 + end + end +end