Files
PecHub/worker/tests/unit/test_smtp_sender.py
T
2026-03-18 18:16:44 +01:00

270 lines
9.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Test unitari per SmtpSender.
Verifica la costruzione del messaggio MIME senza connessioni SMTP reali.
Il test del send() effettivo verso server reali è un test di integrazione
(eseguito separatamente con flag --real-smtp).
"""
import email as email_lib
import email.policy
from email.mime.multipart import MIMEMultipart
import pytest
# ─── Chiave test fissa deve coincidere con ENCRYPTION_KEY ──────────────────
_TEST_KEY_HEX = "b" * 64
# Imposta la variabile d'ambiente e invalida la cache prima di qualsiasi import
import os as _os
_os.environ["ENCRYPTION_KEY"] = _TEST_KEY_HEX
_os.environ.setdefault("SECRET_KEY", "test-secret-worker")
# Invalida la cache di get_settings se già caricata
try:
from app.config import get_settings as _gs
_gs.cache_clear()
except Exception:
pass
# ─────────────────────────────────────────────────────────────────────────────
# ─── Fixtures helper ─────────────────────────────────────────────────────────
def _make_fake_mailbox():
"""Crea un oggetto mailbox-like con attributi minimi per SmtpSender."""
import base64
import os
from unittest.mock import MagicMock
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
key = bytes.fromhex(_TEST_KEY_HEX)
def _enc(value: str) -> str:
nonce = os.urandom(12)
ct = AESGCM(key).encrypt(nonce, value.encode(), None)
return base64.b64encode(nonce + ct).decode()
mailbox = MagicMock()
mailbox.email_address = "test@pec.example.it"
mailbox.smtp_host_enc = _enc("smtp.example.it")
mailbox.smtp_port_enc = _enc("465")
mailbox.smtp_user_enc = _enc("test@pec.example.it")
mailbox.smtp_pass_enc = _enc("secret")
mailbox.smtp_use_tls = True
return mailbox
# ─── Test costruzione MIME ────────────────────────────────────────────────────
class TestBuildMimeMessage:
"""Verifica la costruzione del messaggio MIME con varie combinazioni."""
def _get_sender(self):
"""Restituisce SmtpSender con mailbox mock."""
# La chiave è già impostata a livello di modulo (_TEST_KEY_HEX)
from app.config import get_settings
get_settings.cache_clear() # forza rilettura env var
from app.smtp.sender import SmtpSender
return SmtpSender(_make_fake_mailbox())
def test_build_plain_text_only(self):
"""Verifica struttura MIME con solo testo semplice."""
sender = self._get_sender()
msg, msg_id = sender.build_mime_message(
to_addresses=["dest@pec.it"],
cc_addresses=[],
subject="Test oggetto",
body_text="Testo del corpo",
)
assert isinstance(msg, MIMEMultipart)
assert msg["From"] == "test@pec.example.it"
assert msg["To"] == "dest@pec.it"
assert msg["Subject"] == "Test oggetto"
assert msg_id.startswith("<")
assert msg_id.endswith(">")
assert "Cc" not in msg
def test_build_with_cc(self):
"""Verifica che il campo Cc venga incluso correttamente."""
sender = self._get_sender()
msg, _ = sender.build_mime_message(
to_addresses=["dest1@pec.it"],
cc_addresses=["cc@pec.it", "cc2@pec.it"],
subject="Test Cc",
body_text="corpo",
)
assert "Cc" in msg
assert "cc@pec.it" in msg["Cc"]
assert "cc2@pec.it" in msg["Cc"]
def test_build_multiple_to(self):
"""Verifica destinatari multipli nel campo To."""
sender = self._get_sender()
msg, _ = sender.build_mime_message(
to_addresses=["dest1@pec.it", "dest2@pec.it"],
cc_addresses=[],
subject="Multi dest",
body_text="corpo",
)
assert "dest1@pec.it" in msg["To"]
assert "dest2@pec.it" in msg["To"]
def test_build_with_html(self):
"""Verifica che corpo HTML venga aggiunto come parte MIME."""
sender = self._get_sender()
msg, _ = sender.build_mime_message(
to_addresses=["dest@pec.it"],
cc_addresses=[],
subject="Test HTML",
body_text="Testo semplice",
body_html="<p>Testo <b>HTML</b></p>",
)
# Trova le parti del messaggio
raw = msg.as_string()
assert "text/plain" in raw
assert "text/html" in raw
def test_build_with_attachment(self):
"""Verifica che un allegato venga incluso nel messaggio."""
sender = self._get_sender()
attachments = [
{
"filename": "documento.pdf",
"content": b"%PDF-1.4 fake content",
"content_type": "application/pdf",
}
]
msg, _ = sender.build_mime_message(
to_addresses=["dest@pec.it"],
cc_addresses=[],
subject="Test allegato",
body_text="Vedi allegato",
attachments=attachments,
)
raw = msg.as_string()
assert "documento.pdf" in raw
def test_build_multiple_attachments(self):
"""Verifica più allegati in un unico messaggio."""
sender = self._get_sender()
attachments = [
{"filename": "file1.txt", "content": b"contenuto 1", "content_type": "text/plain"},
{"filename": "file2.txt", "content": b"contenuto 2", "content_type": "text/plain"},
]
msg, _ = sender.build_mime_message(
to_addresses=["dest@pec.it"],
cc_addresses=[],
subject="Multi allegati",
body_text="Due allegati",
attachments=attachments,
)
raw = msg.as_string()
assert "file1.txt" in raw
assert "file2.txt" in raw
def test_message_id_unique(self):
"""Verifica che ogni messaggio abbia un Message-ID unico."""
sender = self._get_sender()
_, id1 = sender.build_mime_message(
to_addresses=["a@pec.it"], cc_addresses=[], subject="A", body_text="a"
)
_, id2 = sender.build_mime_message(
to_addresses=["b@pec.it"], cc_addresses=[], subject="B", body_text="b"
)
assert id1 != id2
def test_required_headers_present(self):
"""Verifica che tutti gli header obbligatori siano presenti."""
sender = self._get_sender()
msg, _ = sender.build_mime_message(
to_addresses=["dest@pec.it"],
cc_addresses=[],
subject="Test headers",
body_text="corpo",
)
required_headers = ["From", "To", "Subject", "Date", "Message-ID", "MIME-Version"]
for header in required_headers:
assert header in msg, f"Header mancante: {header}"
def test_eml_bytes_serializable(self):
"""Verifica che il messaggio sia serializzabile in bytes."""
sender = self._get_sender()
msg, _ = sender.build_mime_message(
to_addresses=["dest@pec.it"],
cc_addresses=[],
subject="Serializzazione",
body_text="corpo",
)
raw = msg.as_bytes()
assert len(raw) > 0
assert isinstance(raw, bytes)
def test_empty_body_creates_valid_message(self):
"""Verifica che un messaggio con corpo vuoto sia comunque valido."""
sender = self._get_sender()
msg, _ = sender.build_mime_message(
to_addresses=["dest@pec.it"],
cc_addresses=[],
subject="Corpo vuoto",
body_text="",
)
raw = msg.as_bytes()
assert len(raw) > 0
# ─── Test decifrazione credenziali ───────────────────────────────────────────
class TestDecryptSmtpCredentials:
"""Verifica la decifrazione delle credenziali SMTP."""
def test_decrypt_returns_correct_values(self):
"""Le credenziali decifrate devono corrispondere ai valori originali."""
from app.config import get_settings
get_settings.cache_clear()
from app.smtp.sender import decrypt_smtp_credentials
mailbox = _make_fake_mailbox()
creds = decrypt_smtp_credentials(mailbox)
assert creds["host"] == "smtp.example.it"
assert creds["port"] == 465
assert creds["user"] == "test@pec.example.it"
assert creds["password"] == "secret"
assert creds["use_tls"] is True
def test_wrong_key_raises_error(self):
"""Una chiave errata deve sollevare un'eccezione."""
import os
from app.config import get_settings
# Imposta chiave sbagliata
os.environ["ENCRYPTION_KEY"] = "a" * 64
get_settings.cache_clear()
from app.smtp.sender import decrypt_smtp_credentials
mailbox = _make_fake_mailbox() # cifrato con chiave "b"*64
with pytest.raises(Exception):
decrypt_smtp_credentials(mailbox)
# Ripristina chiave corretta per test successivi
os.environ["ENCRYPTION_KEY"] = _TEST_KEY_HEX
get_settings.cache_clear()