prop/test/microwaveprop_web/controllers/weather_tile_controller_test.exs
Graham McIntire 9a3e956cf8
Some checks failed
Build and Push / Build CI test image (push) Successful in 20s
Build base image / Build and push base image (push) Successful in 26s
Build prop-grid-rs / Test, build, push (push) Failing after 2m53s
Build and Push / Build and Push Docker Image (push) Has been cancelled
fix(weather): serve the fully zoomed-out viewport instead of 413ing it
The map sets minZoom: 4, and a z=4 viewport on a wide window clamps to
the full grid extent (45° lat × 100° lon = 4500 sq deg), which exceeded
the 4000 sq deg cap. The request 413'd and the hook dropped it
silently, so zooming all the way out blanked the overlay.

Raise the cap to 4600 — just above the largest area the clamp can
emit. The clamp, not this cap, is what bounds the work: post-clamp no
request can exceed the full grid (~92k cells, ~1.4 MB), which is
already materialized once per valid_time and served from cache. The
cap stays as defense-in-depth against the clamp window being widened
later.

Consequence: :viewport_too_large is now unreachable via clamp/1, so
the tests that pinned global bounds to a 413 are inverted to assert
the clamped extent is served.
2026-08-05 17:44:17 -05:00

188 lines
5.8 KiB
Elixir

