add admin email settings page with provider selection
All checks were successful
deploy / deploy (push) Successful in 56s
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:
78
test/berrypod/mailer/adapters_test.exs
Normal file
78
test/berrypod/mailer/adapters_test.exs
Normal 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
|
||||
79
test/berrypod/mailer_test.exs
Normal file
79
test/berrypod/mailer_test.exs
Normal 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
|
||||
154
test/berrypod_web/live/admin/email_settings_test.exs
Normal file
154
test/berrypod_web/live/admin/email_settings_test.exs
Normal file
@@ -0,0 +1,154 @@
|
||||
defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
use BerrypodWeb.ConnCase, async: false
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Berrypod.AccountsFixtures
|
||||
|
||||
alias Berrypod.Settings
|
||||
|
||||
setup do
|
||||
# Ensure mailer starts as test adapter and restore on exit
|
||||
original = Application.get_env(:berrypod, Berrypod.Mailer)
|
||||
Application.put_env(:berrypod, Berrypod.Mailer, adapter: Swoosh.Adapters.Test)
|
||||
|
||||
on_exit(fn ->
|
||||
Application.put_env(:berrypod, Berrypod.Mailer, original)
|
||||
end)
|
||||
|
||||
user = user_fixture()
|
||||
%{user: user}
|
||||
end
|
||||
|
||||
describe "authenticated" do
|
||||
setup %{conn: conn, user: user} do
|
||||
conn = log_in_user(conn, user)
|
||||
%{conn: conn}
|
||||
end
|
||||
|
||||
test "renders email settings page with provider cards", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
assert html =~ "Email settings"
|
||||
assert html =~ "Email provider"
|
||||
# Provider names rendered as radio cards
|
||||
assert html =~ "Postmark"
|
||||
assert html =~ "Brevo"
|
||||
assert html =~ "Mailjet"
|
||||
assert html =~ "MailPace"
|
||||
assert html =~ "Postal"
|
||||
end
|
||||
|
||||
test "shows provider descriptions", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
assert html =~ "Excellent deliverability tracking"
|
||||
assert html =~ "All-in-one platform, GDPR-friendly"
|
||||
assert html =~ "EU data processing"
|
||||
end
|
||||
|
||||
test "selecting a provider shows its config fields", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
# Select SMTP via form change (radio inputs fire phx-change)
|
||||
html =
|
||||
view
|
||||
|> form("form[phx-change=\"change_adapter\"]", %{email: %{adapter: "smtp"}})
|
||||
|> render_change()
|
||||
|
||||
assert html =~ "Server host"
|
||||
assert html =~ "Port"
|
||||
assert html =~ "Username"
|
||||
assert html =~ "Password"
|
||||
end
|
||||
|
||||
test "selecting a different provider shows different fields", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
# Select Mailgun which needs api_key + domain
|
||||
html =
|
||||
view
|
||||
|> form("form[phx-change=\"change_adapter\"]", %{email: %{adapter: "mailgun"}})
|
||||
|> render_change()
|
||||
|
||||
assert html =~ "API key"
|
||||
assert html =~ "Domain"
|
||||
end
|
||||
|
||||
test "saving config persists settings", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
# Select Postmark via form change
|
||||
view
|
||||
|> form("form[phx-change=\"change_adapter\"]", %{email: %{adapter: "postmark"}})
|
||||
|> render_change()
|
||||
|
||||
# Submit with an API key
|
||||
html =
|
||||
view
|
||||
|> form("form[phx-submit=\"save\"]", %{
|
||||
email: %{adapter: "postmark", api_key: "pm_test_123"}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "Email settings saved"
|
||||
assert Settings.get_setting("email_adapter") == "postmark"
|
||||
end
|
||||
|
||||
test "saving without required fields shows error", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
# Select Postmark
|
||||
view
|
||||
|> form("form[phx-change=\"change_adapter\"]", %{email: %{adapter: "postmark"}})
|
||||
|> render_change()
|
||||
|
||||
# Submit without API key
|
||||
html =
|
||||
view
|
||||
|> form("form[phx-submit=\"save\"]", %{email: %{adapter: "postmark", api_key: ""}})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "Missing required fields"
|
||||
end
|
||||
|
||||
test "disconnect clears email configuration", %{conn: conn} do
|
||||
Settings.put_setting("email_adapter", "postmark")
|
||||
Settings.put_secret("email_api_key", "pm_test_abc")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
html = render_click(view, "disconnect")
|
||||
|
||||
assert html =~ "Email provider disconnected"
|
||||
assert is_nil(Settings.get_setting("email_adapter"))
|
||||
end
|
||||
|
||||
test "shows test email section when configured", %{conn: conn} do
|
||||
Settings.put_setting("email_adapter", "postmark")
|
||||
Settings.put_secret("email_api_key", "pm_test_abc")
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
assert html =~ "Test email"
|
||||
assert html =~ "Send test email"
|
||||
end
|
||||
|
||||
test "hides test email section when not configured", %{conn: conn} do
|
||||
# Ensure clean state — no adapter configured
|
||||
Settings.delete_setting("email_adapter")
|
||||
Application.put_env(:berrypod, Berrypod.Mailer, adapter: Swoosh.Adapters.Local)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
refute html =~ "Send test email"
|
||||
end
|
||||
end
|
||||
|
||||
describe "unauthenticated" do
|
||||
test "redirects to login", %{conn: conn} do
|
||||
{:error, redirect} = live(conn, ~p"/admin/settings/email")
|
||||
assert {:redirect, %{to: path}} = redirect
|
||||
assert path == ~p"/users/log-in"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -103,8 +103,8 @@ defmodule BerrypodWeb.Setup.OnboardingTest do
|
||||
|
||||
html =
|
||||
view
|
||||
|> element(~s(button[phx-value-type="printify"]))
|
||||
|> render_click()
|
||||
|> form(~s(form[phx-change="select_provider"]), %{provider_select: %{type: "printify"}})
|
||||
|> render_change()
|
||||
|
||||
assert html =~ "API token"
|
||||
assert html =~ "Printify"
|
||||
|
||||
Reference in New Issue
Block a user