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

386 lines
16 KiB
Python

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