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,198 @@
|
||||
"""
|
||||
Router — Auth : gestion des clients API (CRUD + rotation de clé)
|
||||
"""
|
||||
import logging
|
||||
import secrets
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.dependencies.auth import get_current_client, hash_api_key, require_scope
|
||||
from app.models.client import APIClient
|
||||
from app.schemas.auth import (
|
||||
ClientCreate,
|
||||
ClientCreateResponse,
|
||||
ClientResponse,
|
||||
ClientUpdate,
|
||||
KeyRotateResponse,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/auth", tags=["Authentification"])
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# CRÉER UN CLIENT
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post(
|
||||
"/clients",
|
||||
response_model=ClientCreateResponse,
|
||||
status_code=status.HTTP_201_CREATED,
|
||||
summary="Créer un nouveau client API",
|
||||
description=(
|
||||
"Crée un client et retourne la clé API **en clair une seule fois**. "
|
||||
"Stockez-la immédiatement — elle ne sera plus jamais affichée."
|
||||
),
|
||||
dependencies=[Depends(require_scope("admin"))],
|
||||
)
|
||||
async def create_client(
|
||||
body: ClientCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ClientCreateResponse:
|
||||
# Génération de la clé API
|
||||
raw_key = secrets.token_urlsafe(32)
|
||||
key_hash = hash_api_key(raw_key)
|
||||
|
||||
client = APIClient(
|
||||
name=body.name,
|
||||
api_key_hash=key_hash,
|
||||
scopes=body.scopes,
|
||||
plan=body.plan,
|
||||
)
|
||||
db.add(client)
|
||||
await db.flush()
|
||||
await db.refresh(client)
|
||||
|
||||
logger.info("Client créé : %s (%s)", client.name, client.id)
|
||||
|
||||
return ClientCreateResponse(
|
||||
id=client.id,
|
||||
name=client.name,
|
||||
scopes=client.scopes,
|
||||
plan=client.plan,
|
||||
is_active=client.is_active,
|
||||
created_at=client.created_at,
|
||||
updated_at=client.updated_at,
|
||||
api_key=raw_key,
|
||||
)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# LISTER LES CLIENTS
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get(
|
||||
"/clients",
|
||||
response_model=list[ClientResponse],
|
||||
summary="Lister tous les clients API",
|
||||
dependencies=[Depends(require_scope("admin"))],
|
||||
)
|
||||
async def list_clients(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> list[ClientResponse]:
|
||||
result = await db.execute(select(APIClient).order_by(APIClient.created_at.desc()))
|
||||
clients = result.scalars().all()
|
||||
return [ClientResponse.model_validate(c) for c in clients]
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# DÉTAIL D'UN CLIENT
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.get(
|
||||
"/clients/{client_id}",
|
||||
response_model=ClientResponse,
|
||||
summary="Détail d'un client API",
|
||||
dependencies=[Depends(require_scope("admin"))],
|
||||
)
|
||||
async def get_client(
|
||||
client_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ClientResponse:
|
||||
result = await db.execute(select(APIClient).where(APIClient.id == client_id))
|
||||
client = result.scalar_one_or_none()
|
||||
if not client:
|
||||
raise HTTPException(status_code=404, detail="Client introuvable")
|
||||
return ClientResponse.model_validate(client)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# MODIFIER UN CLIENT
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.patch(
|
||||
"/clients/{client_id}",
|
||||
response_model=ClientResponse,
|
||||
summary="Modifier un client API",
|
||||
dependencies=[Depends(require_scope("admin"))],
|
||||
)
|
||||
async def update_client(
|
||||
client_id: str,
|
||||
body: ClientUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ClientResponse:
|
||||
result = await db.execute(select(APIClient).where(APIClient.id == client_id))
|
||||
client = result.scalar_one_or_none()
|
||||
if not client:
|
||||
raise HTTPException(status_code=404, detail="Client introuvable")
|
||||
|
||||
update_data = body.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(client, field, value)
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(client)
|
||||
|
||||
logger.info("Client mis à jour : %s (%s)", client.name, client.id)
|
||||
return ClientResponse.model_validate(client)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# ROTATION DE CLÉ
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.post(
|
||||
"/clients/{client_id}/rotate-key",
|
||||
response_model=KeyRotateResponse,
|
||||
summary="Régénérer la clé API d'un client",
|
||||
description="Invalide l'ancienne clé et en génère une nouvelle.",
|
||||
dependencies=[Depends(require_scope("admin"))],
|
||||
)
|
||||
async def rotate_key(
|
||||
client_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> KeyRotateResponse:
|
||||
result = await db.execute(select(APIClient).where(APIClient.id == client_id))
|
||||
client = result.scalar_one_or_none()
|
||||
if not client:
|
||||
raise HTTPException(status_code=404, detail="Client introuvable")
|
||||
|
||||
raw_key = secrets.token_urlsafe(32)
|
||||
client.api_key_hash = hash_api_key(raw_key)
|
||||
await db.flush()
|
||||
|
||||
logger.info("Clé API rotée pour client : %s (%s)", client.name, client.id)
|
||||
|
||||
return KeyRotateResponse(id=client.id, api_key=raw_key)
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
# DÉSACTIVER UN CLIENT (soft delete)
|
||||
# ─────────────────────────────────────────────────────────────
|
||||
|
||||
@router.delete(
|
||||
"/clients/{client_id}",
|
||||
response_model=ClientResponse,
|
||||
summary="Désactiver un client API",
|
||||
description="Soft delete — marque le client comme inactif sans supprimer les données.",
|
||||
dependencies=[Depends(require_scope("admin"))],
|
||||
)
|
||||
async def delete_client(
|
||||
client_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
) -> ClientResponse:
|
||||
result = await db.execute(select(APIClient).where(APIClient.id == client_id))
|
||||
client = result.scalar_one_or_none()
|
||||
if not client:
|
||||
raise HTTPException(status_code=404, detail="Client introuvable")
|
||||
|
||||
client.is_active = False
|
||||
await db.flush()
|
||||
await db.refresh(client)
|
||||
|
||||
logger.info("Client désactivé : %s (%s)", client.name, client.id)
|
||||
return ClientResponse.model_validate(client)
|
||||
Reference in New Issue
Block a user