defmodule MicrowavepropWeb.WeatherTileControllerTest do
use MicrowavepropWeb.ConnCase, async: false
alias Microwaveprop.Propagation.ProfilesFile
alias Microwaveprop.Weather.GridCache
alias Microwaveprop.Weather.ScalarFile
setup do
GridCache.clear()
ProfilesFile.prune_older_than(~U[2099-12-31 23:59:59Z])
ScalarFile.prune_older_than(~U[2099-12-31 23:59:59Z])
on_exit(fn ->
GridCache.clear()
ProfilesFile.prune_older_than(~U[2099-12-31 23:59:59Z])
ScalarFile.prune_older_than(~U[2099-12-31 23:59:59Z])
end)
:ok
end
describe "GET /weather/cells" do
test "returns 400 for missing or invalid params", %{conn: conn} do
conn = get(conn, ~p"/weather/cells?time=not-a-time")
assert json_response(conn, 400)
end
test "returns the binary cell pack with all default layers when ?layers is absent", %{conn: conn} do
valid_time = ~U[2026-04-28 12:00:00Z]
ProfilesFile.write!(valid_time, %{
{33.0, -97.0} => %{
surface_temp_c: 22.0,
surface_dewpoint_c: 12.0,
surface_pressure_mb: 1010.0,
hpbl_m: 800.0,
pwat_mm: 25.0,
profile: []
}
})
time = DateTime.to_iso8601(valid_time)
conn =
get(
conn,
~p"/weather/cells?time=#{time}&south=32.5&north=33.5&west=-97.5&east=-96.5"
)
assert ["application/octet-stream"] = get_resp_header(conn, "content-type")
body = response(conn, 200)
assert <<"WCEL", 1::8, _cell_count::little-32, _layer_count::8, _rest::binary>> = body
end
test "with ?layers=foo,bar limits the binary pack to those layers", %{conn: conn} do
valid_time = ~U[2026-04-28 12:00:00Z]
ProfilesFile.write!(valid_time, %{
{33.0, -97.0} => %{
surface_temp_c: 22.0,
surface_dewpoint_c: 12.0,
surface_pressure_mb: 1010.0,
hpbl_m: 800.0,
pwat_mm: 25.0,
profile: []
},
{33.125, -97.0} => %{
surface_temp_c: 23.0,
surface_dewpoint_c: 13.0,
surface_pressure_mb: 1011.0,
hpbl_m: 700.0,
pwat_mm: 26.0,
profile: []
}
})
time = DateTime.to_iso8601(valid_time)
conn =
get(
conn,
~p"/weather/cells?time=#{time}&south=32.0&north=34.0&west=-98.0&east=-96.0&layers=temperature,pwat"
)
assert ["application/octet-stream"] = get_resp_header(conn, "content-type")
body = response(conn, 200)
assert <<"WCEL", 1::8, cell_count::little-32, 2::8, rest::binary>> = body
assert cell_count == 2
# Two layer names: "temperature" (11), "pwat" (4).
<<11::8, "temperature", 4::8, "pwat", padded_rest::binary>> = rest
# Header so far: 4(magic) + 1 + 4 + 1 + (1+11) + (1+4) = 27 bytes. One
# byte of zero padding aligns the f32 arrays to a 4-byte boundary.
<<0::8, lats_and_more::binary>> = padded_rest
<<lat1::little-float-32, lat2::little-float-32, _rest::binary>> = lats_and_more
assert_in_delta lat1, 33.0, 0.001
assert_in_delta lat2, 33.125, 0.001
end
test "returns an empty pack when no data exists for the valid_time", %{conn: conn} do
valid_time = ~U[2099-04-28 12:00:00Z]
time = DateTime.to_iso8601(valid_time)
conn =
get(
conn,
~p"/weather/cells?time=#{time}&south=32.0&north=34.0&west=-98.0&east=-96.0"
)
assert ["application/octet-stream"] = get_resp_header(conn, "content-type")
body = response(conn, 200)
assert <<"WCEL", 1::8, 0::little-32, _layer_count::8, _rest::binary>> = body
end
test "with ?source=hrdps returns only HRDPS rows (skips HRRR scalars and ProfilesFile)", %{conn: conn} do
valid_time = ~U[2026-04-28 12:00:00Z]
# HRRR side: write a Texas row via the standard pipeline.
ProfilesFile.write!(valid_time, %{
{33.0, -97.0} => %{
surface_temp_c: 22.0,
surface_dewpoint_c: 12.0,
surface_pressure_mb: 1010.0,
hpbl_m: 800.0,
pwat_mm: 25.0,
profile: []
}
})
# HRDPS side: hand-write a Canadian chunk into the HRDPS sibling dir.
hrdps_dir = ScalarFile.dir_for_hrdps(valid_time)
File.mkdir_p!(hrdps_dir)
lat_band = (53.0 / 5) |> Float.floor() |> trunc()
lon_band = (-60.0 / 5) |> Float.floor() |> trunc()
path = Path.join(hrdps_dir, "#{lat_band}_#{lon_band}.mp.gz")
payload =
Msgpax.pack!(
[
%{
"lat" => 53.0,
"lon" => -60.0,
"valid_time" => "2026-04-28T12:00:00Z",
"temperature" => 8.0,
"surface_rh" => 70.0
}
],
iodata: false
)
File.write!(path, :zlib.gzip(payload))
time = DateTime.to_iso8601(valid_time)
conn =
get(
conn,
~p"/weather/cells?time=#{time}&south=40.0&north=70.0&west=-150.0&east=-50.0&source=hrdps"
)
body = response(conn, 200)
assert <<"WCEL", 1::8, cell_count::little-32, _rest::binary>> = body
# Only the Canadian (53, -60) row — HRRR's (33, -97) is excluded.
assert cell_count == 1
File.rm_rf!(hrdps_dir)
end
# A fully zoomed-out map (minZoom: 4) sends bounds well past the grid
# extent. Those clamp down to the supported region and are served —
# rejecting them left the overlay blank with no feedback.
test "serves global / oversized viewport bounds by clamping them", %{conn: conn} do
time = DateTime.to_iso8601(~U[2026-04-28 12:00:00Z])
conn =
get(
conn,
~p"/weather/cells?time=#{time}&south=-90&north=90&west=-180&east=180"
)
assert conn.status == 200
assert <<"WCEL", 1::8, _rest::binary>> = conn.resp_body
end
end
end