gabriel / musehub public
test_musehub_ui_repo_home_ssr.py python
220 lines 6.7 KB
c0f0b481 release: merge dev → main (#5) Gabriel Cardona <cgcardona@gmail.com> 5d ago
1 """SSR tests for the MuseHub repo home page (issue #575).
2
3 Covers GET /{owner}/{repo_slug} after SSR migration:
4
5 - test_repo_home_renders_repo_description_server_side
6 Seed repo with description, GET home, assert description in HTML body.
7
8 - test_repo_home_renders_file_tree_server_side
9 Seed a file tree entry via a commit object, assert filename in HTML.
10
11 - test_repo_home_branch_picker_has_hx_get
12 Branch select form carries ``hx-get`` attribute pointing to repo base URL.
13
14 - test_repo_home_htmx_fragment_on_branch_switch
15 GET with ``HX-Request: true`` → file tree fragment (no <html> wrapper).
16
17 - test_repo_home_shows_tempo_bpm
18 Repo with tempo_bpm set → BPM value appears in sidebar HTML.
19
20 - test_repo_home_empty_tree_shows_empty_state
21 Empty repo (no objects) → empty state message in HTML.
22
23 - test_repo_home_json_format_returns_json
24 GET with ``?format=json`` → JSON response with camelCase keys.
25 """
26 from __future__ import annotations
27
28 import pytest
29 from httpx import AsyncClient
30 from sqlalchemy.ext.asyncio import AsyncSession
31
32 from musehub.db.musehub_models import MusehubCommit, MusehubObject, MusehubRepo
33
34 pytestmark = pytest.mark.anyio
35
36 _OWNER = "ssr-home-owner"
37 _SLUG = "ssr-home-repo"
38
39
40 # ---------------------------------------------------------------------------
41 # Helpers
42 # ---------------------------------------------------------------------------
43
44
45 async def _make_repo(
46 db: AsyncSession,
47 *,
48 description: str = "A test music repo",
49 key_signature: str | None = None,
50 tempo_bpm: int | None = None,
51 ) -> str:
52 """Seed a minimal public repo and return its repo_id string."""
53 repo = MusehubRepo(
54 name=_SLUG,
55 owner=_OWNER,
56 slug=_SLUG,
57 visibility="public",
58 owner_user_id="ssr-home-owner-uid",
59 description=description,
60 key_signature=key_signature,
61 tempo_bpm=tempo_bpm,
62 )
63 db.add(repo)
64 await db.commit()
65 await db.refresh(repo)
66 return str(repo.repo_id)
67
68
69 async def _add_object(
70 db: AsyncSession,
71 repo_id: str,
72 path: str,
73 *,
74 size_bytes: int = 1024,
75 ) -> None:
76 """Seed a MusehubObject so the file tree has entries."""
77 import uuid
78
79 obj = MusehubObject(
80 object_id=f"sha256:{uuid.uuid4().hex}",
81 repo_id=repo_id,
82 path=path,
83 size_bytes=size_bytes,
84 disk_path=f"/tmp/test/{path}",
85 )
86 db.add(obj)
87 await db.commit()
88
89
90 # ---------------------------------------------------------------------------
91 # Tests
92 # ---------------------------------------------------------------------------
93
94
95 async def test_repo_home_renders_repo_description_server_side(
96 client: AsyncClient,
97 db_session: AsyncSession,
98 ) -> None:
99 """Seed a repo with a description, GET the home page, assert description in HTML.
100
101 The SSR migration means the description must be in the initial HTML response —
102 not fetched by JavaScript after page load.
103 """
104 description = "Jazz standards arranged for modern quartet"
105 await _make_repo(db_session, description=description)
106
107 resp = await client.get(f"/{_OWNER}/{_SLUG}")
108 assert resp.status_code == 200
109 assert description in resp.text
110
111
112 async def test_repo_home_renders_file_tree_server_side(
113 client: AsyncClient,
114 db_session: AsyncSession,
115 ) -> None:
116 """Seed a file object, GET the home page, assert filename appears in HTML.
117
118 The SSR migration means the file tree is rendered server-side.
119 This test fails if the handler omits ``tree`` from the template context.
120 """
121 repo_id = await _make_repo(db_session)
122 await _add_object(db_session, repo_id, "bass_line.mid")
123
124 resp = await client.get(f"/{_OWNER}/{_SLUG}")
125 assert resp.status_code == 200
126 assert "bass_line.mid" in resp.text
127
128
129 async def test_repo_home_branch_picker_has_hx_get(
130 client: AsyncClient,
131 db_session: AsyncSession,
132 ) -> None:
133 """Branch picker form carries ``hx-get`` for HTMX branch switching.
134
135 The form submits via HTMX (not a full page reload), targeting ``#file-tree``
136 so only the file tree updates when the user switches branches.
137 """
138 await _make_repo(db_session)
139
140 resp = await client.get(f"/{_OWNER}/{_SLUG}")
141 assert resp.status_code == 200
142 assert "hx-get" in resp.text
143
144
145 async def test_repo_home_htmx_fragment_on_branch_switch(
146 client: AsyncClient,
147 db_session: AsyncSession,
148 ) -> None:
149 """GET with ``HX-Request: true`` returns only the bare file tree fragment.
150
151 The fragment must not contain a full HTML document shell (<html>, <head>)
152 — it is swapped directly into ``#file-tree`` by HTMX on branch change.
153 """
154 repo_id = await _make_repo(db_session)
155 await _add_object(db_session, repo_id, "melody.mid")
156
157 resp = await client.get(
158 f"/{_OWNER}/{_SLUG}",
159 headers={"HX-Request": "true"},
160 )
161 assert resp.status_code == 200
162 body = resp.text
163 assert "melody.mid" in body
164 assert "<html" not in body
165 assert "<head" not in body
166
167
168 async def test_repo_home_shows_tempo_bpm(
169 client: AsyncClient,
170 db_session: AsyncSession,
171 ) -> None:
172 """Repo with tempo_bpm set → BPM value appears in the rendered HTML sidebar.
173
174 The SSR migration must pass repo metadata to the template so music-specific
175 properties (key, tempo) are visible without a client-side API call.
176 """
177 await _make_repo(db_session, tempo_bpm=132)
178
179 resp = await client.get(f"/{_OWNER}/{_SLUG}")
180 assert resp.status_code == 200
181 assert "132" in resp.text
182 assert "BPM" in resp.text
183
184
185 async def test_repo_home_empty_tree_shows_empty_state(
186 client: AsyncClient,
187 db_session: AsyncSession,
188 ) -> None:
189 """Empty repo (no objects) renders an empty state message in the file tree area.
190
191 The file_tree fragment falls through to the empty_state macro when
192 ``tree`` is an empty list.
193 """
194 await _make_repo(db_session)
195
196 resp = await client.get(f"/{_OWNER}/{_SLUG}")
197 assert resp.status_code == 200
198 # empty_state macro renders an icon + "Empty repository" message
199 assert "Empty repository" in resp.text
200
201
202 async def test_repo_home_json_format_returns_json(
203 client: AsyncClient,
204 db_session: AsyncSession,
205 ) -> None:
206 """GET with ``?format=json`` returns a JSON response with camelCase keys.
207
208 The JSON shortcut preserves backward compatibility for API consumers
209 (agents, curl scripts) that rely on the structured repo data.
210 """
211 description = "Jazz standards for JSON test"
212 await _make_repo(db_session, description=description)
213
214 resp = await client.get(f"/{_OWNER}/{_SLUG}?format=json")
215 assert resp.status_code == 200
216 assert resp.headers["content-type"].startswith("application/json")
217 data = resp.json()
218 assert data["slug"] == _SLUG
219 assert data["owner"] == _OWNER
220 assert data["description"] == description