add ip change notification and refine sender display
This commit is contained in:
@@ -61,6 +61,7 @@ def test_settings_support_smtp_fields(monkeypatch) -> None:
|
||||
monkeypatch.setenv("SMTP_PORT", "2525")
|
||||
monkeypatch.setenv("SMTP_USERNAME", "smtp-user")
|
||||
monkeypatch.setenv("SMTP_PASSWORD", "smtp-password")
|
||||
monkeypatch.setenv("SMTP_FROM_NAME", "Home Automation")
|
||||
monkeypatch.setenv("SMTP_FROM_ADDRESS", "sender@example.com")
|
||||
monkeypatch.setenv("SMTP_TO_ADDRESS", "recipient@example.com")
|
||||
monkeypatch.setenv("SMTP_USE_STARTTLS", "false")
|
||||
@@ -72,6 +73,7 @@ def test_settings_support_smtp_fields(monkeypatch) -> None:
|
||||
assert settings.smtp_port == 2525
|
||||
assert settings.smtp_username == "smtp-user"
|
||||
assert settings.smtp_password == "smtp-password"
|
||||
assert settings.smtp_from_name == "Home Automation"
|
||||
assert settings.smtp_from_address == "sender@example.com"
|
||||
assert settings.smtp_to_address == "recipient@example.com"
|
||||
assert settings.smtp_use_starttls is False
|
||||
|
||||
+87
-3
@@ -6,7 +6,9 @@ from fastapi.testclient import TestClient
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from app.services.public_ip import PublicIPCheckResult, check_public_ipv4
|
||||
from app.config import Settings
|
||||
from app.services.email import EmailDeliveryError
|
||||
from app.services.public_ip import PublicIPCheckResult, check_public_ipv4, check_public_ipv4_and_notify
|
||||
|
||||
|
||||
def _make_session(database_url: str) -> Session:
|
||||
@@ -152,8 +154,8 @@ def test_public_ip_check_endpoint_hides_ip_values(client: TestClient, monkeypatc
|
||||
|
||||
monkeypatch.setattr(
|
||||
public_ip_route,
|
||||
"check_public_ipv4",
|
||||
lambda session: PublicIPCheckResult(
|
||||
"check_public_ipv4_and_notify",
|
||||
lambda session, bootstrap_settings: PublicIPCheckResult(
|
||||
status="changed",
|
||||
checked_at=fixed_checked_at,
|
||||
changed=True,
|
||||
@@ -172,3 +174,85 @@ def test_public_ip_check_endpoint_hides_ip_values(client: TestClient, monkeypatc
|
||||
assert "current_ipv4" not in response.text
|
||||
assert "previous_ipv4" not in response.text
|
||||
assert "203.0.113.10" not in response.text
|
||||
|
||||
|
||||
def _notification_settings() -> Settings:
|
||||
return Settings(
|
||||
_env_file=None,
|
||||
app_env="development",
|
||||
app_hostname="localhost:8000",
|
||||
app_database_url="sqlite:///./data/app.db",
|
||||
location_database_url="sqlite:///./data/locationRecorder.db",
|
||||
poo_database_url="sqlite:///./data/pooRecorder.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_address="sender@example.com",
|
||||
smtp_to_address="recipient@example.com",
|
||||
smtp_use_starttls=True,
|
||||
)
|
||||
|
||||
|
||||
def test_public_ip_notification_sends_only_when_changed(auth_database, monkeypatch) -> None:
|
||||
session = _make_session(auth_database["app_url"])
|
||||
sent = []
|
||||
monkeypatch.setattr(
|
||||
"app.services.public_ip.send_public_ip_changed_email",
|
||||
lambda settings, *, previous_ipv4, current_ipv4, detected_at: sent.append(
|
||||
(previous_ipv4, current_ipv4, detected_at)
|
||||
),
|
||||
)
|
||||
try:
|
||||
first_seen = check_public_ipv4_and_notify(
|
||||
session,
|
||||
bootstrap_settings=_notification_settings(),
|
||||
fetch_public_ipv4=lambda: "203.0.113.10",
|
||||
)
|
||||
unchanged = check_public_ipv4_and_notify(
|
||||
session,
|
||||
bootstrap_settings=_notification_settings(),
|
||||
fetch_public_ipv4=lambda: "203.0.113.10",
|
||||
)
|
||||
changed = check_public_ipv4_and_notify(
|
||||
session,
|
||||
bootstrap_settings=_notification_settings(),
|
||||
fetch_public_ipv4=lambda: "198.51.100.25",
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
assert first_seen.status == "first_seen"
|
||||
assert unchanged.status == "unchanged"
|
||||
assert changed.status == "changed"
|
||||
assert len(sent) == 1
|
||||
assert sent[0][0] == "203.0.113.10"
|
||||
assert sent[0][1] == "198.51.100.25"
|
||||
assert sent[0][2] == changed.checked_at
|
||||
|
||||
|
||||
def test_public_ip_notification_failure_does_not_break_changed_result(auth_database, monkeypatch) -> None:
|
||||
session = _make_session(auth_database["app_url"])
|
||||
monkeypatch.setattr(
|
||||
"app.services.public_ip.send_public_ip_changed_email",
|
||||
lambda settings, *, previous_ipv4, current_ipv4, detected_at: (_ for _ in ()).throw(
|
||||
EmailDeliveryError("smtp down")
|
||||
),
|
||||
)
|
||||
try:
|
||||
check_public_ipv4(session, fetch_public_ipv4=lambda: "203.0.113.10")
|
||||
result = check_public_ipv4_and_notify(
|
||||
session,
|
||||
bootstrap_settings=_notification_settings(),
|
||||
fetch_public_ipv4=lambda: "198.51.100.25",
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
assert result.status == "changed"
|
||||
assert result.changed is True
|
||||
assert result.previous_ipv4 == "203.0.113.10"
|
||||
assert result.current_ipv4 == "198.51.100.25"
|
||||
|
||||
+67
-4
@@ -5,7 +5,13 @@ import smtplib
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.config import Settings
|
||||
from app.services.email import EmailDeliveryError, get_smtp_config, is_smtp_ready, send_smtp_test_email
|
||||
from app.services.email import (
|
||||
EmailDeliveryError,
|
||||
get_smtp_config,
|
||||
is_smtp_ready,
|
||||
send_public_ip_changed_email,
|
||||
send_smtp_test_email,
|
||||
)
|
||||
|
||||
|
||||
def _extract_csrf_token(html: str) -> str:
|
||||
@@ -43,6 +49,7 @@ def _smtp_settings(**overrides) -> Settings:
|
||||
"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,
|
||||
@@ -60,6 +67,7 @@ def test_get_smtp_config_reads_runtime_values() -> None:
|
||||
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
|
||||
@@ -96,11 +104,13 @@ def test_send_smtp_test_email_success(monkeypatch) -> None:
|
||||
sent["username"] = username
|
||||
sent["password"] = password
|
||||
|
||||
def send_message(self, message):
|
||||
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)
|
||||
|
||||
@@ -113,8 +123,10 @@ def test_send_smtp_test_email_success(monkeypatch) -> None:
|
||||
assert sent["username"] == "smtp-user"
|
||||
assert sent["password"] == "super-secret-password"
|
||||
assert sent["subject"] == "Home Automation SMTP Test"
|
||||
assert sent["from"] == "sender@example.com"
|
||||
assert sent["from"] == "Home Automation <sender@example.com>"
|
||||
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"]
|
||||
|
||||
|
||||
@@ -140,8 +152,10 @@ def test_send_smtp_test_email_does_not_require_smtp_enabled(monkeypatch) -> None
|
||||
def login(self, username, password):
|
||||
return None
|
||||
|
||||
def send_message(self, message):
|
||||
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)
|
||||
|
||||
@@ -149,6 +163,8 @@ def test_send_smtp_test_email_does_not_require_smtp_enabled(monkeypatch) -> None
|
||||
|
||||
assert sent["host"] == "smtp.example.com"
|
||||
assert sent["subject"] == "Home Automation SMTP Test"
|
||||
assert sent["from"] == "Home Automation <sender@example.com>"
|
||||
assert sent["envelope_from"] == "sender@example.com"
|
||||
|
||||
|
||||
def test_send_smtp_test_email_failure_sanitizes_password(monkeypatch) -> None:
|
||||
@@ -178,6 +194,53 @@ def test_send_smtp_test_email_failure_sanitizes_password(monkeypatch) -> None:
|
||||
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 <sender@example.com>"
|
||||
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"]
|
||||
|
||||
|
||||
def test_config_update_does_not_clear_existing_smtp_password(
|
||||
client: TestClient, test_database_urls
|
||||
) -> None:
|
||||
|
||||
Reference in New Issue
Block a user