gabriel / musehub public
test_musehub_ui_labels.py python
518 lines 17.2 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 71 days ago
1 """Tests for MuseHub label management UI endpoints.
2
3 Covers GET /{owner}/{repo_slug}/labels:
4 - test_labels_page_returns_200 — page renders without auth
5 - test_labels_page_no_auth_required — GET needs no auth
6 - test_labels_page_unknown_repo_404 — unknown owner/slug → 404
7 - test_labels_page_has_color_picker_js — colour picker rendered in template
8 - test_labels_page_has_label_list_js — label list JS present
9 - test_labels_page_json_format — ?format=json returns structured data
10 - test_labels_page_json_has_items_key — JSON payload includes 'items' array
11 - test_labels_page_shows_issue_count — issue counts included in JSON response
12 - test_labels_page_base_url_uses_slug — base_url in context uses owner/slug not repo_id
13
14 Covers POST /{owner}/{repo_slug}/labels:
15 - test_create_label_success — 201 + label_id returned
16 - test_create_label_requires_auth — 401 without MSign auth
17 - test_create_label_duplicate_name_409 — duplicate name → 409
18 - test_create_label_invalid_color_422 — bad hex color → 422
19 - test_create_label_unknown_repo_404 — unknown repo → 404
20
21 Covers POST /{owner}/{repo_slug}/labels/{label_id}/edit:
22 - test_edit_label_success — 200 + updated values
23 - test_edit_label_requires_auth — 401 without token
24 - test_edit_label_unknown_label_404 — unknown label_id → 404
25 - test_edit_label_name_conflict_409 — name collision → 409
26 - test_edit_label_partial_update — partial body updates only supplied fields
27
28 Covers POST /{owner}/{repo_slug}/labels/{label_id}/delete:
29 - test_delete_label_success — 200 ok=True
30 - test_delete_label_requires_auth — 401 without token
31 - test_delete_label_unknown_label_404 — unknown label_id → 404
32
33 Covers POST /{owner}/{repo_slug}/labels/reset:
34 - test_reset_labels_success — 200 + 10 defaults seeded
35 - test_reset_labels_requires_auth — 401 without token
36 - test_reset_labels_wipes_custom_labels — existing labels replaced
37 - test_reset_labels_unknown_repo_404 — unknown repo → 404
38 """
39 from __future__ import annotations
40
41 import pytest
42 from httpx import AsyncClient
43 from sqlalchemy.ext.asyncio import AsyncSession
44
45 from musehub.db.musehub_label_models import MusehubLabel
46 from musehub.db.musehub_models import MusehubIssue, MusehubRepo
47 from musehub.muse_contracts.json_types import StrDict
48
49
50 # ---------------------------------------------------------------------------
51 # Helpers
52 # ---------------------------------------------------------------------------
53
54
55 async def _make_repo(
56 db: AsyncSession,
57 owner: str = "beatmaker",
58 slug: str = "deep-cuts",
59 ) -> str:
60 """Seed a public repo and return its repo_id string."""
61 repo = MusehubRepo(
62 name=slug,
63 owner=owner,
64 slug=slug,
65 visibility="public",
66 owner_user_id="uid-beatmaker",
67 )
68 db.add(repo)
69 await db.commit()
70 await db.refresh(repo)
71 return str(repo.repo_id)
72
73
74 async def _make_label(
75 db: AsyncSession,
76 repo_id: str,
77 *,
78 name: str = "bug",
79 color: str = "#d73a4a",
80 description: str | None = "Something isn't working",
81 ) -> MusehubLabel:
82 """Seed a label and return it."""
83 label = MusehubLabel(
84 repo_id=repo_id,
85 name=name,
86 color=color,
87 description=description,
88 )
89 db.add(label)
90 await db.commit()
91 await db.refresh(label)
92 return label
93
94
95 # ---------------------------------------------------------------------------
96 # GET /{owner}/{repo_slug}/labels — label list page
97 # ---------------------------------------------------------------------------
98
99
100 @pytest.mark.anyio
101 async def test_labels_page_returns_200(
102 client: AsyncClient, db_session: AsyncSession
103 ) -> None:
104 """GET /{owner}/{slug}/labels returns 200 HTML."""
105 await _make_repo(db_session)
106 response = await client.get("/beatmaker/deep-cuts/labels")
107 assert response.status_code == 200
108 assert "text/html" in response.headers["content-type"]
109
110
111 @pytest.mark.anyio
112 async def test_labels_page_no_auth_required(
113 client: AsyncClient, db_session: AsyncSession
114 ) -> None:
115 """Label list page is publicly accessible — no auth required."""
116 await _make_repo(db_session)
117 response = await client.get("/beatmaker/deep-cuts/labels")
118 assert response.status_code == 200
119
120
121 @pytest.mark.anyio
122 async def test_labels_page_unknown_repo_404(
123 client: AsyncClient, db_session: AsyncSession
124 ) -> None:
125 """Unknown owner/slug combination → 404."""
126 response = await client.get("/nobody/nonexistent/labels")
127 assert response.status_code == 404
128
129
130 @pytest.mark.anyio
131 async def test_labels_page_has_color_picker_js(
132 client: AsyncClient, db_session: AsyncSession
133 ) -> None:
134 """The labels page HTML includes a colour picker for creating labels."""
135 await _make_repo(db_session)
136 response = await client.get("/beatmaker/deep-cuts/labels")
137 assert response.status_code == 200
138 body = response.text
139 assert 'type="color"' in body or "color-picker" in body or "input" in body
140
141
142 @pytest.mark.anyio
143 async def test_labels_page_has_label_list_js(
144 client: AsyncClient, db_session: AsyncSession
145 ) -> None:
146 """The labels page contains JavaScript to render the label list."""
147 await _make_repo(db_session)
148 response = await client.get("/beatmaker/deep-cuts/labels")
149 assert response.status_code == 200
150 body = response.text
151 assert "renderLabel" in body or "label-list" in body or "label-row" in body
152
153
154 @pytest.mark.anyio
155 async def test_labels_page_json_format(
156 client: AsyncClient, db_session: AsyncSession
157 ) -> None:
158 """?format=json returns a JSON response with a 200 status."""
159 await _make_repo(db_session)
160 response = await client.get("/beatmaker/deep-cuts/labels?format=json")
161 assert response.status_code == 200
162 assert "application/json" in response.headers["content-type"]
163
164
165 @pytest.mark.anyio
166 async def test_labels_page_json_has_items_key(
167 client: AsyncClient, db_session: AsyncSession
168 ) -> None:
169 """JSON response contains 'labels' and 'total' keys."""
170 repo_id = await _make_repo(db_session)
171 await _make_label(db_session, repo_id, name="bug", color="#d73a4a")
172 response = await client.get("/beatmaker/deep-cuts/labels?format=json")
173 assert response.status_code == 200
174 data = response.json()
175 assert "labels" in data
176 assert "total" in data
177 assert data["total"] == 1
178
179
180 @pytest.mark.anyio
181 async def test_labels_page_shows_issue_count(
182 client: AsyncClient, db_session: AsyncSession
183 ) -> None:
184 """JSON response includes issue_count for each label."""
185 repo_id = await _make_repo(db_session)
186 await _make_label(db_session, repo_id, name="enhancement", color="#a2eeef")
187 response = await client.get("/beatmaker/deep-cuts/labels?format=json")
188 assert response.status_code == 200
189 data = response.json()
190 labels = data["labels"]
191 assert len(labels) == 1
192 assert "issue_count" in labels[0]
193 assert labels[0]["issue_count"] == 0
194
195
196 @pytest.mark.anyio
197 async def test_labels_page_base_url_uses_slug(
198 client: AsyncClient, db_session: AsyncSession
199 ) -> None:
200 """The HTML page embeds the owner/slug base URL, not the repo UUID."""
201 await _make_repo(db_session)
202 response = await client.get("/beatmaker/deep-cuts/labels")
203 assert response.status_code == 200
204 body = response.text
205 assert "beatmaker" in body
206 assert "deep-cuts" in body
207
208
209 # ---------------------------------------------------------------------------
210 # POST /{owner}/{repo_slug}/labels — create label
211 # ---------------------------------------------------------------------------
212
213
214 @pytest.mark.anyio
215 async def test_create_label_success(
216 client: AsyncClient,
217 db_session: AsyncSession,
218 auth_headers: StrDict,
219 ) -> None:
220 """POST /labels with valid body + auth returns 201 with label_id."""
221 await _make_repo(db_session)
222 response = await client.post(
223 "/beatmaker/deep-cuts/labels",
224 json={"name": "needs-arrangement", "color": "#e4e669", "description": "Track needs arrangement"},
225 headers=auth_headers,
226 )
227 assert response.status_code == 201
228 data = response.json()
229 assert data["ok"] is True
230 assert "label_id" in data
231 assert data["label_id"] is not None
232
233
234 @pytest.mark.anyio
235 async def test_create_label_requires_auth(
236 client: AsyncClient, db_session: AsyncSession
237 ) -> None:
238 """POST /labels without MSign auth returns 401 or 403."""
239 await _make_repo(db_session)
240 response = await client.post(
241 "/beatmaker/deep-cuts/labels",
242 json={"name": "bug", "color": "#d73a4a"},
243 )
244 assert response.status_code in (401, 403)
245
246
247 @pytest.mark.anyio
248 async def test_create_label_duplicate_name_409(
249 client: AsyncClient,
250 db_session: AsyncSession,
251 auth_headers: StrDict,
252 ) -> None:
253 """Creating a label with an existing name within the repo → 409."""
254 repo_id = await _make_repo(db_session)
255 await _make_label(db_session, repo_id, name="bug", color="#d73a4a")
256 response = await client.post(
257 "/beatmaker/deep-cuts/labels",
258 json={"name": "bug", "color": "#ff0000"},
259 headers=auth_headers,
260 )
261 assert response.status_code == 409
262
263
264 @pytest.mark.anyio
265 async def test_create_label_invalid_color_422(
266 client: AsyncClient,
267 db_session: AsyncSession,
268 auth_headers: StrDict,
269 ) -> None:
270 """A malformed hex colour string → 422 validation error."""
271 await _make_repo(db_session)
272 response = await client.post(
273 "/beatmaker/deep-cuts/labels",
274 json={"name": "test", "color": "not-a-color"},
275 headers=auth_headers,
276 )
277 assert response.status_code == 422
278
279
280 @pytest.mark.anyio
281 async def test_create_label_unknown_repo_404(
282 client: AsyncClient,
283 db_session: AsyncSession,
284 auth_headers: StrDict,
285 ) -> None:
286 """Creating a label on a nonexistent repo → 404."""
287 response = await client.post(
288 "/nobody/ghost-repo/labels",
289 json={"name": "bug", "color": "#d73a4a"},
290 headers=auth_headers,
291 )
292 assert response.status_code == 404
293
294
295 # ---------------------------------------------------------------------------
296 # POST /{owner}/{repo_slug}/labels/{label_id}/edit
297 # ---------------------------------------------------------------------------
298
299
300 @pytest.mark.anyio
301 async def test_edit_label_success(
302 client: AsyncClient,
303 db_session: AsyncSession,
304 auth_headers: StrDict,
305 ) -> None:
306 """POST /labels/{label_id}/edit updates the label and returns ok=True."""
307 repo_id = await _make_repo(db_session)
308 label = await _make_label(db_session, repo_id, name="bug", color="#d73a4a")
309 response = await client.post(
310 f"/beatmaker/deep-cuts/labels/{label.id}/edit",
311 json={"name": "critical-bug", "color": "#ff0000"},
312 headers=auth_headers,
313 )
314 assert response.status_code == 200
315 data = response.json()
316 assert data["ok"] is True
317 assert data["label_id"] == str(label.id)
318
319
320 @pytest.mark.anyio
321 async def test_edit_label_requires_auth(
322 client: AsyncClient, db_session: AsyncSession
323 ) -> None:
324 """POST /labels/{label_id}/edit without MSign auth → 401 or 403."""
325 repo_id = await _make_repo(db_session)
326 label = await _make_label(db_session, repo_id, name="bug", color="#d73a4a")
327 response = await client.post(
328 f"/beatmaker/deep-cuts/labels/{label.id}/edit",
329 json={"name": "new-name"},
330 )
331 assert response.status_code in (401, 403)
332
333
334 @pytest.mark.anyio
335 async def test_edit_label_unknown_label_404(
336 client: AsyncClient,
337 db_session: AsyncSession,
338 auth_headers: StrDict,
339 ) -> None:
340 """Editing a non-existent label_id → 404."""
341 await _make_repo(db_session)
342 response = await client.post(
343 "/beatmaker/deep-cuts/labels/00000000-0000-0000-0000-000000000000/edit",
344 json={"name": "x"},
345 headers=auth_headers,
346 )
347 assert response.status_code == 404
348
349
350 @pytest.mark.anyio
351 async def test_edit_label_name_conflict_409(
352 client: AsyncClient,
353 db_session: AsyncSession,
354 auth_headers: StrDict,
355 ) -> None:
356 """Renaming a label to an already-existing name in the same repo → 409."""
357 repo_id = await _make_repo(db_session)
358 await _make_label(db_session, repo_id, name="bug", color="#d73a4a")
359 label_b = await _make_label(db_session, repo_id, name="enhancement", color="#a2eeef")
360 response = await client.post(
361 f"/beatmaker/deep-cuts/labels/{label_b.id}/edit",
362 json={"name": "bug"},
363 headers=auth_headers,
364 )
365 assert response.status_code == 409
366
367
368 @pytest.mark.anyio
369 async def test_edit_label_partial_update(
370 client: AsyncClient,
371 db_session: AsyncSession,
372 auth_headers: StrDict,
373 ) -> None:
374 """Sending only 'color' in the body preserves the existing name."""
375 repo_id = await _make_repo(db_session)
376 label = await _make_label(db_session, repo_id, name="bug", color="#d73a4a")
377 response = await client.post(
378 f"/beatmaker/deep-cuts/labels/{label.id}/edit",
379 json={"color": "#ff6600"},
380 headers=auth_headers,
381 )
382 assert response.status_code == 200
383 data = response.json()
384 assert data["ok"] is True
385 # Name should remain "bug" — verified by the message containing the name
386 assert "bug" in data["message"]
387
388
389 # ---------------------------------------------------------------------------
390 # POST /{owner}/{repo_slug}/labels/{label_id}/delete
391 # ---------------------------------------------------------------------------
392
393
394 @pytest.mark.anyio
395 async def test_delete_label_success(
396 client: AsyncClient,
397 db_session: AsyncSession,
398 auth_headers: StrDict,
399 ) -> None:
400 """POST /labels/{label_id}/delete removes the label and returns ok=True."""
401 repo_id = await _make_repo(db_session)
402 label = await _make_label(db_session, repo_id, name="bug", color="#d73a4a")
403 response = await client.post(
404 f"/beatmaker/deep-cuts/labels/{label.id}/delete",
405 headers=auth_headers,
406 )
407 assert response.status_code == 200
408 data = response.json()
409 assert data["ok"] is True
410
411
412 @pytest.mark.anyio
413 async def test_delete_label_requires_auth(
414 client: AsyncClient, db_session: AsyncSession
415 ) -> None:
416 """POST /labels/{label_id}/delete without MSign auth → 401 or 403."""
417 repo_id = await _make_repo(db_session)
418 label = await _make_label(db_session, repo_id, name="bug", color="#d73a4a")
419 response = await client.post(
420 f"/beatmaker/deep-cuts/labels/{label.id}/delete",
421 )
422 assert response.status_code in (401, 403)
423
424
425 @pytest.mark.anyio
426 async def test_delete_label_unknown_label_404(
427 client: AsyncClient,
428 db_session: AsyncSession,
429 auth_headers: StrDict,
430 ) -> None:
431 """Deleting a non-existent label_id → 404."""
432 await _make_repo(db_session)
433 response = await client.post(
434 "/beatmaker/deep-cuts/labels/00000000-0000-0000-0000-000000000000/delete",
435 headers=auth_headers,
436 )
437 assert response.status_code == 404
438
439
440 # ---------------------------------------------------------------------------
441 # POST /{owner}/{repo_slug}/labels/reset
442 # ---------------------------------------------------------------------------
443
444
445 @pytest.mark.anyio
446 async def test_reset_labels_success(
447 client: AsyncClient,
448 db_session: AsyncSession,
449 auth_headers: StrDict,
450 ) -> None:
451 """POST /labels/reset returns 200 with ok=True and seeds 10 defaults."""
452 from musehub.api.routes.musehub.labels import DEFAULT_LABELS
453
454 await _make_repo(db_session)
455 response = await client.post(
456 "/beatmaker/deep-cuts/labels/reset",
457 headers=auth_headers,
458 )
459 assert response.status_code == 200
460 data = response.json()
461 assert data["ok"] is True
462 assert str(len(DEFAULT_LABELS)) in data["message"]
463
464
465 @pytest.mark.anyio
466 async def test_reset_labels_requires_auth(
467 client: AsyncClient, db_session: AsyncSession
468 ) -> None:
469 """POST /labels/reset without MSign auth → 401 or 403."""
470 await _make_repo(db_session)
471 response = await client.post("/beatmaker/deep-cuts/labels/reset")
472 assert response.status_code in (401, 403)
473
474
475 @pytest.mark.anyio
476 async def test_reset_labels_wipes_custom_labels(
477 client: AsyncClient,
478 db_session: AsyncSession,
479 auth_headers: StrDict,
480 ) -> None:
481 """Reset removes all existing custom labels and replaces with defaults."""
482 from musehub.api.routes.musehub.labels import DEFAULT_LABELS
483
484 repo_id = await _make_repo(db_session)
485 # Seed a custom label that is NOT in the defaults list.
486 await _make_label(db_session, repo_id, name="my-custom-label", color="#123456")
487
488 response = await client.post(
489 "/beatmaker/deep-cuts/labels/reset",
490 headers=auth_headers,
491 )
492 assert response.status_code == 200
493
494 # After reset, the JSON endpoint should return exactly the defaults.
495 list_response = await client.get(
496 "/beatmaker/deep-cuts/labels?format=json"
497 )
498 assert list_response.status_code == 200
499 data = list_response.json()
500 label_names = {lbl["name"] for lbl in data["labels"]}
501 default_names = {d["name"] for d in DEFAULT_LABELS}
502 # Custom label must be gone; all defaults must be present.
503 assert "my-custom-label" not in label_names
504 assert default_names == label_names
505
506
507 @pytest.mark.anyio
508 async def test_reset_labels_unknown_repo_404(
509 client: AsyncClient,
510 db_session: AsyncSession,
511 auth_headers: StrDict,
512 ) -> None:
513 """POST /labels/reset on nonexistent repo → 404."""
514 response = await client.post(
515 "/nobody/ghost-repo/labels/reset",
516 headers=auth_headers,
517 )
518 assert response.status_code == 404
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 71 days ago