gabriel / musehub public
test_musehub_ui_sessions_ssr.py python
248 lines 7.7 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago
1 """SSR tests for the MuseHub sessions list and session detail pages (issue #573).
2
3 Verifies that both ``GET /{owner}/{repo_slug}/sessions`` and
4 ``GET /{owner}/{repo_slug}/sessions/{session_id}`` render session
5 data server-side rather than relying on client-side JavaScript fetches.
6
7 Tests:
8 - test_sessions_list_renders_session_name_server_side
9 — Seed a session, GET page, assert session_id present in HTML without JS
10 - test_sessions_list_active_badge_present
11 — Active session → badge with "live" in HTML
12 - test_sessions_list_htmx_fragment_path
13 — GET with HX-Request: true → fragment only (no <html>)
14 - test_sessions_list_empty_state_when_no_sessions
15 — No sessions → empty state rendered server-side
16 - test_session_detail_renders_session_id
17 — GET detail page, assert session metadata in HTML
18 - test_session_detail_renders_participants
19 — Seed participant, assert user_id in HTML
20 - test_session_detail_unknown_id_404
21 — Non-existent session_id → 404
22 """
23 from __future__ import annotations
24
25 from musehub.db.musehub_models import MusehubIdentity
26
27 import uuid
28 from datetime import datetime, timezone
29
30 import pytest
31 from httpx import AsyncClient
32 from sqlalchemy.ext.asyncio import AsyncSession
33
34 from musehub.db.musehub_models import MusehubRepo, MusehubSession
35 from musehub.muse_contracts.json_types import StrDict
36
37 _OWNER = "composer"
38 _SLUG = "symphony-no-9"
39 _USER_ID = "550e8400-e29b-41d4-a716-446655440000" # matches test_user fixture
40
41
42 # ---------------------------------------------------------------------------
43 # Seed helpers
44 # ---------------------------------------------------------------------------
45
46
47 async def _make_repo(db: AsyncSession) -> str:
48 """Seed a repo and return its repo_id string."""
49 repo = MusehubRepo(
50 name=_SLUG,
51 owner=_OWNER,
52 slug=_SLUG,
53 visibility="public",
54 owner_user_id=_USER_ID,
55 )
56 db.add(repo)
57 await db.commit()
58 await db.refresh(repo)
59 return str(repo.repo_id)
60
61
62 async def _make_session(
63 db: AsyncSession,
64 repo_id: str,
65 *,
66 is_active: bool = False,
67 participants: list[str] | None = None,
68 intent: str = "Record the final movement",
69 location: str = "Studio A",
70 notes: str = "",
71 commits: list[str] | None = None,
72 ) -> MusehubSession:
73 """Seed a recording session and return the ORM row."""
74 session_id = str(uuid.uuid4())
75 started_at = datetime.now(timezone.utc)
76 ended_at = None if is_active else started_at
77 row = MusehubSession(
78 session_id=session_id,
79 repo_id=repo_id,
80 started_at=started_at,
81 ended_at=ended_at,
82 participants=participants or [],
83 intent=intent,
84 location=location,
85 notes=notes,
86 commits=commits or [],
87 is_active=is_active,
88 )
89 db.add(row)
90 await db.commit()
91 await db.refresh(row)
92 return row
93
94
95 # ---------------------------------------------------------------------------
96 # Sessions list SSR tests
97 # ---------------------------------------------------------------------------
98
99
100 @pytest.mark.anyio
101 async def test_sessions_list_renders_session_name_server_side(
102 client: AsyncClient,
103 auth_headers: StrDict,
104 db_session: AsyncSession,
105 test_user: MusehubIdentity,
106 ) -> None:
107 """Session ID appears in the HTML response without a JS round-trip.
108
109 The handler queries the DB during the request and inlines the session
110 identifier into the Jinja2 template so browsers receive a complete page
111 on first load.
112 """
113 repo_id = await _make_repo(db_session)
114 row = await _make_session(db_session, repo_id, intent="Compose bridge section")
115 resp = await client.get(
116 f"/{_OWNER}/{_SLUG}/sessions", headers=auth_headers
117 )
118 assert resp.status_code == 200
119 body = resp.text
120 assert row.session_id[:8] in body
121 assert "session-row" in body
122
123
124 @pytest.mark.anyio
125 async def test_sessions_list_active_badge_present(
126 client: AsyncClient,
127 auth_headers: StrDict,
128 db_session: AsyncSession,
129 test_user: MusehubIdentity,
130 ) -> None:
131 """Active session renders a live badge in the server-rendered HTML."""
132 repo_id = await _make_repo(db_session)
133 await _make_session(db_session, repo_id, is_active=True)
134 resp = await client.get(
135 f"/{_OWNER}/{_SLUG}/sessions", headers=auth_headers
136 )
137 assert resp.status_code == 200
138 body = resp.text
139 assert "live" in body.lower() or "Live" in body
140
141
142 @pytest.mark.anyio
143 async def test_sessions_list_htmx_fragment_path(
144 client: AsyncClient,
145 auth_headers: StrDict,
146 db_session: AsyncSession,
147 test_user: MusehubIdentity,
148 ) -> None:
149 """GET with HX-Request: true returns rows fragment, not the full page.
150
151 When HTMX issues a partial swap request the response must NOT contain
152 the full page chrome and MUST contain the session row markup.
153 """
154 repo_id = await _make_repo(db_session)
155 row = await _make_session(db_session, repo_id)
156 htmx_headers = {**auth_headers, "HX-Request": "true"}
157 resp = await client.get(
158 f"/{_OWNER}/{_SLUG}/sessions", headers=htmx_headers
159 )
160 assert resp.status_code == 200
161 body = resp.text
162 assert row.session_id[:8] in body
163 assert "<!DOCTYPE html>" not in body
164 assert "<html" not in body
165
166
167 @pytest.mark.anyio
168 async def test_sessions_list_empty_state_when_no_sessions(
169 client: AsyncClient,
170 auth_headers: StrDict,
171 db_session: AsyncSession,
172 test_user: MusehubIdentity,
173 ) -> None:
174 """Empty session list renders an empty-state component server-side (no JS fetch needed)."""
175 await _make_repo(db_session)
176 resp = await client.get(
177 f"/{_OWNER}/{_SLUG}/sessions", headers=auth_headers
178 )
179 assert resp.status_code == 200
180 body = resp.text
181 assert '<div class="session-row' not in body
182 assert "empty-state" in body or "No sessions yet" in body
183
184
185 # ---------------------------------------------------------------------------
186 # Session detail SSR tests
187 # ---------------------------------------------------------------------------
188
189
190 @pytest.mark.anyio
191 async def test_session_detail_renders_session_id(
192 client: AsyncClient,
193 auth_headers: StrDict,
194 db_session: AsyncSession,
195 test_user: MusehubIdentity,
196 ) -> None:
197 """Session detail page renders the session ID and metadata server-side."""
198 repo_id = await _make_repo(db_session)
199 row = await _make_session(
200 db_session, repo_id, intent="Lay down the horn section", location="Studio B"
201 )
202 resp = await client.get(
203 f"/{_OWNER}/{_SLUG}/sessions/{row.session_id}",
204 headers=auth_headers,
205 )
206 assert resp.status_code == 200
207 body = resp.text
208 assert row.session_id[:8] in body
209 assert "Studio B" in body
210
211
212 @pytest.mark.anyio
213 async def test_session_detail_renders_participants(
214 client: AsyncClient,
215 auth_headers: StrDict,
216 db_session: AsyncSession,
217 test_user: MusehubIdentity,
218 ) -> None:
219 """Participant user IDs appear in the session detail HTML response."""
220 repo_id = await _make_repo(db_session)
221 row = await _make_session(
222 db_session, repo_id, participants=["alice", "bob"]
223 )
224 resp = await client.get(
225 f"/{_OWNER}/{_SLUG}/sessions/{row.session_id}",
226 headers=auth_headers,
227 )
228 assert resp.status_code == 200
229 body = resp.text
230 assert "alice" in body
231 assert "bob" in body
232
233
234 @pytest.mark.anyio
235 async def test_session_detail_unknown_id_404(
236 client: AsyncClient,
237 auth_headers: StrDict,
238 db_session: AsyncSession,
239 test_user: MusehubIdentity,
240 ) -> None:
241 """Non-existent session_id returns HTTP 404."""
242 await _make_repo(db_session)
243 fake_id = str(uuid.uuid4())
244 resp = await client.get(
245 f"/{_OWNER}/{_SLUG}/sessions/{fake_id}",
246 headers=auth_headers,
247 )
248 assert resp.status_code == 404
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago