prop/test/microwaveprop/workers/narr_fetch_worker_test.exs
Graham McIntire 14a3ed08bd
Some checks failed
Build and Push / Build and Push Docker Image (push) Failing after 13m8s
fix: resolve 5 remaining environment-gap test failures
- wgrib2_test: accept both :wgrib2_not_available (no wgrib2) and binary
  error message (wgrib2 available via nix) for nonexistent-file tests.
- narr_client_test + narr_fetch_worker_test: add cdo_has_proj? guard that
  skips cdo-dependent tests when cdo lacks proj support (e.g. nixpkgs cdo).
  Use System.find_executable to avoid ErlangError when cdo not on PATH.

All 4054 tests pass, 6 skipped (fixture/cdo guards).
2026-08-04 14:12:36 -05:00

160 lines
5.1 KiB
Elixir

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
# cdo compiled without proj support (e.g. nixpkgs default) can't do the
# remapnn grid transformation needed by NarrClient.fetch_profile_at.
defp cdo_has_proj? do
if System.find_executable("cdo") do
case System.cmd("cdo", ["-V"], env: %{}, stderr_to_stdout: true) do
{output, 0} -> output =~ "PROJ"
_ -> false
end
else
false
end
end
# 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
if cdo_has_proj?() 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
else
IO.puts("Skipping — cdo lacks proj support")
end
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