Major changes across backend, frontend, infrastructure: - Provider system with model selection (Google, DeepL, OpenAI, Ollama, Google Cloud) - Admin panel: user management, pricing, settings - Glossary system with CSV import/export - Subscription and tier quota management - Security hardening (rate limiting, API key auth, path traversal fixes) - Docker compose for dev, prod, and IONOS deployment - Alembic migrations for new tables - Frontend: dashboard, pricing page, landing page, i18n (en/fr) - Test suite and verification scripts Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
201 lines
6.3 KiB
Python
201 lines
6.3 KiB
Python
"""
|
|
Tests for User model - Task 1 validation
|
|
"""
|
|
|
|
import uuid
|
|
|
|
import pytest
|
|
import pytest_asyncio
|
|
from sqlalchemy import select, text
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from database.models import User, PlanType
|
|
|
|
|
|
class TestUserModelFields:
|
|
"""Test User model has all required fields (AC1)"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_has_tier_field_with_default_free(
|
|
self, async_session: AsyncSession
|
|
):
|
|
"""AC1: User should have tier field with default 'free'"""
|
|
user = User(
|
|
email="test@example.com",
|
|
name="Test User",
|
|
hashed_password="hashed_abc123",
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
|
|
assert user.tier == "free", "Default tier should be 'free'"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_tier_can_be_pro(self, async_session: AsyncSession):
|
|
"""AC1: User tier can be set to 'pro'"""
|
|
user = User(
|
|
email="pro@example.com",
|
|
name="Pro User",
|
|
hashed_password="hashed_abc123",
|
|
tier="pro",
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
|
|
assert user.tier == "pro"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_has_daily_translation_count_default_zero(
|
|
self, async_session: AsyncSession
|
|
):
|
|
"""AC1: User should have daily_translation_count with default 0"""
|
|
user = User(
|
|
email="daily@example.com",
|
|
name="Daily User",
|
|
hashed_password="hashed_abc123",
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
|
|
assert user.daily_translation_count == 0
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_has_hashed_password_field(self, async_session: AsyncSession):
|
|
"""AC1: User should have hashed_password field (renamed from password_hash)"""
|
|
user = User(
|
|
email="hash@example.com",
|
|
name="Hash User",
|
|
hashed_password="hashed_password_value",
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
|
|
assert hasattr(user, "hashed_password")
|
|
assert user.hashed_password == "hashed_password_value"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_has_uuid_id(self, async_session: AsyncSession):
|
|
"""AC1: User id should be UUID type"""
|
|
user = User(
|
|
email="uuid@example.com",
|
|
name="UUID User",
|
|
hashed_password="hashed_abc123",
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
|
|
assert user.id is not None
|
|
uuid.UUID(user.id)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_has_all_base_fields(self, async_session: AsyncSession):
|
|
"""AC1: User should have all required base fields"""
|
|
user = User(
|
|
email="base@example.com",
|
|
name="Base User",
|
|
hashed_password="hashed_abc123",
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
|
|
assert user.email == "base@example.com"
|
|
assert user.name == "Base User"
|
|
assert user.created_at is not None
|
|
assert user.updated_at is not None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_user_keeps_deprecated_plan_field_for_compatibility(
|
|
self, async_session: AsyncSession
|
|
):
|
|
"""Task 1.4: Keep existing plan field for backward compatibility"""
|
|
user = User(
|
|
email="compat@example.com",
|
|
name="Compat User",
|
|
hashed_password="hashed_abc123",
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
|
|
assert hasattr(user, "plan"), (
|
|
"plan field should still exist for backward compatibility"
|
|
)
|
|
|
|
|
|
class TestUserModelAsyncOperations:
|
|
"""Test async database operations (AC2)"""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_user_async(self, async_session: AsyncSession):
|
|
"""AC2: Should be able to create user with async session"""
|
|
user = User(
|
|
email="async_create@example.com",
|
|
name="Async Create",
|
|
hashed_password="hashed_abc123",
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
|
|
result = await async_session.execute(
|
|
select(User).where(User.email == "async_create@example.com")
|
|
)
|
|
found_user = result.scalar_one()
|
|
assert found_user.email == "async_create@example.com"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_read_user_async(self, async_session: AsyncSession):
|
|
"""AC2: Should be able to read user with async session"""
|
|
user = User(
|
|
email="async_read@example.com",
|
|
name="Async Read",
|
|
hashed_password="hashed_abc123",
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
|
|
result = await async_session.execute(
|
|
select(User).where(User.email == "async_read@example.com")
|
|
)
|
|
found_user = result.scalar_one()
|
|
assert found_user.name == "Async Read"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_user_async(self, async_session: AsyncSession):
|
|
"""AC2: Should be able to update user with async session"""
|
|
user = User(
|
|
email="async_update@example.com",
|
|
name="Async Update",
|
|
hashed_password="hashed_abc123",
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
|
|
user.tier = "pro"
|
|
user.daily_translation_count = 5
|
|
await async_session.commit()
|
|
|
|
result = await async_session.execute(
|
|
select(User).where(User.email == "async_update@example.com")
|
|
)
|
|
updated_user = result.scalar_one()
|
|
assert updated_user.tier == "pro"
|
|
assert updated_user.daily_translation_count == 5
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_delete_user_async(self, async_session: AsyncSession):
|
|
"""AC2: Should be able to delete user with async session"""
|
|
user = User(
|
|
email="async_delete@example.com",
|
|
name="Async Delete",
|
|
hashed_password="hashed_abc123",
|
|
)
|
|
async_session.add(user)
|
|
await async_session.commit()
|
|
|
|
await async_session.delete(user)
|
|
await async_session.commit()
|
|
|
|
result = await async_session.execute(
|
|
select(User).where(User.email == "async_delete@example.com")
|
|
)
|
|
assert result.scalar_one_or_none() is None
|