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,3 @@
|
||||
from app.dependencies.auth import get_current_client, require_scope, verify_api_key
|
||||
|
||||
__all__ = ["get_current_client", "require_scope", "verify_api_key"]
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
Dépendances FastAPI — authentification par API Key + vérification de scopes.
|
||||
|
||||
Usage dans les routers :
|
||||
client = Depends(get_current_client)
|
||||
_ = Depends(require_scope("images:read"))
|
||||
"""
|
||||
import hashlib
|
||||
import logging
|
||||
from typing import Callable
|
||||
|
||||
from fastapi import Depends, Header, HTTPException, Request, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models.client import APIClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def hash_api_key(api_key: str) -> str:
|
||||
"""Hash SHA-256 d'une clé API — fonction utilitaire réutilisable."""
|
||||
return hashlib.sha256(api_key.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
async def verify_api_key(
|
||||
request: Request,
|
||||
authorization: str = Header(
|
||||
...,
|
||||
alias="Authorization",
|
||||
description="Clé API au format 'Bearer <key>'",
|
||||
),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> APIClient:
|
||||
"""
|
||||
Vérifie la clé API fournie dans le header Authorization.
|
||||
Injecte client_id et client_plan dans request.state pour le rate limiter.
|
||||
|
||||
Raises:
|
||||
HTTPException 401: clé absente, invalide ou client inactif.
|
||||
"""
|
||||
# ── Extraction du token ───────────────────────────────────
|
||||
if not authorization.startswith("Bearer "):
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentification requise",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
raw_key = authorization[7:] # strip "Bearer "
|
||||
if not raw_key:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentification requise",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# ── Lookup par hash ───────────────────────────────────────
|
||||
key_hash = hash_api_key(raw_key)
|
||||
result = await db.execute(
|
||||
select(APIClient).where(APIClient.api_key_hash == key_hash)
|
||||
)
|
||||
client = result.scalar_one_or_none()
|
||||
|
||||
if client is None:
|
||||
logger.warning("Tentative d'authentification avec une clé invalide")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentification requise",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
if not client.is_active:
|
||||
logger.warning("Tentative d'authentification avec un client inactif: %s", client.id)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Authentification requise",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# Injecter dans request.state pour le rate limiter
|
||||
request.state.client_id = client.id
|
||||
request.state.client_plan = client.plan.value if client.plan else "free"
|
||||
|
||||
return client
|
||||
|
||||
|
||||
# Alias pratique pour injection dans les routers
|
||||
get_current_client = verify_api_key
|
||||
|
||||
|
||||
def require_scope(scope: str) -> Callable:
|
||||
"""
|
||||
Factory qui retourne une dépendance FastAPI vérifiant qu'un scope est accordé.
|
||||
|
||||
Usage:
|
||||
@router.get("/...", dependencies=[Depends(require_scope("images:read"))])
|
||||
"""
|
||||
|
||||
async def _check_scope(
|
||||
client: APIClient = Depends(get_current_client),
|
||||
) -> APIClient:
|
||||
if not client.has_scope(scope):
|
||||
logger.warning(
|
||||
"Client %s (%s) a tenté d'accéder au scope '%s' sans autorisation",
|
||||
client.id, client.name, scope,
|
||||
)
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Permission insuffisante",
|
||||
)
|
||||
return client
|
||||
|
||||
return _check_scope
|
||||
Reference in New Issue
Block a user