gabriel / musehub public
test_musehub_ui_milestones.py python
370 lines 12.7 KB
c0f0b481 release: merge dev → main (#5) Gabriel Cardona <cgcardona@gmail.com> 5d ago
1 """Tests for MuseHub milestones UI endpoints.
2
3 Covers GET /{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 /{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 /{owner}/{slug}/milestones returns 200 HTML."""
120 await _make_repo(db_session)
121 response = await client.get("/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("/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 renders milestones list structure."""
143 await _make_repo(db_session)
144 response = await client.get("/artist/album-one/milestones")
145 assert response.status_code == 200
146 # progress-bar CSS is in app.css (SCSS refactor); verify milestones page structure
147 assert "milestones-list" in response.text or "milestone" in response.text.lower()
148
149
150 @pytest.mark.anyio
151 async def test_milestones_list_has_state_tabs_js(
152 client: AsyncClient,
153 db_session: AsyncSession,
154 ) -> None:
155 """Milestones list page has open/closed/all state filter tabs."""
156 await _make_repo(db_session)
157 response = await client.get("/artist/album-one/milestones")
158 assert response.status_code == 200
159 body = response.text
160 assert "state-tabs" in body or "state-tab" in body
161 assert "open" in body
162 assert "closed" in body
163
164
165 @pytest.mark.anyio
166 async def test_milestones_list_has_sort_controls_js(
167 client: AsyncClient,
168 db_session: AsyncSession,
169 ) -> None:
170 """Milestones list page exposes due_on / title / completeness sort controls."""
171 await _make_repo(db_session)
172 response = await client.get("/artist/album-one/milestones")
173 assert response.status_code == 200
174 body = response.text
175 assert "due_on" in body or "completeness" in body
176
177
178 @pytest.mark.anyio
179 async def test_milestones_list_json_response(
180 client: AsyncClient,
181 db_session: AsyncSession,
182 ) -> None:
183 """?format=json returns JSON with HTTP 200."""
184 repo_id = await _make_repo(db_session)
185 await _make_milestone(db_session, repo_id, title="Mix Revision 2")
186 response = await client.get("/artist/album-one/milestones?format=json")
187 assert response.status_code == 200
188 assert response.headers["content-type"].startswith("application/json")
189
190
191 @pytest.mark.anyio
192 async def test_milestones_list_json_has_milestones_key(
193 client: AsyncClient,
194 db_session: AsyncSession,
195 ) -> None:
196 """JSON response contains a milestones array."""
197 repo_id = await _make_repo(db_session)
198 await _make_milestone(db_session, repo_id, title="Album v1.0")
199 response = await client.get("/artist/album-one/milestones?format=json&state=all")
200 assert response.status_code == 200
201 data = response.json()
202 assert "milestones" in data
203 assert isinstance(data["milestones"], list)
204 assert len(data["milestones"]) >= 1
205
206
207 @pytest.mark.anyio
208 async def test_milestones_list_unknown_repo_404(
209 client: AsyncClient,
210 db_session: AsyncSession,
211 ) -> None:
212 """Unknown owner/slug returns 404."""
213 response = await client.get("/nobody/nonexistent/milestones")
214 assert response.status_code == 404
215
216
217 @pytest.mark.anyio
218 async def test_milestones_list_shows_base_url_not_repo_id(
219 client: AsyncClient,
220 db_session: AsyncSession,
221 ) -> None:
222 """HTML page uses owner/slug base_url pattern, not raw repo_id UUID."""
223 await _make_repo(db_session)
224 response = await client.get("/artist/album-one/milestones")
225 assert response.status_code == 200
226 assert "/artist/album-one" in response.text
227
228
229 # ---------------------------------------------------------------------------
230 # Milestone detail page tests
231 # ---------------------------------------------------------------------------
232
233
234 @pytest.mark.anyio
235 async def test_milestone_detail_page_returns_200(
236 client: AsyncClient,
237 db_session: AsyncSession,
238 ) -> None:
239 """GET /{owner}/{slug}/milestones/{number} returns 200 HTML."""
240 repo_id = await _make_repo(db_session)
241 await _make_milestone(db_session, repo_id, number=1)
242 response = await client.get("/artist/album-one/milestones/1")
243 assert response.status_code == 200
244 assert "text/html" in response.headers["content-type"]
245
246
247 @pytest.mark.anyio
248 async def test_milestone_detail_no_auth_required(
249 client: AsyncClient,
250 db_session: AsyncSession,
251 ) -> None:
252 """Milestone detail page is accessible without a JWT token."""
253 repo_id = await _make_repo(db_session)
254 await _make_milestone(db_session, repo_id, number=1)
255 response = await client.get("/artist/album-one/milestones/1")
256 assert response.status_code == 200
257
258
259 @pytest.mark.anyio
260 async def test_milestone_detail_has_progress_bar(
261 client: AsyncClient,
262 db_session: AsyncSession,
263 ) -> None:
264 """Milestone detail page renders correctly."""
265 repo_id = await _make_repo(db_session)
266 await _make_milestone(db_session, repo_id, number=1)
267 response = await client.get("/artist/album-one/milestones/1")
268 assert response.status_code == 200
269 # progress-bar CSS is in app.css (SCSS refactor); verify milestone detail structure
270 assert "milestone" in response.text.lower()
271
272
273 @pytest.mark.anyio
274 async def test_milestone_detail_has_linked_issues_js(
275 client: AsyncClient,
276 db_session: AsyncSession,
277 ) -> None:
278 """Milestone detail page has JavaScript to render linked issues."""
279 repo_id = await _make_repo(db_session)
280 await _make_milestone(db_session, repo_id, number=1)
281 response = await client.get("/artist/album-one/milestones/1")
282 assert response.status_code == 200
283 body = response.text
284 assert "issue-rows" in body or "renderIssueRows" in body
285
286
287 @pytest.mark.anyio
288 async def test_milestone_detail_has_state_filter_tabs(
289 client: AsyncClient,
290 db_session: AsyncSession,
291 ) -> None:
292 """Milestone detail page has open/closed/all tabs to filter linked issues."""
293 repo_id = await _make_repo(db_session)
294 await _make_milestone(db_session, repo_id, number=1)
295 response = await client.get("/artist/album-one/milestones/1")
296 assert response.status_code == 200
297 body = response.text
298 assert "state-tabs" in body or "state-tab" in body
299
300
301 @pytest.mark.anyio
302 async def test_milestone_detail_json_response(
303 client: AsyncClient,
304 db_session: AsyncSession,
305 ) -> None:
306 """?format=json returns JSON with HTTP 200."""
307 repo_id = await _make_repo(db_session)
308 await _make_milestone(db_session, repo_id, number=1)
309 response = await client.get("/artist/album-one/milestones/1?format=json")
310 assert response.status_code == 200
311 assert response.headers["content-type"].startswith("application/json")
312
313
314 @pytest.mark.anyio
315 async def test_milestone_detail_json_has_linked_issues(
316 client: AsyncClient,
317 db_session: AsyncSession,
318 ) -> None:
319 """JSON response includes the milestone data and linkedIssues."""
320 repo_id = await _make_repo(db_session)
321 ms = await _make_milestone(db_session, repo_id, number=1, title="Album v1.0")
322 await _make_issue(db_session, repo_id, number=1, milestone_id=str(ms.milestone_id))
323 response = await client.get("/artist/album-one/milestones/1?format=json")
324 assert response.status_code == 200
325 data = response.json()
326 assert "title" in data
327 assert data["title"] == "Album v1.0"
328 assert "linkedIssues" in data
329 assert "issues" in data["linkedIssues"]
330
331
332 @pytest.mark.anyio
333 async def test_milestone_detail_unknown_number_404(
334 client: AsyncClient,
335 db_session: AsyncSession,
336 ) -> None:
337 """Non-existent milestone number returns 404."""
338 await _make_repo(db_session)
339 response = await client.get("/artist/album-one/milestones/999")
340 assert response.status_code == 404
341
342
343 @pytest.mark.anyio
344 async def test_milestone_detail_unknown_repo_404(
345 client: AsyncClient,
346 db_session: AsyncSession,
347 ) -> None:
348 """Unknown owner/slug returns 404 for detail page."""
349 response = await client.get("/nobody/nonexistent/milestones/1")
350 assert response.status_code == 404
351
352
353 @pytest.mark.anyio
354 async def test_milestone_detail_json_issue_counts(
355 client: AsyncClient,
356 db_session: AsyncSession,
357 ) -> None:
358 """JSON response has correct open_issues and closed_issues counts."""
359 repo_id = await _make_repo(db_session)
360 ms = await _make_milestone(db_session, repo_id, number=1)
361 ms_id = str(ms.milestone_id)
362 # 2 open + 1 closed
363 await _make_issue(db_session, repo_id, number=1, state="open", milestone_id=ms_id)
364 await _make_issue(db_session, repo_id, number=2, state="open", milestone_id=ms_id)
365 await _make_issue(db_session, repo_id, number=3, state="closed", milestone_id=ms_id)
366 response = await client.get("/artist/album-one/milestones/1?format=json")
367 assert response.status_code == 200
368 data = response.json()
369 assert data.get("openIssues") == 2
370 assert data.get("closedIssues") == 1