- Nouveau router app/routers/collections.py - GET/POST/PUT/DELETE /db pour les collections - POST/GET/PUT/DELETE pour les pages dans une collection - Vue HTML basique par collection + vue par défaut auto-créée - CSRF exempté pour /db/ - 5 nouveaux tests (collections list, CRUD, pages CRUD, validation, HTML render) - 33/33 tests passent
350 lines
10 KiB
Python
350 lines
10 KiB
Python
"""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}")
|