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,278 @@
|
||||
"""
|
||||
Unit tests for notification config endpoints (api/v1/endpoints/notifications.py).
|
||||
|
||||
All database interactions and auth dependencies are mocked.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
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,
|
||||
NotificationConfig,
|
||||
NotificationChannel,
|
||||
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_notification_config(**overrides) -> MagicMock:
|
||||
defaults = dict(
|
||||
id=1,
|
||||
user_id=1,
|
||||
name="Test Notification",
|
||||
apprise_url="json://localhost",
|
||||
channel=NotificationChannel.WEBHOOK,
|
||||
is_enabled=True,
|
||||
config={},
|
||||
notify_on_errors=True,
|
||||
notify_on_success=False,
|
||||
notify_threshold=3,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
updated_at=datetime.now(timezone.utc),
|
||||
)
|
||||
defaults.update(overrides)
|
||||
cfg = MagicMock(spec=NotificationConfig)
|
||||
for k, v in defaults.items():
|
||||
setattr(cfg, k, v)
|
||||
return cfg
|
||||
|
||||
|
||||
def _scalar_one_or_none(value):
|
||||
r = MagicMock()
|
||||
r.scalar_one_or_none.return_value = value
|
||||
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()
|
||||
db.commit = AsyncMock()
|
||||
db.refresh = AsyncMock()
|
||||
db.add = MagicMock()
|
||||
db.delete = 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()
|
||||
|
||||
|
||||
# ── POST /notifications ───────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestCreateNotificationConfig:
|
||||
async def test_creates_config_201(self, auth_client, mock_db, current_user):
|
||||
created_cfg = _make_notification_config()
|
||||
|
||||
# db.refresh must populate the object returned from the endpoint
|
||||
async def _refresh(obj):
|
||||
for k, v in vars(created_cfg).items():
|
||||
if not k.startswith("_"):
|
||||
try:
|
||||
setattr(obj, k, v)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
mock_db.refresh = AsyncMock(side_effect=_refresh)
|
||||
|
||||
response = await auth_client.post(
|
||||
"/api/v1/notifications",
|
||||
json={
|
||||
"name": "My Webhook",
|
||||
"apprise_url": "json://localhost",
|
||||
"channel": "webhook",
|
||||
"is_enabled": True,
|
||||
"config": {},
|
||||
"notify_on_errors": True,
|
||||
"notify_on_success": False,
|
||||
"notify_threshold": 3,
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201
|
||||
|
||||
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.post(
|
||||
"/api/v1/notifications",
|
||||
json={
|
||||
"name": "x",
|
||||
"apprise_url": "json://localhost",
|
||||
"channel": "webhook",
|
||||
"config": {},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
# ── GET /notifications ────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestListNotificationConfigs:
|
||||
async def test_returns_list(self, auth_client, mock_db):
|
||||
cfg1 = _make_notification_config(id=1)
|
||||
cfg2 = _make_notification_config(id=2)
|
||||
mock_db.execute = AsyncMock(return_value=_scalars_all([cfg1, cfg2]))
|
||||
response = await auth_client.get("/api/v1/notifications")
|
||||
assert response.status_code == 200
|
||||
assert isinstance(response.json(), list)
|
||||
assert len(response.json()) == 2
|
||||
|
||||
async def test_returns_empty_list(self, auth_client, mock_db):
|
||||
mock_db.execute = AsyncMock(return_value=_scalars_all([]))
|
||||
response = await auth_client.get("/api/v1/notifications")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == []
|
||||
|
||||
|
||||
# ── GET /notifications/{id} ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestGetNotificationConfig:
|
||||
async def test_returns_config(self, auth_client, mock_db):
|
||||
cfg = _make_notification_config(id=5)
|
||||
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(cfg))
|
||||
response = await auth_client.get("/api/v1/notifications/5")
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_404_when_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/notifications/999")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# ── PUT /notifications/{id} ───────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestUpdateNotificationConfig:
|
||||
async def test_updates_config(self, auth_client, mock_db):
|
||||
cfg = _make_notification_config(id=5)
|
||||
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(cfg))
|
||||
mock_db.refresh = AsyncMock(side_effect=lambda obj: None)
|
||||
|
||||
response = await auth_client.put(
|
||||
"/api/v1/notifications/5",
|
||||
json={"name": "Updated Name"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
async def test_404_when_not_found(self, auth_client, mock_db):
|
||||
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(None))
|
||||
response = await auth_client.put(
|
||||
"/api/v1/notifications/999",
|
||||
json={"name": "Updated"},
|
||||
)
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# ── DELETE /notifications/{id} ────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestDeleteNotificationConfig:
|
||||
async def test_deletes_config_204(self, auth_client, mock_db):
|
||||
cfg = _make_notification_config(id=5)
|
||||
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(cfg))
|
||||
response = await auth_client.delete("/api/v1/notifications/5")
|
||||
assert response.status_code == 204
|
||||
mock_db.delete.assert_awaited_once_with(cfg)
|
||||
|
||||
async def test_404_when_not_found(self, auth_client, mock_db):
|
||||
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(None))
|
||||
response = await auth_client.delete("/api/v1/notifications/999")
|
||||
assert response.status_code == 404
|
||||
|
||||
|
||||
# ── POST /notifications/test ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestTestNotificationConfig:
|
||||
async def test_test_success(self, auth_client):
|
||||
with patch(
|
||||
"app.api.v1.endpoints.notifications.test_notification",
|
||||
new=AsyncMock(return_value=(True, "sent successfully")),
|
||||
):
|
||||
response = await auth_client.post(
|
||||
"/api/v1/notifications/test",
|
||||
json={"apprise_url": "json://localhost"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is True
|
||||
|
||||
async def test_test_failure(self, auth_client):
|
||||
with patch(
|
||||
"app.api.v1.endpoints.notifications.test_notification",
|
||||
new=AsyncMock(return_value=(False, "delivery failed")),
|
||||
):
|
||||
response = await auth_client.post(
|
||||
"/api/v1/notifications/test",
|
||||
json={"apprise_url": "invalid://url"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["success"] is False
|
||||
Reference in New Issue
Block a user