""" 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="
contenu
"), \ 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="ok
"), \ 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="À remplir en fin de session"), \ 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