diff --git a/test/microwaveprop/radio/edit_notifier_test.exs b/test/microwaveprop/radio/edit_notifier_test.exs new file mode 100644 index 00000000..8f3c5a31 --- /dev/null +++ b/test/microwaveprop/radio/edit_notifier_test.exs @@ -0,0 +1,241 @@ +defmodule Microwaveprop.Radio.EditNotifierTest do + @moduledoc """ + Characterization tests for the edit-review email notifier. + + These pin down the current behavior of `deliver_edit_approved/1` and + `deliver_edit_rejected/1` so future refactors get a compile-in safety net. + """ + use Microwaveprop.DataCase, async: true + + import Swoosh.TestAssertions + + alias Microwaveprop.Accounts + alias Microwaveprop.Radio.Contact + alias Microwaveprop.Radio.ContactEdit + alias Microwaveprop.Radio.EditNotifier + + @contact_attrs %{ + station1: "W5ISP", + station2: "K5TR", + qso_timestamp: ~U[2026-04-01 14:30: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" + } + + defp insert_contact(attrs \\ %{}) do + {:ok, contact} = + %Contact{} + |> Contact.changeset(Map.merge(@contact_attrs, attrs)) + |> Repo.insert() + + contact + end + + defp insert_user(attrs \\ %{}) do + {:ok, user} = Accounts.register_user(Map.merge(@user_attrs, attrs)) + user + end + + defp insert_edit(contact, user, attrs \\ %{}) do + defaults = %{ + contact_id: contact.id, + user_id: user.id, + proposed_changes: %{"grid1" => "EM13kk"} + } + + {:ok, edit} = + %ContactEdit{} + |> ContactEdit.changeset(Map.merge(defaults, attrs)) + |> Repo.insert() + + # apply anything the review_changeset path wouldn't (admin_note, etc.) + extras = + attrs + |> Map.drop([:contact_id, :user_id, :proposed_changes]) + |> Map.to_list() + + if extras == [] do + edit + else + edit |> Ecto.Changeset.change(extras) |> Repo.update!() + end + end + + describe "deliver_edit_approved/1" do + test "sends a single email to the submitter from the configured defaults" do + contact = insert_contact() + user = insert_user() + edit = insert_edit(contact, user, %{admin_note: "Looks right, thanks"}) + + assert {:ok, %Swoosh.Email{}} = EditNotifier.deliver_edit_approved(edit) + + assert_email_sent(fn email -> + assert email.to == [{"", "test@example.com"}] + assert email.from == {"NTMS Propagation", "prop@w5isp.com"} + assert email.reply_to == {"", "graham@mcintire.me"} + assert email.subject == "Your contact edit was approved" + end) + end + + test "body addresses the submitter by callsign and names APPROVED" do + contact = insert_contact() + user = insert_user(%{callsign: "K5ABC", email: "k5abc@example.com"}) + edit = insert_edit(contact, user) + + {:ok, email} = EditNotifier.deliver_edit_approved(edit) + + assert email.text_body =~ "Hi K5ABC," + assert email.text_body =~ "APPROVED" + refute email.text_body =~ "REJECTED" + end + + test "body lists the contact endpoints, band, and qso_timestamp" do + contact = insert_contact() + user = insert_user() + edit = insert_edit(contact, user) + + {:ok, email} = EditNotifier.deliver_edit_approved(edit) + + assert email.text_body =~ "W5ISP <-> K5TR" + assert email.text_body =~ "10 GHz" + assert email.text_body =~ "2026-04-01 14:30 UTC" + end + + test "formats sub-GHz bands in MHz" do + contact = insert_contact(%{band: Decimal.new("432")}) + user = insert_user() + edit = insert_edit(contact, user) + + {:ok, email} = EditNotifier.deliver_edit_approved(edit) + + assert email.text_body =~ "432 MHz" + refute email.text_body =~ "GHz" + end + + test "body lists each proposed change on its own line" do + contact = insert_contact() + user = insert_user() + + edit = + insert_edit(contact, user, %{ + proposed_changes: %{"grid1" => "EM13kk", "mode" => "SSB"} + }) + + {:ok, email} = EditNotifier.deliver_edit_approved(edit) + + assert email.text_body =~ " - grid1: EM13kk" + assert email.text_body =~ " - mode: SSB" + end + + test "includes an admin note when present" do + contact = insert_contact() + user = insert_user() + edit = insert_edit(contact, user, %{admin_note: "Verified against QRZ"}) + + {:ok, email} = EditNotifier.deliver_edit_approved(edit) + + assert email.text_body =~ "Admin note: Verified against QRZ" + end + + test "omits the admin-note line when note is nil" do + contact = insert_contact() + user = insert_user() + edit = insert_edit(contact, user, %{admin_note: nil}) + + {:ok, email} = EditNotifier.deliver_edit_approved(edit) + + refute email.text_body =~ "Admin note:" + end + + test "omits the admin-note line when note is empty string" do + contact = insert_contact() + user = insert_user() + edit = insert_edit(contact, user, %{admin_note: ""}) + + {:ok, email} = EditNotifier.deliver_edit_approved(edit) + + refute email.text_body =~ "Admin note:" + end + + test "includes a contact view link built from the endpoint url" do + contact = insert_contact() + user = insert_user() + edit = insert_edit(contact, user) + + {:ok, email} = EditNotifier.deliver_edit_approved(edit) + + assert email.text_body =~ "#{MicrowavepropWeb.Endpoint.url()}/contacts/#{contact.id}" + end + + test "preloads associations if the caller passes a bare record" do + contact = insert_contact() + user = insert_user() + edit = insert_edit(contact, user) + + # Pass the schema back after re-fetch without preloads — the notifier + # must tolerate that and still resolve user.callsign / contact.station1. + bare = Repo.get!(ContactEdit, edit.id) + + assert {:ok, email} = EditNotifier.deliver_edit_approved(bare) + assert email.text_body =~ "Hi W5TEST," + assert email.text_body =~ "W5ISP <-> K5TR" + end + end + + describe "deliver_edit_rejected/1" do + test "sends an email with the rejected subject to the submitter" do + contact = insert_contact() + user = insert_user() + edit = insert_edit(contact, user) + + assert {:ok, %Swoosh.Email{}} = EditNotifier.deliver_edit_rejected(edit) + + assert_email_sent(fn email -> + assert email.to == [{"", "test@example.com"}] + assert email.from == {"NTMS Propagation", "prop@w5isp.com"} + assert email.reply_to == {"", "graham@mcintire.me"} + assert email.subject == "Your contact edit was rejected" + end) + end + + test "body says REJECTED and labels the changes as 'Proposed changes:'" do + contact = insert_contact() + user = insert_user() + + edit = + insert_edit(contact, user, %{ + proposed_changes: %{"station2" => "W5XYZ"}, + admin_note: "Mismatch with log" + }) + + {:ok, email} = EditNotifier.deliver_edit_rejected(edit) + + assert email.text_body =~ "REJECTED" + refute email.text_body =~ "APPROVED" + assert email.text_body =~ "Proposed changes:\n - station2: W5XYZ" + assert email.text_body =~ "Admin note: Mismatch with log" + end + + test "includes the contact view link for rejected edits too" do + contact = insert_contact() + user = insert_user() + edit = insert_edit(contact, user) + + {:ok, email} = EditNotifier.deliver_edit_rejected(edit) + + assert email.text_body =~ "#{MicrowavepropWeb.Endpoint.url()}/contacts/#{contact.id}" + end + end +end