Implements GET /api/v1/weather/nearby endpoint with comprehensive parameter
validation and error handling. The controller validates required parameters
(lat, lon, radius) and optional parameters (hours, limit) with appropriate
range checking. Integrates with PreparedQueries for efficient database access
and WeatherJSON for response serialization.
Key features:
- Required params: lat (-90 to 90), lon (-180 to 180), radius (0-1000 miles)
- Optional params: hours (1-168, default 6), limit (1-100, default 50)
- Proper error responses via FallbackController (400 for bad requests, 422 for validation)
- Full test coverage (16 tests) including edge cases and error scenarios
Also updates FallbackController to handle {:error, status, message} tuples
and fixes WeatherJSON to support both field name formats (lat/lon and latitude/longitude)
for compatibility with existing tests and actual query results.
287 lines
8.2 KiB
Elixir
287 lines
8.2 KiB
Elixir
defmodule AprsmeWeb.Api.V1.WeatherControllerTest do
|
|
use AprsmeWeb.ConnCase, async: false
|
|
|
|
import Aprsme.PacketsFixtures
|
|
|
|
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
|