Fix TLS config #473
11 changed files with 298 additions and 43 deletions
12
.env.example
12
.env.example
|
|
@ -41,3 +41,15 @@ ASSOCIATION_NAME="Sportsclub XYZ"
|
||||||
# VEREINFACHT_API_KEY=your-api-key
|
# VEREINFACHT_API_KEY=your-api-key
|
||||||
# VEREINFACHT_CLUB_ID=2
|
# VEREINFACHT_CLUB_ID=2
|
||||||
# VEREINFACHT_APP_URL=https://app.verein.visuel.dev
|
# VEREINFACHT_APP_URL=https://app.verein.visuel.dev
|
||||||
|
|
||||||
|
# Optional: Mail / SMTP (transactional emails). If set, overrides Settings UI.
|
||||||
|
# Export current UI settings to .env: mix mv.export_smtp_to_env
|
||||||
|
# SMTP_HOST=smtp.example.com
|
||||||
|
# SMTP_PORT=587
|
||||||
|
# SMTP_USERNAME=user
|
||||||
|
# SMTP_PASSWORD=secret
|
||||||
|
# SMTP_PASSWORD_FILE=/run/secrets/smtp_password
|
||||||
|
# SMTP_SSL=tls
|
||||||
|
# SMTP_VERIFY_PEER=false
|
||||||
|
# MAIL_FROM_EMAIL=noreply@example.com
|
||||||
|
# MAIL_FROM_NAME=Mila
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- **SMTP configuration** – Repaired so that both port 587 (TLS/STARTTLS) and 465 (SSL) work correctly.
|
||||||
|
|
||||||
## [1.1.0] - 2026-03-13
|
## [1.1.0] - 2026-03-13
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -130,6 +130,8 @@ lib/
|
||||||
│ ├── constants.ex # Application constants (member_fields, custom_field_prefix, vereinfacht_required_member_fields)
|
│ ├── constants.ex # Application constants (member_fields, custom_field_prefix, vereinfacht_required_member_fields)
|
||||||
│ ├── application.ex # OTP application
|
│ ├── application.ex # OTP application
|
||||||
│ ├── mailer.ex # Email mailer
|
│ ├── mailer.ex # Email mailer
|
||||||
|
│ ├── smtp/
|
||||||
|
│ │ └── config_builder.ex # SMTP adapter opts (TLS/sockopts); used by runtime.exs and Mailer
|
||||||
│ ├── release.ex # Release tasks
|
│ ├── release.ex # Release tasks
|
||||||
│ ├── repo.ex # Database repository
|
│ ├── repo.ex # Database repository
|
||||||
│ ├── secrets.ex # Secret management
|
│ ├── secrets.ex # Secret management
|
||||||
|
|
|
||||||
|
|
@ -226,11 +226,7 @@ if config_env() == :prod do
|
||||||
# SMTP configuration from environment variables (overrides base adapter in prod).
|
# SMTP configuration from environment variables (overrides base adapter in prod).
|
||||||
# When SMTP_HOST is set, configure Swoosh to use the SMTP adapter at boot time.
|
# When SMTP_HOST is set, configure Swoosh to use the SMTP adapter at boot time.
|
||||||
# If SMTP is configured only via Settings (Admin UI), the mailer builds the config
|
# If SMTP is configured only via Settings (Admin UI), the mailer builds the config
|
||||||
# per-send at runtime using Mv.Config.smtp_*() helpers.
|
# per-send at runtime using Mv.Mailer.smtp_config/0 (which uses the same Mv.Smtp.ConfigBuilder).
|
||||||
#
|
|
||||||
# TLS/SSL options (tls_options, sockopts) are duplicated here and in Mv.Mailer.smtp_config/0
|
|
||||||
# because boot config must be set in this file; the Mailer uses the same logic for
|
|
||||||
# Settings-only config. Keep verify behaviour in sync (see SMTP_VERIFY_PEER below).
|
|
||||||
smtp_host_env = System.get_env("SMTP_HOST")
|
smtp_host_env = System.get_env("SMTP_HOST")
|
||||||
|
|
||||||
if smtp_host_env && String.trim(smtp_host_env) != "" do
|
if smtp_host_env && String.trim(smtp_host_env) != "" do
|
||||||
|
|
@ -264,20 +260,14 @@ if config_env() == :prod do
|
||||||
verify_mode = if smtp_verify_peer, do: :verify_peer, else: :verify_none
|
verify_mode = if smtp_verify_peer, do: :verify_peer, else: :verify_none
|
||||||
|
|
||||||
smtp_opts =
|
smtp_opts =
|
||||||
[
|
Mv.Smtp.ConfigBuilder.build_opts(
|
||||||
adapter: Swoosh.Adapters.SMTP,
|
host: String.trim(smtp_host_env),
|
||||||
relay: String.trim(smtp_host_env),
|
|
||||||
port: smtp_port_env,
|
port: smtp_port_env,
|
||||||
username: System.get_env("SMTP_USERNAME"),
|
username: System.get_env("SMTP_USERNAME"),
|
||||||
password: smtp_password_env,
|
password: smtp_password_env,
|
||||||
ssl: smtp_ssl_mode == "ssl",
|
ssl_mode: smtp_ssl_mode,
|
||||||
tls: if(smtp_ssl_mode == "tls", do: :always, else: :never),
|
verify_mode: verify_mode
|
||||||
auth: :always,
|
)
|
||||||
# tls_options: STARTTLS (587); sockopts: direct SSL (465).
|
|
||||||
tls_options: [verify: verify_mode],
|
|
||||||
sockopts: [verify: verify_mode]
|
|
||||||
]
|
|
||||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
|
||||||
|
|
||||||
config :mv, Mv.Mailer, smtp_opts
|
config :mv, Mv.Mailer, smtp_opts
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ By default, TLS certificate verification is relaxed (`verify_none`) so self-sign
|
||||||
- **ENV (prod):** Set `SMTP_VERIFY_PEER=true` (or `1`/`yes`) when configuring SMTP via environment variables in `config/runtime.exs`. This sets `config :mv, :smtp_verify_peer` and is used for both boot-time and per-send config.
|
- **ENV (prod):** Set `SMTP_VERIFY_PEER=true` (or `1`/`yes`) when configuring SMTP via environment variables in `config/runtime.exs`. This sets `config :mv, :smtp_verify_peer` and is used for both boot-time and per-send config.
|
||||||
- **Default:** `false` (verify_none) for backward compatibility and internal/self-signed certs.
|
- **Default:** `false` (verify_none) for backward compatibility and internal/self-signed certs.
|
||||||
|
|
||||||
Both `tls_options` (STARTTLS, port 587) and `sockopts` (direct SSL, port 465) use the same verify mode. The logic is duplicated in `config/runtime.exs` (boot) and `Mv.Mailer.smtp_config/0` (Settings-only); keep in sync.
|
Verify mode is set in `tls_options` for port 587 (STARTTLS). For port 465 (implicit SSL), the initial connection is `ssl:connect`, so we also pass `sockopts: [verify: verify_mode]` so the SSL handshake uses the same mode. For 587 we must not pass `verify` in sockopts—gen_tcp is used first and rejects it (ArgumentError). The logic lives in `Mv.Smtp.ConfigBuilder.build_opts/1` (single source of truth), used by `config/runtime.exs` (boot) and `Mv.Mailer.smtp_config/0` (Settings-only).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
@ -117,7 +117,7 @@ Both `tls_options` (STARTTLS, port 587) and `sockopts` (direct SSL, port 465) us
|
||||||
- [x] Password from file: `SMTP_PASSWORD_FILE` supported in `runtime.exs`.
|
- [x] Password from file: `SMTP_PASSWORD_FILE` supported in `runtime.exs`.
|
||||||
- [x] Mailer: Swoosh SMTP adapter configured from merged ENV + Settings when SMTP is configured.
|
- [x] Mailer: Swoosh SMTP adapter configured from merged ENV + Settings when SMTP is configured.
|
||||||
- [x] Per-request SMTP config via `Mv.Mailer.smtp_config/0` for Settings-only scenarios.
|
- [x] Per-request SMTP config via `Mv.Mailer.smtp_config/0` for Settings-only scenarios.
|
||||||
- [x] TLS certificate validation relaxed for OTP 27 (tls_options + sockopts).
|
- [x] TLS certificate validation relaxed for OTP 27 (tls_options for 587; sockopts with verify only for 465).
|
||||||
- [x] Prod warning: clear message in Settings when SMTP is not configured.
|
- [x] Prod warning: clear message in Settings when SMTP is not configured.
|
||||||
- [x] Test email: form with recipient field, translatable content, classified success/error messages.
|
- [x] Test email: form with recipient field, translatable content, classified success/error messages.
|
||||||
- [x] Join confirmation email: uses `Mailer.smtp_config/0` (same as test mail); on failure returns `{:error, :email_delivery_failed}`, error shown in JoinLive, logged for admin.
|
- [x] Join confirmation email: uses `Mailer.smtp_config/0` (same as test mail); on failure returns `{:error, :email_delivery_failed}`, error shown in JoinLive, logged for admin.
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ defmodule Mv.Mailer do
|
||||||
import Swoosh.Email
|
import Swoosh.Email
|
||||||
use Gettext, backend: MvWeb.Gettext, otp_app: :mv
|
use Gettext, backend: MvWeb.Gettext, otp_app: :mv
|
||||||
|
|
||||||
|
alias Mv.Smtp.ConfigBuilder
|
||||||
require Logger
|
require Logger
|
||||||
|
|
||||||
# Simple format check for test-email recipient only (e.g. allows a@b.c). Not for strict RFC validation.
|
# Simple format check for test-email recipient only (e.g. allows a@b.c). Not for strict RFC validation.
|
||||||
|
|
@ -100,31 +101,19 @@ defmodule Mv.Mailer do
|
||||||
@spec smtp_config() :: keyword()
|
@spec smtp_config() :: keyword()
|
||||||
def smtp_config do
|
def smtp_config do
|
||||||
if Mv.Config.smtp_configured?() and not boot_smtp_configured?() do
|
if Mv.Config.smtp_configured?() and not boot_smtp_configured?() do
|
||||||
host = Mv.Config.smtp_host()
|
|
||||||
port = Mv.Config.smtp_port() || 587
|
|
||||||
username = Mv.Config.smtp_username()
|
|
||||||
password = Mv.Config.smtp_password()
|
|
||||||
ssl_mode = Mv.Config.smtp_ssl() || "tls"
|
|
||||||
|
|
||||||
verify_mode =
|
verify_mode =
|
||||||
if Application.get_env(:mv, :smtp_verify_peer, false),
|
if Application.get_env(:mv, :smtp_verify_peer, false),
|
||||||
do: :verify_peer,
|
do: :verify_peer,
|
||||||
else: :verify_none
|
else: :verify_none
|
||||||
|
|
||||||
[
|
ConfigBuilder.build_opts(
|
||||||
adapter: Swoosh.Adapters.SMTP,
|
host: Mv.Config.smtp_host(),
|
||||||
relay: host,
|
port: Mv.Config.smtp_port() || 587,
|
||||||
port: port,
|
username: Mv.Config.smtp_username(),
|
||||||
ssl: ssl_mode == "ssl",
|
password: Mv.Config.smtp_password(),
|
||||||
tls: if(ssl_mode == "tls", do: :always, else: :never),
|
ssl_mode: Mv.Config.smtp_ssl() || "tls",
|
||||||
auth: :always,
|
verify_mode: verify_mode
|
||||||
username: username,
|
)
|
||||||
password: password,
|
|
||||||
# tls_options: STARTTLS (587); sockopts: direct SSL (465). Verify from :smtp_verify_peer (ENV SMTP_VERIFY_PEER).
|
|
||||||
tls_options: [verify: verify_mode],
|
|
||||||
sockopts: [verify: verify_mode]
|
|
||||||
]
|
|
||||||
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
end
|
end
|
||||||
|
|
|
||||||
58
lib/mv/smtp/config_builder.ex
Normal file
58
lib/mv/smtp/config_builder.ex
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
defmodule Mv.Smtp.ConfigBuilder do
|
||||||
|
@moduledoc """
|
||||||
|
Builds Swoosh/gen_smtp SMTP adapter options from connection parameters.
|
||||||
|
|
||||||
|
Single source of truth for TLS/sockopts logic (port 587 vs 465):
|
||||||
|
- Port 587 (STARTTLS): `gen_tcp` is used first; `sockopts` must NOT contain `:verify`.
|
||||||
|
- Port 465 (implicit SSL): initial connection is `ssl:connect`; `sockopts` must contain `:verify`.
|
||||||
|
|
||||||
|
Used by `config/runtime.exs` (boot-time ENV) and `Mv.Mailer.smtp_config/0` (Settings-only).
|
||||||
|
"""
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Builds the keyword list of Swoosh SMTP adapter options.
|
||||||
|
|
||||||
|
Options (keyword list):
|
||||||
|
- `:host` (required) — relay hostname
|
||||||
|
- `:port` (required) — port number (e.g. 587 or 465)
|
||||||
|
- `:ssl_mode` (required) — `"tls"` or `"ssl"`
|
||||||
|
- `:verify_mode` (required) — `:verify_peer` or `:verify_none`
|
||||||
|
- `:username` (optional)
|
||||||
|
- `:password` (optional)
|
||||||
|
|
||||||
|
Nil values are stripped from the result.
|
||||||
|
"""
|
||||||
|
@spec build_opts(keyword()) :: keyword()
|
||||||
|
def build_opts(opts) do
|
||||||
|
host = Keyword.fetch!(opts, :host)
|
||||||
|
port = Keyword.fetch!(opts, :port)
|
||||||
|
username = Keyword.get(opts, :username)
|
||||||
|
password = Keyword.get(opts, :password)
|
||||||
|
ssl_mode = Keyword.fetch!(opts, :ssl_mode)
|
||||||
|
verify_mode = Keyword.fetch!(opts, :verify_mode)
|
||||||
|
|
||||||
|
base_opts = [
|
||||||
|
adapter: Swoosh.Adapters.SMTP,
|
||||||
|
relay: host,
|
||||||
|
port: port,
|
||||||
|
username: username,
|
||||||
|
password: password,
|
||||||
|
ssl: ssl_mode == "ssl",
|
||||||
|
tls: if(ssl_mode == "tls", do: :always, else: :never),
|
||||||
|
auth: :always,
|
||||||
|
# tls_options: used for STARTTLS (587). For 465, gen_smtp uses sockopts for initial ssl:connect.
|
||||||
|
tls_options: [verify: verify_mode]
|
||||||
|
]
|
||||||
|
|
||||||
|
# Port 465: initial connection is ssl:connect; pass verify in sockopts.
|
||||||
|
# Port 587: initial connection is gen_tcp; sockopts must NOT contain verify (gen_tcp rejects it).
|
||||||
|
opts =
|
||||||
|
if ssl_mode == "ssl" do
|
||||||
|
Keyword.put(base_opts, :sockopts, verify: verify_mode)
|
||||||
|
else
|
||||||
|
base_opts
|
||||||
|
end
|
||||||
|
|
||||||
|
Enum.reject(opts, fn {_k, v} -> is_nil(v) end)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -3854,7 +3854,7 @@ msgstr "Wenn deaktiviert, können sich Nutzer*innen nicht über /register anmeld
|
||||||
#: lib/mv_web/controllers/page_controller.ex
|
#: lib/mv_web/controllers/page_controller.ex
|
||||||
#, elixir-autogen, elixir-format
|
#, elixir-autogen, elixir-format
|
||||||
msgid "Home"
|
msgid "Home"
|
||||||
msgstr ""
|
msgstr "Startseite"
|
||||||
|
|
||||||
#: lib/mv_web/controllers/join_confirm_controller.ex
|
#: lib/mv_web/controllers/join_confirm_controller.ex
|
||||||
#, elixir-autogen, elixir-format, fuzzy
|
#, elixir-autogen, elixir-format, fuzzy
|
||||||
|
|
|
||||||
89
test/mv/mailer_smtp_config_test.exs
Normal file
89
test/mv/mailer_smtp_config_test.exs
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
defmodule Mv.MailerSmtpConfigTest do
|
||||||
|
@moduledoc """
|
||||||
|
Unit tests for Mv.Mailer.smtp_config/0.
|
||||||
|
|
||||||
|
Ensures both port 587 (STARTTLS) and 465 (implicit SSL) work:
|
||||||
|
- 587: sockopts must NOT contain :verify (gen_tcp:connect would raise ArgumentError).
|
||||||
|
- 465: sockopts MUST contain :verify so initial ssl:connect uses verify_none/verify_peer.
|
||||||
|
Uses ENV to drive config; async: false.
|
||||||
|
"""
|
||||||
|
use Mv.DataCase, async: false
|
||||||
|
|
||||||
|
alias Mv.Mailer
|
||||||
|
|
||||||
|
defp set_smtp_env(key, value), do: System.put_env(key, value)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
describe "smtp_config/0" do
|
||||||
|
test "port 587 (TLS): does not include :verify in sockopts so gen_tcp:connect does not crash" do
|
||||||
|
set_smtp_env("SMTP_HOST", "smtp.example.com")
|
||||||
|
set_smtp_env("SMTP_PORT", "587")
|
||||||
|
set_smtp_env("SMTP_SSL", "tls")
|
||||||
|
|
||||||
|
config = Mailer.smtp_config()
|
||||||
|
|
||||||
|
assert config != [], "expected non-empty config when SMTP_HOST is set"
|
||||||
|
|
||||||
|
sockopts = Keyword.get(config, :sockopts, [])
|
||||||
|
|
||||||
|
refute Keyword.has_key?(sockopts, :verify),
|
||||||
|
"for 587 gen_tcp is used first; sockopts must not contain :verify"
|
||||||
|
after
|
||||||
|
clear_smtp_env()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "port 465 (SSL): includes :verify in sockopts so initial ssl:connect accepts verify mode" do
|
||||||
|
set_smtp_env("SMTP_HOST", "smtp.example.com")
|
||||||
|
set_smtp_env("SMTP_PORT", "465")
|
||||||
|
set_smtp_env("SMTP_SSL", "ssl")
|
||||||
|
|
||||||
|
config = Mailer.smtp_config()
|
||||||
|
|
||||||
|
assert config != []
|
||||||
|
sockopts = Keyword.get(config, :sockopts, [])
|
||||||
|
|
||||||
|
assert Keyword.has_key?(sockopts, :verify),
|
||||||
|
"for 465 initial connection is ssl:connect; sockopts must contain :verify"
|
||||||
|
|
||||||
|
assert Keyword.get(sockopts, :verify) in [:verify_none, :verify_peer]
|
||||||
|
after
|
||||||
|
clear_smtp_env()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "builds TLS mode for port 587 (STARTTLS)" do
|
||||||
|
set_smtp_env("SMTP_HOST", "smtp.example.com")
|
||||||
|
set_smtp_env("SMTP_PORT", "587")
|
||||||
|
set_smtp_env("SMTP_SSL", "tls")
|
||||||
|
|
||||||
|
config = Mailer.smtp_config()
|
||||||
|
|
||||||
|
assert config != []
|
||||||
|
assert Keyword.get(config, :tls) == :always
|
||||||
|
assert Keyword.get(config, :ssl) == false
|
||||||
|
after
|
||||||
|
clear_smtp_env()
|
||||||
|
end
|
||||||
|
|
||||||
|
test "builds SSL mode for port 465 (implicit SSL)" do
|
||||||
|
set_smtp_env("SMTP_HOST", "smtp.example.com")
|
||||||
|
set_smtp_env("SMTP_PORT", "465")
|
||||||
|
set_smtp_env("SMTP_SSL", "ssl")
|
||||||
|
|
||||||
|
config = Mailer.smtp_config()
|
||||||
|
|
||||||
|
assert config != []
|
||||||
|
assert Keyword.get(config, :ssl) == true
|
||||||
|
assert Keyword.get(config, :tls) == :never
|
||||||
|
after
|
||||||
|
clear_smtp_env()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
83
test/mv/smtp/config_builder_test.exs
Normal file
83
test/mv/smtp/config_builder_test.exs
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
defmodule Mv.Smtp.ConfigBuilderTest do
|
||||||
|
@moduledoc """
|
||||||
|
Unit tests for Mv.Smtp.ConfigBuilder.build_opts/1.
|
||||||
|
|
||||||
|
Ensures the single source of truth for SMTP opts correctly handles:
|
||||||
|
- Port 587 (TLS): sockopts must NOT contain :verify (gen_tcp rejects it).
|
||||||
|
- Port 465 (SSL): sockopts MUST contain :verify for initial ssl:connect.
|
||||||
|
"""
|
||||||
|
use ExUnit.Case, async: true
|
||||||
|
|
||||||
|
alias Mv.Smtp.ConfigBuilder
|
||||||
|
|
||||||
|
describe "build_opts/1" do
|
||||||
|
test "port 587 (TLS): does not include :verify in sockopts" do
|
||||||
|
opts =
|
||||||
|
ConfigBuilder.build_opts(
|
||||||
|
host: "smtp.example.com",
|
||||||
|
port: 587,
|
||||||
|
username: "user",
|
||||||
|
password: "secret",
|
||||||
|
ssl_mode: "tls",
|
||||||
|
verify_mode: :verify_none
|
||||||
|
)
|
||||||
|
|
||||||
|
sockopts = Keyword.get(opts, :sockopts, [])
|
||||||
|
refute Keyword.has_key?(sockopts, :verify), "for 587 sockopts must not contain :verify"
|
||||||
|
assert Keyword.get(opts, :tls) == :always
|
||||||
|
assert Keyword.get(opts, :ssl) == false
|
||||||
|
end
|
||||||
|
|
||||||
|
test "port 465 (SSL): includes :verify in sockopts" do
|
||||||
|
opts =
|
||||||
|
ConfigBuilder.build_opts(
|
||||||
|
host: "smtp.example.com",
|
||||||
|
port: 465,
|
||||||
|
username: "user",
|
||||||
|
password: "secret",
|
||||||
|
ssl_mode: "ssl",
|
||||||
|
verify_mode: :verify_peer
|
||||||
|
)
|
||||||
|
|
||||||
|
sockopts = Keyword.get(opts, :sockopts, [])
|
||||||
|
assert Keyword.has_key?(sockopts, :verify), "for 465 sockopts must contain :verify"
|
||||||
|
assert Keyword.get(sockopts, :verify) == :verify_peer
|
||||||
|
assert Keyword.get(opts, :ssl) == true
|
||||||
|
assert Keyword.get(opts, :tls) == :never
|
||||||
|
end
|
||||||
|
|
||||||
|
test "strips nil values" do
|
||||||
|
opts =
|
||||||
|
ConfigBuilder.build_opts(
|
||||||
|
host: "smtp.example.com",
|
||||||
|
port: 587,
|
||||||
|
username: nil,
|
||||||
|
password: nil,
|
||||||
|
ssl_mode: "tls",
|
||||||
|
verify_mode: :verify_none
|
||||||
|
)
|
||||||
|
|
||||||
|
refute Keyword.has_key?(opts, :username)
|
||||||
|
refute Keyword.has_key?(opts, :password)
|
||||||
|
assert Keyword.get(opts, :relay) == "smtp.example.com"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "includes adapter and required keys" do
|
||||||
|
opts =
|
||||||
|
ConfigBuilder.build_opts(
|
||||||
|
host: "mail.example.com",
|
||||||
|
port: 587,
|
||||||
|
username: "u",
|
||||||
|
password: "p",
|
||||||
|
ssl_mode: "tls",
|
||||||
|
verify_mode: :verify_none
|
||||||
|
)
|
||||||
|
|
||||||
|
assert Keyword.get(opts, :adapter) == Swoosh.Adapters.SMTP
|
||||||
|
assert Keyword.get(opts, :relay) == "mail.example.com"
|
||||||
|
assert Keyword.get(opts, :port) == 587
|
||||||
|
assert Keyword.get(opts, :auth) == :always
|
||||||
|
assert Keyword.get(opts, :tls_options) == [verify: :verify_none]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -2,6 +2,9 @@ defmodule MvWeb.JoinLiveEmailFailureTest do
|
||||||
@moduledoc """
|
@moduledoc """
|
||||||
When join confirmation email delivery fails, the user sees an error message
|
When join confirmation email delivery fails, the user sees an error message
|
||||||
and no success copy. Uses FailingMailAdapter; async: false to avoid config races.
|
and no success copy. Uses FailingMailAdapter; async: false to avoid config races.
|
||||||
|
|
||||||
|
Ensures SMTP is not configured (ENV cleared, Settings smtp_host nil) so that
|
||||||
|
Mailer.smtp_config/0 returns [] and the Application-configured FailingMailAdapter is used.
|
||||||
"""
|
"""
|
||||||
use MvWeb.ConnCase, async: false
|
use MvWeb.ConnCase, async: false
|
||||||
import Phoenix.LiveViewTest
|
import Phoenix.LiveViewTest
|
||||||
|
|
@ -12,18 +15,21 @@ defmodule MvWeb.JoinLiveEmailFailureTest do
|
||||||
test "when confirmation email fails, user sees error flash and no success message", %{
|
test "when confirmation email fails, user sees error flash and no success message", %{
|
||||||
conn: conn
|
conn: conn
|
||||||
} do
|
} do
|
||||||
|
# Clear SMTP config so Mailer.smtp_config() returns [] and FailingMailAdapter is used.
|
||||||
|
saved_env = clear_smtp_env_and_save()
|
||||||
enable_join_form_for_test()
|
enable_join_form_for_test()
|
||||||
|
|
||||||
saved = Application.get_env(:mv, Mv.Mailer)
|
saved_mailer = Application.get_env(:mv, Mv.Mailer)
|
||||||
|
|
||||||
Application.put_env(
|
Application.put_env(
|
||||||
:mv,
|
:mv,
|
||||||
Mv.Mailer,
|
Mv.Mailer,
|
||||||
Keyword.put(saved || [], :adapter, Mv.TestSupport.FailingMailAdapter)
|
Keyword.put(saved_mailer || [], :adapter, Mv.TestSupport.FailingMailAdapter)
|
||||||
)
|
)
|
||||||
|
|
||||||
on_exit(fn ->
|
on_exit(fn ->
|
||||||
Application.put_env(:mv, Mv.Mailer, saved)
|
Application.put_env(:mv, Mv.Mailer, saved_mailer)
|
||||||
|
restore_smtp_env(saved_env)
|
||||||
end)
|
end)
|
||||||
|
|
||||||
{:ok, view, _html} = live(conn, "/join")
|
{:ok, view, _html} = live(conn, "/join")
|
||||||
|
|
@ -46,13 +52,36 @@ defmodule MvWeb.JoinLiveEmailFailureTest do
|
||||||
refute view |> element("[data-testid='join-success-message']") |> has_element?()
|
refute view |> element("[data-testid='join-success-message']") |> has_element?()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp clear_smtp_env_and_save do
|
||||||
|
keys = [
|
||||||
|
"SMTP_HOST",
|
||||||
|
"SMTP_PORT",
|
||||||
|
"SMTP_USERNAME",
|
||||||
|
"SMTP_PASSWORD",
|
||||||
|
"SMTP_PASSWORD_FILE",
|
||||||
|
"SMTP_SSL"
|
||||||
|
]
|
||||||
|
|
||||||
|
saved = for k <- keys, into: %{}, do: {k, System.get_env(k)}
|
||||||
|
for k <- keys, do: System.delete_env(k)
|
||||||
|
saved
|
||||||
|
end
|
||||||
|
|
||||||
|
defp restore_smtp_env(saved) do
|
||||||
|
for {k, v} <- saved do
|
||||||
|
if v != nil, do: System.put_env(k, v), else: System.delete_env(k)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp enable_join_form_for_test do
|
defp enable_join_form_for_test do
|
||||||
{:ok, settings} = Membership.get_settings()
|
{:ok, settings} = Membership.get_settings()
|
||||||
|
|
||||||
Membership.update_settings(settings, %{
|
Membership.update_settings(settings, %{
|
||||||
join_form_enabled: true,
|
join_form_enabled: true,
|
||||||
join_form_field_ids: ["email", "first_name", "last_name"],
|
join_form_field_ids: ["email", "first_name", "last_name"],
|
||||||
join_form_field_required: %{"email" => true, "first_name" => false, "last_name" => false}
|
join_form_field_required: %{"email" => true, "first_name" => false, "last_name" => false},
|
||||||
|
# Clear SMTP host so Mv.Config.smtp_configured?() is false and Mailer.smtp_config() returns [].
|
||||||
|
smtp_host: nil
|
||||||
})
|
})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue