mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
fase 3
This commit is contained in:
@@ -0,0 +1,509 @@
|
||||
"""
|
||||
Test unitari per app.parsers.eml_parser.
|
||||
|
||||
Copertura:
|
||||
- Parsing header (Subject, From, To, Cc, Message-ID, Date)
|
||||
- Decodifica RFC 2047 (UTF-8, ISO-8859-1, base64, quoted-printable)
|
||||
- Estrazione body text/plain e text/html
|
||||
- Estrazione allegati (singoli e multipli)
|
||||
- Gestione EML-in-EML (message/rfc822)
|
||||
- Flag has_attachments (solo allegati non-PEC-system)
|
||||
- Allegati PEC di sistema (daticert.xml, postacert.eml)
|
||||
- EML vuoto / malformato (no crash)
|
||||
- Messaggio non-multipart
|
||||
"""
|
||||
|
||||
import email
|
||||
import textwrap
|
||||
|
||||
import pytest
|
||||
|
||||
from app.parsers.eml_parser import (
|
||||
AttachmentInfo,
|
||||
ParsedEmail,
|
||||
decode_header,
|
||||
extract_addresses,
|
||||
parse_date,
|
||||
parse_eml,
|
||||
)
|
||||
|
||||
|
||||
# ─── Fixture EML ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
SIMPLE_EML = b"""\
|
||||
From: mittente@pec.it
|
||||
To: destinatario@pec.it
|
||||
Cc: copia@pec.it
|
||||
Subject: Test PEC Fase 3
|
||||
Message-ID: <test123@pec.it>
|
||||
Date: Wed, 18 Mar 2026 14:00:00 +0100
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Corpo del messaggio di test.
|
||||
Seconda riga.
|
||||
"""
|
||||
|
||||
MULTIPART_EML = b"""\
|
||||
From: mittente@pec.it
|
||||
To: dest@pec.it
|
||||
Subject: PEC con allegato
|
||||
Date: Wed, 18 Mar 2026 10:00:00 +0100
|
||||
Content-Type: multipart/mixed; boundary="====boundary123===="
|
||||
|
||||
--====boundary123====
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Testo del messaggio.
|
||||
|
||||
--====boundary123====
|
||||
Content-Type: application/pdf; name="documento.pdf"
|
||||
Content-Disposition: attachment; filename="documento.pdf"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
JVBERi0xLjQ=
|
||||
|
||||
--====boundary123====--
|
||||
"""
|
||||
|
||||
MULTIPART_HTML_EML = b"""\
|
||||
From: mittente@pec.it
|
||||
To: dest@pec.it
|
||||
Subject: PEC multipart/alternative
|
||||
Date: Wed, 18 Mar 2026 10:00:00 +0100
|
||||
Content-Type: multipart/alternative; boundary="====alt===="
|
||||
|
||||
--====alt====
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Testo piano.
|
||||
|
||||
--====alt====
|
||||
Content-Type: text/html; charset=utf-8
|
||||
|
||||
<html><body><p>Testo HTML.</p></body></html>
|
||||
|
||||
--====alt====--
|
||||
"""
|
||||
|
||||
RECEIPT_EML_WITH_NESTED = b"""\
|
||||
From: posta-certificata@pec.aruba.it
|
||||
To: mittente@pec.it
|
||||
Subject: CONSEGNA: Test PEC Fase 3
|
||||
X-Ricevuta: avvenuta-consegna
|
||||
X-Riferimento-Message-ID: <orig001@pec.it>
|
||||
Date: Wed, 18 Mar 2026 14:05:00 +0100
|
||||
Content-Type: multipart/mixed; boundary="====receipt===="
|
||||
|
||||
--====receipt====
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Il messaggio e' stato consegnato al destinatario.
|
||||
|
||||
--====receipt====
|
||||
Content-Type: application/xml; name="daticert.xml"
|
||||
Content-Disposition: attachment; filename="daticert.xml"
|
||||
|
||||
<?xml version="1.0"?><PostaCertificata versione="2.3"></PostaCertificata>
|
||||
|
||||
--====receipt====
|
||||
Content-Type: message/rfc822
|
||||
Content-Disposition: inline
|
||||
|
||||
From: mittente@pec.it
|
||||
To: destinatario@pec.it
|
||||
Subject: Test PEC Fase 3
|
||||
Message-ID: <orig001@pec.it>
|
||||
Date: Wed, 18 Mar 2026 14:00:00 +0100
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Corpo del messaggio originale.
|
||||
|
||||
--====receipt====--
|
||||
"""
|
||||
|
||||
MULTIPLE_ATTACHMENTS_EML = b"""\
|
||||
From: a@pec.it
|
||||
To: b@pec.it
|
||||
Subject: PEC con allegati multipli
|
||||
Date: Wed, 18 Mar 2026 10:00:00 +0100
|
||||
Content-Type: multipart/mixed; boundary="====multi===="
|
||||
|
||||
--====multi====
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Corpo.
|
||||
|
||||
--====multi====
|
||||
Content-Type: application/pdf; name="doc1.pdf"
|
||||
Content-Disposition: attachment; filename="doc1.pdf"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
AAEC
|
||||
|
||||
--====multi====
|
||||
Content-Type: application/pdf; name="doc2.pdf"
|
||||
Content-Disposition: attachment; filename="doc2.pdf"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
BAEC
|
||||
|
||||
--====multi====--
|
||||
"""
|
||||
|
||||
PEC_SYSTEM_EML = b"""\
|
||||
From: posta-certificata@pec.aruba.it
|
||||
To: mittente@pec.it
|
||||
Subject: ACCETTAZIONE: Test
|
||||
X-Ricevuta: accettazione
|
||||
Date: Wed, 18 Mar 2026 14:01:00 +0100
|
||||
Content-Type: multipart/mixed; boundary="====sys===="
|
||||
|
||||
--====sys====
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Ricevuta di accettazione.
|
||||
|
||||
--====sys====
|
||||
Content-Type: application/xml; name="daticert.xml"
|
||||
Content-Disposition: attachment; filename="daticert.xml"
|
||||
|
||||
<?xml version="1.0"?><PostaCertificata versione="2.3"></PostaCertificata>
|
||||
|
||||
--====sys====
|
||||
Content-Type: message/rfc822
|
||||
Content-Disposition: inline; filename="postacert.eml"
|
||||
|
||||
From: mittente@pec.it
|
||||
To: dest@pec.it
|
||||
Subject: Test originale
|
||||
|
||||
Corpo.
|
||||
|
||||
--====sys====--
|
||||
"""
|
||||
|
||||
|
||||
# ─── Test decode_header ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestDecodeHeader:
|
||||
|
||||
def test_stringa_semplice(self):
|
||||
assert decode_header("Hello World") == "Hello World"
|
||||
|
||||
def test_none_ritorna_none(self):
|
||||
assert decode_header(None) is None
|
||||
|
||||
def test_stringa_vuota_ritorna_none(self):
|
||||
assert decode_header("") is None
|
||||
|
||||
def test_utf8_base64(self):
|
||||
# "PEC test" in base64 UTF-8
|
||||
encoded = "=?utf-8?b?UEVDIHRlc3Q=?="
|
||||
assert decode_header(encoded) == "PEC test"
|
||||
|
||||
def test_iso8859_quoted_printable(self):
|
||||
# "Multa n. 123" in QP ISO-8859-1
|
||||
encoded = "=?iso-8859-1?q?Multa_n=2E_123?="
|
||||
result = decode_header(encoded)
|
||||
assert result is not None
|
||||
assert "Multa" in result
|
||||
assert "123" in result
|
||||
|
||||
def test_multipart_header(self):
|
||||
# Header con più parti encodate
|
||||
encoded = "=?utf-8?b?UEVD?= =?utf-8?b?IHRlc3Q=?="
|
||||
result = decode_header(encoded)
|
||||
assert result is not None
|
||||
assert "PEC" in result
|
||||
|
||||
def test_stringa_gia_decodificata(self):
|
||||
assert decode_header("Oggetto normale") == "Oggetto normale"
|
||||
|
||||
|
||||
# ─── Test extract_addresses ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestExtractAddresses:
|
||||
|
||||
def test_singolo_indirizzo(self):
|
||||
addrs = extract_addresses("test@example.com")
|
||||
assert "test@example.com" in addrs
|
||||
|
||||
def test_multipli_indirizzi(self):
|
||||
addrs = extract_addresses("a@x.com, b@y.com, c@z.com")
|
||||
assert len(addrs) == 3
|
||||
assert "a@x.com" in addrs
|
||||
assert "b@y.com" in addrs
|
||||
assert "c@z.com" in addrs
|
||||
|
||||
def test_display_name(self):
|
||||
addrs = extract_addresses('"Mario Rossi" <mario@comune.it>')
|
||||
assert "mario@comune.it" in addrs
|
||||
|
||||
def test_none_ritorna_lista_vuota(self):
|
||||
assert extract_addresses(None) == []
|
||||
|
||||
def test_stringa_vuota_ritorna_lista_vuota(self):
|
||||
assert extract_addresses("") == []
|
||||
|
||||
|
||||
# ─── Test parse_date ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestParseDate:
|
||||
|
||||
def test_data_valida(self):
|
||||
d = parse_date("Wed, 18 Mar 2026 14:00:00 +0100")
|
||||
assert d is not None
|
||||
assert d.year == 2026
|
||||
assert d.month == 3
|
||||
assert d.day == 18
|
||||
|
||||
def test_none_ritorna_none(self):
|
||||
assert parse_date(None) is None
|
||||
|
||||
def test_stringa_invalida_ritorna_none(self):
|
||||
assert parse_date("non-una-data") is None
|
||||
|
||||
def test_data_senza_timezone_aggiunge_utc(self):
|
||||
d = parse_date("18 Mar 2026 14:00:00 +0000")
|
||||
assert d is not None
|
||||
assert d.tzinfo is not None
|
||||
|
||||
|
||||
# ─── Test parse_eml – messaggio semplice ──────────────────────────────────────
|
||||
|
||||
|
||||
class TestParseEmlSimple:
|
||||
|
||||
def test_subject(self):
|
||||
p = parse_eml(SIMPLE_EML)
|
||||
assert p.subject == "Test PEC Fase 3"
|
||||
|
||||
def test_from_address(self):
|
||||
p = parse_eml(SIMPLE_EML)
|
||||
assert p.from_address == "mittente@pec.it"
|
||||
|
||||
def test_to_addresses(self):
|
||||
p = parse_eml(SIMPLE_EML)
|
||||
assert "destinatario@pec.it" in p.to_addresses
|
||||
|
||||
def test_cc_addresses(self):
|
||||
p = parse_eml(SIMPLE_EML)
|
||||
assert "copia@pec.it" in p.cc_addresses
|
||||
|
||||
def test_message_id(self):
|
||||
p = parse_eml(SIMPLE_EML)
|
||||
assert p.message_id == "<test123@pec.it>"
|
||||
|
||||
def test_date(self):
|
||||
p = parse_eml(SIMPLE_EML)
|
||||
assert p.date is not None
|
||||
assert p.date.year == 2026
|
||||
|
||||
def test_body_text(self):
|
||||
p = parse_eml(SIMPLE_EML)
|
||||
assert p.body_text is not None
|
||||
assert "Corpo del messaggio" in p.body_text
|
||||
assert "Seconda riga" in p.body_text
|
||||
|
||||
def test_no_html(self):
|
||||
p = parse_eml(SIMPLE_EML)
|
||||
assert p.body_html is None
|
||||
|
||||
def test_no_attachments(self):
|
||||
p = parse_eml(SIMPLE_EML)
|
||||
assert p.attachments == []
|
||||
assert p.has_attachments is False
|
||||
|
||||
def test_raw_message_presente(self):
|
||||
p = parse_eml(SIMPLE_EML)
|
||||
assert p.raw_message is not None
|
||||
assert isinstance(p.raw_message, email.message.Message)
|
||||
|
||||
|
||||
# ─── Test parse_eml – multipart con allegato ──────────────────────────────────
|
||||
|
||||
|
||||
class TestParseEmlMultipart:
|
||||
|
||||
def test_body_text_estratto(self):
|
||||
p = parse_eml(MULTIPART_EML)
|
||||
assert p.body_text is not None
|
||||
assert "Testo del messaggio" in p.body_text
|
||||
|
||||
def test_allegato_trovato(self):
|
||||
p = parse_eml(MULTIPART_EML)
|
||||
assert len(p.attachments) == 1
|
||||
att = p.attachments[0]
|
||||
assert att.filename == "documento.pdf"
|
||||
assert att.content_type == "application/pdf"
|
||||
assert att.size_bytes > 0
|
||||
assert att.checksum_sha256 is not None
|
||||
assert len(att.checksum_sha256) == 64
|
||||
|
||||
def test_has_attachments_true(self):
|
||||
p = parse_eml(MULTIPART_EML)
|
||||
assert p.has_attachments is True
|
||||
|
||||
def test_allegati_multipli(self):
|
||||
p = parse_eml(MULTIPLE_ATTACHMENTS_EML)
|
||||
filenames = [a.filename for a in p.attachments]
|
||||
assert "doc1.pdf" in filenames
|
||||
assert "doc2.pdf" in filenames
|
||||
assert len(p.attachments) == 2
|
||||
|
||||
|
||||
# ─── Test parse_eml – multipart/alternative ───────────────────────────────────
|
||||
|
||||
|
||||
class TestParseEmlAlternative:
|
||||
|
||||
def test_body_text_e_html(self):
|
||||
p = parse_eml(MULTIPART_HTML_EML)
|
||||
assert p.body_text is not None
|
||||
assert "Testo piano" in p.body_text
|
||||
assert p.body_html is not None
|
||||
assert "<html>" in p.body_html
|
||||
assert "Testo HTML" in p.body_html
|
||||
|
||||
def test_no_attachments_in_alternative(self):
|
||||
p = parse_eml(MULTIPART_HTML_EML)
|
||||
assert p.has_attachments is False
|
||||
|
||||
|
||||
# ─── Test parse_eml – ricevuta con EML-in-EML ────────────────────────────────
|
||||
|
||||
|
||||
class TestParseEmlReceiptWithNested:
|
||||
|
||||
def test_body_text_ricevuta(self):
|
||||
p = parse_eml(RECEIPT_EML_WITH_NESTED)
|
||||
assert p.body_text is not None
|
||||
assert "consegnato" in p.body_text.lower()
|
||||
|
||||
def test_allegato_xml_daticert(self):
|
||||
p = parse_eml(RECEIPT_EML_WITH_NESTED)
|
||||
filenames = [a.filename for a in p.attachments]
|
||||
assert "daticert.xml" in filenames
|
||||
|
||||
def test_allegato_xml_e_pec_system(self):
|
||||
p = parse_eml(RECEIPT_EML_WITH_NESTED)
|
||||
xml_att = next(a for a in p.attachments if a.filename == "daticert.xml")
|
||||
assert xml_att.is_pec_system is True
|
||||
|
||||
def test_eml_annidato_trovato(self):
|
||||
"""Il messaggio originale annidato deve essere presente come allegato."""
|
||||
p = parse_eml(RECEIPT_EML_WITH_NESTED)
|
||||
eml_atts = [a for a in p.attachments if a.content_type == "message/rfc822"]
|
||||
assert len(eml_atts) >= 1
|
||||
|
||||
def test_has_attachments_false_quando_solo_system(self):
|
||||
"""has_attachments deve essere False se ci sono solo allegati PEC di sistema."""
|
||||
p = parse_eml(PEC_SYSTEM_EML)
|
||||
# daticert.xml e postacert.eml sono entrambi system → has_attachments = False
|
||||
assert p.has_attachments is False
|
||||
|
||||
def test_allegati_sistema_marcati_correttamente(self):
|
||||
p = parse_eml(PEC_SYSTEM_EML)
|
||||
for att in p.attachments:
|
||||
if att.filename in ("daticert.xml", "postacert.eml"):
|
||||
assert att.is_pec_system is True, f"{att.filename} dovrebbe essere is_pec_system=True"
|
||||
|
||||
|
||||
# ─── Test parse_eml – edge cases ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestParseEmlEdgeCases:
|
||||
|
||||
def test_eml_vuoto_no_eccezione(self):
|
||||
p = parse_eml(b"")
|
||||
assert isinstance(p, ParsedEmail)
|
||||
assert p.subject is None
|
||||
assert p.body_text is None
|
||||
assert p.attachments == []
|
||||
|
||||
def test_eml_malformato_no_eccezione(self):
|
||||
p = parse_eml(b"questo non e' un EML valido\x00\xFF")
|
||||
assert isinstance(p, ParsedEmail)
|
||||
|
||||
def test_headers_mancanti(self):
|
||||
raw = b"Content-Type: text/plain\r\n\r\nSolo corpo."
|
||||
p = parse_eml(raw)
|
||||
assert p.subject is None
|
||||
assert p.from_address is None
|
||||
assert p.to_addresses == []
|
||||
|
||||
def test_body_con_encoding_windows1252(self):
|
||||
raw = (
|
||||
b"From: a@pec.it\r\nTo: b@pec.it\r\n"
|
||||
b"Content-Type: text/plain; charset=windows-1252\r\n\r\n"
|
||||
b"Buonagiornata\xe0 tutti"
|
||||
)
|
||||
p = parse_eml(raw)
|
||||
assert p.body_text is not None
|
||||
assert "Buonagiornata" in p.body_text
|
||||
|
||||
def test_attachments_senza_filename_ignorati(self):
|
||||
"""
|
||||
Un part senza filename non deve essere aggiunto come allegato
|
||||
se non è text/plain o text/html.
|
||||
"""
|
||||
raw = (
|
||||
b"From: a@pec.it\r\nTo: b@pec.it\r\n"
|
||||
b'Content-Type: multipart/mixed; boundary="B"\r\n\r\n'
|
||||
b"--B\r\nContent-Type: text/plain\r\n\r\nBody\r\n"
|
||||
b"--B\r\nContent-Type: application/octet-stream\r\n"
|
||||
b"Content-Disposition: attachment\r\n\r\nDATA\r\n"
|
||||
b"--B--\r\n"
|
||||
)
|
||||
p = parse_eml(raw)
|
||||
# L'allegato senza filename non deve comparire
|
||||
for att in p.attachments:
|
||||
assert att.filename is not None and att.filename != ""
|
||||
|
||||
def test_checksum_sha256_corretto(self):
|
||||
"""Il checksum SHA-256 dell'allegato deve essere valido."""
|
||||
import hashlib
|
||||
p = parse_eml(MULTIPART_EML)
|
||||
assert len(p.attachments) == 1
|
||||
att = p.attachments[0]
|
||||
expected = hashlib.sha256(att.content).hexdigest()
|
||||
assert att.checksum_sha256 == expected
|
||||
|
||||
|
||||
# ─── Test AttachmentInfo dataclass ────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestAttachmentInfoDataclass:
|
||||
|
||||
def test_campi_base(self):
|
||||
import hashlib
|
||||
content = b"test content"
|
||||
att = AttachmentInfo(
|
||||
filename="test.pdf",
|
||||
content_type="application/pdf",
|
||||
content=content,
|
||||
size_bytes=len(content),
|
||||
checksum_sha256=hashlib.sha256(content).hexdigest(),
|
||||
)
|
||||
assert att.filename == "test.pdf"
|
||||
assert att.content_type == "application/pdf"
|
||||
assert att.size_bytes == 12
|
||||
assert att.is_inline is False
|
||||
assert att.is_pec_system is False
|
||||
|
||||
def test_inline_flag(self):
|
||||
import hashlib
|
||||
content = b"img"
|
||||
att = AttachmentInfo(
|
||||
filename="img.png",
|
||||
content_type="image/png",
|
||||
content=content,
|
||||
size_bytes=len(content),
|
||||
checksum_sha256=hashlib.sha256(content).hexdigest(),
|
||||
is_inline=True,
|
||||
)
|
||||
assert att.is_inline is True
|
||||
@@ -0,0 +1,385 @@
|
||||
"""
|
||||
Test unitari per app.parsers.pec_parser.
|
||||
|
||||
Copertura:
|
||||
- Tutti i tipi di ricevuta PEC (accettazione, avvenuta-consegna, ecc.)
|
||||
- Varianti per provider: Aruba, Namirial, Legalmail/Poste, InfoCert
|
||||
- Precedenza X-TipoRicevuta su X-Ricevuta
|
||||
- Estrazione X-Riferimento-Message-ID
|
||||
- State machine outbound (apply_outbound_transition)
|
||||
- Funzione get_state_transition per tutti i tipi
|
||||
"""
|
||||
|
||||
import email
|
||||
|
||||
import pytest
|
||||
|
||||
from app.parsers.pec_parser import (
|
||||
VALID_OUTBOUND_TRANSITIONS,
|
||||
PecClassification,
|
||||
apply_outbound_transition,
|
||||
classify_pec_message,
|
||||
get_state_transition,
|
||||
)
|
||||
|
||||
|
||||
# ─── Helper per costruire messaggi di test ────────────────────────────────────
|
||||
|
||||
|
||||
def make_msg(headers: dict[str, str], body: str = "") -> email.message.Message:
|
||||
"""Costruisce un email.message.Message con gli header dati."""
|
||||
raw = ""
|
||||
for k, v in headers.items():
|
||||
raw += f"{k}: {v}\r\n"
|
||||
raw += f"\r\n{body}"
|
||||
return email.message_from_string(raw)
|
||||
|
||||
|
||||
# ─── Test classificazione tipi ricevuta (provider standard / Aruba) ───────────
|
||||
|
||||
|
||||
class TestArubaProvider:
|
||||
"""Header con trattini, stile Aruba PEC (provider più diffuso)."""
|
||||
|
||||
def test_accettazione(self):
|
||||
msg = make_msg({"X-Ricevuta": "accettazione"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "accettazione"
|
||||
assert result.is_receipt is True
|
||||
|
||||
def test_avvenuta_consegna(self):
|
||||
msg = make_msg({"X-Ricevuta": "avvenuta-consegna"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "avvenuta_consegna"
|
||||
assert result.is_receipt is True
|
||||
|
||||
def test_mancata_consegna(self):
|
||||
msg = make_msg({"X-Ricevuta": "mancata-consegna"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "mancata_consegna"
|
||||
assert result.is_receipt is True
|
||||
|
||||
def test_non_accettazione(self):
|
||||
msg = make_msg({"X-Ricevuta": "non-accettazione"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "non_accettazione"
|
||||
assert result.is_receipt is True
|
||||
|
||||
def test_presa_in_carico(self):
|
||||
msg = make_msg({"X-Ricevuta": "presa-in-carico"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "presa_in_carico"
|
||||
assert result.is_receipt is True
|
||||
|
||||
def test_errore_consegna(self):
|
||||
msg = make_msg({"X-Ricevuta": "errore-consegna"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "errore_consegna"
|
||||
assert result.is_receipt is True
|
||||
|
||||
def test_preavviso_mancata_consegna(self):
|
||||
msg = make_msg({"X-Ricevuta": "preavviso-mancata-consegna"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "preavviso_mancata_consegna"
|
||||
assert result.is_receipt is True
|
||||
|
||||
def test_rilevazione_virus(self):
|
||||
msg = make_msg({"X-Ricevuta": "rilevazione-virus"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "rilevazione_virus"
|
||||
assert result.is_receipt is True
|
||||
|
||||
def test_posta_certificata_assenza_header(self):
|
||||
"""Messaggio PEC originale: nessun header X-Ricevuta."""
|
||||
msg = make_msg({
|
||||
"From": "mittente@pec.it",
|
||||
"To": "destinatario@pec.it",
|
||||
"Subject": "Test PEC",
|
||||
})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "posta_certificata"
|
||||
assert result.is_receipt is False
|
||||
|
||||
def test_posta_certificata_esplicita(self):
|
||||
"""X-Ricevuta: posta-certificata (usato da Aruba per indicare il messaggio stesso)."""
|
||||
msg = make_msg({"X-Ricevuta": "posta-certificata"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "posta_certificata"
|
||||
assert result.is_receipt is False
|
||||
|
||||
|
||||
# ─── Test provider Namirial (usa X-TipoRicevuta) ─────────────────────────────
|
||||
|
||||
|
||||
class TestNamirialProvider:
|
||||
"""Namirial usa X-TipoRicevuta invece di X-Ricevuta."""
|
||||
|
||||
def test_x_tipo_ricevuta_avvenuta_consegna(self):
|
||||
msg = make_msg({"X-TipoRicevuta": "avvenuta-consegna"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "avvenuta_consegna"
|
||||
assert result.x_tipo_ricevuta == "avvenuta-consegna"
|
||||
|
||||
def test_x_tipo_ricevuta_accettazione(self):
|
||||
msg = make_msg({"X-TipoRicevuta": "accettazione"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "accettazione"
|
||||
|
||||
def test_x_tipo_ricevuta_ha_precedenza_su_x_ricevuta(self):
|
||||
"""X-TipoRicevuta deve avere precedenza su X-Ricevuta."""
|
||||
msg = make_msg({
|
||||
"X-Ricevuta": "accettazione",
|
||||
"X-TipoRicevuta": "avvenuta-consegna",
|
||||
})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "avvenuta_consegna" # X-TipoRicevuta vince
|
||||
|
||||
def test_mancata_consegna(self):
|
||||
msg = make_msg({"X-TipoRicevuta": "mancata-consegna"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "mancata_consegna"
|
||||
|
||||
|
||||
# ─── Test provider Legalmail/Poste (varianti underscore e spazi) ──────────────
|
||||
|
||||
|
||||
class TestLegalmailProvider:
|
||||
"""Legalmail/Poste Italiane può usare underscore o spazi nei valori."""
|
||||
|
||||
def test_avvenuta_consegna_underscore(self):
|
||||
msg = make_msg({"X-Ricevuta": "avvenuta_consegna"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "avvenuta_consegna"
|
||||
|
||||
def test_non_accettazione_underscore(self):
|
||||
msg = make_msg({"X-Ricevuta": "non_accettazione"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "non_accettazione"
|
||||
|
||||
def test_mancata_consegna_spazio(self):
|
||||
msg = make_msg({"X-Ricevuta": "mancata consegna"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "mancata_consegna"
|
||||
|
||||
def test_avvenuta_consegna_spazio(self):
|
||||
msg = make_msg({"X-Ricevuta": "avvenuta consegna"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "avvenuta_consegna"
|
||||
|
||||
def test_presa_in_carico_underscore(self):
|
||||
msg = make_msg({"X-Ricevuta": "presa_in_carico"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "presa_in_carico"
|
||||
|
||||
def test_abbreviazione_consegna(self):
|
||||
"""Legalmail usa talvolta solo 'consegna' come abbreviazione."""
|
||||
msg = make_msg({"X-Ricevuta": "consegna"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "avvenuta_consegna"
|
||||
|
||||
def test_rilevazione_virus_underscore(self):
|
||||
msg = make_msg({"X-Ricevuta": "rilevazione_virus"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "rilevazione_virus"
|
||||
|
||||
|
||||
# ─── Test case insensitivity ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestCaseInsensitivity:
|
||||
"""I valori degli header PEC devono essere trattati case-insensitive."""
|
||||
|
||||
def test_uppercase_accettazione(self):
|
||||
msg = make_msg({"X-Ricevuta": "ACCETTAZIONE"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "accettazione"
|
||||
|
||||
def test_mixed_case_avvenuta_consegna(self):
|
||||
msg = make_msg({"X-Ricevuta": "Avvenuta-Consegna"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "avvenuta_consegna"
|
||||
|
||||
def test_uppercase_x_tipo(self):
|
||||
msg = make_msg({"X-TipoRicevuta": "MANCATA-CONSEGNA"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "mancata_consegna"
|
||||
|
||||
|
||||
# ─── Test X-Riferimento-Message-ID ───────────────────────────────────────────
|
||||
|
||||
|
||||
class TestRiferimentoMessageId:
|
||||
"""Il campo X-Riferimento-Message-ID collega la ricevuta al messaggio originale."""
|
||||
|
||||
def test_estrazione_riferimento(self):
|
||||
msg = make_msg({
|
||||
"X-Ricevuta": "avvenuta-consegna",
|
||||
"X-Riferimento-Message-ID": "<msg123@pec.it>",
|
||||
})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.riferimento_message_id == "<msg123@pec.it>"
|
||||
|
||||
def test_riferimento_assente(self):
|
||||
msg = make_msg({"X-Ricevuta": "accettazione"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.riferimento_message_id is None
|
||||
|
||||
def test_posta_certificata_senza_riferimento(self):
|
||||
"""I messaggi originali non hanno X-Riferimento-Message-ID."""
|
||||
msg = make_msg({"From": "mittente@pec.it", "To": "dest@pec.it"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.riferimento_message_id is None
|
||||
|
||||
def test_riferimento_con_spazi_extra(self):
|
||||
"""Il valore deve essere pulito da spazi iniziali/finali."""
|
||||
msg = make_msg({
|
||||
"X-Ricevuta": "accettazione",
|
||||
"X-Riferimento-Message-ID": " <msg456@pec.it> ",
|
||||
})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.riferimento_message_id == "<msg456@pec.it>"
|
||||
|
||||
def test_x_raw_headers_conservati(self):
|
||||
"""I valori raw degli header devono essere conservati nel risultato."""
|
||||
msg = make_msg({
|
||||
"X-Ricevuta": "accettazione",
|
||||
"X-TipoRicevuta": "avvenuta-consegna",
|
||||
})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.x_ricevuta == "accettazione"
|
||||
assert result.x_tipo_ricevuta == "avvenuta-consegna"
|
||||
|
||||
def test_tipo_sconosciuto_mappa_posta_certificata(self):
|
||||
"""Valori sconosciuti devono mappare a posta_certificata."""
|
||||
msg = make_msg({"X-Ricevuta": "valore-sconosciuto"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "posta_certificata"
|
||||
assert result.is_receipt is False
|
||||
|
||||
|
||||
# ─── Test state machine outbound ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestStateMachine:
|
||||
"""Test della state machine per messaggi outbound."""
|
||||
|
||||
# ── get_state_transition ──────────────────────────────────────────────────
|
||||
|
||||
def test_accettazione_produce_accepted(self):
|
||||
assert get_state_transition("accettazione") == "accepted"
|
||||
|
||||
def test_presa_in_carico_produce_accepted(self):
|
||||
assert get_state_transition("presa_in_carico") == "accepted"
|
||||
|
||||
def test_avvenuta_consegna_produce_delivered(self):
|
||||
assert get_state_transition("avvenuta_consegna") == "delivered"
|
||||
|
||||
def test_non_accettazione_produce_anomaly(self):
|
||||
assert get_state_transition("non_accettazione") == "anomaly"
|
||||
|
||||
def test_mancata_consegna_produce_anomaly(self):
|
||||
assert get_state_transition("mancata_consegna") == "anomaly"
|
||||
|
||||
def test_errore_consegna_produce_anomaly(self):
|
||||
assert get_state_transition("errore_consegna") == "anomaly"
|
||||
|
||||
def test_preavviso_mancata_produce_anomaly(self):
|
||||
assert get_state_transition("preavviso_mancata_consegna") == "anomaly"
|
||||
|
||||
def test_rilevazione_virus_produce_anomaly(self):
|
||||
assert get_state_transition("rilevazione_virus") == "anomaly"
|
||||
|
||||
def test_posta_certificata_nessuna_transizione(self):
|
||||
"""posta_certificata non è una ricevuta: nessuna transizione."""
|
||||
assert get_state_transition("posta_certificata") is None
|
||||
|
||||
def test_tipo_sconosciuto_nessuna_transizione(self):
|
||||
assert get_state_transition("unknown") is None
|
||||
|
||||
# ── apply_outbound_transition ─────────────────────────────────────────────
|
||||
|
||||
def test_sent_plus_accettazione_da_accepted(self):
|
||||
assert apply_outbound_transition("sent", "accettazione") == "accepted"
|
||||
|
||||
def test_queued_plus_accettazione_da_accepted(self):
|
||||
assert apply_outbound_transition("queued", "accettazione") == "accepted"
|
||||
|
||||
def test_accepted_plus_avvenuta_consegna_da_delivered(self):
|
||||
assert apply_outbound_transition("accepted", "avvenuta_consegna") == "delivered"
|
||||
|
||||
def test_sent_plus_avvenuta_consegna_non_valido(self):
|
||||
"""Non si può saltare direttamente da sent a delivered."""
|
||||
assert apply_outbound_transition("sent", "avvenuta_consegna") is None
|
||||
|
||||
def test_delivered_terminal(self):
|
||||
"""delivered è uno stato terminale: nessuna transizione possibile."""
|
||||
assert apply_outbound_transition("delivered", "avvenuta_consegna") is None
|
||||
assert apply_outbound_transition("delivered", "mancata_consegna") is None
|
||||
|
||||
def test_anomaly_terminal(self):
|
||||
"""anomaly è uno stato terminale: nessuna transizione possibile."""
|
||||
assert apply_outbound_transition("anomaly", "avvenuta_consegna") is None
|
||||
|
||||
def test_sent_plus_mancata_consegna_da_anomaly(self):
|
||||
assert apply_outbound_transition("sent", "mancata_consegna") == "anomaly"
|
||||
|
||||
def test_accepted_plus_non_accettazione_da_anomaly(self):
|
||||
assert apply_outbound_transition("accepted", "non_accettazione") == "anomaly"
|
||||
|
||||
def test_posta_certificata_nessun_effetto(self):
|
||||
"""posta_certificata non è una ricevuta: nessuna transizione."""
|
||||
assert apply_outbound_transition("sent", "posta_certificata") is None
|
||||
|
||||
# ── VALID_OUTBOUND_TRANSITIONS struttura ──────────────────────────────────
|
||||
|
||||
def test_valid_transitions_queued(self):
|
||||
assert "accepted" in VALID_OUTBOUND_TRANSITIONS["queued"]
|
||||
assert "anomaly" in VALID_OUTBOUND_TRANSITIONS["queued"]
|
||||
assert "delivered" not in VALID_OUTBOUND_TRANSITIONS["queued"]
|
||||
|
||||
def test_valid_transitions_sent(self):
|
||||
assert "accepted" in VALID_OUTBOUND_TRANSITIONS["sent"]
|
||||
assert "anomaly" in VALID_OUTBOUND_TRANSITIONS["sent"]
|
||||
|
||||
def test_valid_transitions_accepted(self):
|
||||
assert "delivered" in VALID_OUTBOUND_TRANSITIONS["accepted"]
|
||||
assert "anomaly" in VALID_OUTBOUND_TRANSITIONS["accepted"]
|
||||
assert "accepted" not in VALID_OUTBOUND_TRANSITIONS["accepted"]
|
||||
|
||||
def test_delivered_non_in_valid_transitions(self):
|
||||
"""delivered è terminale: non deve comparire come chiave."""
|
||||
assert "delivered" not in VALID_OUTBOUND_TRANSITIONS
|
||||
|
||||
def test_anomaly_non_in_valid_transitions(self):
|
||||
"""anomaly è terminale: non deve comparire come chiave."""
|
||||
assert "anomaly" not in VALID_OUTBOUND_TRANSITIONS
|
||||
|
||||
|
||||
# ─── Test PecClassification dataclass ────────────────────────────────────────
|
||||
|
||||
|
||||
class TestPecClassificationDataclass:
|
||||
"""Test dei campi del dataclass PecClassification."""
|
||||
|
||||
def test_tutti_i_campi_presenti(self):
|
||||
msg = make_msg({
|
||||
"X-Ricevuta": "accettazione",
|
||||
"X-TipoRicevuta": "avvenuta-consegna",
|
||||
"X-Riferimento-Message-ID": "<ref@pec.it>",
|
||||
})
|
||||
result = classify_pec_message(msg)
|
||||
assert isinstance(result, PecClassification)
|
||||
assert result.pec_type == "avvenuta_consegna"
|
||||
assert result.is_receipt is True
|
||||
assert result.riferimento_message_id == "<ref@pec.it>"
|
||||
assert result.x_ricevuta == "accettazione"
|
||||
assert result.x_tipo_ricevuta == "avvenuta-consegna"
|
||||
|
||||
def test_messaggio_puro_tutti_none(self):
|
||||
msg = make_msg({"From": "a@pec.it", "To": "b@pec.it"})
|
||||
result = classify_pec_message(msg)
|
||||
assert result.pec_type == "posta_certificata"
|
||||
assert result.is_receipt is False
|
||||
assert result.riferimento_message_id is None
|
||||
assert result.x_ricevuta is None
|
||||
assert result.x_tipo_ricevuta is None
|
||||
@@ -0,0 +1,393 @@
|
||||
"""
|
||||
Test unitari per app.parsers.receipt_extractor.
|
||||
|
||||
Copertura:
|
||||
- Estrazione EML annidato (message/rfc822) da ricevuta PEC
|
||||
- Campi estratti dall'EML annidato (message_id, subject, from, to)
|
||||
- Estrazione XML strutturato (daticert.xml)
|
||||
- Messaggio senza EML annidato (ritorna None)
|
||||
- Messaggio non-multipart (ritorna None)
|
||||
- Payload come lista di Message (forma canonica RFC)
|
||||
- Payload come bytes/stringa
|
||||
- Ricevute di tutti i tipi (accettazione, avvenuta-consegna, ecc.)
|
||||
"""
|
||||
|
||||
import email
|
||||
|
||||
import pytest
|
||||
|
||||
from app.parsers.receipt_extractor import (
|
||||
NestedEmlInfo,
|
||||
extract_nested_eml,
|
||||
extract_receipt_xml,
|
||||
)
|
||||
|
||||
|
||||
# ─── Fixture EML ──────────────────────────────────────────────────────────────
|
||||
|
||||
# Ricevuta di accettazione (Aruba style) con EML annidato
|
||||
ACCETTAZIONE_EML = b"""\
|
||||
From: posta-certificata@pec.aruba.it
|
||||
To: mittente@pec.it
|
||||
Subject: ACCETTAZIONE: Test PEC
|
||||
X-Ricevuta: accettazione
|
||||
X-Riferimento-Message-ID: <orig-001@pec.it>
|
||||
Date: Wed, 18 Mar 2026 14:01:00 +0100
|
||||
Content-Type: multipart/mixed; boundary="====acc===="
|
||||
|
||||
--====acc====
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Il messaggio e' stato accettato dal gestore.
|
||||
|
||||
--====acc====
|
||||
Content-Type: application/xml; name="daticert.xml"
|
||||
Content-Disposition: attachment; filename="daticert.xml"
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<PostaCertificata versione="2.3">
|
||||
<Intestazione>
|
||||
<Identificativo>acc-12345</Identificativo>
|
||||
<TipoRicevuta>accettazione</TipoRicevuta>
|
||||
</Intestazione>
|
||||
</PostaCertificata>
|
||||
|
||||
--====acc====
|
||||
Content-Type: message/rfc822
|
||||
Content-Disposition: inline
|
||||
|
||||
From: mittente@pec.it
|
||||
To: destinatario@pec.it
|
||||
Subject: Test PEC originale
|
||||
Message-ID: <orig-001@pec.it>
|
||||
Date: Wed, 18 Mar 2026 14:00:00 +0100
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Corpo del messaggio originale inviato.
|
||||
|
||||
--====acc====--
|
||||
"""
|
||||
|
||||
# Ricevuta di avvenuta consegna con EML annidato
|
||||
AVVENUTA_CONSEGNA_EML = b"""\
|
||||
From: posta-certificata@pec.aruba.it
|
||||
To: mittente@pec.it
|
||||
Subject: CONSEGNA: Test PEC
|
||||
X-Ricevuta: avvenuta-consegna
|
||||
X-Riferimento-Message-ID: <orig-002@pec.it>
|
||||
Date: Wed, 18 Mar 2026 14:10:00 +0100
|
||||
Content-Type: multipart/mixed; boundary="====cons===="
|
||||
|
||||
--====cons====
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Il messaggio e' stato consegnato al destinatario.
|
||||
|
||||
--====cons====
|
||||
Content-Type: application/xml; name="daticert.xml"
|
||||
Content-Disposition: attachment; filename="daticert.xml"
|
||||
|
||||
<?xml version="1.0"?><PostaCertificata versione="2.3"/>
|
||||
|
||||
--====cons====
|
||||
Content-Type: message/rfc822
|
||||
Content-Disposition: inline
|
||||
|
||||
From: mittente@pec.it
|
||||
To: destinatario@pec.it
|
||||
Subject: Test PEC originale
|
||||
Message-ID: <orig-002@pec.it>
|
||||
Date: Wed, 18 Mar 2026 14:00:00 +0100
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Corpo originale.
|
||||
|
||||
--====cons====--
|
||||
"""
|
||||
|
||||
# Messaggio PEC originale (non ricevuta, senza EML annidato)
|
||||
POSTA_CERTIFICATA_EML = b"""\
|
||||
From: mittente@pec.it
|
||||
To: destinatario@pec.it
|
||||
Subject: Messaggio PEC originale
|
||||
Message-ID: <msg-123@pec.it>
|
||||
Date: Wed, 18 Mar 2026 14:00:00 +0100
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Questo e' il corpo del messaggio PEC.
|
||||
"""
|
||||
|
||||
# Messaggio multipart senza EML annidato
|
||||
MULTIPART_NO_NESTED = b"""\
|
||||
From: a@pec.it
|
||||
To: b@pec.it
|
||||
Subject: PEC con allegato PDF (nessun EML annidato)
|
||||
Date: Wed, 18 Mar 2026 10:00:00 +0100
|
||||
Content-Type: multipart/mixed; boundary="====notest===="
|
||||
|
||||
--====notest====
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Corpo.
|
||||
|
||||
--====notest====
|
||||
Content-Type: application/pdf; name="doc.pdf"
|
||||
Content-Disposition: attachment; filename="doc.pdf"
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
JVBERi0xLjQ=
|
||||
|
||||
--====notest====--
|
||||
"""
|
||||
|
||||
# Ricevuta di mancata consegna con EML annidato (Namirial style)
|
||||
MANCATA_CONSEGNA_EML = b"""\
|
||||
From: no-reply@namirial.com
|
||||
To: mittente@pec.it
|
||||
Subject: MANCATA CONSEGNA: Test
|
||||
X-TipoRicevuta: mancata-consegna
|
||||
X-Riferimento-Message-ID: <orig-003@pec.namirial.it>
|
||||
Date: Wed, 18 Mar 2026 15:00:00 +0100
|
||||
Content-Type: multipart/mixed; boundary="====mc===="
|
||||
|
||||
--====mc====
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Il messaggio non e' stato consegnato.
|
||||
|
||||
--====mc====
|
||||
Content-Type: message/rfc822
|
||||
Content-Disposition: inline
|
||||
|
||||
From: mittente@pec.namirial.it
|
||||
To: destinatario@pec.it
|
||||
Subject: Messaggio non consegnato
|
||||
Message-ID: <orig-003@pec.namirial.it>
|
||||
Date: Wed, 18 Mar 2026 14:30:00 +0100
|
||||
Content-Type: text/plain; charset=utf-8
|
||||
|
||||
Corpo originale che non e' stato consegnato.
|
||||
|
||||
--====mc====--
|
||||
"""
|
||||
|
||||
|
||||
# ─── Test extract_nested_eml ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestExtractNestedEml:
|
||||
|
||||
def test_accettazione_eml_annidato_trovato(self):
|
||||
msg = email.message_from_bytes(ACCETTAZIONE_EML)
|
||||
result = extract_nested_eml(msg)
|
||||
assert result is not None
|
||||
|
||||
def test_accettazione_message_id(self):
|
||||
msg = email.message_from_bytes(ACCETTAZIONE_EML)
|
||||
result = extract_nested_eml(msg)
|
||||
assert result is not None
|
||||
assert result.message_id == "<orig-001@pec.it>"
|
||||
|
||||
def test_accettazione_subject(self):
|
||||
msg = email.message_from_bytes(ACCETTAZIONE_EML)
|
||||
result = extract_nested_eml(msg)
|
||||
assert result is not None
|
||||
assert result.subject == "Test PEC originale"
|
||||
|
||||
def test_accettazione_from_address(self):
|
||||
msg = email.message_from_bytes(ACCETTAZIONE_EML)
|
||||
result = extract_nested_eml(msg)
|
||||
assert result is not None
|
||||
assert result.from_address == "mittente@pec.it"
|
||||
|
||||
def test_accettazione_to_addresses(self):
|
||||
msg = email.message_from_bytes(ACCETTAZIONE_EML)
|
||||
result = extract_nested_eml(msg)
|
||||
assert result is not None
|
||||
assert "destinatario@pec.it" in result.to_addresses
|
||||
|
||||
def test_accettazione_raw_bytes_non_vuoti(self):
|
||||
msg = email.message_from_bytes(ACCETTAZIONE_EML)
|
||||
result = extract_nested_eml(msg)
|
||||
assert result is not None
|
||||
assert len(result.raw_bytes) > 0
|
||||
|
||||
def test_avvenuta_consegna_eml_trovato(self):
|
||||
msg = email.message_from_bytes(AVVENUTA_CONSEGNA_EML)
|
||||
result = extract_nested_eml(msg)
|
||||
assert result is not None
|
||||
assert result.message_id == "<orig-002@pec.it>"
|
||||
|
||||
def test_mancata_consegna_namirial(self):
|
||||
"""Test con ricevuta Namirial (X-TipoRicevuta)."""
|
||||
msg = email.message_from_bytes(MANCATA_CONSEGNA_EML)
|
||||
result = extract_nested_eml(msg)
|
||||
assert result is not None
|
||||
assert result.message_id == "<orig-003@pec.namirial.it>"
|
||||
assert result.subject == "Messaggio non consegnato"
|
||||
|
||||
def test_messaggio_non_multipart_ritorna_none(self):
|
||||
"""Un messaggio non-multipart non può avere EML annidato."""
|
||||
msg = email.message_from_bytes(POSTA_CERTIFICATA_EML)
|
||||
result = extract_nested_eml(msg)
|
||||
assert result is None
|
||||
|
||||
def test_multipart_senza_nested_ritorna_none(self):
|
||||
"""Un multipart senza parti message/rfc822 ritorna None."""
|
||||
msg = email.message_from_bytes(MULTIPART_NO_NESTED)
|
||||
result = extract_nested_eml(msg)
|
||||
assert result is None
|
||||
|
||||
def test_risultato_e_nested_eml_info(self):
|
||||
msg = email.message_from_bytes(ACCETTAZIONE_EML)
|
||||
result = extract_nested_eml(msg)
|
||||
assert isinstance(result, NestedEmlInfo)
|
||||
|
||||
def test_raw_bytes_e_valido_eml(self):
|
||||
"""I raw_bytes estratti devono essere un EML valido (ri-parsabile)."""
|
||||
msg = email.message_from_bytes(ACCETTAZIONE_EML)
|
||||
result = extract_nested_eml(msg)
|
||||
assert result is not None
|
||||
# Verifica che i bytes siano ri-parsabili
|
||||
inner = email.message_from_bytes(result.raw_bytes)
|
||||
assert inner.get("Message-ID") == "<orig-001@pec.it>"
|
||||
|
||||
|
||||
# ─── Test extract_receipt_xml ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestExtractReceiptXml:
|
||||
|
||||
def test_daticert_xml_estratto(self):
|
||||
msg = email.message_from_bytes(ACCETTAZIONE_EML)
|
||||
xml = extract_receipt_xml(msg)
|
||||
assert xml is not None
|
||||
assert "PostaCertificata" in xml
|
||||
|
||||
def test_xml_con_contenuto(self):
|
||||
msg = email.message_from_bytes(ACCETTAZIONE_EML)
|
||||
xml = extract_receipt_xml(msg)
|
||||
assert xml is not None
|
||||
assert "accettazione" in xml.lower()
|
||||
|
||||
def test_xml_avvenuta_consegna(self):
|
||||
msg = email.message_from_bytes(AVVENUTA_CONSEGNA_EML)
|
||||
xml = extract_receipt_xml(msg)
|
||||
assert xml is not None
|
||||
assert "PostaCertificata" in xml
|
||||
|
||||
def test_messaggio_senza_xml_ritorna_none(self):
|
||||
"""Un messaggio PEC senza allegato XML ritorna None."""
|
||||
msg = email.message_from_bytes(POSTA_CERTIFICATA_EML)
|
||||
xml = extract_receipt_xml(msg)
|
||||
assert xml is None
|
||||
|
||||
def test_multipart_senza_xml_ritorna_none(self):
|
||||
"""Multipart con solo PDF, senza XML."""
|
||||
msg = email.message_from_bytes(MULTIPART_NO_NESTED)
|
||||
xml = extract_receipt_xml(msg)
|
||||
assert xml is None
|
||||
|
||||
def test_mancata_consegna_senza_xml_ritorna_none(self):
|
||||
"""Alcune ricevute (Namirial) non hanno daticert.xml."""
|
||||
msg = email.message_from_bytes(MANCATA_CONSEGNA_EML)
|
||||
xml = extract_receipt_xml(msg)
|
||||
# Questa ricevuta non ha XML allegato → None
|
||||
assert xml is None
|
||||
|
||||
|
||||
# ─── Test NestedEmlInfo dataclass ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestNestedEmlInfoDataclass:
|
||||
|
||||
def test_campi_base(self):
|
||||
info = NestedEmlInfo(
|
||||
raw_bytes=b"raw",
|
||||
message_id="<test@pec.it>",
|
||||
subject="Test",
|
||||
from_address="a@pec.it",
|
||||
to_addresses=["b@pec.it"],
|
||||
)
|
||||
assert info.raw_bytes == b"raw"
|
||||
assert info.message_id == "<test@pec.it>"
|
||||
assert info.subject == "Test"
|
||||
assert info.from_address == "a@pec.it"
|
||||
assert "b@pec.it" in info.to_addresses
|
||||
|
||||
def test_to_addresses_default_lista_vuota(self):
|
||||
info = NestedEmlInfo(
|
||||
raw_bytes=b"",
|
||||
message_id=None,
|
||||
subject=None,
|
||||
from_address=None,
|
||||
)
|
||||
assert info.to_addresses == []
|
||||
|
||||
def test_campi_opzionali_possono_essere_none(self):
|
||||
info = NestedEmlInfo(
|
||||
raw_bytes=b"data",
|
||||
message_id=None,
|
||||
subject=None,
|
||||
from_address=None,
|
||||
)
|
||||
assert info.message_id is None
|
||||
assert info.subject is None
|
||||
assert info.from_address is None
|
||||
|
||||
|
||||
# ─── Test integrazione pec_parser + receipt_extractor ────────────────────────
|
||||
|
||||
|
||||
class TestIntegrationPecParserAndExtractor:
|
||||
"""
|
||||
Verifica che le informazioni estratte da receipt_extractor corrispondano
|
||||
al X-Riferimento-Message-ID usato da pec_parser per collegare i messaggi.
|
||||
"""
|
||||
|
||||
def test_riferimento_corrisponde_a_message_id_annidato(self):
|
||||
"""
|
||||
X-Riferimento-Message-ID nella ricevuta deve corrispondere
|
||||
al Message-ID dell'EML annidato.
|
||||
"""
|
||||
from app.parsers.pec_parser import classify_pec_message
|
||||
|
||||
msg = email.message_from_bytes(ACCETTAZIONE_EML)
|
||||
|
||||
# Classifica la ricevuta
|
||||
pec_class = classify_pec_message(msg)
|
||||
assert pec_class.riferimento_message_id == "<orig-001@pec.it>"
|
||||
|
||||
# Estrae EML annidato
|
||||
nested = extract_nested_eml(msg)
|
||||
assert nested is not None
|
||||
assert nested.message_id == "<orig-001@pec.it>"
|
||||
|
||||
# Deve corrispondere
|
||||
assert pec_class.riferimento_message_id == nested.message_id
|
||||
|
||||
def test_avvenuta_consegna_corrispondenza_completa(self):
|
||||
from app.parsers.pec_parser import classify_pec_message
|
||||
|
||||
msg = email.message_from_bytes(AVVENUTA_CONSEGNA_EML)
|
||||
|
||||
pec_class = classify_pec_message(msg)
|
||||
assert pec_class.pec_type == "avvenuta_consegna"
|
||||
assert pec_class.riferimento_message_id == "<orig-002@pec.it>"
|
||||
|
||||
nested = extract_nested_eml(msg)
|
||||
assert nested is not None
|
||||
assert nested.message_id == "<orig-002@pec.it>"
|
||||
|
||||
def test_mancata_consegna_namirial_corrispondenza(self):
|
||||
from app.parsers.pec_parser import classify_pec_message
|
||||
|
||||
msg = email.message_from_bytes(MANCATA_CONSEGNA_EML)
|
||||
|
||||
pec_class = classify_pec_message(msg)
|
||||
assert pec_class.pec_type == "mancata_consegna"
|
||||
assert pec_class.riferimento_message_id == "<orig-003@pec.namirial.it>"
|
||||
|
||||
nested = extract_nested_eml(msg)
|
||||
assert nested is not None
|
||||
assert nested.message_id == "<orig-003@pec.namirial.it>"
|
||||
Reference in New Issue
Block a user