prop/test/microwaveprop/workers/terrain_profile_worker_test.exs
Graham McIntire 45e2e69361
Add SRTM terrain path profiles for QSOs
Fetch elevation data along the path between two stations via the
Open-Meteo Elevation API (with Open-Topo-Data fallback), compute
Fresnel zone clearance, earth bulge, and knife-edge diffraction loss,
and store the results per QSO.

- terrain_profiles table with migration and TerrainProfile schema
- ElevationClient with batched API calls and fallback
- TerrainAnalysis with Fresnel/diffraction physics (ITU-R P.526-15)
- TerrainProfileWorker on Oban :terrain queue
- QsoWeatherEnqueueWorker enqueues terrain jobs automatically
- QSO show page displays verdict badge and collapsible elevation table
- Reorder show page: terrain, soundings, solar, HRRR, surface obs
- Fix Dockerfile wgrib2 build (add cmake dependency)
2026-03-29 16:22:29 -05:00

95 lines
2.8 KiB
Elixir

defmodule Microwaveprop.Workers.TerrainProfileWorkerTest do
use Microwaveprop.DataCase, async: true
alias Microwaveprop.Radio.Qso
alias Microwaveprop.Terrain
alias Microwaveprop.Terrain.ElevationClient
alias Microwaveprop.Workers.TerrainProfileWorker
@qso_attrs %{
station1: "W5XD",
station2: "K5TR",
qso_timestamp: ~U[2026-03-28 18:00:00Z],
mode: "CW",
band: Decimal.new("1296"),
pos1: %{"lat" => 32.9, "lon" => -97.0},
pos2: %{"lat" => 30.3, "lon" => -97.7},
distance_km: Decimal.new("295")
}
defp create_qso(attrs \\ %{}) do
{:ok, qso} =
%Qso{}
|> Qso.changeset(Map.merge(@qso_attrs, attrs))
|> Repo.insert()
qso
end
defp stub_elevation_api do
Req.Test.stub(ElevationClient, fn conn ->
params = Plug.Conn.fetch_query_params(conn).query_params
lat_count = params["latitude"] |> String.split(",") |> length()
elevations = List.duplicate(200.0, lat_count)
Req.Test.json(conn, %{"elevation" => elevations})
end)
end
describe "perform/1" do
test "fetches elevation and stores terrain profile" do
stub_elevation_api()
qso = create_qso()
refute Terrain.has_terrain_profile?(qso.id)
job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
assert Terrain.has_terrain_profile?(qso.id)
profile = Terrain.get_terrain_profile(qso.id)
assert profile.sample_count == 65
assert profile.verdict in ["CLEAR", "FRESNEL_MINOR", "FRESNEL_PARTIAL", "BLOCKED"]
assert is_list(profile.path_points)
end
test "skips if terrain profile already exists" do
stub_elevation_api()
qso = create_qso()
{:ok, _} =
Terrain.upsert_terrain_profile(%{
qso_id: qso.id,
sample_count: 65,
path_points: [],
verdict: "CLEAR"
})
job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
end
test "returns error when elevation API fails" do
Req.Test.stub(ElevationClient, fn conn ->
Plug.Conn.send_resp(conn, 500, "error")
end)
qso = create_qso()
job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
assert {:error, _} = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
refute Terrain.has_terrain_profile?(qso.id)
end
test "uses correct frequency from band" do
stub_elevation_api()
qso = create_qso(%{band: Decimal.new("10368")})
job = TerrainProfileWorker.new(%{"qso_id" => qso.id})
assert :ok = TerrainProfileWorker.perform(%Oban.Job{args: job.changes.args})
profile = Terrain.get_terrain_profile(qso.id)
assert profile.verdict
end
end
end