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, "no files here") 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 = """ f """ 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 = """ f """ 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 = """ f """ 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