"""FlowDeck — Comprehensive tests v1.3.0.""" import json import os import tempfile import pytest from fastapi.testclient import TestClient @pytest.fixture def client(): db_file = tempfile.NamedTemporaryFile(suffix=".db", delete=False) db_path = db_file.name db_file.close() os.environ["GITEA_URL"] = "https://git.dracodev.net" os.environ["GITEA_TOKEN"] = "test" os.environ["DATABASE_URL"] = f"sqlite:///{db_path}" os.environ["APP_SECRET_KEY"] = "test-secret-for-tests" from app.main import app from app.db import init_db init_db() yield TestClient(app) os.unlink(db_path) # ── Core ── def test_health(client): resp = client.get("/api/health") assert resp.status_code == 200 data = resp.json() assert "status" in data assert data["db"] is True assert data["version"] == "1.0.0" def test_dashboard(client): resp = client.get("/") assert resp.status_code == 200 assert "sidebar" in resp.text assert "workspace" in resp.text or "app-layout" in resp.text def test_stats(client): resp = client.get("/api/stats") assert resp.status_code == 200 data = resp.json() for key in ("boards", "cards", "notes", "users"): assert key in data def test_projects(client): resp = client.get("/api/projects") assert resp.status_code == 200 data = resp.json() assert "projects" in data def test_board_404(client): resp = client.get("/board/test/test") assert resp.status_code in (200, 500) def test_csrf_rejected(client): resp = client.post("/api/move?owner=x&repo=y&issue_id=1&column=Test") assert resp.status_code == 403 def test_auth_user(client): resp = client.get("/auth/user") assert resp.status_code == 200 assert "authenticated" in resp.json() # ── v0.4.0: UI Notion ── def test_board_page_renders(client): resp = client.get("/board/test/test") assert resp.status_code in (200, 500) if resp.status_code == 200: assert "kanban" in resp.text.lower() or "board" in resp.text.lower() def test_dashboard_notion_ui(client): resp = client.get("/") assert resp.status_code == 200 # Notion-style elements assert "sidebar" in resp.text or "main-wrapper" in resp.text or "topbar" in resp.text # ── v0.5.0: Kanban ── def test_kanban_view(client): resp = client.get("/board/test/test/view/kanban") # May 500 if Gitea is unreachable, but shouldn't crash assert resp.status_code in (200, 500) def test_detailed_view(client): resp = client.get("/board/test/test/view/detailed") assert resp.status_code in (200, 500) def test_card_detail_html(client): resp = client.get("/api/issues/test/test/1?format=html") # 404 expected if issue doesn't exist assert resp.status_code in (200, 404, 500) def test_create_issue_api(client): resp = client.post("/api/issues/test/test?title=Test%20Issue&body=Test%20body") # 403 CSRF or 500 if Gitea down assert resp.status_code in (403, 500) # ── v0.6.0: Table View ── def test_table_view(client): resp = client.get("/board/test/test/view/table") assert resp.status_code in (200, 500) if resp.status_code == 200: assert "table" in resp.text.lower() or "data-table" in resp.text def test_table_view_with_sort(client): resp = client.get("/board/test/test/view/table?sort=name:asc") assert resp.status_code in (200, 500) # ── v0.7.0: Filtres & Tri ── def test_kanban_with_status_filter(client): resp = client.get("/board/test/test/view/kanban?status=todo,progress") assert resp.status_code in (200, 500) def test_kanban_with_assignee_filter(client): resp = client.get("/board/test/test/view/kanban?filter=assignee:testuser") assert resp.status_code in (200, 500) def test_kanban_with_multiple_sorts(client): resp = client.get("/board/test/test/view/kanban?sort=status:asc,name:desc") assert resp.status_code in (200, 500) # ── v0.8.0: Vues Spéciales ── def test_status_overview(client): resp = client.get("/board/test/test/view/status") assert resp.status_code in (200, 500) if resp.status_code == 200: assert "svg" in resp.text.lower() or "donut" in resp.text.lower() or "status_data" in resp.text or "Total" in resp.text def test_team_load(client): resp = client.get("/board/test/test/view/teamload") assert resp.status_code in (200, 500) # ── v0.9.0: Backend ── def test_get_properties(client): resp = client.get("/board/api/properties/test/test") assert resp.status_code == 200 data = resp.json() assert "properties" in data def test_get_ai_keywords(client): resp = client.get("/board/api/ai-keywords/test/test") assert resp.status_code == 200 data = resp.json() assert "keywords" in data def test_csrf_protects_properties_post(client): resp = client.post("/board/api/properties/test/test?name=Priority&prop_type=select") assert resp.status_code == 403 # CSRF def test_csrf_protects_sync(client): resp = client.post("/board/api/sync/test/test") assert resp.status_code == 403 # CSRF # ── v1.0.0: Production ── def test_version_in_health(client): resp = client.get("/api/health") assert resp.json()["version"] == "1.0.0" def test_db_tables_exist(client): from app.db import get_conn with get_conn() as conn: tables = conn.execute( "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name" ).fetchall() names = {t["name"] for t in tables} required = {"boards", "cards", "notes", "checklists", "checklist_items", "col_mapping", "users", "user_tokens", "project_properties", "property_values", "ai_keywords", "collections", "collection_pages", "collection_views"} assert required <= names def test_cors_headers(client): resp = client.options("/api/health", headers={ "Origin": "http://localhost:3000", "Access-Control-Request-Method": "GET", }) assert resp.status_code in (200, 405) def test_all_view_endpoints_respond(client): views = ["kanban", "detailed", "table", "status", "teamload"] for view in views: resp = client.get(f"/board/test/test/view/{view}") assert resp.status_code in (200, 500), f"View {view} failed with {resp.status_code}" # ── v1.3.0: Database Concept ── def test_collections_api_list_empty(client): """List collections API — should return empty when no collections exist.""" resp = client.get("/db/api") assert resp.status_code == 200 data = resp.json() assert "collections" in data assert data["collections"] == [] def test_collections_api_crud(client): """Full CRUD lifecycle: create → read → update → delete.""" # Create resp = client.post("/db/api", json={ "name": "Test Database", "description": "A test collection", "icon": "🗂️", "schema": [ {"name": "Status", "type": "select", "options": ["Todo", "Done"]}, ], }) assert resp.status_code == 200 created = resp.json() assert created["status"] == "created" assert "id" in created coll_id = created["id"] # List — should now have 1 collection resp = client.get("/db/api") assert resp.status_code == 200 assert len(resp.json()["collections"]) == 1 # Get single resp = client.get(f"/db/{coll_id}/api") assert resp.status_code == 200 data = resp.json() assert data["collection"]["name"] == "Test Database" assert data["collection"]["icon"] == "🗂️" # Update resp = client.put(f"/db/api/{coll_id}", json={ "name": "Updated DB", }) assert resp.status_code == 200 assert resp.json()["status"] == "updated" # Verify update resp = client.get(f"/db/{coll_id}/api") assert resp.json()["collection"]["name"] == "Updated DB" # Delete resp = client.delete(f"/db/api/{coll_id}") assert resp.status_code == 200 assert resp.json()["status"] == "deleted" # Verify deletion resp = client.get(f"/db/{coll_id}/api") assert resp.status_code == 404 def test_collections_pages_crud(client): """CRUD for pages inside a collection.""" # Create collection first resp = client.post("/db/api", json={"name": "Page Test DB"}) coll_id = resp.json()["id"] # Create page resp = client.post(f"/db/{coll_id}/pages/api", json={ "title": "My First Page", "properties": {"Status": "Todo", "Priority": "P1"}, }) assert resp.status_code == 200 page_data = resp.json() assert page_data["status"] == "created" page_id = page_data["id"] # Get page resp = client.get(f"/db/pages/{page_id}/api") assert resp.status_code == 200 assert resp.json()["title"] == "My First Page" # Update page resp = client.put(f"/db/pages/{page_id}/api", json={ "title": "Updated Page", "properties": {"Status": "Done"}, }) assert resp.status_code == 200 # Verify update resp = client.get(f"/db/pages/{page_id}/api") data = resp.json() assert data["title"] == "Updated Page" props = json.loads(data["property_values_json"]) assert props["Status"] == "Done" # Delete page resp = client.delete(f"/db/pages/{page_id}/api") assert resp.status_code == 200 assert resp.json()["status"] == "deleted" # Cleanup: delete collection client.delete(f"/db/api/{coll_id}") def test_collections_api_validation(client): """Validation: missing name should return 400.""" resp = client.post("/db/api", json={}) assert resp.status_code == 400 assert "name" in resp.json()["detail"].lower() def test_collections_db_page_renders(client): """GET /db/{id} should render an HTML page.""" # Create collection first resp = client.post("/db/api", json={"name": "Render Test"}) coll_id = resp.json()["id"] resp = client.get(f"/db/{coll_id}") assert resp.status_code == 200 assert "Render Test" in resp.text # Cleanup client.delete(f"/db/api/{coll_id}")