Add admin panel, WebSocket support, and API versioning
Introduce an admin portal (React + Nginx), WebSocket routing, and API versioning middleware with `/api/v1/` prefix deprecation. Add master API key authentication, new Prometheus metrics for AI token consumption and active WebSockets, and extend S3 config with a public endpoint URL. Update test paths and fixtures to align with the new routing structure.
This commit is contained in:
+19
-19
@@ -18,14 +18,14 @@ pytestmark = pytest.mark.asyncio
|
||||
|
||||
async def test_no_auth_returns_401(async_client: AsyncClient):
|
||||
"""Requête sans header Authorization → HTTP 401."""
|
||||
response = await async_client.get("/images")
|
||||
response = await async_client.get("/api/v1/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",
|
||||
"/api/v1/images",
|
||||
headers={"Authorization": "Bearer invalid-key-that-does-not-exist"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
@@ -35,7 +35,7 @@ async def test_invalid_key_returns_401(async_client: AsyncClient):
|
||||
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",
|
||||
"/api/v1/images",
|
||||
headers={"Authorization": "Basic some-key"},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
@@ -44,7 +44,7 @@ async def test_no_bearer_prefix_returns_401(async_client: AsyncClient):
|
||||
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",
|
||||
"/api/v1/images",
|
||||
headers={"Authorization": "Bearer "},
|
||||
)
|
||||
assert response.status_code == 401
|
||||
@@ -60,7 +60,7 @@ async def test_valid_key_returns_200(
|
||||
auth_headers_a: dict,
|
||||
):
|
||||
"""Requête avec une clé valide → HTTP 200."""
|
||||
response = await async_client.get("/images", headers=auth_headers_a)
|
||||
response = await async_client.get("/api/v1/images", headers=auth_headers_a)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ async def test_inactive_client_returns_401(
|
||||
db_session.add(client_a)
|
||||
await db_session.commit()
|
||||
|
||||
response = await async_client.get("/images", headers=auth_headers_a)
|
||||
response = await async_client.get("/api/v1/images", headers=auth_headers_a)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
@@ -106,7 +106,7 @@ async def test_missing_scope_returns_403(
|
||||
|
||||
# Tenter d'accéder aux endpoints admin
|
||||
response = await async_client.get(
|
||||
"/auth/clients",
|
||||
"/api/v1/auth/clients",
|
||||
headers={"Authorization": f"Bearer {key}"},
|
||||
)
|
||||
assert response.status_code == 403
|
||||
@@ -119,7 +119,7 @@ async def test_scope_images_read_allowed(
|
||||
auth_headers_a: dict,
|
||||
):
|
||||
"""Client avec scope images:read peut lister les images."""
|
||||
response = await async_client.get("/images", headers=auth_headers_a)
|
||||
response = await async_client.get("/api/v1/images", headers=auth_headers_a)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@@ -136,12 +136,12 @@ async def test_key_rotation(
|
||||
):
|
||||
"""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)
|
||||
response = await async_client.get("/api/v1/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",
|
||||
f"/api/v1/auth/clients/{client_a.id}/rotate-key",
|
||||
headers=admin_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -149,12 +149,12 @@ async def test_key_rotation(
|
||||
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)
|
||||
response = await async_client.get("/api/v1/images", headers=auth_headers_a)
|
||||
assert response.status_code == 401
|
||||
|
||||
# La nouvelle clé fonctionne
|
||||
response = await async_client.get(
|
||||
"/images",
|
||||
"/api/v1/images",
|
||||
headers={"Authorization": f"Bearer {new_key}"},
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -171,7 +171,7 @@ async def test_create_client_returns_key_once(
|
||||
):
|
||||
"""La clé API est retournée une seule fois à la création."""
|
||||
response = await async_client.post(
|
||||
"/auth/clients",
|
||||
"/api/v1/auth/clients",
|
||||
json={
|
||||
"name": "New Test Client",
|
||||
"scopes": ["images:read"],
|
||||
@@ -189,7 +189,7 @@ async def test_create_client_returns_key_once(
|
||||
# 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}",
|
||||
f"/api/v1/auth/clients/{client_id}",
|
||||
headers=admin_headers,
|
||||
)
|
||||
assert response.status_code == 200
|
||||
@@ -209,11 +209,11 @@ async def test_list_clients_admin_only(
|
||||
):
|
||||
"""Seul un admin peut lister les clients."""
|
||||
# Non-admin → 403
|
||||
response = await async_client.get("/auth/clients", headers=auth_headers_a)
|
||||
response = await async_client.get("/api/v1/auth/clients", headers=auth_headers_a)
|
||||
assert response.status_code == 403
|
||||
|
||||
# Admin → 200
|
||||
response = await async_client.get("/auth/clients", headers=admin_headers)
|
||||
response = await async_client.get("/api/v1/auth/clients", headers=admin_headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
@@ -225,7 +225,7 @@ async def test_update_client(
|
||||
):
|
||||
"""L'admin peut modifier les scopes et le plan d'un client."""
|
||||
response = await async_client.patch(
|
||||
f"/auth/clients/{client_a.id}",
|
||||
f"/api/v1/auth/clients/{client_a.id}",
|
||||
json={"plan": "premium", "scopes": ["images:read"]},
|
||||
headers=admin_headers,
|
||||
)
|
||||
@@ -243,12 +243,12 @@ async def test_soft_delete_client(
|
||||
):
|
||||
"""Soft delete désactive le client — les requêtes suivantes retournent 401."""
|
||||
response = await async_client.delete(
|
||||
f"/auth/clients/{client_a.id}",
|
||||
f"/api/v1/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)
|
||||
response = await async_client.get("/api/v1/images", headers=auth_headers_a)
|
||||
assert response.status_code == 401
|
||||
|
||||
Reference in New Issue
Block a user