defmodule Microwaveprop.Ionosphere.GiroClientTest do use ExUnit.Case, async: true alias Microwaveprop.Ionosphere.GiroClient describe "parse_tabular/1" do test "decodes the real Millstone Hill fixture into observation maps" do body = File.read!("test/fixtures/giro/mhj45_sample.txt") assert {:ok, rows} = GiroClient.parse_tabular(body) # The fixture is 2026-04-15 16:00 → 19:00 UTC at 7.5-minute cadence, # so 25 observations. assert length(rows) == 25 first = hd(rows) assert first.valid_time == ~U[2026-04-15 16:00:00Z] assert first.confidence_score == 60 assert_in_delta first.fo_f2_mhz, 6.856, 0.001 assert_in_delta first.mufd_mhz, 23.63, 0.001 # foEs was missing ("---") at 16:00. assert first.fo_es_mhz == nil assert_in_delta first.fo_e_mhz, 3.50, 0.001 assert_in_delta first.hm_f2_km, 217.2, 0.1 # 16:07:30 has foEs == 3.42. second = Enum.at(rows, 1) assert second.valid_time == ~U[2026-04-15 16:07:30Z] assert_in_delta second.fo_es_mhz, 3.42, 0.001 last = List.last(rows) assert last.valid_time == ~U[2026-04-15 19:00:00Z] assert_in_delta last.fo_f2_mhz, 7.400, 0.001 end test "returns {:ok, []} for a body with only the header" do body = """ # Global Ionospheric Radio Observatory # Location: GEO 42.6N 288.5E, URSI-Code MHJ45 MILLSTONE HILL # #Time CS foF2 QD MUFD QD foEs QD foE QD hmF2 QD """ assert {:ok, []} = GiroClient.parse_tabular(body) end test "returns {:error, :no_header} when the column header is missing" do body = """ # some junk 2026-04-15T18:00:00.000Z 80 7.113 // 23.00 // """ assert {:error, :no_header} = GiroClient.parse_tabular(body) end test "skips data lines whose token count doesn't match the header" do # Header declares 5 value columns (10 tokens after time+CS) but the # data row is truncated. Should be skipped rather than crashing. body = """ #Time CS foF2 QD MUFD QD foEs QD foE QD hmF2 QD 2026-04-15T18:00:00.000Z 80 7.113 // 2026-04-15T18:07:30.000Z 80 7.088 // 22.76 // 3.35 // 3.13 // 246.0 // """ assert {:ok, [row]} = GiroClient.parse_tabular(body) assert row.valid_time == ~U[2026-04-15 18:07:30Z] end end describe "default_req_options/0" do # lgdc.uml.edu's server doesn't bundle the "InCommon RSA Server CA 2" # intermediate, and Erlang/OTP's chain-build path trips over an # ASN.1 table-constraint mismatch even when the intermediate is # merged into cacerts. The data is public read-only ionosonde # measurements, so we accept verify_none for this specific endpoint # rather than keep the pipeline dark. Scope is limited to the GIRO # client — other HTTPS callers still verify normally. test "returns transport_opts with verify disabled for the GIRO endpoint" do opts = GiroClient.default_req_options() transport_opts = opts |> Keyword.fetch!(:connect_options) |> Keyword.fetch!(:transport_opts) assert Keyword.get(transport_opts, :verify) == :verify_none end end end