"""SMTP service-layer unit tests. Jinja-based HTTP flow tests (POST /config, POST /config/smtp/test via form) were removed in M2-T11 when the Jinja routes were deleted. HTTP-level SMTP test endpoint coverage lives in test_api_config.py. """ import smtplib from app.config import Settings from app.services.email import ( EmailDeliveryError, get_smtp_config, is_smtp_ready, send_public_ip_changed_email, send_smtp_test_email, ) def _smtp_settings(**overrides) -> Settings: payload = { "app_env": "development", "app_hostname": "localhost:8000", "app_database_url": "sqlite:///./data/app.db", "auth_bootstrap_username": "admin", "auth_bootstrap_password": "secret-password", "smtp_enabled": True, "smtp_host": "smtp.example.com", "smtp_port": 587, "smtp_username": "smtp-user", "smtp_password": "super-secret-password", "smtp_from_name": "Home Automation", "smtp_from_address": "sender@example.com", "smtp_to_address": "recipient@example.com", "smtp_use_starttls": True, } payload.update(overrides) return Settings(_env_file=None, **payload) def test_get_smtp_config_reads_runtime_values() -> None: settings = _smtp_settings(smtp_port=2525, smtp_use_starttls=False) smtp_config = get_smtp_config(settings) assert smtp_config.host == "smtp.example.com" assert smtp_config.port == 2525 assert smtp_config.username == "smtp-user" assert smtp_config.password == "super-secret-password" assert smtp_config.from_name == "Home Automation" assert smtp_config.from_address == "sender@example.com" assert smtp_config.to_address == "recipient@example.com" assert smtp_config.use_starttls is False def test_smtp_test_readiness_does_not_require_smtp_enabled() -> None: settings = _smtp_settings(smtp_enabled=False) assert is_smtp_ready(settings) is True def test_send_smtp_test_email_success(monkeypatch) -> None: sent = {} class FakeSMTP: def __init__(self, host, port, timeout): sent["host"] = host sent["port"] = port sent["timeout"] = timeout def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return None def ehlo(self): sent["ehlo"] = sent.get("ehlo", 0) + 1 def starttls(self): sent["starttls"] = True def login(self, username, password): sent["username"] = username sent["password"] = password def send_message(self, message, from_addr=None, to_addrs=None): sent["subject"] = message["Subject"] sent["from"] = message["From"] sent["to"] = message["To"] sent["body"] = message.get_content() sent["envelope_from"] = from_addr sent["envelope_to"] = to_addrs monkeypatch.setattr("app.services.email.smtplib.SMTP", FakeSMTP) send_smtp_test_email(_smtp_settings()) assert sent["host"] == "smtp.example.com" assert sent["port"] == 587 assert sent["timeout"] == 10 assert sent["starttls"] is True assert sent["username"] == "smtp-user" assert sent["password"] == "super-secret-password" assert sent["subject"] == "Home Automation SMTP Test" assert sent["from"] == "Home Automation " assert sent["to"] == "recipient@example.com" assert sent["envelope_from"] == "sender@example.com" assert sent["envelope_to"] == ["recipient@example.com"] assert "This is a test email" in sent["body"] def test_send_smtp_test_email_does_not_require_smtp_enabled(monkeypatch) -> None: sent = {} class FakeSMTP: def __init__(self, host, port, timeout): sent["host"] = host def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return None def ehlo(self): return None def starttls(self): return None def login(self, username, password): return None def send_message(self, message, from_addr=None, to_addrs=None): sent["subject"] = message["Subject"] sent["from"] = message["From"] sent["envelope_from"] = from_addr monkeypatch.setattr("app.services.email.smtplib.SMTP", FakeSMTP) send_smtp_test_email(_smtp_settings(smtp_enabled=False)) assert sent["host"] == "smtp.example.com" assert sent["subject"] == "Home Automation SMTP Test" assert sent["from"] == "Home Automation " assert sent["envelope_from"] == "sender@example.com" def test_send_smtp_test_email_failure_sanitizes_password(monkeypatch) -> None: class FakeSMTP: def __init__(self, host, port, timeout): pass def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return None def ehlo(self): return None def starttls(self): raise smtplib.SMTPException("authentication failed for super-secret-password") monkeypatch.setattr("app.services.email.smtplib.SMTP", FakeSMTP) try: send_smtp_test_email(_smtp_settings()) assert False, "expected EmailDeliveryError" except EmailDeliveryError as exc: assert "super-secret-password" not in str(exc) assert "[redacted]" in str(exc) def test_send_public_ip_changed_email_contains_expected_english_content(monkeypatch) -> None: sent = {} class FakeSMTP: def __init__(self, host, port, timeout): sent["host"] = host def __enter__(self): return self def __exit__(self, exc_type, exc, tb): return None def ehlo(self): return None def starttls(self): return None def login(self, username, password): return None def send_message(self, message, from_addr=None, to_addrs=None): sent["subject"] = message["Subject"] sent["body"] = message.get_content() sent["from"] = message["From"] sent["envelope_from"] = from_addr monkeypatch.setattr("app.services.email.smtplib.SMTP", FakeSMTP) send_public_ip_changed_email( _smtp_settings(), previous_ipv4="203.0.113.10", current_ipv4="198.51.100.25", detected_at=__import__("datetime").datetime(2026, 4, 29, 10, 0, tzinfo=__import__("datetime").UTC), ) assert sent["subject"] == "Public IP changed" assert sent["from"] == "Home Automation " assert sent["envelope_from"] == "sender@example.com" assert "Your public IPv4 address has changed." in sent["body"] assert "Previous IP: 203.0.113.10" in sent["body"] assert "Current IP: 198.51.100.25" in sent["body"] assert "Detected at: 2026-04-29 10:00:00 UTC" in sent["body"] assert "update the trusted IP manually" in sent["body"]