aprs.me/test/aprsme/swoosh/resend_adapter_test.exs
Graham McIntire 1e32ce7051
Improve test coverage to 87% and point vendor/aprs submodule at codeberg
Coverage:
- Adds focused tests across 13 files covering previously-uncovered
  branches: Packet changeset normalisation (course, wind_direction,
  struct data_extended); Packets store_packet edge cases (nested
  position shapes, ssid handling, string-keyed raw_packet);
  DeviceIdentification HTTP success/404 via Req plug stub; ResendAdapter
  catch-all body, deliver_many, nil/binary formatters; ApiDocsLive
  packets with missing fields; AprsSymbol normalize helpers;
  BadPacketsLive refresh and postgres_notify; MobileChannel handle_info
  catch-all and postgres_packet path; PacketProcessor existing-marker
  movement detection; PacketReplay sanitize_value type clauses;
  InsertOptimizer GenServer lifecycle and negative-duration metric;
  PageController shutdown-handler integration; UrlParams delegated
  helpers.
- mix.exs: configure test_coverage with summary threshold of 87 and
  ignore_modules for test fixtures and macro-only template modules.

Bug fix:
- MapLive.Index handle_info: add a clause for the raw {:new_deployment,
  _} tuple. Phoenix.PubSub.broadcast delivers the raw payload to plain
  subscribers, but the existing handler only matched the %Broadcast{}
  fast-lane wrapper. DeploymentNotifier broadcasts via the raw path,
  which crashed the LiveView under test concurrency.

Submodule:
- Update .gitmodules vendor/aprs URL from
  https://github.com/aprsme/aprs.git to
  ssh://git@codeberg.org/gmcintire/aprs.git.
2026-05-05 15:18:28 -05:00

198 lines
6.2 KiB
Elixir

