gabriel / musehub public
test_musehub_ui_milestones.py python
368 lines 12.7 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """Tests for Muse Hub milestones UI endpoints.
2
3 Covers GET /musehub/ui/{owner}/{repo_slug}/milestones:
4 - test_milestones_list_page_returns_200 — page renders without auth
5 - test_milestones_list_no_auth_required — no JWT needed for HTML shell
6 - test_milestones_list_has_progress_bar_js — progress bar rendering present
7 - test_milestones_list_has_state_tabs_js — open/closed/all state tabs present
8 - test_milestones_list_has_sort_controls_js — due_on/title/completeness sort buttons
9 - test_milestones_list_json_response — ?format=json returns MilestoneListResponse
10 - test_milestones_list_json_has_milestones_key — JSON contains milestones array
11 - test_milestones_list_unknown_repo_404 — unknown owner/slug → 404
12 - test_milestones_list_shows_base_url_not_repo_id — base_url uses owner/slug pattern
13
14 Covers GET /musehub/ui/{owner}/{repo_slug}/milestones/{number}:
15 - test_milestone_detail_page_returns_200 — page renders without auth
16 - test_milestone_detail_no_auth_required — no JWT needed for HTML shell
17 - test_milestone_detail_has_progress_bar — progress bar JS present
18 - test_milestone_detail_has_linked_issues_js — issue list rendering JS present
19 - test_milestone_detail_has_state_filter_tabs — open/closed/all issue filter tabs
20 - test_milestone_detail_json_response — ?format=json returns composite response
21 - test_milestone_detail_json_has_linked_issues — JSON contains linked_issues key
22 - test_milestone_detail_unknown_number_404 — non-existent milestone number → 404
23 - test_milestone_detail_unknown_repo_404 — unknown owner/slug → 404
24 - test_milestone_detail_json_issue_counts — JSON open_issues/closed_issues counts correct
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 (
33 MusehubIssue,
34 MusehubMilestone,
35 MusehubRepo,
36 )
37
38
39 # ---------------------------------------------------------------------------
40 # Helpers
41 # ---------------------------------------------------------------------------
42
43
44 async def _make_repo(db: AsyncSession, owner: str = "artist", slug: str = "album-one") -> str:
45 """Seed a public repo and return its repo_id string."""
46 repo = MusehubRepo(
47 name=slug,
48 owner=owner,
49 slug=slug,
50 visibility="public",
51 owner_user_id="uid-test-artist",
52 )
53 db.add(repo)
54 await db.commit()
55 await db.refresh(repo)
56 return str(repo.repo_id)
57
58
59 async def _make_milestone(
60 db: AsyncSession,
61 repo_id: str,
62 *,
63 number: int = 1,
64 title: str = "Album v1.0",
65 description: str = "First release milestone",
66 state: str = "open",
67 ) -> MusehubMilestone:
68 """Seed a milestone and return it."""
69 ms = MusehubMilestone(
70 repo_id=repo_id,
71 number=number,
72 title=title,
73 description=description,
74 state=state,
75 author="artist",
76 )
77 db.add(ms)
78 await db.commit()
79 await db.refresh(ms)
80 return ms
81
82
83 async def _make_issue(
84 db: AsyncSession,
85 repo_id: str,
86 *,
87 number: int = 1,
88 title: str = "Bass mix is too loud",
89 state: str = "open",
90 milestone_id: str | None = None,
91 ) -> MusehubIssue:
92 """Seed an issue and return it."""
93 issue = MusehubIssue(
94 repo_id=repo_id,
95 number=number,
96 title=title,
97 body="Description of the issue.",
98 state=state,
99 labels=["mix"],
100 author="artist",
101 milestone_id=milestone_id,
102 )
103 db.add(issue)
104 await db.commit()
105 await db.refresh(issue)
106 return issue
107
108
109 # ---------------------------------------------------------------------------
110 # Milestones list page tests
111 # ---------------------------------------------------------------------------
112
113
114 @pytest.mark.anyio
115 async def test_milestones_list_page_returns_200(
116 client: AsyncClient,
117 db_session: AsyncSession,
118 ) -> None:
119 """GET /musehub/ui/{owner}/{slug}/milestones returns 200 HTML."""
120 await _make_repo(db_session)
121 response = await client.get("/musehub/ui/artist/album-one/milestones")
122 assert response.status_code == 200
123 assert "text/html" in response.headers["content-type"]
124
125
126 @pytest.mark.anyio
127 async def test_milestones_list_no_auth_required(
128 client: AsyncClient,
129 db_session: AsyncSession,
130 ) -> None:
131 """Milestones list page is accessible without a JWT token."""
132 await _make_repo(db_session)
133 response = await client.get("/musehub/ui/artist/album-one/milestones")
134 assert response.status_code == 200
135
136
137 @pytest.mark.anyio
138 async def test_milestones_list_has_progress_bar_js(
139 client: AsyncClient,
140 db_session: AsyncSession,
141 ) -> None:
142 """Page HTML contains progress bar rendering logic."""
143 await _make_repo(db_session)
144 response = await client.get("/musehub/ui/artist/album-one/milestones")
145 assert response.status_code == 200
146 assert "progress-bar" in response.text
147
148
149 @pytest.mark.anyio
150 async def test_milestones_list_has_state_tabs_js(
151 client: AsyncClient,
152 db_session: AsyncSession,
153 ) -> None:
154 """Milestones list page has open/closed/all state filter tabs."""
155 await _make_repo(db_session)
156 response = await client.get("/musehub/ui/artist/album-one/milestones")
157 assert response.status_code == 200
158 body = response.text
159 assert "state-tabs" in body or "state-tab" in body
160 assert "open" in body
161 assert "closed" in body
162
163
164 @pytest.mark.anyio
165 async def test_milestones_list_has_sort_controls_js(
166 client: AsyncClient,
167 db_session: AsyncSession,
168 ) -> None:
169 """Milestones list page exposes due_on / title / completeness sort controls."""
170 await _make_repo(db_session)
171 response = await client.get("/musehub/ui/artist/album-one/milestones")
172 assert response.status_code == 200
173 body = response.text
174 assert "due_on" in body or "completeness" in body
175
176
177 @pytest.mark.anyio
178 async def test_milestones_list_json_response(
179 client: AsyncClient,
180 db_session: AsyncSession,
181 ) -> None:
182 """?format=json returns JSON with HTTP 200."""
183 repo_id = await _make_repo(db_session)
184 await _make_milestone(db_session, repo_id, title="Mix Revision 2")
185 response = await client.get("/musehub/ui/artist/album-one/milestones?format=json")
186 assert response.status_code == 200
187 assert response.headers["content-type"].startswith("application/json")
188
189
190 @pytest.mark.anyio
191 async def test_milestones_list_json_has_milestones_key(
192 client: AsyncClient,
193 db_session: AsyncSession,
194 ) -> None:
195 """JSON response contains a milestones array."""
196 repo_id = await _make_repo(db_session)
197 await _make_milestone(db_session, repo_id, title="Album v1.0")
198 response = await client.get("/musehub/ui/artist/album-one/milestones?format=json&state=all")
199 assert response.status_code == 200
200 data = response.json()
201 assert "milestones" in data
202 assert isinstance(data["milestones"], list)
203 assert len(data["milestones"]) >= 1
204
205
206 @pytest.mark.anyio
207 async def test_milestones_list_unknown_repo_404(
208 client: AsyncClient,
209 db_session: AsyncSession,
210 ) -> None:
211 """Unknown owner/slug returns 404."""
212 response = await client.get("/musehub/ui/nobody/nonexistent/milestones")
213 assert response.status_code == 404
214
215
216 @pytest.mark.anyio
217 async def test_milestones_list_shows_base_url_not_repo_id(
218 client: AsyncClient,
219 db_session: AsyncSession,
220 ) -> None:
221 """HTML page uses owner/slug base_url pattern, not raw repo_id UUID."""
222 await _make_repo(db_session)
223 response = await client.get("/musehub/ui/artist/album-one/milestones")
224 assert response.status_code == 200
225 assert "/musehub/ui/artist/album-one" in response.text
226
227
228 # ---------------------------------------------------------------------------
229 # Milestone detail page tests
230 # ---------------------------------------------------------------------------
231
232
233 @pytest.mark.anyio
234 async def test_milestone_detail_page_returns_200(
235 client: AsyncClient,
236 db_session: AsyncSession,
237 ) -> None:
238 """GET /musehub/ui/{owner}/{slug}/milestones/{number} returns 200 HTML."""
239 repo_id = await _make_repo(db_session)
240 await _make_milestone(db_session, repo_id, number=1)
241 response = await client.get("/musehub/ui/artist/album-one/milestones/1")
242 assert response.status_code == 200
243 assert "text/html" in response.headers["content-type"]
244
245
246 @pytest.mark.anyio
247 async def test_milestone_detail_no_auth_required(
248 client: AsyncClient,
249 db_session: AsyncSession,
250 ) -> None:
251 """Milestone detail page is accessible without a JWT token."""
252 repo_id = await _make_repo(db_session)
253 await _make_milestone(db_session, repo_id, number=1)
254 response = await client.get("/musehub/ui/artist/album-one/milestones/1")
255 assert response.status_code == 200
256
257
258 @pytest.mark.anyio
259 async def test_milestone_detail_has_progress_bar(
260 client: AsyncClient,
261 db_session: AsyncSession,
262 ) -> None:
263 """Milestone detail page contains progress bar element."""
264 repo_id = await _make_repo(db_session)
265 await _make_milestone(db_session, repo_id, number=1)
266 response = await client.get("/musehub/ui/artist/album-one/milestones/1")
267 assert response.status_code == 200
268 assert "progress-bar" in response.text
269
270
271 @pytest.mark.anyio
272 async def test_milestone_detail_has_linked_issues_js(
273 client: AsyncClient,
274 db_session: AsyncSession,
275 ) -> None:
276 """Milestone detail page has JavaScript to render linked issues."""
277 repo_id = await _make_repo(db_session)
278 await _make_milestone(db_session, repo_id, number=1)
279 response = await client.get("/musehub/ui/artist/album-one/milestones/1")
280 assert response.status_code == 200
281 body = response.text
282 assert "issue-rows" in body or "renderIssueRows" in body
283
284
285 @pytest.mark.anyio
286 async def test_milestone_detail_has_state_filter_tabs(
287 client: AsyncClient,
288 db_session: AsyncSession,
289 ) -> None:
290 """Milestone detail page has open/closed/all tabs to filter linked issues."""
291 repo_id = await _make_repo(db_session)
292 await _make_milestone(db_session, repo_id, number=1)
293 response = await client.get("/musehub/ui/artist/album-one/milestones/1")
294 assert response.status_code == 200
295 body = response.text
296 assert "state-tabs" in body or "state-tab" in body
297
298
299 @pytest.mark.anyio
300 async def test_milestone_detail_json_response(
301 client: AsyncClient,
302 db_session: AsyncSession,
303 ) -> None:
304 """?format=json returns JSON with HTTP 200."""
305 repo_id = await _make_repo(db_session)
306 await _make_milestone(db_session, repo_id, number=1)
307 response = await client.get("/musehub/ui/artist/album-one/milestones/1?format=json")
308 assert response.status_code == 200
309 assert response.headers["content-type"].startswith("application/json")
310
311
312 @pytest.mark.anyio
313 async def test_milestone_detail_json_has_linked_issues(
314 client: AsyncClient,
315 db_session: AsyncSession,
316 ) -> None:
317 """JSON response includes the milestone data and linkedIssues."""
318 repo_id = await _make_repo(db_session)
319 ms = await _make_milestone(db_session, repo_id, number=1, title="Album v1.0")
320 await _make_issue(db_session, repo_id, number=1, milestone_id=str(ms.milestone_id))
321 response = await client.get("/musehub/ui/artist/album-one/milestones/1?format=json")
322 assert response.status_code == 200
323 data = response.json()
324 assert "title" in data
325 assert data["title"] == "Album v1.0"
326 assert "linkedIssues" in data
327 assert "issues" in data["linkedIssues"]
328
329
330 @pytest.mark.anyio
331 async def test_milestone_detail_unknown_number_404(
332 client: AsyncClient,
333 db_session: AsyncSession,
334 ) -> None:
335 """Non-existent milestone number returns 404."""
336 await _make_repo(db_session)
337 response = await client.get("/musehub/ui/artist/album-one/milestones/999")
338 assert response.status_code == 404
339
340
341 @pytest.mark.anyio
342 async def test_milestone_detail_unknown_repo_404(
343 client: AsyncClient,
344 db_session: AsyncSession,
345 ) -> None:
346 """Unknown owner/slug returns 404 for detail page."""
347 response = await client.get("/musehub/ui/nobody/nonexistent/milestones/1")
348 assert response.status_code == 404
349
350
351 @pytest.mark.anyio
352 async def test_milestone_detail_json_issue_counts(
353 client: AsyncClient,
354 db_session: AsyncSession,
355 ) -> None:
356 """JSON response has correct open_issues and closed_issues counts."""
357 repo_id = await _make_repo(db_session)
358 ms = await _make_milestone(db_session, repo_id, number=1)
359 ms_id = str(ms.milestone_id)
360 # 2 open + 1 closed
361 await _make_issue(db_session, repo_id, number=1, state="open", milestone_id=ms_id)
362 await _make_issue(db_session, repo_id, number=2, state="open", milestone_id=ms_id)
363 await _make_issue(db_session, repo_id, number=3, state="closed", milestone_id=ms_id)
364 response = await client.get("/musehub/ui/artist/album-one/milestones/1?format=json")
365 assert response.status_code == 200
366 data = response.json()
367 assert data.get("openIssues") == 2
368 assert data.get("closedIssues") == 1