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() on_exit(fn -> GridCache.clear() ProfilesFile.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 <> = 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 test "returns 413 for global / oversized viewport bounds", %{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" ) body = json_response(conn, 413) assert body["error"] == "viewport_too_large" end end end