""" 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": "", }) result = classify_pec_message(msg) assert result.riferimento_message_id == "" 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": " ", }) result = classify_pec_message(msg) assert result.riferimento_message_id == "" 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": "", }) 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 == "" 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