prop/test/microwaveprop/space_weather/swpc_client_test.exs
Graham McIntire 26ef50c965
fix(ingest): dedupe ASOS duplicates + shrink SWPC decode errors
Two unrelated production warnings from the same log window:

WeatherFetchWorker raised Postgrex 21000 cardinality_violation on
ASOS upserts when IEM returned two METAR records for the same
timestamp (routine + special at the same minute). Dedupe by
(station_id, observed_at) in upsert_surface_observations, keeping
the last occurrence since IEM's ordering is chronological.

SwpcClient.fetch_xrays warned with the full truncated response
body (~650 KB of GOES X-ray JSON) dumped into the log aggregator
whenever SWPC's CDN cut the response mid-stream. Intercept
Jason.DecodeError in fetch_and_parse and format it short: keep
the byte position and an 80-char preview, drop the rest.
2026-04-21 16:39:17 -05:00

88 lines
3.1 KiB
Elixir
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

defmodule Microwaveprop.SpaceWeather.SwpcClientTest do
use ExUnit.Case, async: true
alias Microwaveprop.SpaceWeather.SwpcClient
describe "parse_kp/1" do
test "decodes the SWPC planetary_k_index_1m fixture" do
body = File.read!("test/fixtures/swpc/kp_sample.json")
assert {:ok, rows} = SwpcClient.parse_kp(body)
assert length(rows) == 10
first = hd(rows)
assert first.valid_time == ~U[2026-04-15 13:49:00Z]
assert first.kp_index == 0
assert_in_delta first.estimated_kp, 0.33, 0.001
end
test "returns {:error, _} for malformed bodies" do
assert {:error, _} = SwpcClient.parse_kp("not json")
assert {:error, _} = SwpcClient.parse_kp(~s({"not": "an array"}))
end
end
describe "fetch_xrays/0 upstream truncation" do
# SWPC occasionally returns a truncated JSON body (TCP reset mid-stream).
# Req's auto-decoder raises a Jason.DecodeError carrying the entire
# raw body in :data — historically that dumped hundreds of KB into
# our Logger. The error should be returned, but formatted short.
test "returns {:error, reason} without dumping the full truncated body" do
big_body = "[" <> String.duplicate(~s({"time_tag":"2026-04-20T21:35:00Z","energy":"0.1-0.8nm","flux":1.0},), 20_000)
# Truncate — no closing bracket.
truncated = big_body
Req.Test.stub(SwpcClient, fn conn ->
conn
|> Plug.Conn.put_resp_content_type("application/json")
|> Plug.Conn.send_resp(200, truncated)
end)
assert {:error, reason} = SwpcClient.fetch_xrays()
assert is_binary(reason)
# Upper-bound the log-noise blast radius.
assert byte_size(reason) < 500
# Still descriptive.
assert reason =~ ~r/(decode|JSON|truncated|Jason)/i
end
end
describe "parse_f107/1" do
test "decodes the SWPC f107_cm_flux fixture" do
body = File.read!("test/fixtures/swpc/f107_sample.json")
assert {:ok, rows} = SwpcClient.parse_f107(body)
assert length(rows) == 10
first = hd(rows)
assert first.valid_time == ~U[2026-04-15 17:00:00Z]
assert first.frequency_mhz == 2800
assert first.flux_sfu == 112.0
assert first.reporting_schedule == "Morning"
end
test "keeps nullable historical stats as nil when absent" do
body = File.read!("test/fixtures/swpc/f107_sample.json")
{:ok, [first | _]} = SwpcClient.parse_f107(body)
assert first.ninety_day_mean == nil
end
end
describe "parse_xrays/1" do
test "decodes the GOES xrays-1-day fixture, keeping the 0.1-0.8nm band only" do
body = File.read!("test/fixtures/swpc/xrays_sample.json")
assert {:ok, rows} = SwpcClient.parse_xrays(body)
# Fixture has 12 records (6 timestamps × 2 bands). We keep the
# long-wavelength band (0.1-0.8nm) which is the standard SWF /
# D-layer absorption proxy.
assert length(rows) == 6
assert Enum.all?(rows, fn r -> r.energy_band == "0.1-0.8nm" end)
first = hd(rows)
assert first.valid_time == ~U[2026-04-14 19:49:00Z]
assert first.satellite == 18
assert_in_delta first.flux_wm2, 3.523e-07, 1.0e-09
end
end
end