311 lines
14 KiB
Python
311 lines
14 KiB
Python
|
|
"""
|
||
|
|
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
|