RtmaClient was the only weather client without a Req.Test plug seam — added the standard req_options() helper matching NarrClient/HrrrClient/ etc. so its HTTP calls can be stubbed in test. 17 characterization tests: URL construction, idx fetch 404/503, hour truncation, malformed idx, range-GET error propagation, worker's existing-row short-circuit, client-error propagation. GRIB2 byte parsing paths deferred to the wgrib2 coverage task.
107 lines
3.7 KiB
Elixir
107 lines
3.7 KiB
Elixir
defmodule Microwaveprop.Workers.RtmaFetchWorkerTest do
|
|
use Microwaveprop.DataCase, async: false
|
|
use Oban.Testing, repo: Microwaveprop.Repo
|
|
|
|
alias Microwaveprop.Weather.RtmaClient
|
|
alias Microwaveprop.Weather.RtmaObservation
|
|
alias Microwaveprop.Workers.RtmaFetchWorker
|
|
|
|
describe "perform/1" do
|
|
test "skips the fetch (no HTTP) when a matching row already exists in the snapped bounding box" do
|
|
valid_time = ~U[2026-04-15 18:00:00Z]
|
|
|
|
# Pre-populate a row at the snapped coordinates (32.9 → 32.9 at 1/40°,
|
|
# -97.0 → -97.0). The worker's bounding-box short-circuit must find it
|
|
# and return :ok without contacting RtmaClient.
|
|
%RtmaObservation{}
|
|
|> RtmaObservation.changeset(%{
|
|
lat: 32.9,
|
|
lon: -97.0,
|
|
valid_time: valid_time,
|
|
temp_c: 20.0
|
|
})
|
|
|> Repo.insert!()
|
|
|
|
Req.Test.stub(RtmaClient, fn _conn ->
|
|
raise "RtmaClient should not be contacted when an observation already exists"
|
|
end)
|
|
|
|
args = %{
|
|
"lat" => 32.9,
|
|
"lon" => -97.0,
|
|
"valid_time" => DateTime.to_iso8601(valid_time)
|
|
}
|
|
|
|
assert :ok = RtmaFetchWorker.perform(%Oban.Job{args: args})
|
|
assert Repo.aggregate(RtmaObservation, :count, :id) == 1
|
|
end
|
|
|
|
# With garbage-bytes-on-the-wire (no real GRIB2 fixture), the extractor
|
|
# yields no fields and RtmaClient returns {:error, "RTMA: no data ..."},
|
|
# which the worker surfaces as {:error, _}. This characterizes that the
|
|
# worker propagates the client error (and does NOT insert a blank row).
|
|
test "returns {:error, _} and inserts nothing when the client fails to extract" do
|
|
idx = """
|
|
1:0:d=2026041518:TMP:2 m above ground:anl:
|
|
2:100:d=2026041518:DPT:2 m above ground:anl:
|
|
"""
|
|
|
|
Req.Test.stub(RtmaClient, fn conn ->
|
|
if String.ends_with?(conn.request_path, ".idx") do
|
|
Plug.Conn.send_resp(conn, 200, idx)
|
|
else
|
|
Plug.Conn.send_resp(conn, 206, :binary.copy(<<0>>, 1024))
|
|
end
|
|
end)
|
|
|
|
args = %{
|
|
"lat" => 32.9,
|
|
"lon" => -97.0,
|
|
"valid_time" => "2026-04-15T18:00:00Z"
|
|
}
|
|
|
|
assert {:error, reason} = RtmaFetchWorker.perform(%Oban.Job{args: args})
|
|
assert reason =~ "RTMA: no data"
|
|
assert Repo.aggregate(RtmaObservation, :count, :id) == 0
|
|
end
|
|
|
|
test "returns {:error, _} when upstream idx returns 404" do
|
|
Req.Test.stub(RtmaClient, fn conn ->
|
|
if String.ends_with?(conn.request_path, ".idx") do
|
|
Plug.Conn.send_resp(conn, 404, "not yet published")
|
|
else
|
|
Plug.Conn.send_resp(conn, 500, "unexpected")
|
|
end
|
|
end)
|
|
|
|
args = %{
|
|
"lat" => 32.9,
|
|
"lon" => -97.0,
|
|
"valid_time" => "2026-04-15T18:00:00Z"
|
|
}
|
|
|
|
assert {:error, reason} = RtmaFetchWorker.perform(%Oban.Job{args: args})
|
|
assert reason =~ "RTMA idx HTTP 404"
|
|
assert Repo.aggregate(RtmaObservation, :count, :id) == 0
|
|
end
|
|
|
|
test "raises FunctionClauseError when args are missing required keys" do
|
|
# The worker only matches the shape with lat/lon/valid_time — missing
|
|
# keys must crash loudly rather than silently succeed. This pins the
|
|
# current fail-fast contract.
|
|
assert_raise FunctionClauseError, fn ->
|
|
RtmaFetchWorker.perform(%Oban.Job{args: %{}})
|
|
end
|
|
end
|
|
|
|
test "raises when valid_time is not an ISO8601 string" do
|
|
# DateTime.from_iso8601/1 returns {:error, _}, and the `{:ok, _, _} =`
|
|
# match raises — another fail-fast contract worth pinning.
|
|
assert_raise MatchError, fn ->
|
|
RtmaFetchWorker.perform(%Oban.Job{
|
|
args: %{"lat" => 32.9, "lon" => -97.0, "valid_time" => "not-a-timestamp"}
|
|
})
|
|
end
|
|
end
|
|
end
|
|
end
|