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.
137 lines
5.1 KiB
Elixir
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
|