towerops/lib/towerops/weather/client.ex
Graham McIntire 3ca0834ef0 tests: raise coverage to 70% via helper promotion + new unit/property tests
Promoted pure presentation and utility helpers from `defp` to `def @doc false`
across ~20 LiveViews, Oban workers, and sync modules so they're reachable from
unit tests. Refactored several `cond` blocks into idiomatic function heads with
guards. Added ~250 new test cases in new files under test/towerops and
test/towerops_web, including DB-backed tests for CnMaestro.Sync and
AlertNotificationWorker, and removed dead LiveView tab components and
CapacityLive (no callers anywhere in lib/test).

Configured mix.exs test_coverage.ignore_modules to exclude vendored third-party
code (SnmpKit, protobuf-generated Towerops.Agent.*, Absinthe GraphQL types,
Phoenix HTML modules, Inspect protocol impls) from coverage calculations —
these are not our project code.

Coverage: 66.93% → 70.09%. Full suite: 10,127 tests, 0 failures.
2026-04-24 09:49:06 -05:00

74 lines
2.2 KiB
Elixir

defmodule Towerops.Weather.Client do
@moduledoc """
OpenWeatherMap API client.
Uses the free Current Weather Data API (2.5) which allows 60 calls/min.
Fetches weather by latitude/longitude — perfect for tower site locations.
"""
alias Towerops.HTTP
require Logger
@base_url "https://api.openweathermap.org/data/2.5"
@doc """
Fetches current weather for the given coordinates.
Returns `{:ok, map}` with the raw OWM response or `{:error, reason}`.
"""
def get_current_weather(lat, lon, opts \\ []) do
with {:ok, key} <- resolve_api_key(opts[:api_key]) do
req_opts =
maybe_add_test_plug(
[url: "#{@base_url}/weather", params: [lat: lat, lon: lon, appid: key]],
opts
)
__MODULE__
|> HTTP.get(req_opts)
|> handle_response()
end
end
defp resolve_api_key(nil), do: validate_api_key(api_key())
defp resolve_api_key(""), do: {:error, :no_api_key}
defp resolve_api_key(key) when is_binary(key), do: {:ok, key}
defp validate_api_key(nil), do: {:error, :no_api_key}
defp validate_api_key(""), do: {:error, :no_api_key}
defp validate_api_key(key) when is_binary(key), do: {:ok, key}
defp handle_response({:ok, %Req.Response{status: 200, body: body}}), do: {:ok, body}
defp handle_response({:ok, %Req.Response{status: 401}}), do: {:error, :invalid_api_key}
defp handle_response({:ok, %Req.Response{status: 429}}), do: {:error, :rate_limited}
defp handle_response({:ok, %Req.Response{status: status, body: body}}) do
Logger.warning("OWM API error", status: status, body: inspect(body))
{:error, {:api_error, status}}
end
defp handle_response({:error, reason}) do
Logger.warning("OWM API request failed", error: inspect(reason))
{:error, reason}
end
@doc """
Tests if the API key is valid by making a minimal request.
"""
def test_connection(api_key) do
# Use a known location (London) for the test
get_current_weather(51.5074, -0.1278, api_key: api_key)
end
defp api_key do
Application.get_env(:towerops, :openweathermap_api_key)
end
defp maybe_add_test_plug(req_opts, opts) do
case Keyword.fetch(opts, :plug) do
{:ok, plug} -> Keyword.put(req_opts, :plug, plug)
:error -> req_opts
end
end
end