test: add tests for smtp mailer config

This commit is contained in:
Simon 2026-03-11 09:18:37 +01:00
parent f53a3ce3cc
commit c4135308e6
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
9 changed files with 440 additions and 0 deletions

View file

@ -0,0 +1,63 @@
defmodule Mv.Membership.SettingSmtpTest do
@moduledoc """
Unit tests for Setting resource SMTP attributes.
TDD: tests expect smtp_host, smtp_port, smtp_username, smtp_password, smtp_ssl
to be accepted on update and persisted. Password must not be exposed in plaintext
when reading settings (sensitive). Tests will fail until Setting has these attributes.
"""
use Mv.DataCase, async: false
alias Mv.Helpers.SystemActor
alias Mv.Membership
setup do
{:ok, settings} = Membership.get_settings()
# Save current SMTP values to restore in on_exit (when attributes exist)
saved = %{
smtp_host: Map.get(settings, :smtp_host),
smtp_port: Map.get(settings, :smtp_port),
smtp_username: Map.get(settings, :smtp_username),
smtp_ssl: Map.get(settings, :smtp_ssl)
}
on_exit(fn ->
{:ok, s} = Membership.get_settings()
attrs = Enum.reject(saved, fn {_k, v} -> is_nil(v) end) |> Map.new()
if attrs != %{}, do: Membership.update_settings(s, attrs)
end)
{:ok, settings: settings, saved: saved}
end
describe "SMTP attributes update and persistence" do
test "update_settings accepts smtp_host, smtp_port, smtp_username, smtp_ssl and persists", %{
settings: settings
} do
attrs = %{
smtp_host: "smtp.example.com",
smtp_port: 587,
smtp_username: "user",
smtp_ssl: "tls"
}
assert {:ok, updated} = Membership.update_settings(settings, attrs)
assert updated.smtp_host == "smtp.example.com"
assert updated.smtp_port == 587
assert updated.smtp_username == "user"
assert updated.smtp_ssl == "tls"
end
test "smtp_password can be set and is not exposed in plaintext when reading settings", %{
settings: settings
} do
secret = "sensitive-password-#{System.unique_integer([:positive])}"
assert {:ok, _} = Membership.update_settings(settings, %{smtp_password: secret})
{:ok, read_back} = Membership.get_settings()
# Sensitive: raw password must not be returned (e.g. nil or redacted)
refute read_back.smtp_password == secret,
"smtp_password must not be returned in plaintext when reading settings"
end
end
end

View file

@ -0,0 +1,129 @@
defmodule Mv.ConfigSmtpTest do
@moduledoc """
Unit tests for Mv.Config SMTP-related helpers.
ENV overrides Settings (same pattern as OIDC/Vereinfacht). Uses real ENV and
Settings; no mocking so we test the actual precedence. async: false because
we mutate ENV.
"""
use Mv.DataCase, async: false
describe "smtp_host/0" do
test "returns ENV value when SMTP_HOST is set" do
set_smtp_env("SMTP_HOST", "smtp.example.com")
assert Mv.Config.smtp_host() == "smtp.example.com"
after
clear_smtp_env()
end
test "returns nil when SMTP_HOST is not set and Settings have no smtp_host" do
clear_smtp_env()
assert Mv.Config.smtp_host() == nil
end
end
describe "smtp_port/0" do
test "returns parsed integer when SMTP_PORT ENV is set" do
set_smtp_env("SMTP_PORT", "587")
assert Mv.Config.smtp_port() == 587
after
clear_smtp_env()
end
test "returns nil or default when SMTP_PORT is not set" do
clear_smtp_env()
port = Mv.Config.smtp_port()
assert port == nil or (is_integer(port) and port in [25, 465, 587])
end
end
describe "smtp_configured?/0" do
test "returns true when smtp_host is present (from ENV or Settings)" do
set_smtp_env("SMTP_HOST", "smtp.example.com")
assert Mv.Config.smtp_configured?() == true
after
clear_smtp_env()
end
test "returns false when no SMTP host is set" do
clear_smtp_env()
refute Mv.Config.smtp_configured?()
end
end
describe "smtp_env_configured?/0" do
test "returns true when any SMTP ENV variable is set" do
set_smtp_env("SMTP_HOST", "smtp.example.com")
assert Mv.Config.smtp_env_configured?() == true
after
clear_smtp_env()
end
test "returns false when no SMTP ENV variables are set" do
clear_smtp_env()
refute Mv.Config.smtp_env_configured?()
end
end
describe "smtp_password/0 and SMTP_PASSWORD_FILE" do
test "returns value from SMTP_PASSWORD when set" do
set_smtp_env("SMTP_PASSWORD", "env-secret")
assert Mv.Config.smtp_password() == "env-secret"
after
clear_smtp_env()
end
test "returns content of file when SMTP_PASSWORD_FILE is set and SMTP_PASSWORD is not" do
clear_smtp_env()
path = Path.join(System.tmp_dir!(), "mv_smtp_test_#{System.unique_integer([:positive])}")
File.write!(path, "file-secret\n")
Process.put(:smtp_password_file_path, path)
set_smtp_env("SMTP_PASSWORD_FILE", path)
assert Mv.Config.smtp_password() == "file-secret"
after
clear_smtp_env()
if path = Process.get(:smtp_password_file_path), do: File.rm(path)
end
test "SMTP_PASSWORD overrides SMTP_PASSWORD_FILE when both are set" do
path = Path.join(System.tmp_dir!(), "mv_smtp_test_#{System.unique_integer([:positive])}")
File.write!(path, "file-secret")
Process.put(:smtp_password_file_path, path)
set_smtp_env("SMTP_PASSWORD_FILE", path)
set_smtp_env("SMTP_PASSWORD", "env-wins")
assert Mv.Config.smtp_password() == "env-wins"
after
clear_smtp_env()
if path = Process.get(:smtp_password_file_path), do: File.rm(path)
end
end
describe "smtp_*_env_set?/0" do
test "smtp_host_env_set? returns true when SMTP_HOST is set" do
set_smtp_env("SMTP_HOST", "x")
assert Mv.Config.smtp_host_env_set?() == true
after
clear_smtp_env()
end
test "smtp_password_env_set? returns true when SMTP_PASSWORD or SMTP_PASSWORD_FILE is set" do
set_smtp_env("SMTP_PASSWORD", "x")
assert Mv.Config.smtp_password_env_set?() == true
after
clear_smtp_env()
end
end
defp set_smtp_env(key, value) do
System.put_env(key, value)
end
defp clear_smtp_env do
System.delete_env("SMTP_HOST")
System.delete_env("SMTP_PORT")
System.delete_env("SMTP_USERNAME")
System.delete_env("SMTP_PASSWORD")
System.delete_env("SMTP_PASSWORD_FILE")
System.delete_env("SMTP_SSL")
end
end

