Registered users can suggest edits to any contact's core fields (callsigns, grids, band, mode, timestamp). Edits enter an admin approval queue with field-by-field diff view. On approve, changes are applied and enrichment re-enqueued if grids/band changed. Users receive email notification on approve or reject. Also updates dependabot.yml for mix ecosystem.
248 lines
7.4 KiB
Elixir
248 lines
7.4 KiB
Elixir
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
|