Imago/tests/test_auth.py
Bruno Charest cc99fea20a
Some checks failed
CI / Lint & Format (push) Has been cancelled
CI / Tests (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / Docker Build (push) Has been cancelled
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.
2026-02-24 11:22:10 -05:00

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