# tests/test_api_main.py — Integration tests for main.py API endpoints # Uses the existing app_with_vault / client fixtures from conftest.py import pytest # ═══════════════════════════════════════════════════════════════════ # Health & Metadata # ═══════════════════════════════════════════════════════════════════ class TestHealth: def test_health_ok(self, client): resp = client.get("/api/health") assert resp.status_code == 200 data = resp.json() assert data["status"] == "ok" assert "version" in data assert data["vaults"] >= 1 assert data["total_files"] >= 1 def test_health_has_vault_files(self, client): resp = client.get("/api/health") data = resp.json() assert data["vaults"] == 1 assert data["total_files"] >= 4 # note1, note2, projet, café_crème + config.json class TestVaults: def test_list_vaults(self, client): resp = client.get("/api/vaults") assert resp.status_code == 200 data = resp.json() assert len(data) >= 1 vault = data[0] assert "name" in vault assert "file_count" in vault assert "tag_count" in vault assert vault["file_count"] >= 1 def test_vault_has_name(self, client): resp = client.get("/api/vaults") data = resp.json() names = [v["name"] for v in data] assert "TestVault" in names def test_vault_type_default(self, client): resp = client.get("/api/vaults") data = resp.json() for v in data: assert v.get("type") in ("VAULT", "DIR") # ═══════════════════════════════════════════════════════════════════ # Browse # ═══════════════════════════════════════════════════════════════════ class TestBrowse: def test_browse_root(self, client): resp = client.get("/api/browse/TestVault") assert resp.status_code == 200 data = resp.json() assert "items" in data assert "path" in data assert data.get("path") == "" or data.get("path") == "/" def test_browse_subdirectory(self, client): resp = client.get("/api/browse/TestVault", params={"path": "Projets"}) assert resp.status_code == 200 data = resp.json() # Should contain projet.md names = [i["name"] for i in data.get("items", [])] assert "projet.md" in names def test_browse_nonexistent_vault(self, client): resp = client.get("/api/browse/NoSuchVault") assert resp.status_code == 404 def test_browse_nonexistent_path(self, client): resp = client.get("/api/browse/TestVault", params={"path": "Nope"}) assert resp.status_code == 404 # ═══════════════════════════════════════════════════════════════════ # File Content & Raw # ═══════════════════════════════════════════════════════════════════ class TestFileContent: def test_get_file(self, client): resp = client.get("/api/file/TestVault", params={"path": "note1.md"}) assert resp.status_code == 200 data = resp.json() assert data["title"] == "Introduction à Python" assert "html" in data assert data["vault"] == "TestVault" def test_get_file_pdf_unavailable(self, client): """PDF export should return 501 on systems without GTK (like Windows).""" resp = client.get("/api/file/TestVault/pdf", params={"path": "note1.md"}) # 501 = Not Implemented (GTK/WeasyPrint unavailable) # May also be 500 or 200 depending on platform assert resp.status_code in (200, 501, 500) def test_get_file_with_frontmatter(self, client): resp = client.get("/api/file/TestVault", params={"path": "note2.md"}) data = resp.json() assert data["title"] == "Docker Guide" assert "docker" in data.get("tags", []) assert "frontmatter" in data def test_get_file_in_subdir(self, client): resp = client.get("/api/file/TestVault", params={"path": "Projets/projet.md"}) assert resp.status_code == 200 data = resp.json() assert "Projet" in data["title"] def test_get_nonexistent_file(self, client): resp = client.get("/api/file/TestVault", params={"path": "noexist.md"}) assert resp.status_code == 404 def test_get_file_raw(self, client): resp = client.get("/api/file/TestVault/raw", params={"path": "note1.md"}) assert resp.status_code == 200 data = resp.json() assert "raw" in data assert "# Introduction à Python" in data["raw"] def test_get_file_download(self, client): resp = client.get("/api/file/TestVault/download", params={"path": "note1.md"}) assert resp.status_code == 200 # Download returns the raw file content directly assert b"Introduction" in resp.content def test_get_file_backlinks(self, client): """note2.md has [[Introduction à Python]] which should link back to note1.md""" resp = client.get("/api/file/TestVault/backlinks", params={"path": "note1.md"}) assert resp.status_code == 200 data = resp.json() assert "backlinks" in data # The backlinks may be empty in test due to index state, # but the endpoint should return a valid response def test_get_file_backlinks_empty(self, client): """projet.md has no backlinks (no one links to it)""" resp = client.get("/api/file/TestVault/backlinks", params={"path": "Projets/projet.md"}) assert resp.status_code == 200 data = resp.json() assert len(data["backlinks"]) == 0 # ═══════════════════════════════════════════════════════════════════ # File CRUD (requires writable vault — tmp_path is writable) # ═══════════════════════════════════════════════════════════════════ class TestFileCRUD: """Note: uses app_with_vault which creates a fresh index each time. The vault is backed by tmp_path so writes are allowed. """ def test_create_file(self, client): resp = client.post("/api/file/TestVault", json={ "path": "newfile.md", "content": "# New File\nCreated during test.\n", }) assert resp.status_code == 200 data = resp.json() assert data["success"] is True # Verify it exists resp2 = client.get("/api/file/TestVault", params={"path": "newfile.md"}) assert resp2.status_code == 200 def test_create_file_in_subdir(self, client): """Create inside a subdirectory that exists""" resp = client.post("/api/file/TestVault", json={ "path": "Projets/newsub.md", "content": "# Sub File\n", }) assert resp.status_code == 200 data = resp.json() assert data["success"] is True def test_create_file_new_subdir(self, client): """Create inside a subdirectory that doesn't exist — should auto-create""" resp = client.post("/api/file/TestVault", json={ "path": "NewDir/test.md", "content": "# Auto-created dir\n", }) assert resp.status_code == 200 data = resp.json() assert data["success"] is True def test_save_file(self, client): """Save (update) an existing file""" resp = client.put("/api/file/TestVault/save?path=note1.md", json={ "content": "# Updated Python\nContenu modifié.\n", }) assert resp.status_code == 200 data = resp.json() assert data["status"] == "ok" # Verify content changed resp2 = client.get("/api/file/TestVault", params={"path": "note1.md"}) assert "Updated" in resp2.json()["html"] def test_rename_file(self, client): resp = client.patch("/api/file/TestVault", json={ "path": "note1.md", "new_name": "renamed_note1.md", }) assert resp.status_code == 200 data = resp.json() assert data["success"] is True # Old path should 404 resp2 = client.get("/api/file/TestVault", params={"path": "note1.md"}) assert resp2.status_code == 404 # New path should exist resp3 = client.get("/api/file/TestVault", params={"path": "renamed_note1.md"}) assert resp3.status_code == 200 def test_delete_file(self, client): resp = client.delete("/api/file/TestVault", params={"path": "note1.md"}) assert resp.status_code == 200 data = resp.json() assert data["status"] == "ok" # Verify deletion resp2 = client.get("/api/file/TestVault", params={"path": "note1.md"}) assert resp2.status_code == 404 def test_delete_nonexistent_file(self, client): resp = client.delete("/api/file/TestVault", params={"path": "nosuchfile.md"}) assert resp.status_code == 404 # ═══════════════════════════════════════════════════════════════════ # Directory CRUD # ═══════════════════════════════════════════════════════════════════ class TestDirectoryCRUD: def test_create_directory(self, client): resp = client.post("/api/directory/TestVault", json={ "path": "TestDir", }) assert resp.status_code == 200 data = resp.json() assert data["success"] is True # Verify in browse resp2 = client.get("/api/browse/TestVault") names = [i["name"] for i in resp2.json().get("items", [])] assert "TestDir" in names def test_create_directory_nested(self, client): resp = client.post("/api/directory/TestVault", json={ "path": "A/B/C", }) assert resp.status_code == 200 data = resp.json() assert data["success"] is True def test_rename_directory(self, client): # Create first client.post("/api/directory/TestVault", json={"path": "OldName"}) # Rename resp = client.patch("/api/directory/TestVault", json={ "path": "OldName", "new_name": "NewName", }) assert resp.status_code == 200 data = resp.json() assert data["success"] is True def test_delete_directory(self, client): # Create first client.post("/api/directory/TestVault", json={"path": "ToDelete"}) # Delete resp = client.delete("/api/directory/TestVault", params={"path": "ToDelete"}) assert resp.status_code == 200 data = resp.json() assert data["success"] is True # ═══════════════════════════════════════════════════════════════════ # Search & Tags (supplement existing tests) # ═══════════════════════════════════════════════════════════════════ class TestTreeSearch: def test_tree_search_root(self, client): resp = client.get("/api/tree-search", params={"q": "note", "vault": "all"}) assert resp.status_code == 200 data = resp.json() assert "results" in data # Should find note1 and note2 assert len(data["results"]) >= 2 def test_tree_search_no_match(self, client): resp = client.get("/api/tree-search", params={"q": "zzzznonexistent", "vault": "all"}) assert resp.status_code == 200 data = resp.json() assert len(data["results"]) == 0 class TestSearchReplace: def test_search_replace_invalid(self, client): """Search/replace with no matching vault should return 400 or appropriate error""" resp = client.post("/api/search/replace", json={ "vault": "TestVault", "search": "Python", "replace": "Java", }) # Accept 400 if validation fails, 200 if it works assert resp.status_code in (200, 400) class TestSearchAPIEdgeCases: def test_search_empty_query(self, client): resp = client.get("/api/search?q=&vault=all") assert resp.status_code in (200, 422) def test_suggest_with_query(self, client): resp = client.get("/api/suggest?q=Python&vault=all") assert resp.status_code == 200 data = resp.json() assert "suggestions" in data class TestIndexReload: def test_reload_index(self, client): resp = client.get("/api/index/reload") assert resp.status_code == 200 data = resp.json() assert "vaults" in data def test_reload_single_vault(self, client): resp = client.get("/api/index/reload/TestVault") assert resp.status_code == 200 # ═══════════════════════════════════════════════════════════════════ # Bookmarks & Recent # ═══════════════════════════════════════════════════════════════════ class TestBookmarks: def test_get_bookmarks_empty(self, client): resp = client.get("/api/bookmarks") assert resp.status_code == 200 data = resp.json() # Response: { files: [], total: 0 } assert isinstance(data.get("files", data), list) def test_toggle_bookmark(self, client): resp = client.post("/api/bookmarks/toggle", json={ "vault": "TestVault", "path": "note1.md", }) assert resp.status_code == 200 data = resp.json() assert "bookmarked" in data def test_bookmark_vault_filter(self, client): client.post("/api/bookmarks/toggle", json={ "vault": "TestVault", "path": "note1.md", }) resp = client.get("/api/bookmarks", params={"vault": "TestVault"}) assert resp.status_code == 200 def test_recent_files(self, client): resp = client.get("/api/recent") assert resp.status_code == 200 data = resp.json() assert "recent" in data or "files" in data def test_recent_vault_filter(self, client): resp = client.get("/api/recent", params={"vault": "TestVault"}) assert resp.status_code == 200 # ═══════════════════════════════════════════════════════════════════ # Graph # ═══════════════════════════════════════════════════════════════════ class TestGraph: def test_graph_full(self, client): resp = client.get("/api/graph/TestVault") assert resp.status_code == 200 data = resp.json() assert "nodes" in data assert "edges" in data assert len(data["nodes"]) >= 1 def test_graph_with_filters(self, client): resp = client.get("/api/graph/TestVault", params={"depth": "2", "types": "file"}) assert resp.status_code == 200 data = resp.json() assert "nodes" in data def test_graph_nonexistent_vault(self, client): resp = client.get("/api/graph/NoVault") assert resp.status_code == 404 # ═══════════════════════════════════════════════════════════════════ # Config & Diagnostics # ═══════════════════════════════════════════════════════════════════ class TestConfig: def test_get_config(self, client): resp = client.get("/api/config") assert resp.status_code == 200 data = resp.json() assert "config" in data or isinstance(data, dict) def test_post_config(self, client): resp = client.post("/api/config", json={ "search_workers": 4, }) assert resp.status_code == 200 data = resp.json() # POST /api/config returns the updated config object directly def test_diagnostics(self, client): resp = client.get("/api/diagnostics") assert resp.status_code == 200 data = resp.json() # Should have some diagnostic info assert len(data) > 0 # ═══════════════════════════════════════════════════════════════════ # Dashboard # ═══════════════════════════════════════════════════════════════════ class TestDashboard: def test_dashboard(self, client): resp = client.get("/api/dashboard") assert resp.status_code == 200 data = resp.json() assert isinstance(data, dict) # Should have stats per vault keys = [k.lower() for k in data.keys()] assert any("file" in k for k in keys) or any("tag" in k for k in keys) # ═══════════════════════════════════════════════════════════════════ # Vault Settings # ═══════════════════════════════════════════════════════════════════ class TestVaultSettingsAPI: def test_get_vault_settings(self, client): resp = client.get("/api/vaults/TestVault/settings") assert resp.status_code == 200 data = resp.json() assert isinstance(data, dict) def test_update_vault_settings(self, client): resp = client.post("/api/vaults/TestVault/settings", json={ "ignored_dirs": ".trash,.git", }) assert resp.status_code == 200 def test_get_all_settings(self, client): resp = client.get("/api/vaults/settings/all") assert resp.status_code == 200 data = resp.json() assert "TestVault" in data def test_settings_nonexistent_vault(self, client): resp = client.get("/api/vaults/NoVault/settings") assert resp.status_code == 404 # ═══════════════════════════════════════════════════════════════════ # Image & Attachments # ═══════════════════════════════════════════════════════════════════ class TestAttachments: def test_image_endpoint(self, client): """No actual image in test vault, but endpoint should respond""" resp = client.get("/api/image/TestVault", params={"path": "nonexistent.png"}) # Should either 404 or serve a placeholder assert resp.status_code in (200, 404, 415) def test_attachments_stats(self, client): resp = client.get("/api/attachments/stats", params={"vault": "TestVault"}) assert resp.status_code == 200 data = resp.json() assert isinstance(data, dict) or "stats" in data or "vault" in data # ═══════════════════════════════════════════════════════════════════ # Vault Management (dynamic add/remove) # ═══════════════════════════════════════════════════════════════════ class TestVaultManagement: def test_vaults_status(self, client): resp = client.get("/api/vaults/status") assert resp.status_code == 200 data = resp.json() assert isinstance(data, dict) or isinstance(data, list) def test_add_nonexistent_vault(self, client): """Adding a vault to a path that doesn't exist should fail""" resp = client.post("/api/vaults/add", json={ "name": "FakeVault", "path": "/tmp/nonexistent_vault_path_xyz", }) assert resp.status_code in (200, 400, 404, 500) def test_delete_vault_not_found(self, client): resp = client.delete("/api/vaults/NoSuchVault") assert resp.status_code in (404, 200) # May 404 if not found # ═══════════════════════════════════════════════════════════════════ # Humanize mtime (utility function) # ═══════════════════════════════════════════════════════════════════ class TestHumanizeMtime: def test_now(self, client): """Indirectly tested via /api/recent endpoint""" pass def test_humanize_mtime_now(self): from backend.main import humanize_mtime import time assert "instant" in humanize_mtime(time.time()) def test_humanize_mtime_minutes(self): from backend.main import humanize_mtime import time result = humanize_mtime(time.time() - 120) assert "min" in result def test_humanize_mtime_hours(self): from backend.main import humanize_mtime import time result = humanize_mtime(time.time() - 7200) assert "h" in result or "jour" in result def test_humanize_mtime_days(self): from backend.main import humanize_mtime import time result = humanize_mtime(time.time() - 172800) # 2 days assert "j" in result def test_humanize_mtime_old(self): from backend.main import humanize_mtime import time result = humanize_mtime(time.time() - 86400 * 30) assert "202" in result or result # Should show formatted date # ═══════════════════════════════════════════════════════════════════ # Utility functions # ═══════════════════════════════════════════════════════════════════ class TestHeadingSlugify: def test_slugify_simple(self): from backend.main import _heading_slugify assert _heading_slugify("Hello World") == "hello-world" def test_slugify_accented(self): from backend.main import _heading_slugify result = _heading_slugify("Café Crème") assert "cafe" in result or "caf" in result def test_slugify_strips_symbols(self): from backend.main import _heading_slugify result = _heading_slugify("Hello, World! Test?") assert result.startswith("hello") class TestRenderMarkdown: def test_render_basic(self): from backend.main import _render_markdown result = _render_markdown("# Hello\n\nThis is a test.", "TestVault") assert "