This commit is contained in:
2026-03-18 18:16:44 +01:00
parent c89c08c397
commit b3c8b77f12
20 changed files with 1934 additions and 36 deletions
+269
View File
@@ -0,0 +1,269 @@
"""
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()