theo-agent-dashboard

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

test_topics.py (9329B)


      1 """T048 — Tests for topic CRUD endpoints."""
      2 import os
      3 from pathlib import Path
      4 
      5 import pytest
      6 from httpx import ASGITransport, AsyncClient
      7 from app.db import Database
      8 from app.routes.topics import set_db
      9 
     10 @pytest.fixture(autouse=True)
     11 def _isolate_auth_config(tmp_path):
     12     """Each test gets a clean config file."""
     13     config_path = str(tmp_path / "config.json")
     14     os.environ["THEO_CONFIG_PATH"] = config_path
     15     import app.routes.auth as auth_module
     16     old = auth_module._CONFIG_PATH
     17     auth_module._CONFIG_PATH = config_path
     18     yield
     19     auth_module._CONFIG_PATH = old
     20     os.environ.pop("THEO_CONFIG_PATH", None)
     21 
     22 
     23 @pytest.fixture
     24 async def client(tmp_path: Path) -> AsyncClient:
     25     """Fresh async client with isolated DB for each test."""
     26     db_path = str(tmp_path / "test.db")
     27     db = Database(db_path)
     28     await db.connect()
     29     migrations_dir = Path(__file__).parent.parent / "app" / "migrations"
     30     await db.run_migrations(migrations_dir)
     31     set_db(db)
     32     from app.main import app
     33     async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as c:
     34         yield c
     35     await db.close()
     36 
     37 
     38 async def _auth_headers(client: AsyncClient) -> None:
     39     """Set up auth and inject JWT cookie."""
     40     resp = await client.post("/api/auth/setup", json={
     41         "password": "testpass123",
     42         "api_key": "sk-test",
     43     })
     44     token = resp.json()["token"]
     45     client.cookies.set("theo_token", token)
     46 
     47 
     48 class TestTopicCreate:
     49     """POST /api/topics"""
     50 
     51     async def test_create_topic_returns_200(self, client: AsyncClient) -> None:
     52         """Creating a topic returns TopicResponse with id and name."""
     53         await _auth_headers(client)
     54         resp = await client.post("/api/topics", json={"name": "Work"})
     55         assert resp.status_code == 200
     56         body = resp.json()
     57         assert body["name"] == "Work"
     58         assert isinstance(body["id"], str)
     59         assert len(body["id"]) > 0
     60         assert body["color"] == "#6366f1"
     61         assert isinstance(body["sort_order"], int)
     62         assert body["sort_order"] == 0
     63         assert isinstance(body["created_at"], int)
     64         assert body["created_at"] > 0
     65 
     66     async def test_create_topic_with_color(self, client: AsyncClient) -> None:
     67         """Creating a topic with a custom color stores it."""
     68         await _auth_headers(client)
     69         resp = await client.post("/api/topics", json={"name": "Personal", "color": "#ef4444"})
     70         assert resp.status_code == 200
     71         body = resp.json()
     72         assert body["name"] == "Personal"
     73         assert body["color"] == "#ef4444"
     74         assert isinstance(body["id"], str)
     75         assert len(body["id"]) > 0
     76 
     77 
     78 class TestTopicList:
     79     """GET /api/topics"""
     80 
     81     async def test_list_empty(self, client: AsyncClient) -> None:
     82         """Empty database returns empty list."""
     83         await _auth_headers(client)
     84         resp = await client.get("/api/topics")
     85         assert resp.status_code == 200
     86         assert resp.json() == []
     87 
     88     async def test_list_returns_topics_with_session_count(self, client: AsyncClient) -> None:
     89         """Topics include session_count field."""
     90         await _auth_headers(client)
     91         await client.post("/api/topics", json={"name": "Alpha"})
     92         resp = await client.get("/api/topics")
     93         assert resp.status_code == 200
     94         topics = resp.json()
     95         assert len(topics) == 1
     96         assert topics[0]["session_count"] == 0
     97         assert topics[0]["name"] == "Alpha"
     98         assert topics[0]["color"] == "#6366f1"
     99         assert topics[0]["sort_order"] == 0
    100 
    101 
    102 class TestTopicUpdate:
    103     """PATCH /api/topics/{id}"""
    104 
    105     async def test_update_name(self, client: AsyncClient) -> None:
    106         """PATCH updates topic name."""
    107         await _auth_headers(client)
    108         create = await client.post("/api/topics", json={"name": "Old"})
    109         topic_id = create.json()["id"]
    110         resp = await client.patch(f"/api/topics/{topic_id}", json={"name": "New"})
    111         assert resp.status_code == 200
    112         body = resp.json()
    113         assert body["name"] == "New"
    114         assert body["id"] == topic_id
    115 
    116     async def test_update_color(self, client: AsyncClient) -> None:
    117         """PATCH updates topic color."""
    118         await _auth_headers(client)
    119         create = await client.post("/api/topics", json={"name": "T"})
    120         topic_id = create.json()["id"]
    121         resp = await client.patch(f"/api/topics/{topic_id}", json={"color": "#10b981"})
    122         assert resp.status_code == 200
    123         body = resp.json()
    124         assert body["color"] == "#10b981"
    125         assert body["id"] == topic_id
    126 
    127     async def test_update_sort_order(self, client: AsyncClient) -> None:
    128         """PATCH updates topic sort_order."""
    129         await _auth_headers(client)
    130         create = await client.post("/api/topics", json={"name": "T"})
    131         topic_id = create.json()["id"]
    132         resp = await client.patch(f"/api/topics/{topic_id}", json={"sort_order": 5})
    133         assert resp.status_code == 200
    134         body = resp.json()
    135         assert body["sort_order"] == 5
    136         assert body["id"] == topic_id
    137 
    138 
    139 class TestTopicDelete:
    140     """DELETE /api/topics/{id}"""
    141 
    142     async def test_delete_removes_topic(self, client: AsyncClient) -> None:
    143         """DELETE removes topic from list."""
    144         await _auth_headers(client)
    145         create = await client.post("/api/topics", json={"name": "Temp"})
    146         topic_id = create.json()["id"]
    147         resp = await client.delete(f"/api/topics/{topic_id}")
    148         assert resp.status_code == 200
    149         body = resp.json()
    150         assert body == {"ok": "deleted"}
    151         list_resp = await client.get("/api/topics")
    152         assert list_resp.json() == []
    153 
    154     async def test_delete_with_sessions_moves_to_unsorted(self, client: AsyncClient) -> None:
    155         """Deleting a topic removes topic_sessions rows (sessions become unsorted)."""
    156         await _auth_headers(client)
    157         create = await client.post("/api/topics", json={"name": "Gone"})
    158         topic_id = create.json()["id"]
    159         await client.post(f"/api/topics/{topic_id}/sessions", json={"session_id": "sess-1"})
    160         resp = await client.delete(f"/api/topics/{topic_id}")
    161         assert resp.status_code == 200
    162         assert resp.json() == {"ok": "deleted"}
    163         list_resp = await client.get("/api/topics")
    164         assert list_resp.json() == []
    165 
    166 
    167 class TestTopicSessionAssign:
    168     """POST /api/topics/{id}/sessions"""
    169 
    170     async def test_assign_session_to_topic(self, client: AsyncClient) -> None:
    171         """POST assigns a session to a topic."""
    172         await _auth_headers(client)
    173         create = await client.post("/api/topics", json={"name": "Proj"})
    174         topic_id = create.json()["id"]
    175         resp = await client.post(
    176             f"/api/topics/{topic_id}/sessions",
    177             json={"session_id": "sess-abc"},
    178         )
    179         assert resp.status_code == 200
    180         assert resp.json() == {"ok": "assigned"}
    181         list_resp = await client.get("/api/topics")
    182         topics = list_resp.json()
    183         assert topics[0]["session_count"] == 1
    184 
    185 
    186 class TestTopicSessionUnassign:
    187     """DELETE /api/topics/{id}/sessions/{session_id}"""
    188 
    189     async def test_unassign_session(self, client: AsyncClient) -> None:
    190         """DELETE removes session assignment."""
    191         await _auth_headers(client)
    192         create = await client.post("/api/topics", json={"name": "Proj"})
    193         topic_id = create.json()["id"]
    194         await client.post(
    195             f"/api/topics/{topic_id}/sessions",
    196             json={"session_id": "sess-xyz"},
    197         )
    198         resp = await client.delete(f"/api/topics/{topic_id}/sessions/sess-xyz")
    199         assert resp.status_code == 200
    200         assert resp.json() == {"ok": "unassigned"}
    201         list_resp = await client.get("/api/topics")
    202         assert list_resp.json()[0]["session_count"] == 0
    203 
    204 
    205 class TestTopicNotFound:
    206     """404 for non-existent topics."""
    207 
    208     async def test_update_nonexistent_returns_404(self, client: AsyncClient) -> None:
    209         """PATCH on non-existent topic returns 404."""
    210         await _auth_headers(client)
    211         resp = await client.patch("/api/topics/nonexistent", json={"name": "X"})
    212         assert resp.status_code == 404
    213         body = resp.json()
    214         assert "detail" in body
    215         assert "not found" in body["detail"].lower()
    216 
    217     async def test_delete_nonexistent_returns_404(self, client: AsyncClient) -> None:
    218         """DELETE on non-existent topic returns 404."""
    219         await _auth_headers(client)
    220         resp = await client.delete("/api/topics/nonexistent")
    221         assert resp.status_code == 404
    222         body = resp.json()
    223         assert "detail" in body
    224         assert "not found" in body["detail"].lower()
    225 
    226     async def test_assign_nonexistent_returns_404(self, client: AsyncClient) -> None:
    227         """POST assign on non-existent topic returns 404."""
    228         await _auth_headers(client)
    229         resp = await client.post(
    230             "/api/topics/nonexistent/sessions",
    231             json={"session_id": "sess-1"},
    232         )
    233         assert resp.status_code == 404
    234 
    235     async def test_unassign_nonexistent_returns_404(self, client: AsyncClient) -> None:
    236         """DELETE unassign on non-existent topic returns 404."""
    237         await _auth_headers(client)
    238         resp = await client.delete("/api/topics/nonexistent/sessions/sess-1")
    239         assert resp.status_code == 404