Add comprehensive test suite for image processing and related services
- Implement tests for database generator to ensure proper session handling. - Create tests for EXIF extraction and conversion functions. - Add tests for image-related endpoints, ensuring proper data retrieval and isolation between clients. - Develop tests for OCR functionality, including language detection and text extraction. - Introduce tests for the image processing pipeline, covering success and failure scenarios. - Validate rate limiting functionality and ensure independent counters for different clients. - Implement scraper tests to verify HTML content fetching and error handling. - Add unit tests for various services, including storage and filename generation. - Establish worker entry point for ARQ to handle background image processing tasks.
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Middleware de rate limiting — par client et par plan.
|
||||
|
||||
Utilise slowapi (basé sur limits) avec identification par client_id
|
||||
plutôt que par IP, pour compter les requêtes par client API.
|
||||
|
||||
Limites par plan (par heure) :
|
||||
- free : 20 uploads, 50 AI
|
||||
- standard : 100 uploads, 200 AI
|
||||
- premium : 500 uploads, 1000 AI
|
||||
"""
|
||||
import logging
|
||||
from slowapi import Limiter
|
||||
from slowapi.util import get_remote_address
|
||||
from starlette.requests import Request
|
||||
|
||||
from app.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_client_id_from_request(request: Request) -> str:
|
||||
"""
|
||||
Extrait le client_id depuis la state de la requête.
|
||||
Fallback vers l'IP si le client n'est pas encore authentifié.
|
||||
"""
|
||||
# Le client_id est injecté par le middleware ou la dépendance auth
|
||||
client_id = getattr(request.state, "client_id", None)
|
||||
if client_id:
|
||||
return str(client_id)
|
||||
return get_remote_address(request)
|
||||
|
||||
|
||||
# Instance globale du limiter
|
||||
limiter = Limiter(key_func=_get_client_id_from_request)
|
||||
|
||||
|
||||
def get_upload_rate_limit(plan: str) -> str:
|
||||
"""Retourne la limite de rate pour les uploads selon le plan."""
|
||||
limits = {
|
||||
"free": f"{settings.RATE_LIMIT_FREE_UPLOAD}/hour",
|
||||
"standard": f"{settings.RATE_LIMIT_STANDARD_UPLOAD}/hour",
|
||||
"premium": f"{settings.RATE_LIMIT_PREMIUM_UPLOAD}/hour",
|
||||
}
|
||||
return limits.get(plan, limits["free"])
|
||||
|
||||
|
||||
def get_ai_rate_limit(plan: str) -> str:
|
||||
"""Retourne la limite de rate pour les endpoints AI selon le plan."""
|
||||
limits = {
|
||||
"free": f"{settings.RATE_LIMIT_FREE_AI}/hour",
|
||||
"standard": f"{settings.RATE_LIMIT_STANDARD_AI}/hour",
|
||||
"premium": f"{settings.RATE_LIMIT_PREMIUM_AI}/hour",
|
||||
}
|
||||
return limits.get(plan, limits["free"])
|
||||
|
||||
|
||||
def upload_rate_limit_key(request: Request) -> str:
|
||||
"""Clé dynamique pour le rate limiting des uploads."""
|
||||
return _get_client_id_from_request(request)
|
||||
|
||||
|
||||
def ai_rate_limit_key(request: Request) -> str:
|
||||
"""Clé dynamique pour le rate limiting des endpoints AI."""
|
||||
return _get_client_id_from_request(request)
|
||||
@@ -0,0 +1,41 @@
|
||||
"""
|
||||
Middleware de logging HTTP — enregistre chaque requête avec structlog.
|
||||
|
||||
Exclut les endpoints de santé (/health, /metrics) pour réduire le bruit.
|
||||
"""
|
||||
import time
|
||||
import logging
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.requests import Request
|
||||
from starlette.responses import Response
|
||||
|
||||
logger = logging.getLogger("http")
|
||||
|
||||
# Chemins exclus du logging
|
||||
_EXCLUDED_PATHS = {"/health", "/health/detailed", "/metrics", "/favicon.ico"}
|
||||
|
||||
|
||||
class LoggingMiddleware(BaseHTTPMiddleware):
|
||||
async def dispatch(self, request: Request, call_next) -> Response:
|
||||
if request.url.path in _EXCLUDED_PATHS:
|
||||
return await call_next(request)
|
||||
|
||||
start = time.monotonic()
|
||||
client_id = getattr(request.state, "client_id", "anonymous")
|
||||
|
||||
response = await call_next(request)
|
||||
|
||||
duration_ms = int((time.monotonic() - start) * 1000)
|
||||
|
||||
logger.info(
|
||||
"http.request",
|
||||
extra={
|
||||
"method": request.method,
|
||||
"path": request.url.path,
|
||||
"status": response.status_code,
|
||||
"duration_ms": duration_ms,
|
||||
"client_id": str(client_id),
|
||||
},
|
||||
)
|
||||
|
||||
return response
|
||||
@@ -0,0 +1,3 @@
|
||||
from app.middleware import limiter, get_upload_rate_limit, get_ai_rate_limit
|
||||
|
||||
__all__ = ["limiter", "get_upload_rate_limit", "get_ai_rate_limit"]
|
||||
Reference in New Issue
Block a user