21ec9cb4c3
- Add auth_user, admin_user, mock_minio_presigned, mock_minio_stat fixtures to conftest.py - Create test_quota.py with 4 xfail stubs (STORE-03, STORE-05, STORE-06, SC2 race) - Append test_migration_0003 to test_alembic.py (full pre-seed + post-migration assertions) - Append 3 classifier xfail stubs (DOC-03, DOC-05, D-15) - Append 6 document xfail stubs (D-05, STORE-04, SEC-04, D-16) - Append 4 topic xfail stubs (DOC-04, D-09, D-17) - Append test_settings_endpoint_removed stub (D-12) - All 19 new test IDs collect cleanly with xfail(strict=False)
142 lines
5.0 KiB
Python
142 lines
5.0 KiB
Python
"""
|
|
Topics API tests — async only (Plan 05 cutover).
|
|
|
|
Legacy sync tests (using the flat-file storage layer and sync TestClient) were
|
|
updated to async in Plan 05 to match the new session-injected API routes.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
|
|
async def test_list_topics_empty(async_client):
|
|
resp = await async_client.get("/api/topics")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["topics"] == []
|
|
|
|
|
|
async def test_create_topic(async_client):
|
|
resp = await async_client.post(
|
|
"/api/topics",
|
|
json={"name": "Finance", "description": "Financial docs", "color": "#ff0000"},
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["name"] == "Finance"
|
|
assert data["color"] == "#ff0000"
|
|
assert "id" in data
|
|
|
|
|
|
async def test_create_topic_deduplication(async_client):
|
|
await async_client.post("/api/topics", json={"name": "Finance"})
|
|
resp = await async_client.post("/api/topics", json={"name": "finance"}) # case-insensitive
|
|
assert resp.status_code == 200
|
|
topics = (await async_client.get("/api/topics")).json()["topics"]
|
|
assert len(topics) == 1
|
|
|
|
|
|
async def test_update_topic(async_client):
|
|
create = (await async_client.post("/api/topics", json={"name": "Old Name"})).json()
|
|
resp = await async_client.patch(f"/api/topics/{create['id']}", json={"name": "New Name"})
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"] == "New Name"
|
|
|
|
|
|
async def test_update_topic_not_found(async_client):
|
|
resp = await async_client.patch(
|
|
"/api/topics/00000000-0000-0000-0000-000000000000",
|
|
json={"name": "X"},
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
async def test_delete_topic(async_client):
|
|
create = (await async_client.post("/api/topics", json={"name": "ToDelete"})).json()
|
|
resp = await async_client.delete(f"/api/topics/{create['id']}")
|
|
assert resp.status_code == 200
|
|
assert resp.json()["success"] is True
|
|
|
|
topics = (await async_client.get("/api/topics")).json()["topics"]
|
|
assert not any(t["name"] == "ToDelete" for t in topics)
|
|
|
|
|
|
async def test_delete_topic_cascades_to_documents(async_client, db_session, sample_txt):
|
|
# Create a topic
|
|
topic = (await async_client.post("/api/topics", json={"name": "Legal"})).json()
|
|
|
|
# Upload doc (no auto classify)
|
|
with open(sample_txt, "rb") as f:
|
|
upload = (
|
|
await async_client.post(
|
|
"/api/documents/upload",
|
|
files={"file": ("sample.txt", f, "text/plain")},
|
|
data={"auto_classify": "false"},
|
|
)
|
|
).json()
|
|
|
|
# Manually set topic via the storage service
|
|
from services import storage
|
|
|
|
await storage.update_document_topics(db_session, upload["id"], ["Legal"])
|
|
|
|
# Delete topic
|
|
await async_client.delete(f"/api/topics/{topic['id']}")
|
|
|
|
# Verify document no longer has the topic
|
|
doc = (await async_client.get(f"/api/documents/{upload['id']}")).json()
|
|
assert "Legal" not in doc["topics"]
|
|
|
|
|
|
async def test_delete_topic_not_found(async_client):
|
|
resp = await async_client.delete("/api/topics/nonexistent")
|
|
assert resp.status_code == 404
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Wave 0 xfail stubs for Phase 3 topic namespace tests — Plan 03-03
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@pytest.mark.xfail(strict=False, reason="implemented in plan 03-03")
|
|
async def test_topic_namespace(async_client, auth_user, db_session):
|
|
"""GET /api/topics returns only system topics (user_id=NULL) + auth_user-owned topics.
|
|
|
|
DOC-04: layered topic namespace — system topics (user_id=NULL) are visible to
|
|
all users; per-user topics (user_id=current_user.id) are visible only to that
|
|
user. A different user's topics must not appear (CONTEXT.md D-08, D-17).
|
|
|
|
Test setup: seed one system topic, one auth_user-owned topic, one topic owned
|
|
by a different user. GET /api/topics must return exactly the first two.
|
|
"""
|
|
assert True # scaffold
|
|
|
|
|
|
@pytest.mark.xfail(strict=False, reason="implemented in plan 03-03")
|
|
async def test_admin_create_system_topic(async_client, admin_user):
|
|
"""POST /api/admin/topics returns 201 and creates a Topic with user_id=NULL.
|
|
|
|
D-09: only admin can create system topics via POST /api/admin/topics.
|
|
The created topic has user_id=NULL and is visible to all users.
|
|
"""
|
|
assert True # scaffold
|
|
|
|
|
|
@pytest.mark.xfail(strict=False, reason="implemented in plan 03-03")
|
|
async def test_regular_user_cannot_create_system_topic(async_client, auth_user):
|
|
"""POST /api/admin/topics with auth_user.headers returns 403.
|
|
|
|
D-09: the admin topics endpoint requires get_current_admin; regular users
|
|
receive 403 Forbidden.
|
|
"""
|
|
assert True # scaffold
|
|
|
|
|
|
@pytest.mark.xfail(strict=False, reason="implemented in plan 03-03")
|
|
async def test_topics_require_auth(async_client):
|
|
"""Anonymous GET /api/topics (no Authorization header) returns 401 or 403.
|
|
|
|
D-17: /api/topics/* gains get_current_user in Phase 3 — anonymous access
|
|
must be rejected.
|
|
"""
|
|
assert True # scaffold
|