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