defmodule Microwaveprop.Radio.ContactEditTest do use Microwaveprop.DataCase, async: true alias Microwaveprop.Accounts alias Microwaveprop.Radio alias Microwaveprop.Radio.Contact alias Microwaveprop.Radio.ContactEdit @contact_attrs %{ station1: "W5ISP", station2: "K5TR", qso_timestamp: ~U[2026-04-01 14:00:00Z], grid1: "EM13", grid2: "EL29", pos1: %{"lat" => 33.5, "lon" => -97.0}, pos2: %{"lat" => 29.5, "lon" => -98.5}, mode: "CW", band: Decimal.new("10000"), distance_km: Decimal.new("450") } @user_attrs %{ callsign: "W5TEST", name: "Test User", email: "test@example.com", password: "testpassword123" } @admin_attrs %{ callsign: "N5ADM", name: "Admin User", email: "admin@example.com", password: "adminpassword123" } defp create_contact(_) do {:ok, contact} = %Contact{} |> Contact.changeset(@contact_attrs) |> Repo.insert() %{contact: contact} end defp create_user(_) do {:ok, user} = Accounts.register_user(@user_attrs) %{user: user} end defp create_admin(_) do {:ok, admin} = Accounts.register_user(@admin_attrs) admin = admin |> Ecto.Changeset.change(is_admin: true) |> Repo.update!() %{admin: admin} end describe "ContactEdit.changeset/2" do setup [:create_contact, :create_user] test "valid with proper proposed changes", %{contact: contact, user: user} do changeset = ContactEdit.changeset(%ContactEdit{}, %{ contact_id: contact.id, user_id: user.id, proposed_changes: %{"grid1" => "EM13kk"} }) assert changeset.valid? end test "rejects empty proposed_changes", %{contact: contact, user: user} do changeset = ContactEdit.changeset(%ContactEdit{}, %{ contact_id: contact.id, user_id: user.id, proposed_changes: %{} }) refute changeset.valid? assert "must contain at least one change" in errors_on(changeset).proposed_changes end test "rejects invalid fields in proposed_changes", %{contact: contact, user: user} do changeset = ContactEdit.changeset(%ContactEdit{}, %{ contact_id: contact.id, user_id: user.id, proposed_changes: %{"distance_km" => 100} }) refute changeset.valid? assert Enum.any?(errors_on(changeset).proposed_changes, &String.contains?(&1, "invalid fields")) end test "validates callsign format", %{contact: contact, user: user} do changeset = ContactEdit.changeset(%ContactEdit{}, %{ contact_id: contact.id, user_id: user.id, proposed_changes: %{"station1" => "bad call!"} }) refute changeset.valid? end test "validates grid format", %{contact: contact, user: user} do changeset = ContactEdit.changeset(%ContactEdit{}, %{ contact_id: contact.id, user_id: user.id, proposed_changes: %{"grid1" => "ZZZZZZ"} }) refute changeset.valid? end test "validates band value", %{contact: contact, user: user} do changeset = ContactEdit.changeset(%ContactEdit{}, %{ contact_id: contact.id, user_id: user.id, proposed_changes: %{"band" => 99_999} }) refute changeset.valid? end test "validates mode value", %{contact: contact, user: user} do changeset = ContactEdit.changeset(%ContactEdit{}, %{ contact_id: contact.id, user_id: user.id, proposed_changes: %{"mode" => "INVALID"} }) refute changeset.valid? end test "requires contact_id, user_id, proposed_changes" do changeset = ContactEdit.changeset(%ContactEdit{}, %{}) assert %{ contact_id: ["can't be blank"], user_id: ["can't be blank"], proposed_changes: ["can't be blank"] } = errors_on(changeset) end end describe "Radio.create_contact_edit/3" do setup [:create_contact, :create_user] test "creates a pending edit with only changed fields", %{contact: contact, user: user} do proposed = %{"grid1" => "EM13kk", "station1" => "W5ISP"} assert {:ok, edit} = Radio.create_contact_edit(contact, user, proposed) assert edit.status == :pending assert edit.proposed_changes == %{"grid1" => "EM13KK"} assert edit.contact_id == contact.id assert edit.user_id == user.id end test "rejects when nothing actually changed", %{contact: contact, user: user} do proposed = %{"station1" => "W5ISP", "mode" => "CW"} assert {:error, changeset} = Radio.create_contact_edit(contact, user, proposed) assert "must contain at least one change" in errors_on(changeset).proposed_changes end end describe "Radio.list_pending_edits/0" do setup [:create_contact, :create_user] test "returns pending edits", %{contact: contact, user: user} do {:ok, _e1} = Radio.create_contact_edit(contact, user, %{"grid1" => "EM13kk"}) {:ok, _e2} = Radio.create_contact_edit(contact, user, %{"grid2" => "EL29ab"}) edits = Radio.list_pending_edits() assert length(edits) == 2 fields = Enum.flat_map(edits, &Map.keys(&1.proposed_changes)) assert "grid1" in fields assert "grid2" in fields end end describe "Radio.approve_edit/3" do setup [:create_contact, :create_user, :create_admin] test "applies changes to the contact and marks approved", %{ contact: contact, user: user, admin: admin } do {:ok, edit} = Radio.create_contact_edit(contact, user, %{"grid1" => "EM13kk"}) assert {:ok, approved_edit} = Radio.approve_edit(edit, admin, "Confirmed via LOTW") assert approved_edit.status == :approved assert approved_edit.admin_note == "Confirmed via LOTW" assert approved_edit.reviewed_by_id == admin.id assert approved_edit.reviewed_at updated_contact = Radio.get_contact!(contact.id) assert updated_contact.grid1 == "EM13KK" end test "re-enqueues enrichment when grid changes", %{ contact: contact, user: user, admin: admin } do {:ok, edit} = Radio.create_contact_edit(contact, user, %{"grid1" => "EM15ab"}) {:ok, _approved} = Radio.approve_edit(edit, admin, nil) updated = Radio.get_contact!(contact.id) assert updated.grid1 == "EM15AB" assert updated.pos1 != contact.pos1 end end describe "Radio.reject_edit/3" do setup [:create_contact, :create_user, :create_admin] test "marks edit as rejected without changing contact", %{ contact: contact, user: user, admin: admin } do {:ok, edit} = Radio.create_contact_edit(contact, user, %{"grid1" => "EM13kk"}) assert {:ok, rejected_edit} = Radio.reject_edit(edit, admin, "Original data correct") assert rejected_edit.status == :rejected assert rejected_edit.admin_note == "Original data correct" unchanged_contact = Radio.get_contact!(contact.id) assert unchanged_contact.grid1 == contact.grid1 end end describe "Radio.pending_edit_count/0" do setup [:create_contact, :create_user] test "counts only pending edits", %{contact: contact, user: user} do {:ok, _} = Radio.create_contact_edit(contact, user, %{"grid1" => "EM13kk"}) {:ok, _} = Radio.create_contact_edit(contact, user, %{"grid2" => "EL29ab"}) assert Radio.pending_edit_count() == 2 end end end