feat(v2.0.0): Multi-User Workspaces + Comments + History + Favorites + Templates + CSV + Public Sharing
- 7 new tables: workspaces, workspace_members, comments, page_history, favorites, database_templates, page_templates - Router /workspace: 20 endpoints (CRUD workspaces, members, comments, favorites, history, templates, CSV import/export, public sharing) - Roles: admin, editor, commenter, viewer - FK user auto-insert for test compatibility - CSRF exempt for /workspace paths - 67/67 tests passent (+7 tests v2.0) - Version 1.9.0 → 2.0.0 - Docs: ROADMAP updated (7/7 blocs completed)
This commit is contained in:
+114
-8
@@ -34,7 +34,7 @@ def test_health(client):
|
||||
data = resp.json()
|
||||
assert "status" in data
|
||||
assert data["db"] is True
|
||||
assert data["version"] == "1.9.0"
|
||||
assert data["version"] == "2.0.0"
|
||||
|
||||
|
||||
def test_dashboard(client):
|
||||
@@ -191,7 +191,7 @@ def test_csrf_protects_sync(client):
|
||||
|
||||
def test_version_in_health(client):
|
||||
resp = client.get("/api/health")
|
||||
assert resp.json()["version"] == "1.9.0"
|
||||
assert resp.json()["version"] == "2.0.0"
|
||||
|
||||
|
||||
def test_db_tables_exist(client):
|
||||
@@ -205,7 +205,9 @@ def test_db_tables_exist(client):
|
||||
"col_mapping", "users", "user_tokens",
|
||||
"project_properties", "property_values", "ai_keywords",
|
||||
"collections", "collection_pages", "collection_views",
|
||||
"collection_properties"}
|
||||
"collection_properties", "workspaces", "workspace_members",
|
||||
"comments", "page_history", "favorites",
|
||||
"database_templates", "page_templates"}
|
||||
assert required <= names
|
||||
|
||||
|
||||
@@ -359,7 +361,7 @@ def test_boards_as_collections(client):
|
||||
assert isinstance(data["boards"], list)
|
||||
|
||||
|
||||
# ── v1.9.0: Collection Properties ──
|
||||
# ── v2.0.0: Collection Properties ──
|
||||
|
||||
def test_property_types_api(client):
|
||||
"""GET /db/property-types/api — should list available types."""
|
||||
@@ -469,7 +471,7 @@ def test_collection_properties_validation(client):
|
||||
client.delete(f"/db/api/{coll_id}")
|
||||
|
||||
|
||||
# ── v1.9.0: Relations, Rollups, Formulas ──
|
||||
# ── v2.0.0: Relations, Rollups, Formulas ──
|
||||
|
||||
def test_create_relation_property(client):
|
||||
"""Create a relation property between two collections."""
|
||||
@@ -607,7 +609,7 @@ def test_formula_empty_expression(client):
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
# ── v1.9.0: Views (Calendar, Gallery, List, Timeline) ──
|
||||
# ── v2.0.0: Views (Calendar, Gallery, List, Timeline) ──
|
||||
|
||||
def test_views_calendar(client):
|
||||
"""Calendar view renders with navigation."""
|
||||
@@ -717,7 +719,7 @@ def test_list_views(client):
|
||||
client.delete(f"/db/api/{cid}")
|
||||
|
||||
|
||||
# ── v1.9.0: Sub-items & Dependencies ──
|
||||
# ── v2.0.0: Sub-items & Dependencies ──
|
||||
|
||||
def test_sub_items_crud(client):
|
||||
"""Create and list sub-items."""
|
||||
@@ -798,7 +800,7 @@ def test_dependencies_blocked(client):
|
||||
client.delete(f"/db/api/{cid}")
|
||||
|
||||
|
||||
# ── v1.9.0: My Tasks ──
|
||||
# ── v2.0.0: My Tasks ──
|
||||
|
||||
def test_my_tasks_page(client):
|
||||
"""My Tasks dashboard renders."""
|
||||
@@ -830,3 +832,107 @@ def test_my_tasks_view_today(client):
|
||||
def test_my_tasks_view_overdue(client):
|
||||
resp = client.get("/my-tasks?view=overdue")
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
# ── v2.0.0: Workspace, Comments, Favorites, CSV ──
|
||||
|
||||
def test_workspace_crud(client):
|
||||
resp = client.post("/workspace", json={"name": "Team WS"})
|
||||
assert resp.status_code == 200
|
||||
ws_id = resp.json()["id"]
|
||||
members = client.get(f"/workspace/{ws_id}/members")
|
||||
assert len(members.json()["members"]) >= 1
|
||||
# cleanup
|
||||
with client as _:
|
||||
from app.db import get_conn
|
||||
with get_conn() as conn:
|
||||
conn.execute("DELETE FROM workspace_members WHERE workspace_id=?", (ws_id,))
|
||||
conn.execute("DELETE FROM workspaces WHERE id=?", (ws_id,))
|
||||
conn.commit()
|
||||
|
||||
|
||||
def test_comments_crud(client):
|
||||
r = client.post("/db/api", json={"name": "Comment DB"})
|
||||
cid = r.json()["id"]
|
||||
p = client.post(f"/db/{cid}/pages/api", json={"title": "Discuss"})
|
||||
pid = p.json()["id"]
|
||||
|
||||
resp = client.post(f"/workspace/pages/{pid}/comments", json={"body": "Nice work!"})
|
||||
assert resp.status_code == 200
|
||||
|
||||
comments = client.get(f"/workspace/pages/{pid}/comments").json()["comments"]
|
||||
assert len(comments) == 1
|
||||
assert comments[0]["body"] == "Nice work!"
|
||||
|
||||
client.delete(f"/db/api/{cid}")
|
||||
|
||||
|
||||
def test_favorites_crud(client):
|
||||
r = client.post("/db/api", json={"name": "Fav DB"})
|
||||
cid = r.json()["id"]
|
||||
|
||||
resp = client.post("/workspace/favorites", json={"collection_id": cid})
|
||||
assert resp.status_code == 200
|
||||
|
||||
favs = client.get("/workspace/favorites").json()["favorites"]
|
||||
assert len(favs) >= 1
|
||||
|
||||
fav_id = favs[0]["id"]
|
||||
client.delete(f"/workspace/favorites/{fav_id}")
|
||||
assert len(client.get("/workspace/favorites").json()["favorites"]) == 0
|
||||
client.delete(f"/db/api/{cid}")
|
||||
|
||||
|
||||
def test_csv_import_export(client):
|
||||
r = client.post("/db/api", json={"name": "CSV DB"})
|
||||
cid = r.json()["id"]
|
||||
|
||||
csv_data = "title,Status,Priority\nTask 1,Todo,P1\nTask 2,Done,P2"
|
||||
resp = client.post(f"/workspace/collections/{cid}/import/csv", json={"csv": csv_data})
|
||||
assert resp.status_code == 200
|
||||
assert resp.json()["imported"] == 2
|
||||
|
||||
export = client.get(f"/workspace/collections/{cid}/export/csv")
|
||||
assert export.status_code == 200
|
||||
assert "Task 1" in export.text
|
||||
|
||||
client.delete(f"/db/api/{cid}")
|
||||
|
||||
|
||||
def test_public_view(client):
|
||||
r = client.post("/db/api", json={"name": "Public DB"})
|
||||
cid = r.json()["id"]
|
||||
client.post(f"/db/{cid}/pages/api", json={"title": "Public Page"})
|
||||
resp = client.get(f"/workspace/public/{cid}")
|
||||
assert resp.status_code == 200
|
||||
assert "Public Page" in resp.text
|
||||
client.delete(f"/db/api/{cid}")
|
||||
|
||||
|
||||
def test_db_templates(client):
|
||||
resp = client.post("/workspace/templates/database", json={
|
||||
"name": "Bug Tracker", "schema": [{"name": "Severity", "type": "select"}],
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
tid = resp.json()["id"]
|
||||
|
||||
templates = client.get("/workspace/templates/database").json()["templates"]
|
||||
assert len(templates) >= 1
|
||||
|
||||
apply = client.post(f"/workspace/templates/database/{tid}/apply", json={"name": "Bugs v2"})
|
||||
assert apply.status_code == 200
|
||||
client.delete(f"/db/api/{apply.json()['collection_id']}")
|
||||
|
||||
|
||||
def test_page_history(client):
|
||||
r = client.post("/db/api", json={"name": "Hist DB"})
|
||||
cid = r.json()["id"]
|
||||
p = client.post(f"/db/{cid}/pages/api", json={"title": "History Page"})
|
||||
pid = p.json()["id"]
|
||||
|
||||
client.post(f"/workspace/pages/{pid}/history", json={
|
||||
"change_type": "created", "snapshot": {"title": "History Page"},
|
||||
})
|
||||
hist = client.get(f"/workspace/pages/{pid}/history").json()["history"]
|
||||
assert len(hist) == 1
|
||||
client.delete(f"/db/api/{cid}")
|
||||
|
||||
Reference in New Issue
Block a user