defmodule AprsmeWeb.Api.V1.WeatherControllerTest do use AprsmeWeb.ConnCase, async: false import Aprsme.PacketsFixtures setup do # Reset the Hammer rate-limiter ETS table so we don't trigger 429s from # accumulated hits across the full test suite. case :ets.info(Aprsme.RateLimiter) do :undefined -> :ok _ -> :ets.delete_all_objects(Aprsme.RateLimiter) end :ok end describe "GET /api/v1/weather/nearby" do setup do # Create a weather station packet station = packet_fixture(%{ sender: "TEST-1", raw_packet: "TEST-1>APRS,TCPIP*:@121045z3745.50N/12159.75W_090/000g000t068r000p000P000h50b10120", data_type: "weather", lat: Decimal.new("37.7583"), lon: Decimal.new("-121.9958"), has_weather: true, temperature: 68.0, humidity: 50.0, pressure: 1012.0, wind_speed: 0.0, wind_direction: 90, rain_1h: 0.0, rain_24h: 0.0, rain_since_midnight: 0.0 }) # Create a non-weather packet (should not appear) packet_fixture(%{ sender: "TEST-2", data_type: "position", lat: Decimal.new("37.7583"), lon: Decimal.new("-121.9958"), has_weather: false }) # Create a weather station outside radius packet_fixture(%{ sender: "TEST-3", data_type: "weather", lat: Decimal.new("40.0"), lon: Decimal.new("-120.0"), has_weather: true, temperature: 75.0 }) %{station: station} end test "returns nearby weather stations with valid params", %{conn: conn, station: station} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-121.9958", "radius" => "10" }) assert %{ "data" => [weather_station], "meta" => %{ "count" => 1, "params" => %{ "latitude" => 37.7583, "longitude" => -121.9958, "radius_miles" => 10.0, "hours" => 6, "limit" => 50 } } } = json_response(conn, 200) assert weather_station["callsign"] == station.sender assert weather_station["distance_miles"] < 10.0 assert weather_station["weather"]["temperature"] == 68.0 end test "returns 400 when lat is missing", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lon" => "-121.9958", "radius" => "10" }) assert %{"error" => %{"message" => message}} = json_response(conn, 400) assert message =~ "lat" end test "returns 400 when lon is missing", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "radius" => "10" }) assert %{"error" => %{"message" => message}} = json_response(conn, 400) assert message =~ "lon" end test "returns 400 when radius is missing", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-121.9958" }) assert %{"error" => %{"message" => message}} = json_response(conn, 400) assert message =~ "radius" end test "returns 422 when lat is out of range", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "91", "lon" => "-121.9958", "radius" => "10" }) assert %{"error" => %{"message" => message}} = json_response(conn, 422) assert message =~ "latitude" assert message =~ "-90" assert message =~ "90" end test "returns 422 when lon is out of range", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-181", "radius" => "10" }) assert %{"error" => %{"message" => message}} = json_response(conn, 422) assert message =~ "longitude" assert message =~ "-180" assert message =~ "180" end test "returns 422 when radius is zero", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-121.9958", "radius" => "0" }) assert %{"error" => %{"message" => message}} = json_response(conn, 422) assert message =~ "radius" assert message =~ "greater than 0" end test "returns 422 when radius is negative", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-121.9958", "radius" => "-1" }) assert %{"error" => %{"message" => message}} = json_response(conn, 422) assert message =~ "radius" assert message =~ "greater than 0" end test "returns 422 when radius exceeds maximum", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-121.9958", "radius" => "1001" }) assert %{"error" => %{"message" => message}} = json_response(conn, 422) assert message =~ "radius" assert message =~ "1000" end test "accepts optional hours parameter", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-121.9958", "radius" => "10", "hours" => "24" }) assert %{"meta" => %{"params" => %{"hours" => 24}}} = json_response(conn, 200) end test "returns 422 when hours is out of range", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-121.9958", "radius" => "10", "hours" => "169" }) assert %{"error" => %{"message" => message}} = json_response(conn, 422) assert message =~ "hours" assert message =~ "1" assert message =~ "168" end test "accepts optional limit parameter", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-121.9958", "radius" => "10", "limit" => "25" }) assert %{"meta" => %{"params" => %{"limit" => 25}}} = json_response(conn, 200) end test "returns 422 when limit is out of range", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-121.9958", "radius" => "10", "limit" => "101" }) assert %{"error" => %{"message" => message}} = json_response(conn, 422) assert message =~ "limit" assert message =~ "1" assert message =~ "100" end test "uses default values when optional params not provided", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-121.9958", "radius" => "10" }) assert %{"meta" => %{"params" => params}} = json_response(conn, 200) assert params["hours"] == 6 assert params["limit"] == 50 end test "returns empty list when no weather stations in range", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "0.0", "lon" => "0.0", "radius" => "1" }) assert %{ "data" => [], "meta" => %{"count" => 0} } = json_response(conn, 200) end test "coerces string parameters to correct types", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "37.7583", "lon" => "-121.9958", "radius" => "10", "hours" => "12", "limit" => "25" }) assert %{"meta" => %{"params" => params}} = json_response(conn, 200) assert is_float(params["latitude"]) assert is_float(params["longitude"]) assert is_float(params["radius_miles"]) assert is_integer(params["hours"]) assert is_integer(params["limit"]) end test "handles invalid numeric strings", %{conn: conn} do conn = get(conn, ~p"/api/v1/weather/nearby", %{ "lat" => "not_a_number", "lon" => "-121.9958", "radius" => "10" }) assert %{"error" => %{"message" => message}} = json_response(conn, 400) assert message =~ "lat" end end end