feat: migrate to Pydantic V2 and implement rate limiting middleware

- Migrate settings.py to Pydantic V2 (SettingsConfigDict, validation_alias)
- Update config models to use @field_validator with @classmethod
- Replace deprecated datetime.utcnow() with datetime.now(timezone.utc)
- Migrate FastAPI app from @app.on_event to lifespan context manager
- Implement comprehensive rate limiting middleware with:
  * Endpoint-specific rate limits (login: 5/min, register: 3/min)
  * IP-based and user-based tracking
  * Authenticated user multiplier (2x limits)
  * Bypass paths for health, docs, static, websocket endpoints
  * Rate limit headers in responses
- Add 13 comprehensive tests for rate limiting (all passing)
- Update instructions.md to mark completed tasks
- Fix asyncio.create_task usage in anime_service.py

All 714 tests passing. No deprecation warnings.
This commit is contained in:
2025-10-23 22:03:15 +02:00
parent 6a6ae7e059
commit 17e5a551e1
23 changed files with 949 additions and 269 deletions

View File

@@ -3,7 +3,7 @@
Tests CRUD operations for all database services using in-memory SQLite.
"""
import asyncio
from datetime import datetime, timedelta
from datetime import datetime, timedelta, timezone
import pytest
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
@@ -538,7 +538,7 @@ async def test_retry_failed_downloads(db_session):
@pytest.mark.asyncio
async def test_create_user_session(db_session):
"""Test creating a user session."""
expires_at = datetime.utcnow() + timedelta(hours=24)
expires_at = datetime.now(timezone.utc) + timedelta(hours=24)
session = await UserSessionService.create(
db_session,
session_id="test-session-1",
@@ -556,7 +556,7 @@ async def test_create_user_session(db_session):
@pytest.mark.asyncio
async def test_get_session_by_id(db_session):
"""Test retrieving session by ID."""
expires_at = datetime.utcnow() + timedelta(hours=24)
expires_at = datetime.now(timezone.utc) + timedelta(hours=24)
session = await UserSessionService.create(
db_session,
session_id="test-session-2",
@@ -578,7 +578,7 @@ async def test_get_session_by_id(db_session):
@pytest.mark.asyncio
async def test_get_active_sessions(db_session):
"""Test retrieving active sessions."""
expires_at = datetime.utcnow() + timedelta(hours=24)
expires_at = datetime.now(timezone.utc) + timedelta(hours=24)
# Create active session
await UserSessionService.create(
@@ -593,7 +593,7 @@ async def test_get_active_sessions(db_session):
db_session,
session_id="expired-session",
token_hash="hashed-token",
expires_at=datetime.utcnow() - timedelta(hours=1),
expires_at=datetime.now(timezone.utc) - timedelta(hours=1),
)
await db_session.commit()
@@ -606,7 +606,7 @@ async def test_get_active_sessions(db_session):
@pytest.mark.asyncio
async def test_revoke_session(db_session):
"""Test revoking a session."""
expires_at = datetime.utcnow() + timedelta(hours=24)
expires_at = datetime.now(timezone.utc) + timedelta(hours=24)
session = await UserSessionService.create(
db_session,
session_id="test-session-3",
@@ -637,13 +637,13 @@ async def test_cleanup_expired_sessions(db_session):
db_session,
session_id="expired-1",
token_hash="hashed-token",
expires_at=datetime.utcnow() - timedelta(hours=1),
expires_at=datetime.now(timezone.utc) - timedelta(hours=1),
)
await UserSessionService.create(
db_session,
session_id="expired-2",
token_hash="hashed-token",
expires_at=datetime.utcnow() - timedelta(hours=2),
expires_at=datetime.now(timezone.utc) - timedelta(hours=2),
)
await db_session.commit()
@@ -657,7 +657,7 @@ async def test_cleanup_expired_sessions(db_session):
@pytest.mark.asyncio
async def test_update_session_activity(db_session):
"""Test updating session last activity."""
expires_at = datetime.utcnow() + timedelta(hours=24)
expires_at = datetime.now(timezone.utc) + timedelta(hours=24)
session = await UserSessionService.create(
db_session,
session_id="test-session-4",