Operators want a place to jot observations that aren't captured by
the structured fields — weather anecdotes, propagation mode
commentary, equipment details, band conditions. Add a text column
on contacts plus the two ingest paths users submit through:
* /submit single-contact form gets a 3-row textarea under the
other fields. Optional, max 2000 chars, with a live length cap
via maxlength. Whitespace-only input collapses to NULL so the
column reflects "no notes" rather than an empty string.
* CSV importer recognises a `notes` (or `note`) column via the
existing header_aliases table and flows values straight through
submission_changeset. Sample template gets a matching example
row with embedded commas so the quoted-field round-trip is
exercised on download.
ADIF import and the refinement/refinement-notify paths are out of
scope — users specifically asked for CSV + single-contact. Tests
cover the changeset (accept/blank/over-length), CSV header parsing
(plain + RFC-4180 quoted), and LiveView form submit end-to-end.
176 lines
6.3 KiB
Elixir
176 lines
6.3 KiB
Elixir
defmodule Microwaveprop.Radio.ContactSubmissionTest do
|
|
use Microwaveprop.DataCase, async: true
|
|
|
|
alias Microwaveprop.Radio.Contact
|
|
|
|
@valid_attrs %{
|
|
station1: "W5XD",
|
|
station2: "K5TR",
|
|
qso_timestamp: ~U[2026-03-28 18:00:00Z],
|
|
mode: "CW",
|
|
band: "1296",
|
|
grid1: "EM12",
|
|
grid2: "EM00",
|
|
submitter_email: "test@example.com"
|
|
}
|
|
|
|
describe "submission_changeset/2" do
|
|
test "valid attrs produce a valid changeset" do
|
|
changeset = Contact.submission_changeset(%Contact{}, @valid_attrs)
|
|
assert changeset.valid?
|
|
end
|
|
|
|
test "requires station1, station2, qso_timestamp, band, grid1, grid2, submitter_email" do
|
|
changeset = Contact.submission_changeset(%Contact{}, %{})
|
|
errors = errors_on(changeset)
|
|
|
|
assert errors[:station1]
|
|
assert errors[:station2]
|
|
assert errors[:qso_timestamp]
|
|
assert errors[:band]
|
|
assert errors[:grid1]
|
|
assert errors[:grid2]
|
|
assert errors[:submitter_email]
|
|
|
|
# Mode is optional on submission
|
|
refute errors[:mode]
|
|
end
|
|
|
|
test "accepts a blank mode and normalises it to nil" do
|
|
for blank <- ["", " ", nil] do
|
|
attrs = Map.put(@valid_attrs, :mode, blank)
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert changeset.valid?, "Expected blank mode #{inspect(blank)} to validate"
|
|
assert Ecto.Changeset.get_field(changeset, :mode) == nil
|
|
end
|
|
end
|
|
|
|
test "rejects invalid grid1" do
|
|
attrs = Map.put(@valid_attrs, :grid1, "ZZ99")
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert "is not a valid Maidenhead grid square" in errors_on(changeset).grid1
|
|
end
|
|
|
|
test "rejects invalid grid2" do
|
|
attrs = Map.put(@valid_attrs, :grid2, "ZZ99")
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert "is not a valid Maidenhead grid square" in errors_on(changeset).grid2
|
|
end
|
|
|
|
test "accepts valid 6-char grids" do
|
|
attrs = %{@valid_attrs | grid1: "EM12ab", grid2: "EM00cd"}
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert changeset.valid?
|
|
end
|
|
|
|
test "rejects invalid mode" do
|
|
attrs = Map.put(@valid_attrs, :mode, "RTTY")
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert errors_on(changeset).mode
|
|
end
|
|
|
|
test "accepts valid modes" do
|
|
for mode <- ~w(CW SSB FM FT8 FT4 Q65) do
|
|
attrs = Map.put(@valid_attrs, :mode, mode)
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert changeset.valid?, "Expected mode #{mode} to be valid"
|
|
end
|
|
end
|
|
|
|
test "rejects invalid band" do
|
|
attrs = Map.put(@valid_attrs, :band, "28")
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert errors_on(changeset).band
|
|
end
|
|
|
|
test "accepts valid bands" do
|
|
for band <- ~w(50 144 222 432 902 1296 2304 3400 5760 10000 24000 47000 75000) do
|
|
attrs = Map.put(@valid_attrs, :band, band)
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert changeset.valid?, "Expected band #{band} to be valid"
|
|
end
|
|
end
|
|
|
|
test "rejects email without @" do
|
|
attrs = Map.put(@valid_attrs, :submitter_email, "notanemail")
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert errors_on(changeset).submitter_email
|
|
end
|
|
|
|
test "accepts private flag and defaults to false" do
|
|
changeset = Contact.submission_changeset(%Contact{}, @valid_attrs)
|
|
assert changeset.valid?
|
|
assert Ecto.Changeset.get_field(changeset, :private) == false
|
|
|
|
attrs = Map.put(@valid_attrs, :private, true)
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert changeset.valid?
|
|
assert Ecto.Changeset.get_field(changeset, :private) == true
|
|
end
|
|
|
|
test "does not cast user_submitted" do
|
|
attrs = Map.put(@valid_attrs, :user_submitted, true)
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
refute Ecto.Changeset.get_change(changeset, :user_submitted)
|
|
end
|
|
end
|
|
|
|
describe "notes field" do
|
|
test "accepts a notes string" do
|
|
attrs = Map.put(@valid_attrs, :notes, "Rare opening via sporadic-E along the TX/OK front")
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert changeset.valid?
|
|
assert Ecto.Changeset.get_change(changeset, :notes) =~ "sporadic-E"
|
|
end
|
|
|
|
test "trims whitespace and treats blank notes as nil" do
|
|
for blank <- ["", " ", "\n\t "] do
|
|
attrs = Map.put(@valid_attrs, :notes, blank)
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
assert changeset.valid?, "expected blank notes #{inspect(blank)} to validate"
|
|
refute Ecto.Changeset.get_change(changeset, :notes) in [blank]
|
|
end
|
|
end
|
|
|
|
test "rejects notes longer than 2000 characters" do
|
|
attrs = Map.put(@valid_attrs, :notes, String.duplicate("x", 2001))
|
|
changeset = Contact.submission_changeset(%Contact{}, attrs)
|
|
refute changeset.valid?
|
|
assert errors_on(changeset)[:notes]
|
|
end
|
|
end
|
|
|
|
describe "create_contact/1 deduplication" do
|
|
alias Microwaveprop.Radio
|
|
|
|
test "rejects duplicate and returns the existing contact" do
|
|
assert {:ok, original} = Radio.create_contact(@valid_attrs)
|
|
|
|
assert {:error, :duplicate, ^original} = Radio.create_contact(@valid_attrs)
|
|
end
|
|
|
|
test "allows same stations on a different band" do
|
|
assert {:ok, _} = Radio.create_contact(@valid_attrs)
|
|
assert {:ok, _} = Radio.create_contact(%{@valid_attrs | band: "2304"})
|
|
end
|
|
|
|
test "allows same stations with different grids" do
|
|
assert {:ok, _} = Radio.create_contact(@valid_attrs)
|
|
assert {:ok, _} = Radio.create_contact(%{@valid_attrs | grid1: "EM13"})
|
|
end
|
|
|
|
test "allows contact outside the 1-hour window" do
|
|
assert {:ok, _} = Radio.create_contact(@valid_attrs)
|
|
|
|
later = DateTime.add(@valid_attrs.qso_timestamp, 3601, :second)
|
|
assert {:ok, _} = Radio.create_contact(%{@valid_attrs | qso_timestamp: later})
|
|
end
|
|
|
|
test "detects duplicates regardless of station order" do
|
|
assert {:ok, original} = Radio.create_contact(@valid_attrs)
|
|
|
|
swapped = %{@valid_attrs | station1: "K5TR", grid1: "EM00", station2: "W5XD", grid2: "EM12"}
|
|
assert {:error, :duplicate, ^original} = Radio.create_contact(swapped)
|
|
end
|
|
end
|
|
end
|