58f9ad72ed
Agent-Logs-Url: https://github.com/christianlouis/InboxConverge/sessions/8cb47b1c-b744-4212-8b6d-8132d6642d98 Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
248 lines
8.6 KiB
Python
248 lines
8.6 KiB
Python
"""
|
|
Unit tests for user profile and SMTP config endpoints (api/v1/endpoints/users.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, UserSmtpConfig, 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_smtp_config(**overrides) -> MagicMock:
|
|
defaults = dict(
|
|
id=1,
|
|
user_id=1,
|
|
host="smtp.example.com",
|
|
port=587,
|
|
username="user@example.com",
|
|
encrypted_password="encrypted",
|
|
use_tls=True,
|
|
created_at=datetime.now(timezone.utc),
|
|
updated_at=datetime.now(timezone.utc),
|
|
)
|
|
defaults.update(overrides)
|
|
cfg = MagicMock(spec=UserSmtpConfig)
|
|
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
|
|
|
|
|
|
@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()
|
|
|
|
|
|
# ── GET /me ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestGetMe:
|
|
async def test_returns_user_data(self, auth_client, current_user):
|
|
response = await auth_client.get("/api/v1/users/me")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["email"] == current_user.email
|
|
|
|
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/users/me")
|
|
assert response.status_code == 401
|
|
|
|
|
|
# ── PUT /me ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestUpdateMe:
|
|
async def test_update_full_name(self, auth_client, mock_db, current_user):
|
|
mock_db.refresh = AsyncMock(
|
|
side_effect=lambda obj: setattr(obj, "full_name", "Updated Name")
|
|
)
|
|
response = await auth_client.put(
|
|
"/api/v1/users/me", json={"full_name": "Updated Name"}
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
async def test_update_email(self, auth_client, mock_db, current_user):
|
|
mock_db.refresh = AsyncMock(
|
|
side_effect=lambda obj: setattr(obj, "email", "new@example.com")
|
|
)
|
|
response = await auth_client.put(
|
|
"/api/v1/users/me", json={"email": "new@example.com"}
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ── GET /smtp-config ──────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestGetSmtpConfig:
|
|
async def test_returns_config_when_exists(self, auth_client, mock_db):
|
|
cfg = _make_smtp_config()
|
|
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(cfg))
|
|
response = await auth_client.get("/api/v1/users/smtp-config")
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["host"] == "smtp.example.com"
|
|
assert data["port"] == 587
|
|
assert "has_password" in data
|
|
# Password must not be exposed
|
|
assert "encrypted_password" not in data
|
|
|
|
async def test_returns_404_when_no_config(self, auth_client, mock_db):
|
|
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(None))
|
|
response = await auth_client.get("/api/v1/users/smtp-config")
|
|
assert response.status_code == 404
|
|
|
|
|
|
# ── PUT /smtp-config ──────────────────────────────────────────────────────────
|
|
|
|
|
|
class TestUpsertSmtpConfig:
|
|
async def test_creates_new_config(self, auth_client, mock_db):
|
|
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(None))
|
|
|
|
# db.refresh populates the new config object with required fields
|
|
async def _refresh(obj):
|
|
obj.id = 1
|
|
obj.user_id = 1
|
|
obj.host = "smtp.example.com"
|
|
obj.port = 587
|
|
obj.username = "user@example.com"
|
|
obj.encrypted_password = "encrypted"
|
|
obj.use_tls = True
|
|
obj.created_at = datetime.now(timezone.utc)
|
|
obj.updated_at = datetime.now(timezone.utc)
|
|
|
|
mock_db.refresh = AsyncMock(side_effect=_refresh)
|
|
|
|
from unittest.mock import patch
|
|
|
|
with patch(
|
|
"app.api.v1.endpoints.users.encrypt_credential", return_value="encrypted"
|
|
):
|
|
response = await auth_client.put(
|
|
"/api/v1/users/smtp-config",
|
|
json={
|
|
"host": "smtp.example.com",
|
|
"port": 587,
|
|
"username": "user@example.com",
|
|
"password": "secret",
|
|
"use_tls": True,
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
async def test_updates_existing_config(self, auth_client, mock_db):
|
|
existing = _make_smtp_config()
|
|
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(existing))
|
|
mock_db.refresh = AsyncMock(side_effect=lambda obj: None)
|
|
|
|
from unittest.mock import patch
|
|
|
|
with patch(
|
|
"app.api.v1.endpoints.users.encrypt_credential", return_value="encrypted"
|
|
):
|
|
response = await auth_client.put(
|
|
"/api/v1/users/smtp-config",
|
|
json={
|
|
"host": "newsmtp.example.com",
|
|
"port": 465,
|
|
"username": "newuser@example.com",
|
|
"password": "newpass",
|
|
"use_tls": False,
|
|
},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ── DELETE /smtp-config ───────────────────────────────────────────────────────
|
|
|
|
|
|
class TestDeleteSmtpConfig:
|
|
async def test_deletes_existing_config(self, auth_client, mock_db):
|
|
existing = _make_smtp_config()
|
|
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(existing))
|
|
response = await auth_client.delete("/api/v1/users/smtp-config")
|
|
assert response.status_code == 204
|
|
mock_db.delete.assert_awaited_once_with(existing)
|
|
|
|
async def test_no_config_is_noop(self, auth_client, mock_db):
|
|
mock_db.execute = AsyncMock(return_value=_scalar_one_or_none(None))
|
|
response = await auth_client.delete("/api/v1/users/smtp-config")
|
|
assert response.status_code == 204
|
|
mock_db.delete.assert_not_awaited()
|