rework email settings for true progressive enhancement
All checks were successful
deploy / deploy (push) Successful in 1m19s
All checks were successful
deploy / deploy (push) Successful in 1m19s
Render all adapter field sections in the form with CSS :has(:checked) controlling visibility. Selecting a provider instantly shows its config fields — no JS, no page reload, no server round-trip needed. - Render all 6 adapter configs with data-adapter attribute - CSS :has(:checked) show/hide rules per adapter in admin stylesheet - Namespace field names per adapter (email[brevo][api_key] etc) - Drop 4 transactional-only providers (Resend, Postmark, Mailgun, MailPace) - Remove noscript "Switch provider" button and controller redirect workaround - Remove configured_adapter hidden input tracking - Hide JS-only test email button for no-JS users via noscript style - LiveView progressively enhances with async save and test email Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -112,52 +112,6 @@ defmodule Berrypod.KeyValidationTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "validate_email_key/3 - Postmark" do
|
||||
test "accepts valid UUID" do
|
||||
key = "abc12345-abcd-1234-abcd-123456789abc"
|
||||
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "postmark", "api_key")
|
||||
end
|
||||
|
||||
test "accepts uppercase UUID" do
|
||||
key = "ABC12345-ABCD-1234-ABCD-123456789ABC"
|
||||
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "postmark", "api_key")
|
||||
end
|
||||
|
||||
test "rejects non-UUID" do
|
||||
assert {:error, msg} = KeyValidation.validate_email_key("not-a-uuid", "postmark", "api_key")
|
||||
assert msg =~ "UUID"
|
||||
end
|
||||
end
|
||||
|
||||
describe "validate_email_key/3 - Resend" do
|
||||
test "accepts valid Resend key" do
|
||||
assert {:ok, "re_abc123xyz"} =
|
||||
KeyValidation.validate_email_key("re_abc123xyz", "resend", "api_key")
|
||||
end
|
||||
|
||||
test "rejects key without re_ prefix" do
|
||||
assert {:error, msg} = KeyValidation.validate_email_key("not_resend", "resend", "api_key")
|
||||
assert msg =~ "re_"
|
||||
end
|
||||
end
|
||||
|
||||
describe "validate_email_key/3 - Mailgun" do
|
||||
test "accepts classic key- format" do
|
||||
key = "key-abcdef1234567890abcdef1234567890"
|
||||
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "mailgun", "api_key")
|
||||
end
|
||||
|
||||
test "accepts newer long key without prefix" do
|
||||
key = "abcdef1234567890abcdef1234567890abcdef1234"
|
||||
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "mailgun", "api_key")
|
||||
end
|
||||
|
||||
test "rejects short key without prefix" do
|
||||
assert {:error, msg} = KeyValidation.validate_email_key("short", "mailgun", "api_key")
|
||||
assert msg =~ "key-"
|
||||
end
|
||||
end
|
||||
|
||||
describe "validate_email_key/3 - Brevo" do
|
||||
test "accepts valid Brevo key" do
|
||||
key = "xkeysib-" <> String.duplicate("ab", 32)
|
||||
@@ -211,19 +165,6 @@ defmodule Berrypod.KeyValidationTest do
|
||||
end
|
||||
end
|
||||
|
||||
describe "validate_email_key/3 - MailPace" do
|
||||
test "accepts valid token" do
|
||||
key = "abc123def456ghi789"
|
||||
assert {:ok, ^key} = KeyValidation.validate_email_key(key, "mailpace", "api_key")
|
||||
end
|
||||
|
||||
test "rejects short token" do
|
||||
assert {:error, msg} = KeyValidation.validate_email_key("short", "mailpace", "api_key")
|
||||
assert msg =~ "too short"
|
||||
assert msg =~ "MailPace"
|
||||
end
|
||||
end
|
||||
|
||||
describe "validate_email_key/3 - cross-provider detection" do
|
||||
test "detects Brevo key pasted into SendGrid" do
|
||||
key = "xkeysib-abc123def456"
|
||||
@@ -237,25 +178,14 @@ defmodule Berrypod.KeyValidationTest do
|
||||
assert msg =~ "SendGrid key"
|
||||
end
|
||||
|
||||
test "detects Resend key pasted into Postmark" do
|
||||
key = "re_abc123xyz456"
|
||||
assert {:error, msg} = KeyValidation.validate_email_key(key, "postmark", "api_key")
|
||||
assert msg =~ "Resend key"
|
||||
end
|
||||
|
||||
test "detects MailerSend key pasted into Mailgun" do
|
||||
test "detects MailerSend key pasted into Brevo" do
|
||||
key = "mlsn.abc123"
|
||||
assert {:error, msg} = KeyValidation.validate_email_key(key, "mailgun", "api_key")
|
||||
assert {:error, msg} = KeyValidation.validate_email_key(key, "brevo", "api_key")
|
||||
assert msg =~ "MailerSend key"
|
||||
end
|
||||
end
|
||||
|
||||
describe "validate_email_key/3 - non-api_key fields" do
|
||||
test "accepts reasonable value for domain" do
|
||||
assert {:ok, "mg.example.com"} =
|
||||
KeyValidation.validate_email_key("mg.example.com", "mailgun", "domain")
|
||||
end
|
||||
|
||||
test "accepts reasonable value for relay" do
|
||||
assert {:ok, "smtp.example.com"} =
|
||||
KeyValidation.validate_email_key("smtp.example.com", "smtp", "relay")
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule Berrypod.Mailer.AdaptersTest do
|
||||
test "returns a list of adapters" do
|
||||
adapters = Adapters.all()
|
||||
assert is_list(adapters)
|
||||
assert length(adapters) >= 9
|
||||
assert length(adapters) >= 5
|
||||
end
|
||||
|
||||
test "each adapter has required keys" do
|
||||
@@ -43,10 +43,10 @@ defmodule Berrypod.Mailer.AdaptersTest do
|
||||
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")
|
||||
assert is_nil(Adapters.get("postmark"))
|
||||
assert is_nil(Adapters.get("mailpace"))
|
||||
end
|
||||
|
||||
test "returns nil for unknown key" do
|
||||
@@ -70,7 +70,6 @@ defmodule Berrypod.Mailer.AdaptersTest do
|
||||
test "returns unique keys from all adapters" do
|
||||
keys = Adapters.all_field_keys()
|
||||
assert is_list(keys)
|
||||
assert "email_postmark_api_key" in keys
|
||||
assert "email_smtp_relay" in keys
|
||||
assert length(keys) == length(Enum.uniq(keys))
|
||||
end
|
||||
@@ -78,7 +77,7 @@ defmodule Berrypod.Mailer.AdaptersTest do
|
||||
|
||||
describe "settings_key/2" do
|
||||
test "namespaces key with adapter" do
|
||||
assert Adapters.settings_key("postmark", "api_key") == "email_postmark_api_key"
|
||||
assert Adapters.settings_key("brevo", "api_key") == "email_brevo_api_key"
|
||||
assert Adapters.settings_key("smtp", "relay") == "email_smtp_relay"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -25,14 +25,14 @@ defmodule Berrypod.MailerTest do
|
||||
|
||||
describe "load_config/0" do
|
||||
test "loads adapter config from settings" do
|
||||
Settings.put_setting("email_adapter", "postmark")
|
||||
Settings.put_secret("email_postmark_api_key", "pm_test_key_123")
|
||||
Settings.put_setting("email_adapter", "brevo")
|
||||
Settings.put_secret("email_brevo_api_key", "xkeysib-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"
|
||||
assert config[:adapter] == Swoosh.Adapters.Brevo
|
||||
assert config[:api_key] == "xkeysib-test-key-123"
|
||||
end
|
||||
|
||||
test "loads SMTP config with multiple fields" do
|
||||
@@ -283,13 +283,13 @@ defmodule Berrypod.MailerTest do
|
||||
end
|
||||
|
||||
test "returns adapter key and config when configured from settings" do
|
||||
Settings.put_setting("email_adapter", "postmark")
|
||||
Settings.put_secret("email_postmark_api_key", "pm_test_key_123")
|
||||
Settings.put_setting("email_adapter", "brevo")
|
||||
Settings.put_secret("email_brevo_api_key", "xkeysib-test-key-123")
|
||||
|
||||
Mailer.load_config()
|
||||
|
||||
{adapter_key, config} = Mailer.current_config()
|
||||
assert adapter_key == "postmark"
|
||||
assert adapter_key == "brevo"
|
||||
assert config["api_key"] =~ "•••"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,8 +24,7 @@ defmodule BerrypodWeb.EmailSettingsControllerTest do
|
||||
post(conn, ~p"/admin/settings/email", %{
|
||||
email: %{
|
||||
adapter: "brevo",
|
||||
configured_adapter: "brevo",
|
||||
api_key: "xkeysib-abc123def456"
|
||||
brevo: %{api_key: "xkeysib-abc123def456"}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -34,19 +33,10 @@ defmodule BerrypodWeb.EmailSettingsControllerTest do
|
||||
assert Settings.get_setting("email_adapter") == "brevo"
|
||||
end
|
||||
|
||||
test "redirects with adapter param when adapter changed", %{conn: conn} do
|
||||
conn =
|
||||
post(conn, ~p"/admin/settings/email", %{
|
||||
email: %{adapter: "resend", configured_adapter: "brevo", api_key: ""}
|
||||
})
|
||||
|
||||
assert redirected_to(conn) == ~p"/admin/settings/email?adapter=resend"
|
||||
end
|
||||
|
||||
test "redirects with error on validation failure", %{conn: conn} do
|
||||
conn =
|
||||
post(conn, ~p"/admin/settings/email", %{
|
||||
email: %{adapter: "brevo", configured_adapter: "brevo", api_key: ""}
|
||||
email: %{adapter: "brevo", brevo: %{api_key: ""}}
|
||||
})
|
||||
|
||||
assert redirected_to(conn) =~ ~p"/admin/settings/email"
|
||||
@@ -56,8 +46,8 @@ defmodule BerrypodWeb.EmailSettingsControllerTest do
|
||||
|
||||
describe "POST /admin/settings/email/test" do
|
||||
test "sends test email and redirects", %{conn: conn} do
|
||||
Settings.put_setting("email_adapter", "postmark")
|
||||
Settings.put_secret("email_postmark_api_key", "pm_test_abc")
|
||||
Settings.put_setting("email_adapter", "brevo")
|
||||
Settings.put_secret("email_brevo_api_key", "xkeysib-test-abc")
|
||||
|
||||
conn = post(conn, ~p"/admin/settings/email/test")
|
||||
|
||||
|
||||
@@ -33,27 +33,17 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
# Provider names rendered as radio cards
|
||||
assert html =~ "Brevo"
|
||||
assert html =~ "Mailjet"
|
||||
assert html =~ "MailPace"
|
||||
assert html =~ "Postal"
|
||||
end
|
||||
|
||||
test "shows all three provider groups", %{conn: conn} do
|
||||
test "shows providers and advanced section", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
# All-email providers
|
||||
assert html =~ "Popular providers"
|
||||
assert html =~ "Brevo"
|
||||
assert html =~ "SendGrid"
|
||||
assert html =~ "Mailjet"
|
||||
assert html =~ "MailerSend"
|
||||
|
||||
# Transactional providers
|
||||
assert html =~ "Transactional only"
|
||||
assert html =~ "Resend"
|
||||
assert html =~ "Postmark"
|
||||
assert html =~ "Mailgun"
|
||||
assert html =~ "MailPace"
|
||||
|
||||
# Advanced in details
|
||||
assert html =~ "Already have your own email server?"
|
||||
assert html =~ "SMTP"
|
||||
@@ -64,10 +54,20 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
assert html =~ "needs an email provider"
|
||||
assert html =~ "300 emails/day free"
|
||||
end
|
||||
|
||||
test "selecting a provider shows its config fields", %{conn: conn} do
|
||||
test "renders all adapter field sections in the form", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
# All adapters have their config sections rendered (CSS controls visibility)
|
||||
assert html =~ ~s(data-adapter="brevo")
|
||||
assert html =~ ~s(data-adapter="sendgrid")
|
||||
assert html =~ ~s(data-adapter="mailjet")
|
||||
assert html =~ ~s(data-adapter="smtp")
|
||||
assert html =~ ~s(data-adapter="postal")
|
||||
end
|
||||
|
||||
test "selecting a provider updates via phx-change", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
# Select SMTP via form change (radio inputs fire phx-change)
|
||||
@@ -76,35 +76,9 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
|> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "smtp"}})
|
||||
|> render_change()
|
||||
|
||||
# SMTP fields are always rendered; check the step title changes
|
||||
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 Brevo which needs just an api_key
|
||||
html =
|
||||
view
|
||||
|> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "brevo"}})
|
||||
|> render_change()
|
||||
|
||||
assert html =~ "API key"
|
||||
assert html =~ "Brevo"
|
||||
end
|
||||
|
||||
test "selecting a transactional provider shows its config", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
html =
|
||||
view
|
||||
|> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "resend"}})
|
||||
|> render_change()
|
||||
|
||||
assert html =~ "API key"
|
||||
assert html =~ "Resend"
|
||||
end
|
||||
|
||||
test "saving config persists settings", %{conn: conn} do
|
||||
@@ -115,11 +89,11 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
|> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "brevo"}})
|
||||
|> render_change()
|
||||
|
||||
# Submit with an API key
|
||||
# Submit with namespaced fields: email[brevo][api_key]
|
||||
html =
|
||||
view
|
||||
|> form("form[phx-submit=\"save\"]", %{
|
||||
email: %{adapter: "brevo", api_key: "xkeysib-abc123def456"}
|
||||
email: %{adapter: "brevo", brevo: %{api_key: "xkeysib-abc123def456"}}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
@@ -138,7 +112,9 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
# Submit without API key
|
||||
html =
|
||||
view
|
||||
|> form("form[phx-submit=\"save\"]", %{email: %{adapter: "brevo", api_key: ""}})
|
||||
|> form("form[phx-submit=\"save\"]", %{
|
||||
email: %{adapter: "brevo", brevo: %{api_key: ""}}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
assert html =~ "API key is required"
|
||||
@@ -154,7 +130,7 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
html =
|
||||
view
|
||||
|> form("form[phx-submit=\"save\"]", %{
|
||||
email: %{adapter: "smtp", relay: "smtp.example.com", port: "abc"}
|
||||
email: %{adapter: "smtp", smtp: %{relay: "smtp.example.com", port: "abc"}}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
@@ -162,8 +138,8 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
end
|
||||
|
||||
test "shows test email section when configured", %{conn: conn} do
|
||||
Settings.put_setting("email_adapter", "postmark")
|
||||
Settings.put_secret("email_postmark_api_key", "pm_test_abc")
|
||||
Settings.put_setting("email_adapter", "brevo")
|
||||
Settings.put_secret("email_brevo_api_key", "xkeysib-test-abc")
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
@@ -182,8 +158,8 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
end
|
||||
|
||||
test "sending test email shows success and sets verified flag", %{conn: conn} do
|
||||
Settings.put_setting("email_adapter", "postmark")
|
||||
Settings.put_secret("email_postmark_api_key", "pm_test_abc")
|
||||
Settings.put_setting("email_adapter", "brevo")
|
||||
Settings.put_secret("email_brevo_api_key", "xkeysib-test-abc")
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
@@ -205,14 +181,14 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/settings/email")
|
||||
|
||||
# Switch to Brevo and save
|
||||
# Switch to Brevo and save (namespaced fields)
|
||||
view
|
||||
|> form("form[phx-change=\"form_change\"]", %{email: %{adapter: "brevo"}})
|
||||
|> render_change()
|
||||
|
||||
view
|
||||
|> form("form[phx-submit=\"save\"]", %{
|
||||
email: %{adapter: "brevo", api_key: "xkeysib-switch-test"}
|
||||
email: %{adapter: "brevo", brevo: %{api_key: "xkeysib-switch-test"}}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
@@ -236,7 +212,7 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
|
||||
view
|
||||
|> form("form[phx-submit=\"save\"]", %{
|
||||
email: %{adapter: "brevo", api_key: "xkeysib-def789ghi012"}
|
||||
email: %{adapter: "brevo", brevo: %{api_key: "xkeysib-def789ghi012"}}
|
||||
})
|
||||
|> render_submit()
|
||||
|
||||
@@ -262,13 +238,6 @@ defmodule BerrypodWeb.Admin.EmailSettingsTest do
|
||||
assert html =~ "API key"
|
||||
end
|
||||
|
||||
test "adapter query param preselects provider", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/settings/email?adapter=resend")
|
||||
|
||||
assert html =~ "Resend"
|
||||
assert html =~ "API key"
|
||||
end
|
||||
|
||||
test "from_checklist param shows checklist banner", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/settings/email?from=checklist")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user