""" 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="
Testo HTML
", ) # 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()