defmodule Microwaveprop.Radio.CsvImportTest do use Microwaveprop.DataCase, async: true alias Microwaveprop.Radio.Contact alias Microwaveprop.Radio.CsvImport alias Microwaveprop.Terrain.ElevationClient alias Microwaveprop.Weather.HrrrClient alias Microwaveprop.Weather.IemClient setup do Req.Test.stub(IemClient, fn conn -> case conn.request_path do "/cgi-bin/request/asos.py" -> Req.Test.text(conn, "#DEBUG,\nstation,valid,tmpf\n") _ -> Req.Test.json(conn, %{"profiles" => []}) end end) Req.Test.stub(HrrrClient, fn conn -> Plug.Conn.send_resp(conn, 404, "not found") end) Req.Test.stub(ElevationClient, fn conn -> params = Plug.Conn.fetch_query_params(conn).query_params lat_count = params["latitude"] |> String.split(",") |> length() Req.Test.json(conn, %{"elevation" => List.duplicate(200.0, lat_count)}) end) :ok end @valid_header "station1,station2,grid1,grid2,band,mode,qso_timestamp" @valid_row "W5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z" @submitter "test@example.com" describe "import/2 with empty or missing data" do test "returns error for empty string" do assert {:error, :empty_csv} = CsvImport.import("", @submitter) end test "returns error for whitespace-only string" do assert {:error, :empty_csv} = CsvImport.import(" \n \n ", @submitter) end test "returns error for header-only CSV" do assert {:error, :no_data_rows} = CsvImport.import(@valid_header, @submitter) end test "returns error for header plus blank lines only" do csv = @valid_header <> "\n\n\n" assert {:error, :no_data_rows} = CsvImport.import(csv, @submitter) end end describe "import/2 with valid data" do test "imports a single valid row" do csv = @valid_header <> "\n" <> @valid_row assert {:ok, %{imported: [contact], errors: []}} = CsvImport.import(csv, @submitter) assert %Contact{} = contact assert contact.station1 == "W5XD" assert contact.station2 == "K5TR" assert contact.grid1 == "EM12" assert contact.grid2 == "EM00" assert contact.mode == "CW" assert contact.user_submitted == true assert contact.submitter_email == @submitter assert contact.pos1 assert contact.pos2 assert contact.distance_km end test "imports multiple valid rows" do csv = Enum.join( [ @valid_header, "W5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z", "N5AC,W5LUA,EM13,EM20,24000,SSB,2026-03-28T19:00:00Z" ], "\n" ) assert {:ok, %{imported: imported, errors: []}} = CsvImport.import(csv, @submitter) assert length(imported) == 2 end test "trims whitespace from fields" do csv = @valid_header <> "\n" <> " W5XD , K5TR , EM12 , EM00 , 10000 , CW , 2026-03-28T18:00:00Z " assert {:ok, %{imported: [contact], errors: []}} = CsvImport.import(csv, @submitter) assert contact.station1 == "W5XD" assert contact.station2 == "K5TR" end test "handles Windows line endings" do csv = @valid_header <> "\r\n" <> @valid_row <> "\r\n" assert {:ok, %{imported: [contact], errors: []}} = CsvImport.import(csv, @submitter) assert contact.station1 == "W5XD" end test "skips blank lines between data rows" do csv = Enum.join( [ @valid_header, @valid_row, "", "", "N5AC,W5LUA,EM13,EM20,24000,SSB,2026-03-28T19:00:00Z" ], "\n" ) assert {:ok, %{imported: imported, errors: []}} = CsvImport.import(csv, @submitter) assert length(imported) == 2 end test "contacts are persisted to the database" do csv = @valid_header <> "\n" <> @valid_row assert {:ok, _result} = CsvImport.import(csv, @submitter) assert Repo.aggregate(Contact, :count) == 1 end end describe "import/2 with invalid data" do test "reports wrong column count with row number" do csv = @valid_header <> "\n" <> "W5XD,K5TR,EM12" assert {:ok, %{imported: [], errors: [{2, errors}]}} = CsvImport.import(csv, @submitter) assert Enum.any?(errors, &String.contains?(&1, "column")) end test "reports invalid band" do csv = @valid_header <> "\n" <> "W5XD,K5TR,EM12,EM00,99999,CW,2026-03-28T18:00:00Z" assert {:ok, %{imported: [], errors: [{2, errors}]}} = CsvImport.import(csv, @submitter) assert length(errors) > 0 end test "reports invalid grid square" do csv = @valid_header <> "\n" <> "W5XD,K5TR,ZZZZ,EM00,10000,CW,2026-03-28T18:00:00Z" assert {:ok, %{imported: [], errors: [{2, errors}]}} = CsvImport.import(csv, @submitter) assert length(errors) > 0 end test "reports invalid mode" do csv = @valid_header <> "\n" <> "W5XD,K5TR,EM12,EM00,10000,RTTY,2026-03-28T18:00:00Z" assert {:ok, %{imported: [], errors: [{2, errors}]}} = CsvImport.import(csv, @submitter) assert length(errors) > 0 end test "reports missing required fields" do csv = @valid_header <> "\n" <> ",K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z" assert {:ok, %{imported: [], errors: [{2, errors}]}} = CsvImport.import(csv, @submitter) assert length(errors) > 0 end test "reports invalid timestamp" do csv = @valid_header <> "\n" <> "W5XD,K5TR,EM12,EM00,10000,CW,not-a-date" assert {:ok, %{imported: [], errors: [{2, errors}]}} = CsvImport.import(csv, @submitter) assert length(errors) > 0 end end describe "import/2 with mixed valid and invalid rows" do test "imports valid rows and reports invalid rows" do csv = Enum.join( [ @valid_header, @valid_row, "W5XD,K5TR,EM12,EM00,99999,CW,2026-03-28T18:00:00Z", "N5AC,W5LUA,EM13,EM20,24000,SSB,2026-03-28T19:00:00Z" ], "\n" ) assert {:ok, %{imported: imported, errors: errors}} = CsvImport.import(csv, @submitter) assert length(imported) == 2 assert [{3, _error_msgs}] = errors end test "row numbers account for header and blank lines" do csv = Enum.join( [ @valid_header, @valid_row, "", "bad row with wrong columns", "N5AC,W5LUA,EM13,EM20,24000,SSB,2026-03-28T19:00:00Z" ], "\n" ) assert {:ok, %{imported: imported, errors: errors}} = CsvImport.import(csv, @submitter) assert length(imported) == 2 # Row 4 is the bad row (header=1, data=2, blank=3, bad=4, good=5) assert [{4, _error_msgs}] = errors end end end