mirror of
https://github.com/idrainformatica/PecFlow.git
synced 2026-06-16 12:45:42 +02:00
fase 4
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
"""
|
||||
Test di integrazione – API invio PEC (POST /send e GET /send/jobs).
|
||||
|
||||
Usa SQLite in-memory + mock dell'arq pool per evitare dipendenze esterne.
|
||||
"""
|
||||
|
||||
import uuid
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
import pytest_asyncio
|
||||
from httpx import AsyncClient
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.core.security import encrypt_credential
|
||||
|
||||
|
||||
# ─── Fixtures ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def active_mailbox(db_session: AsyncSession, demo_tenant):
|
||||
"""Crea una casella PEC attiva nel tenant di test."""
|
||||
from app.models.mailbox import Mailbox
|
||||
|
||||
mailbox = Mailbox(
|
||||
tenant_id=demo_tenant.id,
|
||||
email_address="test@pec.example.it",
|
||||
display_name="Test PEC",
|
||||
provider="test",
|
||||
imap_host_enc=encrypt_credential("imap.example.it"),
|
||||
imap_port_enc=encrypt_credential("993"),
|
||||
imap_user_enc=encrypt_credential("test@pec.example.it"),
|
||||
imap_pass_enc=encrypt_credential("secret"),
|
||||
imap_use_ssl=True,
|
||||
smtp_host_enc=encrypt_credential("smtp.example.it"),
|
||||
smtp_port_enc=encrypt_credential("465"),
|
||||
smtp_user_enc=encrypt_credential("test@pec.example.it"),
|
||||
smtp_pass_enc=encrypt_credential("secret"),
|
||||
smtp_use_tls=True,
|
||||
status="active",
|
||||
)
|
||||
db_session.add(mailbox)
|
||||
await db_session.commit()
|
||||
await db_session.refresh(mailbox)
|
||||
return mailbox
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def auth_headers(admin_token: str) -> dict:
|
||||
"""Header Authorization con token admin."""
|
||||
return {"Authorization": f"Bearer {admin_token}"}
|
||||
|
||||
|
||||
# ─── Test POST /send ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestCreateSendJob:
|
||||
"""Test endpoint POST /api/v1/send."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_requires_authentication(self, client: AsyncClient, active_mailbox):
|
||||
"""Senza token → 401."""
|
||||
response = await client.post(
|
||||
"/api/v1/send",
|
||||
json={
|
||||
"mailbox_id": str(active_mailbox.id),
|
||||
"to_addresses": ["dest@pec.it"],
|
||||
"subject": "Test",
|
||||
"body_text": "corpo",
|
||||
},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_missing_to_addresses(
|
||||
self, client: AsyncClient, auth_headers: dict, active_mailbox
|
||||
):
|
||||
"""Lista destinatari vuota → 422 validazione."""
|
||||
with patch("app.services.send_service._get_arq_pool") as mock_pool:
|
||||
mock_pool.return_value = AsyncMock()
|
||||
mock_pool.return_value.enqueue_job = AsyncMock()
|
||||
|
||||
response = await client.post(
|
||||
"/api/v1/send",
|
||||
json={
|
||||
"mailbox_id": str(active_mailbox.id),
|
||||
"to_addresses": [],
|
||||
"subject": "Test",
|
||||
"body_text": "corpo",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_missing_subject(
|
||||
self, client: AsyncClient, auth_headers: dict, active_mailbox
|
||||
):
|
||||
"""Oggetto vuoto → 422 validazione."""
|
||||
with patch("app.services.send_service._get_arq_pool") as mock_pool:
|
||||
mock_pool.return_value = AsyncMock()
|
||||
response = await client.post(
|
||||
"/api/v1/send",
|
||||
json={
|
||||
"mailbox_id": str(active_mailbox.id),
|
||||
"to_addresses": ["dest@pec.it"],
|
||||
"subject": " ",
|
||||
"body_text": "corpo",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 422
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_mailbox_not_found(self, client: AsyncClient, auth_headers: dict):
|
||||
"""Casella inesistente → 404."""
|
||||
with patch("app.services.send_service._get_arq_pool") as mock_pool:
|
||||
mock_pool.return_value = AsyncMock()
|
||||
response = await client.post(
|
||||
"/api/v1/send",
|
||||
json={
|
||||
"mailbox_id": str(uuid.uuid4()),
|
||||
"to_addresses": ["dest@pec.it"],
|
||||
"subject": "Test",
|
||||
"body_text": "corpo",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_success_creates_job(
|
||||
self, client: AsyncClient, auth_headers: dict, active_mailbox
|
||||
):
|
||||
"""Invio valido → 201 con SendJobResponse."""
|
||||
mock_arq = AsyncMock()
|
||||
mock_arq.enqueue_job = AsyncMock(return_value=None)
|
||||
|
||||
with patch("app.services.send_service._get_arq_pool", return_value=mock_arq):
|
||||
response = await client.post(
|
||||
"/api/v1/send",
|
||||
json={
|
||||
"mailbox_id": str(active_mailbox.id),
|
||||
"to_addresses": ["matteo1801@spidmail.it"],
|
||||
"subject": "Test PecFlow Fase 4",
|
||||
"body_text": "Messaggio di test inviato da PecFlow.",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
data = response.json()
|
||||
assert data["status"] == "pending"
|
||||
assert data["mailbox_id"] == str(active_mailbox.id)
|
||||
assert data["attempt_count"] == 0
|
||||
assert data["max_attempts"] == 5
|
||||
assert "id" in data
|
||||
# Verifica che arq sia stato chiamato
|
||||
mock_arq.enqueue_job.assert_called_once()
|
||||
call_args = mock_arq.enqueue_job.call_args
|
||||
assert call_args[0][0] == "send_pec"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_with_cc(
|
||||
self, client: AsyncClient, auth_headers: dict, active_mailbox
|
||||
):
|
||||
"""Invio con Cc → 201 con cc_addresses nel messaggio."""
|
||||
mock_arq = AsyncMock()
|
||||
mock_arq.enqueue_job = AsyncMock(return_value=None)
|
||||
|
||||
with patch("app.services.send_service._get_arq_pool", return_value=mock_arq):
|
||||
response = await client.post(
|
||||
"/api/v1/send",
|
||||
json={
|
||||
"mailbox_id": str(active_mailbox.id),
|
||||
"to_addresses": ["dest@pec.it"],
|
||||
"cc_addresses": ["cc@pec.it"],
|
||||
"subject": "Test con Cc",
|
||||
"body_text": "corpo",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert response.status_code == 201
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_arq_failure_still_returns_201(
|
||||
self, client: AsyncClient, auth_headers: dict, active_mailbox
|
||||
):
|
||||
"""
|
||||
Se arq fallisce (Redis down), il job viene comunque creato nel DB
|
||||
e l'API risponde 201 (il job resta pending).
|
||||
"""
|
||||
mock_arq = AsyncMock()
|
||||
mock_arq.enqueue_job = AsyncMock(side_effect=ConnectionError("Redis non disponibile"))
|
||||
|
||||
with patch("app.services.send_service._get_arq_pool", return_value=mock_arq):
|
||||
response = await client.post(
|
||||
"/api/v1/send",
|
||||
json={
|
||||
"mailbox_id": str(active_mailbox.id),
|
||||
"to_addresses": ["dest@pec.it"],
|
||||
"subject": "Test Redis down",
|
||||
"body_text": "corpo",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
# Il job deve essere creato anche se Redis fallisce
|
||||
assert response.status_code == 201
|
||||
assert response.json()["status"] == "pending"
|
||||
|
||||
|
||||
# ─── Test GET /send/jobs ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestListSendJobs:
|
||||
"""Test endpoint GET /api/v1/send/jobs."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_requires_authentication(self, client: AsyncClient):
|
||||
"""Senza token → 401."""
|
||||
response = await client.get("/api/v1/send/jobs")
|
||||
assert response.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_returns_empty_for_new_tenant(
|
||||
self, client: AsyncClient, auth_headers: dict
|
||||
):
|
||||
"""Tenant senza job → lista vuota."""
|
||||
response = await client.get("/api/v1/send/jobs", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert "total" in data
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_after_send(
|
||||
self, client: AsyncClient, auth_headers: dict, active_mailbox
|
||||
):
|
||||
"""Dopo un invio, la lista deve contenere almeno un job."""
|
||||
mock_arq = AsyncMock()
|
||||
mock_arq.enqueue_job = AsyncMock(return_value=None)
|
||||
|
||||
# Crea un job
|
||||
with patch("app.services.send_service._get_arq_pool", return_value=mock_arq):
|
||||
await client.post(
|
||||
"/api/v1/send",
|
||||
json={
|
||||
"mailbox_id": str(active_mailbox.id),
|
||||
"to_addresses": ["dest@pec.it"],
|
||||
"subject": "Job per lista",
|
||||
"body_text": "corpo",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
# Lista
|
||||
response = await client.get("/api/v1/send/jobs", headers=auth_headers)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["total"] >= 1
|
||||
|
||||
|
||||
# ─── Test GET /send/jobs/{id} ─────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestGetSendJob:
|
||||
"""Test endpoint GET /api/v1/send/jobs/{job_id}."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_nonexistent_job(self, client: AsyncClient, auth_headers: dict):
|
||||
"""Job inesistente → 404."""
|
||||
response = await client.get(
|
||||
f"/api/v1/send/jobs/{uuid.uuid4()}", headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_existing_job(
|
||||
self, client: AsyncClient, auth_headers: dict, active_mailbox
|
||||
):
|
||||
"""Recupera un job esistente."""
|
||||
mock_arq = AsyncMock()
|
||||
mock_arq.enqueue_job = AsyncMock(return_value=None)
|
||||
|
||||
# Crea
|
||||
with patch("app.services.send_service._get_arq_pool", return_value=mock_arq):
|
||||
create_resp = await client.post(
|
||||
"/api/v1/send",
|
||||
json={
|
||||
"mailbox_id": str(active_mailbox.id),
|
||||
"to_addresses": ["dest@pec.it"],
|
||||
"subject": "Job da recuperare",
|
||||
"body_text": "corpo",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
|
||||
assert create_resp.status_code == 201
|
||||
job_id = create_resp.json()["id"]
|
||||
|
||||
# Recupera
|
||||
response = await client.get(
|
||||
f"/api/v1/send/jobs/{job_id}", headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == job_id
|
||||
|
||||
|
||||
# ─── Test DELETE /send/jobs/{id} ─────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestCancelSendJob:
|
||||
"""Test endpoint DELETE /api/v1/send/jobs/{job_id}."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cancel_pending_job(
|
||||
self, client: AsyncClient, auth_headers: dict, active_mailbox
|
||||
):
|
||||
"""Annulla un job in stato pending → 204."""
|
||||
mock_arq = AsyncMock()
|
||||
mock_arq.enqueue_job = AsyncMock(return_value=None)
|
||||
|
||||
# Crea job
|
||||
with patch("app.services.send_service._get_arq_pool", return_value=mock_arq):
|
||||
create_resp = await client.post(
|
||||
"/api/v1/send",
|
||||
json={
|
||||
"mailbox_id": str(active_mailbox.id),
|
||||
"to_addresses": ["dest@pec.it"],
|
||||
"subject": "Job da annullare",
|
||||
"body_text": "corpo",
|
||||
},
|
||||
headers=auth_headers,
|
||||
)
|
||||
job_id = create_resp.json()["id"]
|
||||
|
||||
# Annulla
|
||||
response = await client.delete(
|
||||
f"/api/v1/send/jobs/{job_id}", headers=auth_headers
|
||||
)
|
||||
assert response.status_code == 204
|
||||
|
||||
# Verifica stato
|
||||
get_resp = await client.get(
|
||||
f"/api/v1/send/jobs/{job_id}", headers=auth_headers
|
||||
)
|
||||
assert get_resp.json()["status"] == "failed"
|
||||
assert "Annullato" in get_resp.json()["last_error"]
|
||||
Reference in New Issue
Block a user