This commit is contained in:
2026-03-18 17:30:13 +01:00
parent 58a233236c
commit d80d912fb3
36 changed files with 3502 additions and 4 deletions
View File
+103
View File
@@ -0,0 +1,103 @@
"""
Unit test: ExponentialBackoff
"""
import asyncio
import time
import pytest
from app.imap.reconnect import ExponentialBackoff
@pytest.mark.asyncio
async def test_backoff_increases():
"""Il tempo di attesa aumenta ad ogni tentativo."""
backoff = ExponentialBackoff(label="test", jitter=False)
# Registra i tempi senza aspettare davvero (patch asyncio.sleep)
waits = []
original_sleep = asyncio.sleep
async def fake_sleep(t):
waits.append(t)
asyncio.sleep = fake_sleep
try:
await backoff.wait()
await backoff.wait()
await backoff.wait()
finally:
asyncio.sleep = original_sleep
assert waits[1] > waits[0], "Il secondo wait deve essere maggiore del primo"
assert waits[2] > waits[1], "Il terzo wait deve essere maggiore del secondo"
@pytest.mark.asyncio
async def test_backoff_reset():
"""reset() riporta il contatore a zero e il wait riparte dal valore iniziale."""
backoff = ExponentialBackoff(label="test", jitter=False)
waits = []
async def fake_sleep(t):
waits.append(t)
original_sleep = asyncio.sleep
asyncio.sleep = fake_sleep
try:
await backoff.wait() # waits[0]: valore iniziale (es. 1.0)
await backoff.wait() # waits[1]: valore incrementato (es. 2.0)
backoff.reset()
await backoff.wait() # waits[2]: deve tornare al valore iniziale
finally:
asyncio.sleep = original_sleep
# Dopo reset il wait riparte dal valore iniziale
assert backoff.attempt == 1
assert waits[2] == waits[0], (
f"Dopo reset il wait deve tornare a {waits[0]}, ma era {waits[2]}"
)
@pytest.mark.asyncio
async def test_backoff_max():
"""Il tempo di attesa non supera backoff_max_seconds."""
from app.config import get_settings
settings = get_settings()
backoff = ExponentialBackoff(label="test", jitter=False)
max_recorded = 0.0
async def fake_sleep(t):
nonlocal max_recorded
max_recorded = max(max_recorded, t)
original_sleep = asyncio.sleep
asyncio.sleep = fake_sleep
try:
for _ in range(20):
await backoff.wait()
finally:
asyncio.sleep = original_sleep
assert max_recorded <= settings.backoff_max_seconds + 0.1
def test_backoff_attempt_count():
"""Il contatore tentativi si incrementa correttamente."""
import asyncio
backoff = ExponentialBackoff(label="test", jitter=False)
async def run():
async def fake_sleep(_):
pass
asyncio.sleep = fake_sleep
await backoff.wait()
await backoff.wait()
await backoff.wait()
asyncio.get_event_loop().run_until_complete(run())
assert backoff.attempt == 3
+197
View File
@@ -0,0 +1,197 @@
"""
Unit test: parsing EML e classificazione tipo PEC.
"""
import pytest
from app.imap.sync import _decode_header, _extract_addresses, _parse_date, _parse_eml, _classify_pec_type
import email
# ─── Test _decode_header ─────────────────────────────────────────────────────
def test_decode_header_plain():
assert _decode_header("Hello World") == "Hello World"
def test_decode_header_none():
assert _decode_header(None) is None
def test_decode_header_encoded():
# Header UTF-8 base64 encoded
encoded = "=?utf-8?b?UEVDIHRlc3Q=?=" # "PEC test"
assert _decode_header(encoded) == "PEC test"
def test_decode_header_encoded_iso():
# Header ISO-8859-1 quoted printable
encoded = "=?iso-8859-1?q?Multa_n=2E_123?=" # "Multa n. 123"
result = _decode_header(encoded)
assert result is not None
assert "Multa" in result
# ─── Test _extract_addresses ──────────────────────────────────────────────────
def test_extract_addresses_single():
addrs = _extract_addresses("test@example.com")
assert "test@example.com" in addrs
def test_extract_addresses_multiple():
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
def test_extract_addresses_with_display_name():
addrs = _extract_addresses('"Mario Rossi" <mario@comune.it>')
assert "mario@comune.it" in addrs
def test_extract_addresses_empty():
assert _extract_addresses(None) == []
assert _extract_addresses("") == []
# ─── Test _parse_date ─────────────────────────────────────────────────────────
def test_parse_date_valid():
date = _parse_date("Wed, 18 Mar 2026 14:00:00 +0100")
assert date is not None
assert date.year == 2026
assert date.month == 3
assert date.day == 18
def test_parse_date_none():
assert _parse_date(None) is None
def test_parse_date_invalid():
assert _parse_date("not-a-date") is None
# ─── Test _classify_pec_type ──────────────────────────────────────────────────
def test_classify_pec_type_avvenuta_consegna():
msg = email.message_from_string(
"From: server@pec.it\r\n"
"X-TipoRicevuta: avvenuta-consegna\r\n"
"\r\n"
)
assert _classify_pec_type(msg) == "avvenuta_consegna"
def test_classify_pec_type_accettazione():
msg = email.message_from_string(
"From: server@pec.it\r\n"
"X-Ricevuta: accettazione\r\n"
"\r\n"
)
assert _classify_pec_type(msg) == "accettazione"
def test_classify_pec_type_posta_certificata():
msg = email.message_from_string(
"From: mittente@pec.it\r\n"
"Subject: Messaggio PEC\r\n"
"\r\n"
)
assert _classify_pec_type(msg) == "posta_certificata"
def test_classify_pec_type_x_tipo_prevalente():
"""X-TipoRicevuta ha precedenza su X-Ricevuta."""
msg = email.message_from_string(
"From: server@pec.it\r\n"
"X-Ricevuta: accettazione\r\n"
"X-TipoRicevuta: avvenuta-consegna\r\n"
"\r\n"
)
assert _classify_pec_type(msg) == "avvenuta_consegna"
# ─── Test _parse_eml completo ─────────────────────────────────────────────────
RAW_EML = b"""From: mittente@pec.it
To: destinatario@pec.it
Cc: copia@pec.it
Subject: Test PEC Fase 2
Message-ID: <test123@pec.it>
Date: Wed, 18 Mar 2026 10:00:00 +0100
Content-Type: text/plain; charset=utf-8
Corpo del messaggio di test.
"""
def test_parse_eml_subject():
parsed = _parse_eml(RAW_EML)
assert parsed["subject"] == "Test PEC Fase 2"
def test_parse_eml_from():
parsed = _parse_eml(RAW_EML)
assert parsed["from_address"] == "mittente@pec.it"
def test_parse_eml_to():
parsed = _parse_eml(RAW_EML)
assert "destinatario@pec.it" in parsed["to_addresses"]
def test_parse_eml_cc():
parsed = _parse_eml(RAW_EML)
assert "copia@pec.it" in parsed["cc_addresses"]
def test_parse_eml_message_id():
parsed = _parse_eml(RAW_EML)
assert parsed["message_id_header"] == "<test123@pec.it>"
def test_parse_eml_body_text():
parsed = _parse_eml(RAW_EML)
assert "Corpo del messaggio" in (parsed.get("body_text") or "")
def test_parse_eml_no_attachments():
parsed = _parse_eml(RAW_EML)
assert parsed["has_attachments"] is False
RAW_EML_WITH_ATTACHMENT = b"""From: mittente@pec.it
To: destinatario@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--
"""
def test_parse_eml_with_attachment():
parsed = _parse_eml(RAW_EML_WITH_ATTACHMENT)
assert parsed["has_attachments"] is True
assert "Testo del messaggio" in (parsed.get("body_text") or "")
def test_parse_eml_empty():
parsed = _parse_eml(b"")
# Non deve sollevare eccezione
assert isinstance(parsed, dict)