fix(map): recover with sane defaults on garbage URL params

Every parser already returned nil on malformed input, so the ||
chains fell through to defaults — except parse_time_param, which
happily accepted any well-formed ISO8601 even if it pointed
outside the current HRRR forecast window (e.g. a stale shared
link from last week). Gate it behind the valid-times window and
fall back to the normal 'now' cursor when it's out of range.

Adds explicit coverage: non-numeric band, unknown band MHz,
non-numeric lat/lon/zoom, out-of-range values, malformed time,
ancient time, and all-params-garbage-at-once. Each case mounts
cleanly and lands on the defaults.
This commit is contained in:
Graham McIntire 2026-04-19 08:55:12 -05:00
parent 160439db31
commit bb6333bd27
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 65 additions and 1 deletions

View file

@ -136,16 +136,38 @@ defmodule MicrowavepropWeb.MapLive do
# Resolve the initial band/time/center/zoom from URL params, falling
# back to LiveStash recovery, then session geolocation and defaults.
# Parsers return nil for anything malformed so `||` walks to the next
# sensible source; ISO8601 times that sit outside the current HRRR
# window get dropped too so a stale shared link doesn't land the map
# on a hour with no scores.
defp resolve_view(params, session, recovered_band, recovered_time) do
band = parse_band_param(params["band"]) || recovered_band || @default_band
valid_times = Propagation.available_valid_times(band)
time = parse_time_param(params["t"]) || recovered_time || closest_to_now(valid_times)
time = resolve_time(params["t"], recovered_time, valid_times)
center = parse_center_param(params["lat"], params["lon"]) || initial_center(session)
zoom = parse_zoom_param(params["zoom"]) || @default_zoom
%{band: band, time: time, center: center, zoom: zoom, valid_times: valid_times}
end
defp resolve_time(param, recovered_time, valid_times) do
case parse_time_param(param) do
%DateTime{} = t ->
if time_in_window?(t, valid_times), do: t, else: closest_to_now(valid_times)
nil ->
recovered_time || closest_to_now(valid_times)
end
end
defp time_in_window?(_, []), do: false
defp time_in_window?(%DateTime{} = t, valid_times) do
earliest = Enum.min(valid_times, DateTime)
latest = Enum.max(valid_times, DateTime)
DateTime.compare(t, earliest) != :lt and DateTime.compare(t, latest) != :gt
end
# Build a bounding box roughly matching the hardcoded default (~3.4° × 9°)
# around a given center so the initial HRRR score query still returns a
# useful tile set for the visible area.

View file

@ -145,6 +145,48 @@ defmodule MicrowavepropWeb.MapLiveTest do
{:ok, _lv, html} = live(conn, ~p"/map?band=notanumber")
assert html =~ ~s(data-selected-band="10000")
end
test "unknown band MHz falls back to the default", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map?band=99999")
assert html =~ ~s(data-selected-band="10000")
end
test "malformed lat/lon/zoom are ignored", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map?lat=banana&lon=apple&zoom=over9000")
assert html =~ ~s(data-zoom="7")
refute html =~ ~s(data-center-lat="banana")
end
test "out-of-range lat/lon are ignored", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map?lat=500&lon=-999")
refute html =~ ~s(data-center-lat="500)
end
test "out-of-range zoom falls back to default", %{conn: conn} do
{:ok, _lv, html} = live(conn, ~p"/map?zoom=42")
assert html =~ ~s(data-zoom="7")
end
test "unparseable time is ignored", %{conn: conn} do
{:ok, lv, _html} = live(conn, ~p"/map?t=not-a-date")
# Page renders normally without crashing.
assert render(lv) =~ "data-selected-time"
end
test "ISO8601 time outside the current forecast window falls back", %{conn: conn} do
# Year 2000 is well before any valid HRRR data; page should still
# mount and fall back to the normal 'now' cursor rather than try
# to fetch scores for an empty time.
{:ok, _lv, _html} = live(conn, ~p"/map?t=2000-01-01T00:00:00Z")
end
test "every param garbage at once still mounts cleanly", %{conn: conn} do
{:ok, _lv, html} =
live(conn, ~p"/map?band=xyz&t=nope&lat=abc&lon=def&zoom=foo")
assert html =~ ~s(data-selected-band="10000")
assert html =~ ~s(data-zoom="7")
end
end
describe "cursor_for_now/2" do