towerops/lib/towerops/visp/client.ex
Graham McIntire 532d88ffb9
perf: disable Req retry for faster error tests
- Add retry: false to VISP Client HTTP requests
- Add retry: false to ReleaseChecker GitHub API requests
- Remove unused Plug.Conn import from RemoteIpLogger
- Remove unused default parameter from req_get/2

Results:
- VISP sync test: 7007ms → 113ms (62x faster)
- ReleaseChecker test: 7004ms → 17ms (402x faster)
- UserResetPasswordLive test: 1495ms → 284ms (5x faster)

Req's default retry behavior (1s, 2s, 4s exponential backoff) was
causing 7-second delays for HTTP 500/503 error responses in tests.
For these clients, immediate failure is preferred over retries.
2026-03-10 16:19:31 -05:00

111 lines
2.8 KiB
Elixir

defmodule Towerops.Visp.Client do
@moduledoc """
REST client for the VISP API.
Uses Req with X-API-Key authentication and page-based pagination.
"""
require Logger
@base_url "https://api.visp.net"
@doc "Test the connection to VISP by fetching a single subscriber."
def test_connection(api_key) do
case get(api_key, "/api/v1/subscribers", %{"per_page" => "1"}) do
{:ok, _data} -> {:ok, %{}}
{:error, reason} -> {:error, reason}
end
end
@doc "List all subscribers with automatic pagination."
def list_subscribers(api_key) do
paginate(api_key, "/api/v1/subscribers")
end
@doc "List all sites/towers with automatic pagination."
def list_sites(api_key) do
paginate(api_key, "/api/v1/sites")
end
@doc "List all equipment with automatic pagination."
def list_equipment(api_key) do
paginate(api_key, "/api/v1/equipment")
end
@doc "List all service plans."
def list_services(api_key) do
get(api_key, "/api/v1/services")
end
defp get(api_key, path, params \\ %{}) do
url = @base_url <> path
req_opts = [
method: :get,
url: url,
headers: [
{"x-api-key", api_key},
{"accept", "application/json"}
],
params: params,
# Disable retries - VISP errors should fail immediately
retry: false
]
req_opts =
if Application.get_env(:towerops, :env) == :test do
Keyword.put(req_opts, :plug, {Req.Test, __MODULE__})
else
req_opts
end
case Req.request(req_opts) 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: 429}} ->
{:error, {:rate_limited, 60}}
{:ok, %{status: status, body: body}} ->
Logger.warning("VISP API unexpected status #{status}: #{inspect(body)}")
{:error, {:unexpected_status, status}}
{:error, reason} ->
Logger.error("VISP API connection error: #{inspect(reason)}")
{:error, reason}
end
rescue
exception ->
{:error, Exception.message(exception)}
end
defp paginate(api_key, path, page \\ 1, acc \\ []) do
params = %{"page" => to_string(page), "per_page" => "100"}
case get(api_key, path, params) do
{:ok, %{"data" => data, "meta" => meta}} when is_list(data) ->
all = acc ++ data
if page < (meta["last_page"] || 1) do
paginate(api_key, path, page + 1, all)
else
{:ok, all}
end
{:ok, %{"data" => data}} when is_list(data) ->
{:ok, acc ++ data}
{:ok, data} when is_list(data) ->
{:ok, acc ++ data}
{:error, reason} ->
{:error, reason}
end
end
end