Files

311 lines
14 KiB
Python
Raw Permalink Normal View History

"""
test_api.py — Tests unitaires FastAPI Context Continuity
Usage : pytest test_api.py -v
"""
import pytest
from unittest.mock import patch, MagicMock
from fastapi.testclient import TestClient
from api_context import app
CLIENT = TestClient(app)
HEADERS_CLAUDE = {"Authorization": "VyFeOxT1nfyPULlx4z5uts7p5R_RrJIOZ3y_f1aoMk8"}
HEADERS_LECHAT = {"Authorization": "WE2gw12Eerz0H-kV6SHNDAq-NN9Z6I81KAq3T2giNME"}
HEADERS_INVALID = {"Authorization": "mauvaise_cle"}
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
NOTE_MOCK = {
"noteId": "test123",
"title": "Note Test",
"utcDateCreated": "2026-05-30T10:00:00Z",
"utcDateModified": "2026-05-30T10:00:00Z",
"attributes": [
{"type": "label", "name": "type", "value": "backlogItem"},
{"type": "label", "name": "projet", "value": "TestProjet"},
{"type": "label", "name": "statut", "value": "a faire"},
{"type": "label", "name": "priorite","value": "haute"},
],
"childNoteIds": [],
"parentNoteIds": ["root"],
}
CREATE_RESULT = {
"note": {"noteId": "new456", "title": "Nouvelle Note",
"utcDateCreated": "2026-05-30T10:00:00Z",
"utcDateModified": "2026-05-30T10:00:00Z",
"attributes": [], "childNoteIds": [], "parentNoteIds": []},
"branch": {}
}
# ---------------------------------------------------------------------------
# Tests Auth
# ---------------------------------------------------------------------------
class TestAuth:
def test_health_no_auth(self):
"""Health ne nécessite pas d'auth."""
r = CLIENT.get("/api/health")
assert r.status_code == 200
assert r.json()["status"] == "ok"
def test_protected_no_auth(self):
r = CLIENT.get("/api/backlog/TestProjet")
assert r.status_code == 422 # Header manquant
def test_protected_invalid_key(self):
r = CLIENT.get("/api/backlog/TestProjet", headers=HEADERS_INVALID)
assert r.status_code == 401
def test_auth_claude(self):
with patch("trilium_api.search_by_label", return_value=[]), \
patch("trilium_api.get_label_value", return_value="a faire"):
r = CLIENT.get("/api/backlog/TestProjet", headers=HEADERS_CLAUDE)
assert r.status_code == 200
def test_auth_lechat(self):
with patch("trilium_api.search_by_label", return_value=[]), \
patch("trilium_api.get_label_value", return_value="a faire"):
r = CLIENT.get("/api/backlog/TestProjet", headers=HEADERS_LECHAT)
assert r.status_code == 200
# ---------------------------------------------------------------------------
# Tests Health
# ---------------------------------------------------------------------------
class TestHealth:
def test_health(self):
r = CLIENT.get("/api/health")
assert r.status_code == 200
data = r.json()
assert data["status"] == "ok"
assert "version" in data
# ---------------------------------------------------------------------------
# Tests Backlog
# ---------------------------------------------------------------------------
class TestBacklog:
def test_list_backlog_vide(self):
with patch("trilium_api.search_by_label", return_value=[]), \
patch("trilium_api.get_label_value", return_value="a faire"):
r = CLIENT.get("/api/backlog/ProjetInexistant", headers=HEADERS_CLAUDE)
assert r.status_code == 200
assert r.json() == []
def test_list_backlog(self):
with patch("trilium_api.search_by_label", return_value=[NOTE_MOCK]), \
patch("trilium_api.get_label_value", side_effect=lambda nid, key: {
"projet": "TestProjet", "statut": "a faire", "priorite": "haute"
}.get(key, "")), \
patch("trilium_api.get_note", return_value=NOTE_MOCK):
r = CLIENT.get("/api/backlog/TestProjet", headers=HEADERS_CLAUDE)
assert r.status_code == 200
def test_create_backlog(self):
with patch("builtins.open", MagicMock(
return_value=MagicMock(
__enter__=MagicMock(return_value=MagicMock(
read=MagicMock(return_value='{"Backlog":"root123"}'))),
__exit__=MagicMock(return_value=False)))), \
patch("json.load", return_value={"Backlog": "root123"}), \
patch("trilium_api.create_note", return_value=CREATE_RESULT), \
patch("trilium_api.get_note_id", return_value="new456"), \
patch("trilium_api.set_label", return_value=None):
r = CLIENT.post("/api/backlog", headers=HEADERS_CLAUDE,
json={"projet": "TestProjet", "titre": "Ma tâche",
"priorite": "haute"})
assert r.status_code == 201
assert r.json()["titre"] == "Ma tâche"
assert r.json()["created_by"] == "Claude"
def test_create_backlog_priorite_invalide(self):
r = CLIENT.post("/api/backlog", headers=HEADERS_CLAUDE,
json={"projet": "TestProjet", "titre": "Tâche",
"priorite": "ULTRA"})
assert r.status_code == 422
def test_patch_backlog(self):
with patch("trilium_api.get_note", return_value=NOTE_MOCK), \
patch("trilium_api.set_label", return_value=None):
r = CLIENT.patch("/api/backlog/test123", headers=HEADERS_CLAUDE,
json={"statut": "en cours"})
assert r.status_code == 200
def test_patch_backlog_inexistant(self):
with patch("trilium_api.get_note", side_effect=Exception("404")):
r = CLIENT.patch("/api/backlog/inexistant", headers=HEADERS_CLAUDE,
json={"statut": "fait"})
assert r.status_code == 404
def test_patch_backlog_statut_invalide(self):
r = CLIENT.patch("/api/backlog/test123", headers=HEADERS_CLAUDE,
json={"statut": "statut_invalide"})
assert r.status_code == 422
# ---------------------------------------------------------------------------
# Tests Décisions
# ---------------------------------------------------------------------------
class TestDecisions:
def test_list_decisions_vide(self):
with patch("trilium_api.search_by_label", return_value=[]), \
patch("trilium_api.get_label_value", return_value="active"):
r = CLIENT.get("/api/decisions/TestProjet", headers=HEADERS_CLAUDE)
assert r.status_code == 200
assert r.json() == []
def test_create_decision(self):
with patch("json.load", return_value={"Decisions": "dec123"}), \
patch("builtins.open", MagicMock(
return_value=MagicMock(
__enter__=MagicMock(return_value=MagicMock()),
__exit__=MagicMock(return_value=False)))), \
patch("trilium_api.create_note", return_value=CREATE_RESULT), \
patch("trilium_api.get_note_id", return_value="new456"), \
patch("trilium_api.set_label", return_value=None):
r = CLIENT.post("/api/decisions", headers=HEADERS_LECHAT,
json={"projet": "TestProjet",
"enonce": "On utilise FastAPI",
"justification": "Validation auto Pydantic"})
assert r.status_code == 201
assert r.json()["created_by"] == "LeChat"
def test_patch_decision_annuler(self):
with patch("trilium_api.get_note", return_value=NOTE_MOCK), \
patch("trilium_api.set_label", return_value=None), \
patch("trilium_api.get_note_content", return_value="<p>contenu</p>"), \
patch("trilium_api.update_note_content", return_value=None):
r = CLIENT.patch("/api/decisions/test123", headers=HEADERS_CLAUDE,
json={"statut": "annulee"})
assert r.status_code == 200
# ---------------------------------------------------------------------------
# Tests Historique
# ---------------------------------------------------------------------------
class TestHistorique:
def test_create_historique_valide(self):
with patch("json.load", return_value={"Historique": "hist123"}), \
patch("builtins.open", MagicMock(
return_value=MagicMock(
__enter__=MagicMock(return_value=MagicMock()),
__exit__=MagicMock(return_value=False)))), \
patch("trilium_api.create_note", return_value=CREATE_RESULT), \
patch("trilium_api.get_note_id", return_value="new456"), \
patch("trilium_api.set_label", return_value=None):
r = CLIENT.post("/api/historique", headers=HEADERS_CLAUDE,
json={"projet": "TestProjet",
"enonce": "Test X effectue",
"type": "Test effectue",
"detail": "Resultat OK"})
assert r.status_code == 201
def test_create_historique_type_invalide(self):
r = CLIENT.post("/api/historique", headers=HEADERS_CLAUDE,
json={"projet": "TestProjet",
"enonce": "Test",
"type": "Type invalide"})
assert r.status_code == 422
def test_patch_invalider(self):
with patch("trilium_api.get_note", return_value=NOTE_MOCK), \
patch("trilium_api.set_label", return_value=None), \
patch("trilium_api.get_note_content", return_value="<p>ok</p>"), \
patch("trilium_api.update_note_content", return_value=None):
r = CLIENT.patch("/api/historique/test123", headers=HEADERS_CLAUDE,
json={"encore_valide": False})
assert r.status_code == 200
# ---------------------------------------------------------------------------
# Tests Conversations
# ---------------------------------------------------------------------------
class TestConversations:
def test_create_conversation(self):
with patch("json.load", return_value={"Conversations": "conv123"}), \
patch("builtins.open", MagicMock(
return_value=MagicMock(
__enter__=MagicMock(return_value=MagicMock()),
__exit__=MagicMock(return_value=False)))), \
patch("trilium_api.create_note", return_value=CREATE_RESULT), \
patch("trilium_api.get_note_id", return_value="new456"), \
patch("trilium_api.set_label", return_value=None):
r = CLIENT.post("/api/conversations", headers=HEADERS_CLAUDE,
json={"projet": "TestProjet",
"titre": "Session debug",
"llm": "Claude Sonnet"})
assert r.status_code == 201
def test_create_conversation_llm_invalide(self):
r = CLIENT.post("/api/conversations", headers=HEADERS_CLAUDE,
json={"projet": "TestProjet",
"titre": "Session",
"llm": "GPT-4"})
assert r.status_code == 422
def test_patch_cloture(self):
with patch("trilium_api.get_note", return_value=NOTE_MOCK), \
patch("trilium_api.set_label", return_value=None), \
patch("trilium_api.get_note_content",
return_value="<i>À remplir en fin de session</i>"), \
patch("trilium_api.update_note_content", return_value=None):
r = CLIENT.patch("/api/conversations/test123", headers=HEADERS_CLAUDE,
json={"synthese_cloture": "Session terminee. Reste X."})
assert r.status_code == 200
# ---------------------------------------------------------------------------
# Tests Glossaire
# ---------------------------------------------------------------------------
class TestGlossaire:
def test_create_terme(self):
with patch("json.load", return_value={"Glossaire": "glos123"}), \
patch("builtins.open", MagicMock(
return_value=MagicMock(
__enter__=MagicMock(return_value=MagicMock()),
__exit__=MagicMock(return_value=False)))), \
patch("trilium_api.create_note", return_value=CREATE_RESULT), \
patch("trilium_api.get_note_id", return_value="new456"), \
patch("trilium_api.set_label", return_value=None):
r = CLIENT.post("/api/glossaire", headers=HEADERS_CLAUDE,
json={"projet": "TestProjet",
"terme": "Pipeline",
"definition": "Chaine de traitement complète"})
assert r.status_code == 201
# ---------------------------------------------------------------------------
# Tests Contexte Reprise
# ---------------------------------------------------------------------------
class TestContexteReprise:
def test_get_contexte_vide(self):
with patch("trilium_api.search_by_label", return_value=[]), \
patch("trilium_api.get_label_value", return_value="active"):
r = CLIENT.get("/api/contexte/ProjetVide",
headers=HEADERS_CLAUDE)
assert r.status_code == 200
assert "briefing" in r.json()
assert "tokens" in r.json()
def test_get_last_contexte_inexistant(self):
with patch("trilium_api.search_by_label", return_value=[]), \
patch("trilium_api.get_label_value", return_value=None):
r = CLIENT.get("/api/contexte/ProjetVide/last",
headers=HEADERS_CLAUDE)
assert r.status_code == 404
# ---------------------------------------------------------------------------
# Tests Docs
# ---------------------------------------------------------------------------
class TestDocs:
def test_swagger_accessible(self):
r = CLIENT.get("/api/docs")
assert r.status_code == 200
def test_openapi_accessible(self):
r = CLIENT.get("/api/openapi.json")
assert r.status_code == 200