- Fix N+1 queries in radio.ex (preload :user) - Add encryption_salt to session options - Consolidate token deletion to single query - Wrap gunzip in try/rescue for corrupt files - Add Cache.match_delete/1 to encapsulate ETS access - Precompute sec_i_factor_ref as module attribute - Guard EXLA.Backend config to dev/test only - Split 3505-line contact_live_test.exs into 4 files - Replace Process.sleep with :sys.get_state synchronization - Replace Process.alive? with DOM output assertions - Clarify router.ex comment for non-live_session routes - Update findings.md to reflect fixes
443 lines
12 KiB
Elixir
443 lines
12 KiB
Elixir
defmodule MicrowavepropWeb.SubmitLiveTest do
|
|
use MicrowavepropWeb.ConnCase, async: true
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Microwaveprop.Radio.Contact
|
|
alias Microwaveprop.Repo
|
|
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
|
|
|
|
describe "mount" do
|
|
test "renders the submission form", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/submit")
|
|
assert html =~ "Submit Contact"
|
|
assert html =~ "Station 1"
|
|
assert html =~ "Station 2"
|
|
assert html =~ "Grid 1"
|
|
assert html =~ "Grid 2"
|
|
assert html =~ "Band"
|
|
assert html =~ "Mode"
|
|
end
|
|
end
|
|
|
|
describe "switch_tab" do
|
|
test "ignores an unknown tab string instead of crashing the LiveView", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
# An unknown tab name used to flow into String.to_existing_atom/1
|
|
# and bring the LiveView down. We expect the event to be a no-op
|
|
# and the process to stay alive.
|
|
render_hook(lv, "switch_tab", %{"tab" => "definitely-not-a-tab-#{System.unique_integer()}"})
|
|
|
|
assert render(lv) =~ "Submit Contact"
|
|
end
|
|
|
|
test "honors a known tab name", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
html = render_hook(lv, "switch_tab", %{"tab" => "csv"})
|
|
|
|
# Active tab moved to CSV — the CSV section's heading is now visible.
|
|
assert html =~ "CSV"
|
|
end
|
|
end
|
|
|
|
describe "validate" do
|
|
test "shows validation errors on change", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
html =
|
|
lv
|
|
|> form("#contact-form", contact: %{station1: ""})
|
|
|> render_change()
|
|
|
|
assert html =~ "can't be blank"
|
|
end
|
|
end
|
|
|
|
describe "csv upload" do
|
|
test "renders CSV upload tab and sample download link", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
html =
|
|
lv
|
|
|> element("[phx-value-tab=csv]")
|
|
|> render_click()
|
|
|
|
assert html =~ "Upload CSV"
|
|
assert html =~ "/downloads/sample_contacts.csv"
|
|
assert html =~ "Download sample CSV"
|
|
end
|
|
|
|
test "uploads valid CSV and shows preview with confirm button", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
lv
|
|
|> element("[phx-value-tab=csv]")
|
|
|> render_click()
|
|
|
|
csv_content =
|
|
"station1,station2,grid1,grid2,band,mode,qso_timestamp\nW5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z\n"
|
|
|
|
csv_input =
|
|
file_input(lv, "#csv-upload-form", :csv, [
|
|
%{name: "contacts.csv", content: csv_content, type: "text/csv"}
|
|
])
|
|
|
|
render_upload(csv_input, "contacts.csv")
|
|
|
|
html =
|
|
lv
|
|
|> form("#csv-upload-form", %{submitter_email: "test@example.com"})
|
|
|> render_submit()
|
|
|
|
assert html =~ "Review before submitting"
|
|
assert html =~ "Looks good"
|
|
# Nothing was inserted yet
|
|
assert Repo.aggregate(Contact, :count) == 0
|
|
|
|
lv
|
|
|> element("button[phx-click=confirm_csv]")
|
|
|> render_click()
|
|
|
|
{path, _flash} = assert_redirect(lv)
|
|
assert path =~ ~r"^/imports/[0-9a-f-]{36}$"
|
|
# Oban runs inline, so by the time the redirect fires the chunk
|
|
# worker has already inserted the contact.
|
|
assert Repo.aggregate(Contact, :count) == 1
|
|
end
|
|
|
|
test "preview flags duplicates before confirmation", %{conn: conn} do
|
|
# Seed an existing contact so the next upload is a duplicate of it.
|
|
{:ok, _} =
|
|
Microwaveprop.Radio.create_contact(%{
|
|
"station1" => "W5XD",
|
|
"station2" => "K5TR",
|
|
"grid1" => "EM12",
|
|
"grid2" => "EM00",
|
|
"band" => "10000",
|
|
"mode" => "CW",
|
|
"qso_timestamp" => "2026-03-28T18:00:00Z",
|
|
"submitter_email" => "seed@example.com"
|
|
})
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
lv
|
|
|> element("[phx-value-tab=csv]")
|
|
|> render_click()
|
|
|
|
csv_content =
|
|
"station1,station2,grid1,grid2,band,mode,qso_timestamp\n" <>
|
|
"W5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:15:00Z\n"
|
|
|
|
csv_input =
|
|
file_input(lv, "#csv-upload-form", :csv, [
|
|
%{name: "contacts.csv", content: csv_content, type: "text/csv"}
|
|
])
|
|
|
|
render_upload(csv_input, "contacts.csv")
|
|
|
|
html =
|
|
lv
|
|
|> form("#csv-upload-form", %{submitter_email: "test@example.com"})
|
|
|> render_submit()
|
|
|
|
assert html =~ "Duplicates"
|
|
assert html =~ "Already in database"
|
|
refute html =~ "Looks good"
|
|
end
|
|
|
|
test "preview shows refinements when upload extends an existing grid", %{conn: conn} do
|
|
{:ok, existing} =
|
|
Microwaveprop.Radio.create_contact(%{
|
|
"station1" => "W5XD",
|
|
"station2" => "K5TR",
|
|
"grid1" => "EM12",
|
|
"grid2" => "EM00",
|
|
"band" => "10000",
|
|
"mode" => "CW",
|
|
"qso_timestamp" => "2026-03-28T18:00:00Z",
|
|
"submitter_email" => "seed@example.com"
|
|
})
|
|
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
lv
|
|
|> element("[phx-value-tab=csv]")
|
|
|> render_click()
|
|
|
|
csv_content =
|
|
"station1,station2,grid1,grid2,band,mode,qso_timestamp\n" <>
|
|
"W5XD,K5TR,EM12KP37,EM00CD22,10000,CW,2026-03-28T18:10:00Z\n"
|
|
|
|
csv_input =
|
|
file_input(lv, "#csv-upload-form", :csv, [
|
|
%{name: "contacts.csv", content: csv_content, type: "text/csv"}
|
|
])
|
|
|
|
render_upload(csv_input, "contacts.csv")
|
|
|
|
html =
|
|
lv
|
|
|> form("#csv-upload-form", %{submitter_email: "test@example.com"})
|
|
|> render_submit()
|
|
|
|
assert html =~ "Refinements"
|
|
assert html =~ "EM12KP37"
|
|
assert html =~ "refine 1 existing"
|
|
|
|
lv
|
|
|> element("button[phx-click=confirm_csv]")
|
|
|> render_click()
|
|
|
|
{path, _flash} = assert_redirect(lv)
|
|
assert path =~ ~r"^/imports/[0-9a-f-]{36}$"
|
|
assert Repo.aggregate(Contact, :count) == 1
|
|
|
|
refreshed = Repo.reload(existing)
|
|
assert refreshed.grid1 == "EM12KP37"
|
|
assert refreshed.grid2 == "EM00CD22"
|
|
end
|
|
|
|
test "shows errors for invalid rows and still offers the confirm button", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
lv
|
|
|> element("[phx-value-tab=csv]")
|
|
|> render_click()
|
|
|
|
csv_content =
|
|
"station1,station2,grid1,grid2,band,mode,qso_timestamp\n" <>
|
|
"W5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z\n" <>
|
|
",,,,,,\n"
|
|
|
|
csv_input =
|
|
file_input(lv, "#csv-upload-form", :csv, [
|
|
%{name: "contacts.csv", content: csv_content, type: "text/csv"}
|
|
])
|
|
|
|
render_upload(csv_input, "contacts.csv")
|
|
|
|
html =
|
|
lv
|
|
|> form("#csv-upload-form", %{submitter_email: "test@example.com"})
|
|
|> render_submit()
|
|
|
|
assert html =~ "Invalid rows"
|
|
assert html =~ "Row 3"
|
|
assert html =~ "Looks good"
|
|
end
|
|
|
|
test "cancel_csv clears the preview", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
lv
|
|
|> element("[phx-value-tab=csv]")
|
|
|> render_click()
|
|
|
|
csv_content =
|
|
"station1,station2,grid1,grid2,band,mode,qso_timestamp\nW5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z\n"
|
|
|
|
csv_input =
|
|
file_input(lv, "#csv-upload-form", :csv, [
|
|
%{name: "contacts.csv", content: csv_content, type: "text/csv"}
|
|
])
|
|
|
|
render_upload(csv_input, "contacts.csv")
|
|
|
|
lv
|
|
|> form("#csv-upload-form", %{submitter_email: "test@example.com"})
|
|
|> render_submit()
|
|
|
|
html =
|
|
lv
|
|
|> element("button[phx-click=cancel_csv]")
|
|
|> render_click()
|
|
|
|
assert html =~ "Upload CSV"
|
|
assert Repo.aggregate(Contact, :count) == 0
|
|
end
|
|
|
|
test "requires email for CSV upload", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
lv
|
|
|> element("[phx-value-tab=csv]")
|
|
|> render_click()
|
|
|
|
csv_content =
|
|
"station1,station2,grid1,grid2,band,mode,qso_timestamp\nW5XD,K5TR,EM12,EM00,10000,CW,2026-03-28T18:00:00Z\n"
|
|
|
|
csv_input =
|
|
file_input(lv, "#csv-upload-form", :csv, [
|
|
%{name: "contacts.csv", content: csv_content, type: "text/csv"}
|
|
])
|
|
|
|
render_upload(csv_input, "contacts.csv")
|
|
|
|
html =
|
|
lv
|
|
|> form("#csv-upload-form", %{submitter_email: ""})
|
|
|> render_submit()
|
|
|
|
assert html =~ "Email is required for CSV upload"
|
|
end
|
|
|
|
test "ADIF confirm redirects to /imports/:id", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
lv
|
|
|> element("[phx-value-tab=adif]")
|
|
|> render_click()
|
|
|
|
adif_content =
|
|
"<CALL:4>K5TR" <>
|
|
"<STATION_CALLSIGN:4>W5XD" <>
|
|
"<GRIDSQUARE:4>EM00" <>
|
|
"<MY_GRIDSQUARE:4>EM12" <>
|
|
"<FREQ:5>10368" <>
|
|
"<MODE:2>CW" <>
|
|
"<QSO_DATE:8>20260328" <>
|
|
"<TIME_ON:6>180000" <>
|
|
"<EOR>"
|
|
|
|
adif_input =
|
|
file_input(lv, "#adif-upload-form", :adif, [
|
|
%{name: "contacts.adi", content: adif_content, type: "application/octet-stream"}
|
|
])
|
|
|
|
render_upload(adif_input, "contacts.adi")
|
|
|
|
html =
|
|
lv
|
|
|> form("#adif-upload-form", %{submitter_email: "test@example.com"})
|
|
|> render_submit()
|
|
|
|
assert html =~ "Review before submitting"
|
|
|
|
lv
|
|
|> element("button[phx-click=confirm_csv]")
|
|
|> render_click()
|
|
|
|
{path, _flash} = assert_redirect(lv)
|
|
assert path =~ ~r"^/imports/[0-9a-f-]{36}$"
|
|
assert Repo.aggregate(Contact, :count) == 1
|
|
end
|
|
|
|
test "handles CSV with no data rows", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
lv
|
|
|> element("[phx-value-tab=csv]")
|
|
|> render_click()
|
|
|
|
csv_content = "station1,station2,grid1,grid2,band,mode,qso_timestamp\n"
|
|
|
|
csv_input =
|
|
file_input(lv, "#csv-upload-form", :csv, [
|
|
%{name: "header_only.csv", content: csv_content, type: "text/csv"}
|
|
])
|
|
|
|
render_upload(csv_input, "header_only.csv")
|
|
|
|
html =
|
|
lv
|
|
|> form("#csv-upload-form", %{submitter_email: "test@example.com"})
|
|
|> render_submit()
|
|
|
|
assert html =~ "CSV file has no data rows"
|
|
end
|
|
end
|
|
|
|
describe "save" do
|
|
test "creates QSO and redirects on valid submit", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
lv
|
|
|> form("#contact-form",
|
|
contact: %{
|
|
station1: "W5XD",
|
|
station2: "K5TR",
|
|
qso_timestamp: "2026-03-28T18:00",
|
|
mode: "CW",
|
|
band: "1296",
|
|
grid1: "EM12",
|
|
grid2: "EM00",
|
|
submitter_email: "test@example.com"
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|
|
contact = Repo.one!(Contact)
|
|
assert contact.station1 == "W5XD"
|
|
assert contact.user_submitted == true
|
|
assert contact.pos1["lat"]
|
|
|
|
assert_redirect(lv, ~p"/contacts/#{contact.id}")
|
|
end
|
|
|
|
test "shows errors on invalid submit", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
html =
|
|
lv
|
|
|> form("#contact-form", contact: %{station1: ""})
|
|
|> render_submit()
|
|
|
|
assert html =~ "can't be blank"
|
|
end
|
|
|
|
test "persists notes from the form on save", %{conn: conn} do
|
|
{:ok, lv, _html} = live(conn, ~p"/submit")
|
|
|
|
lv
|
|
|> form("#contact-form",
|
|
contact: %{
|
|
station1: "W5XD",
|
|
station2: "K5TR",
|
|
qso_timestamp: "2026-03-28T18:00",
|
|
mode: "CW",
|
|
band: "1296",
|
|
grid1: "EM12",
|
|
grid2: "EM00",
|
|
submitter_email: "test@example.com",
|
|
notes: "Tropo duct along the coast"
|
|
}
|
|
)
|
|
|> render_submit()
|
|
|
|
contact = Repo.one!(Contact)
|
|
assert contact.notes == "Tropo duct along the coast"
|
|
end
|
|
|
|
test "renders a notes textarea on the single-contact tab", %{conn: conn} do
|
|
{:ok, _lv, html} = live(conn, ~p"/submit")
|
|
assert html =~ ~s(name="contact[notes]")
|
|
assert html =~ "textarea"
|
|
end
|
|
end
|
|
end
|