test: add 150 new unit tests covering auth, users, notifications, logs, settings, GDPR, and Gmail label utilities
Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/8cb47b1c-b744-4212-8b6d-8132d6642d98 Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
6e7139645b
commit
58f9ad72ed
@@ -0,0 +1,335 @@
|
||||
"""
|
||||
Unit tests for processing logs / runs endpoints (api/v1/endpoints/logs.py).
|
||||
|
||||
All database interactions and auth dependencies are mocked.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
|
||||
from app.main import create_application
|
||||
from app.core.database import get_db
|
||||
from app.core.deps import get_current_active_user
|
||||
from app.models.database_models import (
|
||||
User,
|
||||
ProcessingRun,
|
||||
ProcessingLog,
|
||||
SubscriptionTier,
|
||||
)
|
||||
|
||||
# ── helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
def _make_user(**overrides) -> MagicMock:
|
||||
defaults = dict(
|
||||
id=1,
|
||||
email="user@example.com",
|
||||
full_name="Test User",
|
||||
is_active=True,
|
||||
is_superuser=False,
|
||||
subscription_tier=SubscriptionTier.FREE,
|
||||
subscription_status="active",
|
||||
google_id=None,
|
||||
oauth_provider=None,
|
||||
last_login_at=None,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
stripe_customer_id=None,
|
||||
stripe_subscription_id=None,
|
||||
subscription_expires_at=None,
|
||||
)
|
||||
defaults.update(overrides)
|
||||
u = MagicMock(spec=User)
|
||||
for k, v in defaults.items():
|
||||
setattr(u, k, v)
|
||||
return u
|
||||
|
||||
|
||||
def _make_run(**overrides) -> MagicMock:
|
||||
defaults = dict(
|
||||
id=1,
|
||||
mail_account_id=10,
|
||||
started_at=datetime.now(timezone.utc),
|
||||
completed_at=datetime.now(timezone.utc),
|
||||
duration_seconds=1.5,
|
||||
emails_fetched=3,
|
||||
emails_forwarded=3,
|
||||
emails_failed=0,
|
||||
status="completed",
|
||||
error_message=None,
|
||||
)
|
||||
defaults.update(overrides)
|
||||
run = MagicMock(spec=ProcessingRun)
|
||||
for k, v in defaults.items():
|
||||
setattr(run, k, v)
|
||||
return run
|
||||
|
||||
|
||||
def _make_log(**overrides) -> MagicMock:
|
||||
defaults = dict(
|
||||
id=1,
|
||||
timestamp=datetime.now(timezone.utc),
|
||||
level="INFO",
|
||||
message="processed",
|
||||
email_subject="Hello",
|
||||
email_from="sender@example.com",
|
||||
success=True,
|
||||
mail_account_id=10,
|
||||
processing_run_id=1,
|
||||
email_size_bytes=1024,
|
||||
error_details=None,
|
||||
)
|
||||
defaults.update(overrides)
|
||||
log = MagicMock(spec=ProcessingLog)
|
||||
for k, v in defaults.items():
|
||||
setattr(log, k, v)
|
||||
return log
|
||||
|
||||
|
||||
def _scalar_one(value):
|
||||
r = MagicMock()
|
||||
r.scalar_one.return_value = value
|
||||
return r
|
||||
|
||||
|
||||
def _scalar_one_or_none(value):
|
||||
r = MagicMock()
|
||||
r.scalar_one_or_none.return_value = value
|
||||
return r
|
||||
|
||||
|
||||
def _rows_all(rows):
|
||||
r = MagicMock()
|
||||
r.all.return_value = rows
|
||||
return r
|
||||
|
||||
|
||||
def _scalars_all(values):
|
||||
r = MagicMock()
|
||||
scalars = MagicMock()
|
||||
scalars.all.return_value = values
|
||||
r.scalars.return_value = scalars
|
||||
return r
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
return create_application()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_db():
|
||||
db = AsyncMock()
|
||||
return db
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def current_user():
|
||||
return _make_user()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
async def auth_client(app, current_user, mock_db):
|
||||
async def _override_user():
|
||||
return current_user
|
||||
|
||||
async def _override_db():
|
||||
yield mock_db
|
||||
|
||||
app.dependency_overrides[get_current_active_user] = _override_user
|
||||
app.dependency_overrides[get_db] = _override_db
|
||||
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
yield client
|
||||
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
|
||||
# ── GET /logs ─────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestListProcessingRuns:
|
||||
async def test_returns_paginated_list(self, auth_client, mock_db):
|
||||
run = _make_run()
|
||||
# The endpoint queries total count then rows
|
||||
# First execute → count, second execute → rows with joined columns
|
||||
row = MagicMock()
|
||||
row.ProcessingRun = run
|
||||
row.name = "My Account"
|
||||
row.email_address = "me@example.com"
|
||||
|
||||
mock_db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_one(1), # count query
|
||||
_rows_all([row]), # data query
|
||||
]
|
||||
)
|
||||
response = await auth_client.get("/api/v1/processing-runs")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert "total" in data
|
||||
assert data["total"] == 1
|
||||
assert len(data["items"]) == 1
|
||||
|
||||
async def test_empty_result(self, auth_client, mock_db):
|
||||
mock_db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_one(0),
|
||||
_rows_all([]),
|
||||
]
|
||||
)
|
||||
response = await auth_client.get("/api/v1/processing-runs")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["items"] == []
|
||||
assert data["total"] == 0
|
||||
|
||||
async def test_unauthenticated_401(self, app):
|
||||
transport = ASGITransport(app=app)
|
||||
async with AsyncClient(transport=transport, base_url="http://test") as client:
|
||||
response = await client.get("/api/v1/processing-runs")
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_pagination_params(self, auth_client, mock_db):
|
||||
mock_db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_one(0),
|
||||
_rows_all([]),
|
||||
]
|
||||
)
|
||||
response = await auth_client.get("/api/v1/processing-runs?page=2&page_size=5")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["page"] == 2
|
||||
assert data["page_size"] == 5
|
||||
|
||||
async def test_filter_by_account_id(self, auth_client, mock_db):
|
||||
mock_db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_one(0),
|
||||
_rows_all([]),
|
||||
]
|
||||
)
|
||||
response = await auth_client.get("/api/v1/processing-runs?account_id=5")
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_filter_by_status(self, auth_client, mock_db):
|
||||
mock_db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_one(0),
|
||||
_rows_all([]),
|
||||
]
|
||||
)
|
||||
response = await auth_client.get("/api/v1/processing-runs?status=completed")
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_filter_has_emails(self, auth_client, mock_db):
|
||||
mock_db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_one(0),
|
||||
_rows_all([]),
|
||||
]
|
||||
)
|
||||
response = await auth_client.get("/api/v1/processing-runs?has_emails=true")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
# ── GET /logs/{run_id} ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestGetProcessingRun:
|
||||
async def test_returns_run(self, auth_client, mock_db):
|
||||
run = _make_run(id=42)
|
||||
row = MagicMock()
|
||||
row.ProcessingRun = run
|
||||
row.name = "Account"
|
||||
row.email_address = "me@example.com"
|
||||
|
||||
result = MagicMock()
|
||||
result.one_or_none.return_value = row
|
||||
mock_db.execute = AsyncMock(return_value=result)
|
||||
|
||||
response = await auth_client.get("/api/v1/processing-runs/42")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["id"] == 42
|
||||
|
||||
async def test_404_when_not_found(self, auth_client, mock_db):
|
||||
result = MagicMock()
|
||||
result.one_or_none.return_value = None
|
||||
mock_db.execute = AsyncMock(return_value=result)
|
||||
|
||||
response = await auth_client.get("/api/v1/processing-runs/999")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# ── GET /logs/{run_id}/logs ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestGetRunLogs:
|
||||
async def test_returns_log_entries(self, auth_client, mock_db):
|
||||
run = _make_run(id=1)
|
||||
log_entry = _make_log(processing_run_id=1)
|
||||
|
||||
mock_db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_one_or_none(run), # ownership check
|
||||
_scalar_one(1), # count
|
||||
_scalars_all([log_entry]), # log entries
|
||||
]
|
||||
)
|
||||
response = await auth_client.get("/api/v1/processing-runs/1/logs")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "items" in data
|
||||
assert len(data["items"]) == 1
|
||||
|
||||
async def test_404_when_run_not_found(self, auth_client, mock_db):
|
||||
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(None))
|
||||
response = await auth_client.get("/api/v1/processing-runs/999/logs")
|
||||
assert response.status_code == 404
|
||||
|
||||
async def test_empty_log_entries(self, auth_client, mock_db):
|
||||
run = _make_run(id=1)
|
||||
mock_db.execute = AsyncMock(
|
||||
side_effect=[
|
||||
_scalar_one_or_none(run),
|
||||
_scalar_one(0),
|
||||
_scalars_all([]),
|
||||
]
|
||||
)
|
||||
response = await auth_client.get("/api/v1/processing-runs/1/logs")
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["items"] == []
|
||||
assert data["total"] == 0
|
||||
|
||||
|
||||
# ── helper: _paginate ─────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestPaginateHelper:
|
||||
def test_single_page(self):
|
||||
from app.api.v1.endpoints.logs import _paginate
|
||||
|
||||
result = _paginate(total=10, page=1, page_size=20)
|
||||
assert result["total"] == 10
|
||||
assert result["pages"] == 1
|
||||
|
||||
def test_multiple_pages(self):
|
||||
from app.api.v1.endpoints.logs import _paginate
|
||||
|
||||
result = _paginate(total=25, page=2, page_size=10)
|
||||
assert result["pages"] == 3
|
||||
|
||||
def test_zero_total(self):
|
||||
from app.api.v1.endpoints.logs import _paginate
|
||||
|
||||
result = _paginate(total=0, page=1, page_size=20)
|
||||
assert result["pages"] == 1
|
||||
assert result["total"] == 0
|
||||
Reference in New Issue
Block a user