defmodule Aprsme.Swoosh.ResendAdapterTest do
use ExUnit.Case, async: true
alias Aprsme.Swoosh.ResendAdapter
alias Swoosh.Email
defp stub_config(plug_name) do
[api_key: "re_test_key", plug: {Req.Test, plug_name}]
end
defp new_email(opts \\ []) do
defaults = [
from: {"Sender Name", "sender@example.com"},
to: [{"Recipient", "recipient@example.com"}],
subject: "Test Subject",
html_body: "<p>Hello</p>",
text_body: "Hello"
]
Email.new(Keyword.merge(defaults, opts))
end
describe "deliver/2" do
test "posts to Resend API and returns id on success" do
Req.Test.stub(__MODULE__, fn conn ->
Req.Test.json(conn, %{"id" => "msg_abc123"})
end)
email = new_email()
assert {:ok, %{id: "msg_abc123"}} = ResendAdapter.deliver(email, stub_config(__MODULE__))
end
test "returns error on API error response" do
Req.Test.stub(__MODULE__, fn conn ->
conn
|> Plug.Conn.put_status(422)
|> Req.Test.json(%{"name" => "validation_error", "message" => "Invalid email"})
end)
email = new_email()
assert {:error, {"validation_error", "Invalid email"}} =
ResendAdapter.deliver(email, stub_config(__MODULE__))
end
test "returns error on network failure" do
Req.Test.stub(__MODULE__, fn conn ->
Req.Test.transport_error(conn, :econnrefused)
end)
email = new_email()
assert {:error, %Req.TransportError{reason: :econnrefused}} =
ResendAdapter.deliver(email, stub_config(__MODULE__))
end
test "formats named sender as 'Name <email>'" do
Req.Test.stub(__MODULE__, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
send(self(), {:request_body, decoded})
Req.Test.json(conn, %{"id" => "msg_1"})
end)
email = new_email(from: {"My App", "noreply@example.com"})
ResendAdapter.deliver(email, stub_config(__MODULE__))
assert_received {:request_body, body}
assert body["from"] == "My App <noreply@example.com>"
end
test "formats bare email sender without name prefix" do
Req.Test.stub(__MODULE__, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
send(self(), {:request_body, decoded})
Req.Test.json(conn, %{"id" => "msg_1"})
end)
email = new_email(from: {"", "noreply@example.com"})
ResendAdapter.deliver(email, stub_config(__MODULE__))
assert_received {:request_body, body}
assert body["from"] == "noreply@example.com"
end
test "formats list of recipients as list of email strings" do
Req.Test.stub(__MODULE__, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
send(self(), {:request_body, decoded})
Req.Test.json(conn, %{"id" => "msg_1"})
end)
email =
new_email(to: [{"Alice", "alice@example.com"}, {"Bob", "bob@example.com"}])
ResendAdapter.deliver(email, stub_config(__MODULE__))
assert_received {:request_body, body}
assert body["to"] == ["alice@example.com", "bob@example.com"]
end
test "omits nil fields from request body" do
Req.Test.stub(__MODULE__, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
decoded = Jason.decode!(body)
send(self(), {:request_body, decoded})
Req.Test.json(conn, %{"id" => "msg_1"})
end)
email = new_email()
ResendAdapter.deliver(email, stub_config(__MODULE__))
assert_received {:request_body, body}
refute Map.has_key?(body, "cc")
refute Map.has_key?(body, "bcc")
refute Map.has_key?(body, "reply_to")
end
end
describe "validate_config/1" do
test "returns :ok when api_key is present" do
assert :ok = ResendAdapter.validate_config(api_key: "re_abc")
end
test "returns error when api_key is missing" do
assert {:error, _reason} = ResendAdapter.validate_config([])
end
end
describe "deliver/2 catch-all body branch" do
test "returns the raw body as the error when shape is unrecognized" do
Req.Test.stub(__MODULE__, fn conn ->
conn
|> Plug.Conn.put_status(500)
|> Req.Test.json(%{"unexpected" => "shape"})
end)
email = new_email()
assert {:error, %{"unexpected" => "shape"}} = ResendAdapter.deliver(email, stub_config(__MODULE__))
end
end
describe "deliver_many/2" do
test "delivers each email and returns the collected ids" do
Req.Test.stub(__MODULE__, fn conn ->
Req.Test.json(conn, %{"id" => "msg_many"})
end)
emails = [new_email(), new_email(subject: "Second")]
assert {:ok, [%{id: "msg_many"}, %{id: "msg_many"}]} =
ResendAdapter.deliver_many(emails, stub_config(__MODULE__))
end
test "halts and returns the first error" do
Req.Test.stub(__MODULE__, fn conn ->
conn
|> Plug.Conn.put_status(422)
|> Req.Test.json(%{"name" => "validation_error", "message" => "boom"})
end)
emails = [new_email(), new_email()]
assert {:error, {"validation_error", "boom"}} =
ResendAdapter.deliver_many(emails, stub_config(__MODULE__))
end
end
describe "format_sender/1 and format_recipients/1 nil/binary branches" do
test "deliver/2 sends nil and binary fields through the formatters" do
Req.Test.stub(__MODULE__, fn conn ->
{:ok, body, conn} = Plug.Conn.read_body(conn)
send(self(), {:request_body, Jason.decode!(body)})
Req.Test.json(conn, %{"id" => "msg_xyz"})
end)
email =
Email.new(
from: "bare@example.com",
to: "single@example.com",
subject: "Subject",
html_body: "<p>Hi</p>",
text_body: "Hi"
)
assert {:ok, _} = ResendAdapter.deliver(email, api_key: "re_test_key", plug: {Req.Test, __MODULE__})
assert_received {:request_body, body}
assert body["from"] == "bare@example.com"
# Swoosh always wraps `to` in a list; format_recipients/1 maps over it,
# exercising the binary-recipient clause for each element.
assert body["to"] == ["single@example.com"]
end
end
end