- 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.
255 lines
9.5 KiB
Python
255 lines
9.5 KiB
Python
"""
|
|
Tests d'authentification — API Keys + scopes.
|
|
"""
|
|
import pytest
|
|
import pytest_asyncio
|
|
from httpx import AsyncClient
|
|
|
|
from app.dependencies.auth import hash_api_key
|
|
from app.models.client import APIClient
|
|
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# 401 — Pas de clé / clé invalide
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
async def test_no_auth_returns_401(async_client: AsyncClient):
|
|
"""Requête sans header Authorization → HTTP 401."""
|
|
response = await async_client.get("/images")
|
|
assert response.status_code == 422 or response.status_code == 401
|
|
|
|
|
|
async def test_invalid_key_returns_401(async_client: AsyncClient):
|
|
"""Requête avec une clé invalide → HTTP 401."""
|
|
response = await async_client.get(
|
|
"/images",
|
|
headers={"Authorization": "Bearer invalid-key-that-does-not-exist"},
|
|
)
|
|
assert response.status_code == 401
|
|
assert "Authentification requise" in response.json()["detail"]
|
|
|
|
|
|
async def test_no_bearer_prefix_returns_401(async_client: AsyncClient):
|
|
"""Requête avec un header Authorization sans 'Bearer ' → HTTP 401."""
|
|
response = await async_client.get(
|
|
"/images",
|
|
headers={"Authorization": "Basic some-key"},
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
|
|
async def test_empty_bearer_returns_401(async_client: AsyncClient):
|
|
"""Requête avec 'Bearer ' mais sans clé → HTTP 401."""
|
|
response = await async_client.get(
|
|
"/images",
|
|
headers={"Authorization": "Bearer "},
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# 200 — Clé valide
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
async def test_valid_key_returns_200(
|
|
async_client: AsyncClient,
|
|
client_a: APIClient,
|
|
auth_headers_a: dict,
|
|
):
|
|
"""Requête avec une clé valide → HTTP 200."""
|
|
response = await async_client.get("/images", headers=auth_headers_a)
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# 401 — Client inactif
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
async def test_inactive_client_returns_401(
|
|
async_client: AsyncClient,
|
|
client_a: APIClient,
|
|
auth_headers_a: dict,
|
|
db_session,
|
|
):
|
|
"""Client désactivé → HTTP 401 même avec une clé valide."""
|
|
# Désactiver le client
|
|
client_a.is_active = False
|
|
db_session.add(client_a)
|
|
await db_session.commit()
|
|
|
|
response = await async_client.get("/images", headers=auth_headers_a)
|
|
assert response.status_code == 401
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# 403 — Scope manquant
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
async def test_missing_scope_returns_403(
|
|
async_client: AsyncClient,
|
|
db_session,
|
|
):
|
|
"""Client sans le bon scope → HTTP 403."""
|
|
# Créer un client avec seulement images:read (pas images:write)
|
|
key = "test-readonly-key-123"
|
|
client = APIClient(
|
|
name="Read Only Client",
|
|
api_key_hash=hash_api_key(key),
|
|
scopes=["images:read"], # pas images:write ni admin
|
|
plan="free",
|
|
)
|
|
db_session.add(client)
|
|
await db_session.commit()
|
|
|
|
# Tenter d'accéder aux endpoints admin
|
|
response = await async_client.get(
|
|
"/auth/clients",
|
|
headers={"Authorization": f"Bearer {key}"},
|
|
)
|
|
assert response.status_code == 403
|
|
assert "Permission insuffisante" in response.json()["detail"]
|
|
|
|
|
|
async def test_scope_images_read_allowed(
|
|
async_client: AsyncClient,
|
|
client_a: APIClient,
|
|
auth_headers_a: dict,
|
|
):
|
|
"""Client avec scope images:read peut lister les images."""
|
|
response = await async_client.get("/images", headers=auth_headers_a)
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Rotation de clé
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
async def test_key_rotation(
|
|
async_client: AsyncClient,
|
|
admin_client: APIClient,
|
|
admin_headers: dict,
|
|
client_a: APIClient,
|
|
auth_headers_a: dict,
|
|
):
|
|
"""Après rotation, l'ancienne clé est invalide et la nouvelle fonctionne."""
|
|
# Vérifier que la clé actuelle fonctionne
|
|
response = await async_client.get("/images", headers=auth_headers_a)
|
|
assert response.status_code == 200
|
|
|
|
# Rotation de la clé
|
|
response = await async_client.post(
|
|
f"/auth/clients/{client_a.id}/rotate-key",
|
|
headers=admin_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
new_key = response.json()["api_key"]
|
|
assert new_key # La nouvelle clé est retournée
|
|
|
|
# L'ancienne clé ne fonctionne plus
|
|
response = await async_client.get("/images", headers=auth_headers_a)
|
|
assert response.status_code == 401
|
|
|
|
# La nouvelle clé fonctionne
|
|
response = await async_client.get(
|
|
"/images",
|
|
headers={"Authorization": f"Bearer {new_key}"},
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# Création de client
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
async def test_create_client_returns_key_once(
|
|
async_client: AsyncClient,
|
|
admin_client: APIClient,
|
|
admin_headers: dict,
|
|
):
|
|
"""La clé API est retournée une seule fois à la création."""
|
|
response = await async_client.post(
|
|
"/auth/clients",
|
|
json={
|
|
"name": "New Test Client",
|
|
"scopes": ["images:read"],
|
|
"plan": "free",
|
|
},
|
|
headers=admin_headers,
|
|
)
|
|
assert response.status_code == 201
|
|
data = response.json()
|
|
assert "api_key" in data
|
|
assert len(data["api_key"]) > 20 # clé suffisamment longue
|
|
assert data["name"] == "New Test Client"
|
|
assert data["scopes"] == ["images:read"]
|
|
|
|
# La clé n'apparaît pas dans les réponses GET
|
|
client_id = data["id"]
|
|
response = await async_client.get(
|
|
f"/auth/clients/{client_id}",
|
|
headers=admin_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert "api_key" not in response.json()
|
|
|
|
|
|
# ─────────────────────────────────────────────────────────────
|
|
# CRUD clients — admin only
|
|
# ─────────────────────────────────────────────────────────────
|
|
|
|
async def test_list_clients_admin_only(
|
|
async_client: AsyncClient,
|
|
client_a: APIClient,
|
|
auth_headers_a: dict,
|
|
admin_client: APIClient,
|
|
admin_headers: dict,
|
|
):
|
|
"""Seul un admin peut lister les clients."""
|
|
# Non-admin → 403
|
|
response = await async_client.get("/auth/clients", headers=auth_headers_a)
|
|
assert response.status_code == 403
|
|
|
|
# Admin → 200
|
|
response = await async_client.get("/auth/clients", headers=admin_headers)
|
|
assert response.status_code == 200
|
|
|
|
|
|
async def test_update_client(
|
|
async_client: AsyncClient,
|
|
admin_client: APIClient,
|
|
admin_headers: dict,
|
|
client_a: APIClient,
|
|
):
|
|
"""L'admin peut modifier les scopes et le plan d'un client."""
|
|
response = await async_client.patch(
|
|
f"/auth/clients/{client_a.id}",
|
|
json={"plan": "premium", "scopes": ["images:read"]},
|
|
headers=admin_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["plan"] == "premium"
|
|
assert response.json()["scopes"] == ["images:read"]
|
|
|
|
|
|
async def test_soft_delete_client(
|
|
async_client: AsyncClient,
|
|
admin_client: APIClient,
|
|
admin_headers: dict,
|
|
client_a: APIClient,
|
|
auth_headers_a: dict,
|
|
):
|
|
"""Soft delete désactive le client — les requêtes suivantes retournent 401."""
|
|
response = await async_client.delete(
|
|
f"/auth/clients/{client_a.id}",
|
|
headers=admin_headers,
|
|
)
|
|
assert response.status_code == 200
|
|
assert response.json()["is_active"] is False
|
|
|
|
# Le client désactivé ne peut plus s'authentifier
|
|
response = await async_client.get("/images", headers=auth_headers_a)
|
|
assert response.status_code == 401
|