prop/test/microwaveprop/workers/rtma_fetch_worker_test.exs
Graham McIntire cb82160447
test: cover rtma_client + rtma_fetch_worker, add Req.Test seam
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.
2026-04-16 14:58:54 -05:00

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