Files
Antho e7b3944436 feat(api): CRUD des référentiels réservé aux administrateurs
L'écran d'administration du front proposait des boutons Modifier et
Désactiver qui n'appelaient rien (console.log côté client), faute
d'endpoints correspondants. Ajoute POST/PUT/DELETE sur /services,
/categories, /contacts et /monitorings, tous protégés par require_admin.

Deux garde-fous métier :
- la suppression d'un service ou d'une catégorie est refusée (409) tant
  que des monitorings ou contacts y sont rattachés, avec le décompte
  dans le message, plutôt que de laisser remonter une violation de clé
  étrangère ;
- un monitoring est désactivé (actif = 0) et jamais supprimé, car
  TABLE_FINAL référence son identifiant et l'historique doit rester
  consultable.

Le rôle utilisateur est désormais typé par l'enum UserRole : Pydantic
le valide seul (422), ce qui supprime les deux contrôles manuels
dupliqués dans create_user et update_user.

Tests : 8 -> 16. Couvre la validation par enum, le refus 409 sur
rattachement, le 403 pour un non-administrateur et la désactivation
logique du monitoring.
2026-08-15 14:28:58 +02:00

137 lines
5.1 KiB
Python

"""Tests d'endpoints (curseur SQL simulé via la fixture `cur`)."""
from auth import hash_password, create_access_token
def test_health_ok(client, cur):
cur._one = (1,)
r = client.get("/health")
assert r.status_code == 200
assert r.json()["api"] == "ok"
def test_categories_requires_auth(client):
# sans token -> 401
assert client.get("/categories").status_code == 401
def test_services_returns_list(client, cur, auth_headers):
cur.description = [("id_service",), ("nom_service",)]
cur._rows = [(1, "Contrat"), (2, "Fournisseur")]
r = client.get("/services", headers=auth_headers)
assert r.status_code == 200
assert {"id_service": 1, "nom_service": "Contrat"} in r.json()
def test_dashboard_filtres_ne_renvoie_que_les_valeurs_utilisees(client, cur, auth_headers):
# VUE_CONSO ne contient que Contrat / Fournisseur : les services du
# référentiel sans monitoring rattaché ne doivent pas remonter.
cur.description = [("service",), ("categorie",)]
cur._rows = [
("Contrat", "DOM-TOM"),
("Contrat", "Tiers-payeurs"),
("Fournisseur", "Contreparties"),
]
r = client.get("/dashboard/filtres", headers=auth_headers)
assert r.status_code == 200
body = r.json()
assert body["services"] == ["Contrat", "Fournisseur"]
assert "Business Intelligence" not in body["services"]
assert body["categories"] == ["Contreparties", "DOM-TOM", "Tiers-payeurs"]
assert {"service": "Contrat", "categorie": "DOM-TOM"} in body["combinaisons"]
def test_auth_login_success(client, cur):
cur._one = (1, "admin", hash_password("Admin2026!"), "Admin", 1)
r = client.post("/auth/login", data={"username": "admin", "password": "Admin2026!"})
assert r.status_code == 200
body = r.json()
assert body["token_type"] == "bearer"
assert body["user"]["role"] == "Admin"
def test_auth_login_wrong_password(client, cur):
cur._one = (1, "admin", hash_password("Admin2026!"), "Admin", 1)
r = client.post("/auth/login", data={"username": "admin", "password": "WRONG"})
assert r.status_code == 401
def test_auth_me(client, auth_headers):
r = client.get("/auth/me", headers=auth_headers)
assert r.status_code == 200
assert r.json()["username"] == "admin"
def test_monitoring_unknown_returns_404(client, cur, auth_headers):
r = client.get("/monitorings/999/details", headers=auth_headers)
assert r.status_code == 404
def test_create_user_role_invalide_rejete(client, cur, auth_headers):
# Le rôle est contraint par l'enum UserRole : Pydantic rejette en 422
# avant d'atteindre la base, sans validation manuelle dans la route.
r = client.post(
"/admin/users",
headers=auth_headers,
json={"username": "x", "email": "x@x.fr", "password": "p", "role": "Root"},
)
assert r.status_code == 422
def test_create_user_role_valide_accepte(client, cur, auth_headers):
r = client.post(
"/admin/users",
headers=auth_headers,
json={"username": "x", "email": "x@x.fr", "password": "p", "role": "Superviseur"},
)
assert r.status_code == 201
def test_creation_service_reservee_admin(client, cur):
# Un Consultant ne doit pas pouvoir écrire dans le référentiel.
token = create_access_token({"sub": "bob", "role": "Consultant", "uid": 2})
r = client.post(
"/services",
headers={"Authorization": f"Bearer {token}"},
json={"nom_service": "Nouveau"},
)
assert r.status_code == 403
def test_creation_service_par_admin(client, cur, auth_headers):
cur._one = (42,)
r = client.post("/services", headers=auth_headers, json={"nom_service": "Comptabilité"})
assert r.status_code == 201
assert r.json() == {"id_service": 42, "nom_service": "Comptabilité"}
def test_suppression_service_refusee_si_rattachements(client, cur, auth_headers):
# 2 monitorings et 1 contact rattachés : la suppression doit être bloquée
# explicitement plutôt que de laisser remonter une violation de clé étrangère.
cur._one = (2, 1)
r = client.delete("/services/1", headers=auth_headers)
assert r.status_code == 409
assert "2 monitoring(s)" in r.json()["detail"]
def test_suppression_service_sans_rattachement(client, cur, auth_headers):
cur._one = (0, 0)
r = client.delete("/services/1", headers=auth_headers)
assert r.status_code == 200
assert r.json()["status"] == "deleted"
def test_desactivation_monitoring_ne_supprime_pas(client, cur, auth_headers):
# TABLE_FINAL référence id_monito : on désactive, on ne supprime jamais.
r = client.delete("/monitorings/1", headers=auth_headers)
assert r.status_code == 200
assert r.json()["status"] == "deactivated"
requetes = " ".join(q for q, _ in cur.executed)
assert "UPDATE NOMENCLATURE_MONITO SET actif = 0" in requetes
assert "DELETE FROM NOMENCLATURE_MONITO" not in requetes
def test_admin_forbidden_for_consultant(client):
token = create_access_token({"sub": "bob", "role": "Consultant", "uid": 2})
r = client.get("/admin/users", headers={"Authorization": f"Bearer {token}"})
assert r.status_code == 403