prop/test/microwaveprop/workers/mrms_fetch_worker_test.exs
Graham McIntire ea0cbf5205
test: cover mrms_cache + mrms_fetch_worker, add Req.Test seam
Same fix as rtma_client: MrmsClient had a hardcoded req_options/0 —
switched to a Keyword.merge with Application.get_env so tests can stub
HTTP. 13 new characterization tests: cache put/get/clear/broadcast and
worker listing/download/up-to-date paths. Full GRIB2 parse happy path
deferred to the wgrib2 coverage task.
2026-04-16 14:58:54 -05:00

137 lines
5.1 KiB
Elixir

defmodule Microwaveprop.Workers.MrmsFetchWorkerTest do
# async: false because the MrmsCache is an application-wide singleton with
# shared ETS state. Concurrent tests would race on cache contents.
use Microwaveprop.DataCase, async: false
use Oban.Testing, repo: Microwaveprop.Repo
alias Microwaveprop.Weather.MrmsCache
alias Microwaveprop.Weather.MrmsClient
alias Microwaveprop.Workers.MrmsFetchWorker
@topic "mrms:cache"
@pubsub Microwaveprop.PubSub
setup do
MrmsCache.clear()
:ok
end
describe "perform/1" do
test "returns :ok and leaves cache empty when the listing is empty" do
Req.Test.stub(MrmsClient, fn conn ->
# Empty directory index — no filenames matched by the parser.
Plug.Conn.send_resp(conn, 200, "<html><body>no files here</body></html>")
end)
assert :ok = MrmsFetchWorker.perform(%Oban.Job{args: %{}})
assert MrmsCache.fetch() == :miss
end
test "returns :ok and leaves cache empty when the listing request 500s" do
Req.Test.stub(MrmsClient, fn conn ->
Plug.Conn.send_resp(conn, 500, "upstream boom")
end)
assert :ok = MrmsFetchWorker.perform(%Oban.Job{args: %{}})
assert MrmsCache.fetch() == :miss
end
test "short-circuits with :up_to_date when cache already has the latest valid_time" do
latest_vt = ~U[2026-04-12 19:28:00Z]
# Pre-populate the cache so fetch_latest/1 sees known_valid_time >= latest.
MrmsCache.put(latest_vt, %{{32.0, -97.0} => 4.2})
Req.Test.stub(MrmsClient, fn conn ->
# Only the listing URL should be hit — never a .grib2.gz download.
assert String.ends_with?(conn.request_path, "/PrecipRate/") or
String.ends_with?(conn.request_path, "/PrecipRate")
html = """
<a href="MRMS_PrecipRate_00.00_20260412-192800.grib2.gz">f</a>
"""
Plug.Conn.send_resp(conn, 200, html)
end)
assert :ok = MrmsFetchWorker.perform(%Oban.Job{args: %{}})
# Cache is untouched.
assert {:ok, ^latest_vt, %{{32.0, -97.0} => 4.2}} = MrmsCache.fetch()
end
test "returns :ok and leaves cache empty when the download path 404s" do
# Listing succeeds with one file; download 404s. The worker must log
# and swallow the error rather than crash.
#
# The .grib2.gz URL triggers Req's URL-extension-based gzip decode step
# regardless of response status, so even a 404 body must be valid gzip.
gzipped_404 = :zlib.gzip("gone")
Req.Test.stub(MrmsClient, fn conn ->
if String.ends_with?(conn.request_path, ".grib2.gz") do
Plug.Conn.send_resp(conn, 404, gzipped_404)
else
html = """
<a href="MRMS_PrecipRate_00.00_20260412-193000.grib2.gz">f</a>
"""
Plug.Conn.send_resp(conn, 200, html)
end
end)
assert :ok = MrmsFetchWorker.perform(%Oban.Job{args: %{}})
assert MrmsCache.fetch() == :miss
end
# The full happy path — successful listing + download + real GRIB2
# decode into a populated cache grid — requires a real MRMS PrecipRate
# payload that we don't have a hermetic fixture for. We defer that to
# the wgrib2/GRIB2 coverage task.
#
# What we can still pin down here is the worker's delivery contract
# when both listing and download return 200: if fetch_latest/1 returns
# {:ok, vt, grid}, the worker MUST broadcast_put/2 to the mrms:cache
# topic. We stub garbage gzipped bytes; wgrib2 (when installed) will
# yield an empty grid rather than an error on "no matching messages",
# and the worker broadcasts that through. When wgrib2 is NOT installed
# the client returns :wgrib2_not_available, the worker logs a warning
# and still returns :ok — no broadcast. Both arms are valid behavior
# and both are exercised here.
test "returns :ok on a 200 download (broadcasts iff wgrib2 yields :ok)" do
Phoenix.PubSub.subscribe(@pubsub, @topic)
# The .grib2.gz URL triggers Req's URL-extension-based gzip decoding,
# so the response body must be real gzip bytes.
gzipped_garbage = :zlib.gzip(:binary.copy(<<0>>, 1024))
Req.Test.stub(MrmsClient, fn conn ->
if String.ends_with?(conn.request_path, ".grib2.gz") do
Plug.Conn.send_resp(conn, 200, gzipped_garbage)
else
html = """
<a href="MRMS_PrecipRate_00.00_20260412-193200.grib2.gz">f</a>
"""
Plug.Conn.send_resp(conn, 200, html)
end
end)
assert :ok = MrmsFetchWorker.perform(%Oban.Job{args: %{}})
# One of two valid outcomes:
# 1. wgrib2 installed → fetch_latest returns {:ok, vt, %{}} →
# worker calls broadcast_put and the subscriber sees a refresh.
# 2. wgrib2 missing → fetch_latest returns {:error, ...} → no
# broadcast, cache unchanged.
receive do
{:mrms_cache_refresh, vt, grid} ->
assert %DateTime{} = vt
assert is_map(grid)
assert {:ok, ^vt, ^grid} = MrmsCache.fetch()
after
200 ->
assert MrmsCache.fetch() == :miss
end
end
end
end