Files
PecHub/worker/tests/unit/test_receipt_extractor.py
T
2026-03-18 17:43:03 +01:00

394 lines
12 KiB
Python

"""
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>"