defmodule Microwaveprop.Workers.NarrFetchWorkerTest do use Microwaveprop.DataCase, async: false use Oban.Testing, repo: Microwaveprop.Repo alias Microwaveprop.Weather.NarrClient alias Microwaveprop.Weather.NarrProfile alias Microwaveprop.Workers.NarrFetchWorker @inv_fixture "test/fixtures/narr/narr-a_221_20100615_1200_000.inv" @grb_fixture "test/fixtures/narr/narr_dfw_2010-06-15_12z.grb" @file_size 56_221_430 # Stub NarrClient HTTP so `fetch_profile_at/2` returns the spike fixture # composite without hitting NCEI. defp stub_narr_success do inv_body = File.read!(@inv_fixture) grb_body = File.read!(@grb_fixture) Req.Test.stub(NarrClient, fn conn -> cond do String.ends_with?(conn.request_path, ".inv") and conn.method == "GET" -> Plug.Conn.send_resp(conn, 200, inv_body) String.ends_with?(conn.request_path, ".grb") and conn.method == "HEAD" -> conn |> Plug.Conn.put_resp_header("content-length", Integer.to_string(@file_size)) |> Plug.Conn.send_resp(200, "") String.ends_with?(conn.request_path, ".grb") and conn.method == "GET" -> Plug.Conn.send_resp(conn, 200, grb_body) true -> Plug.Conn.send_resp(conn, 500, "unexpected #{conn.method} #{conn.request_path}") end end) end describe "perform/1" do test "fetches a profile via NarrClient and inserts it as a NarrProfile" do stub_narr_success() valid_time = ~U[2010-06-15 12:00:00Z] args = %{ "lat" => 32.9, "lon" => -97.0, "valid_time" => DateTime.to_iso8601(valid_time) } assert :ok = NarrFetchWorker.perform(%Oban.Job{args: args}) # Row snapped to 0.25° grid (32.9 → 33.0, -97.0 → -97.0) and stored at # the exact 3-hourly valid_time. assert [row] = Repo.all(NarrProfile) assert row.lat == 33.0 assert row.lon == -97.0 assert DateTime.compare(row.valid_time, valid_time) == :eq assert_in_delta row.surface_temp_c, 23.79, 1.0 assert_in_delta row.surface_dewpoint_c, 21.34, 1.0 assert_in_delta row.surface_pressure_mb, 993.84, 2.0 assert_in_delta row.hpbl_m, 703.5, 10.0 assert_in_delta row.pwat_mm, 45.1, 2.0 end test "is a no-op when a NarrProfile row already exists for the snapped lat/lon/time" do valid_time = ~U[2010-06-15 12:00:00Z] # Stub that raises if called — this test asserts no HTTP happens. Req.Test.stub(NarrClient, fn _conn -> raise "NarrClient should not be contacted when cache hit" end) # Pre-populate with a row at the snapped coordinates. %NarrProfile{} |> NarrProfile.changeset(%{ lat: 33.0, lon: -97.0, valid_time: valid_time, profile: [] }) |> Repo.insert!() args = %{ "lat" => 32.9, "lon" => -97.0, "valid_time" => DateTime.to_iso8601(valid_time) } assert :ok = NarrFetchWorker.perform(%Oban.Job{args: args}) # Only the pre-seeded row exists — no new insert. assert Repo.aggregate(NarrProfile, :count, :id) == 1 end test "returns {:error, _} when NarrClient.fetch_profile_at fails" do Req.Test.stub(NarrClient, fn conn -> Plug.Conn.send_resp(conn, 500, "ncei server error") end) args = %{ "lat" => 32.9, "lon" => -97.0, "valid_time" => "2010-06-15T12:00:00Z" } assert {:error, _reason} = NarrFetchWorker.perform(%Oban.Job{args: args}) assert Repo.aggregate(NarrProfile, :count, :id) == 0 end test "raises ArgumentError when valid_time is not on a 3-hourly NARR slot" do # 13:00 is NOT a NARR analysis hour (only 00/03/06/09/12/15/18/21). args = %{ "lat" => 32.9, "lon" => -97.0, "valid_time" => "2010-06-15T13:00:00Z" } assert_raise ArgumentError, fn -> NarrFetchWorker.perform(%Oban.Job{args: args}) end end end describe "unique constraint" do test "duplicate enqueues collapse via Oban unique" do args = %{ "lat" => 32.9, "lon" => -97.0, "valid_time" => "2010-06-15T12:00:00Z" } Oban.Testing.with_testing_mode(:manual, fn -> {:ok, job1} = Oban.insert(NarrFetchWorker.new(args)) {:ok, job2} = Oban.insert(NarrFetchWorker.new(args)) # Second insert should hit the unique constraint and return the # existing job (same id) instead of creating a new one. assert job1.id == job2.id assert Repo.aggregate(Oban.Job, :count, :id) == 1 end) end end end