46
test/mv/mailer_test.exs Normal file
View file

@ -0,0 +1,46 @@
defmodule Mv.MailerTest do
@moduledoc """
Unit tests for Mv.Mailer, in particular send_test_email/1.
Uses Swoosh.Adapters.Test (configured in test.exs); no real SMTP. Asserts
success/error contract and that one test email is sent on success.
"""
use Mv.DataCase, async: true
import Swoosh.TestAssertions
alias Mv.Mailer
describe "send_test_email/1" do
test "returns {:ok, email} and sends one email with expected subject/body when successful" do
to_email = "test-#{System.unique_integer([:positive])}@example.com"
assert {:ok, _email} = Mailer.send_test_email(to_email)
assert_email_sent(fn email ->
to_addresses = Enum.map(email.to, &elem(&1, 1))
subject = email.subject || ""
body = email.html_body || email.text_body || ""
to_email in to_addresses and
(String.contains?(subject, "Test") or String.contains?(body, "test"))
end)
end
test "returns {:error, reason} for invalid email address" do
result = Mailer.send_test_email("not-an-email")
assert {:error, _reason} = result
end
test "uses mail_from as sender" do
to_email = "recipient-#{System.unique_integer([:positive])}@example.com"
assert {:ok, _} = Mailer.send_test_email(to_email)
assert_email_sent(fn email ->
{_name, from_email} = Mailer.mail_from()
from_addresses = Enum.map(email.from, &elem(&1, 1))
from_email in from_addresses
end)
end
end
end

View file

@ -65,4 +65,52 @@ defmodule MvWeb.GlobalSettingsLiveTest do
assert html =~ "must be present"
end
end
describe "SMTP / E-Mail section" do
setup %{conn: conn} do
user = create_test_user(%{email: "admin@example.com"})
conn = conn_with_oidc_user(conn, user)
{:ok, conn: conn, user: user}
end
test "renders SMTP section with host/port fields and test email area", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/settings")
# Section title (Gettext key: SMTP or E-Mail per concept)
assert html =~ "SMTP" or html =~ "E-Mail"
end
test "shows Send test email button when SMTP is configured", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
# When Mv.Config.smtp_configured?() is true, button and recipient input should be present
# In test env SMTP is typically not configured; we only assert the section exists
html = render(view)
assert html =~ "SMTP" or html =~ "E-Mail"
end
test "send test email with valid address shows success or error result", %{conn: conn} do
{:ok, view, _html} = live(conn, ~p"/settings")
# If test email UI exists: fill recipient, click button, assert result area updates
# Uses data-testid or button text "Send test email" / "Test email"
if has_element?(view, "[data-testid='smtp-test-email-form']") do
view
|> element("[data-testid='smtp-test-email-input']")
|> render_change(%{"to_email" => "test@example.com"})
view
|> element("[data-testid='smtp-send-test-email']")
|> render_click()
# Result is either success or error message
assert has_element?(view, "[data-testid='smtp-test-result']")
else
# Section not yet implemented: just ensure page still renders
assert render(view) =~ "Settings"
end
end
test "shows warning when SMTP is not configured in production", %{conn: conn} do
# Concept: in prod, show warning "SMTP is not configured. Transactional emails..."
# In test we only check that the section exists; warning visibility is env-dependent
{:ok, view, html} = live(conn, ~p"/settings")
assert html =~ "SMTP" or html =~ "E-Mail" or html =~ "Settings"
end
end
end

View file

@ -39,6 +39,8 @@ defmodule MvWeb.JoinLiveTest do
test "submit with valid allowlist data creates one JoinRequest and shows success copy", %{
conn: conn
} do
# Re-apply allowlist so this test is robust when run in parallel with others (Settings singleton).
enable_join_form_for_test(%{})
count_before = count_join_requests()
{:ok, view, _html} = live(conn, "/join")