add admin email settings page with provider selection
All checks were successful
deploy / deploy (push) Successful in 56s

Card radio component for picking email providers (SMTP, SendGrid, Mailjet, etc.)
with instant client-side switching via JS hook. Adapter configs are pre-rendered
and toggled without a server round-trip. Secrets are preserved when re-saving
with blank password fields. Includes from address field, test email sending,
and disconnect flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-02-21 19:29:34 +00:00
parent a2e46664c6
commit 366a1e6a48
17 changed files with 1176 additions and 39 deletions

View File

@@ -0,0 +1,78 @@
defmodule Berrypod.Mailer.AdaptersTest do
use ExUnit.Case, async: true
alias Berrypod.Mailer.Adapters
describe "all/0" do
test "returns a list of adapters" do
adapters = Adapters.all()
assert is_list(adapters)
assert length(adapters) >= 9
end
test "each adapter has required keys" do
for adapter <- Adapters.all() do
assert is_binary(adapter.key)
assert is_binary(adapter.name)
assert is_atom(adapter.module)
assert is_binary(adapter.description)
assert is_list(adapter.fields)
assert length(adapter.fields) >= 1
end
end
test "each adapter has a url (or nil for SMTP)" do
for adapter <- Adapters.all() do
if adapter.key == "smtp" do
assert is_nil(adapter.url)
else
assert is_binary(adapter.url)
end
end
end
test "each field has required keys" do
for adapter <- Adapters.all(), field <- adapter.fields do
assert is_binary(field.key)
assert is_binary(field.label)
assert field.type in [:string, :integer, :secret]
end
end
end
describe "get/1" do
test "returns adapter by key" do
assert %{key: "smtp", name: "SMTP"} = Adapters.get("smtp")
assert %{key: "postmark", name: "Postmark"} = Adapters.get("postmark")
assert %{key: "mailjet", name: "Mailjet"} = Adapters.get("mailjet")
assert %{key: "mailpace", name: "MailPace"} = Adapters.get("mailpace")
assert %{key: "postal", name: "Postal"} = Adapters.get("postal")
end
test "returns nil for unknown key" do
assert is_nil(Adapters.get("unknown"))
end
end
describe "field_keys/1" do
test "returns settings keys prefixed with email_" do
smtp = Adapters.get("smtp")
keys = Adapters.field_keys(smtp)
assert "email_relay" in keys
assert "email_port" in keys
assert "email_username" in keys
assert "email_password" in keys
end
end
describe "all_field_keys/0" do
test "returns unique keys from all adapters" do
keys = Adapters.all_field_keys()
assert is_list(keys)
assert "email_api_key" in keys
assert "email_relay" in keys
assert length(keys) == length(Enum.uniq(keys))
end
end
end

View File

@@ -0,0 +1,79 @@
defmodule Berrypod.MailerTest do
use Berrypod.DataCase, async: false
alias Berrypod.Mailer
alias Berrypod.Settings
setup do
# Store original config to restore after each test
original = Application.get_env(:berrypod, Mailer)
on_exit(fn -> Application.put_env(:berrypod, Mailer, original) end)
:ok
end
describe "email_configured?/0" do
test "returns false with Local adapter" do
Application.put_env(:berrypod, Mailer, adapter: Swoosh.Adapters.Local)
refute Mailer.email_configured?()
end
test "returns true with a real adapter" do
Application.put_env(:berrypod, Mailer, adapter: Swoosh.Adapters.Postmark, api_key: "test")
assert Mailer.email_configured?()
end
end
describe "load_config/0" do
test "loads adapter config from settings" do
Settings.put_setting("email_adapter", "postmark")
Settings.put_secret("email_api_key", "pm_test_key_123")
Mailer.load_config()
config = Application.get_env(:berrypod, Mailer)
assert config[:adapter] == Swoosh.Adapters.Postmark
assert config[:api_key] == "pm_test_key_123"
end
test "loads SMTP config with multiple fields" do
Settings.put_setting("email_adapter", "smtp")
Settings.put_setting("email_relay", "smtp.example.com")
Settings.put_setting("email_port", 465, "integer")
Settings.put_setting("email_username", "user@example.com")
Settings.put_secret("email_password", "secret123")
Mailer.load_config()
config = Application.get_env(:berrypod, Mailer)
assert config[:adapter] == Swoosh.Adapters.SMTP
assert config[:relay] == "smtp.example.com"
assert config[:port] == 465
assert config[:username] == "user@example.com"
assert config[:password] == "secret123"
end
test "is a no-op when no email_adapter is set" do
original = Application.get_env(:berrypod, Mailer)
Mailer.load_config()
assert Application.get_env(:berrypod, Mailer) == original
end
end
describe "current_config/0" do
test "returns {nil, %{}} when no adapter configured" do
Application.put_env(:berrypod, Mailer, adapter: Swoosh.Adapters.Local)
assert {nil, %{}} = Mailer.current_config()
end
test "returns adapter key and config when configured from settings" do
Settings.put_setting("email_adapter", "postmark")
Settings.put_secret("email_api_key", "pm_test_key_123")
Mailer.load_config()
{adapter_key, config} = Mailer.current_config()
assert adapter_key == "postmark"
assert config["api_key"] =~ "•••"
end
end
end