feat: production deployment - full update with providers, admin, glossaries, pricing, tests
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>
This commit is contained in:
200
tests/test_user_model.py
Normal file
200
tests/test_user_model.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user