gabriel / musehub public
test_musehub_ui_new_repo.py python
365 lines 12.1 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """Tests for the Muse Hub new-repo creation wizard.
2
3 Covers ``maestro/api/routes/musehub/ui_new_repo.py``:
4
5 GET /musehub/ui/new
6 POST /musehub/ui/new
7 GET /musehub/ui/new/check
8
9 Test matrix:
10 test_new_repo_page_returns_200 — GET returns HTTP 200 HTML
11 test_new_repo_page_no_auth_required — GET works without a JWT
12 test_new_repo_page_has_form — HTML contains the wizard form
13 test_new_repo_page_has_owner_input — HTML has owner input field
14 test_new_repo_page_has_visibility_options — HTML has Public/Private toggle
15 test_new_repo_page_has_license_options — JS references LICENSES constant
16 test_new_repo_page_has_topics_input — HTML has topics container
17 test_new_repo_page_has_initialize_checkbox — HTML has initialize checkbox
18 test_new_repo_page_has_branch_input — HTML has default branch input
19 test_new_repo_page_has_template_search — HTML has template search input
20 test_check_available_returns_true — GET /new/check → available=true
21 test_check_taken_returns_false — GET /new/check → available=false
22 test_check_requires_owner_and_slug — GET /new/check → 422 when missing params
23 test_create_repo_requires_auth — POST without token → 401/403
24 test_create_repo_success — POST with valid body → 201 + redirect
25 test_create_repo_409_on_duplicate — POST duplicate → 409
26 test_create_repo_redirect_url_format — redirect URL contains /musehub/ui/{owner}/{slug}?welcome=1
27 test_create_repo_private_default — POST without visibility → defaults to private
28 test_create_repo_initializes_repo — POST with initialize=true creates the repo
29 test_create_repo_with_license — POST with license field stored correctly
30 test_create_repo_with_topics — POST with topics stored as tags
31 """
32 from __future__ import annotations
33
34 import pytest
35 from httpx import AsyncClient
36 from sqlalchemy.ext.asyncio import AsyncSession
37
38 from musehub.db.musehub_models import MusehubRepo
39
40
41 # ---------------------------------------------------------------------------
42 # Helpers
43 # ---------------------------------------------------------------------------
44
45 async def _seed_repo(
46 db_session: AsyncSession,
47 owner: str = "wizowner",
48 slug: str = "existing-repo",
49 ) -> MusehubRepo:
50 """Seed a repo with a known owner/slug for uniqueness-check tests."""
51 repo = MusehubRepo(
52 name=slug,
53 owner=owner,
54 slug=slug,
55 visibility="public",
56 owner_user_id="seed-uid",
57 )
58 db_session.add(repo)
59 await db_session.commit()
60 await db_session.refresh(repo)
61 return repo
62
63
64 # ---------------------------------------------------------------------------
65 # GET /musehub/ui/new — HTML wizard
66 # ---------------------------------------------------------------------------
67
68
69 @pytest.mark.anyio
70 async def test_new_repo_page_returns_200(client: AsyncClient) -> None:
71 """GET /musehub/ui/new returns HTTP 200."""
72 resp = await client.get("/musehub/ui/new")
73 assert resp.status_code == 200
74
75
76 @pytest.mark.anyio
77 async def test_new_repo_page_no_auth_required(client: AsyncClient) -> None:
78 """The wizard HTML shell is accessible without a JWT — consistent with all other UI pages."""
79 resp = await client.get("/musehub/ui/new")
80 assert resp.status_code == 200
81 assert "text/html" in resp.headers.get("content-type", "")
82
83
84 @pytest.mark.anyio
85 async def test_new_repo_page_has_form(client: AsyncClient) -> None:
86 """The wizard page contains the HTML wizard form."""
87 resp = await client.get("/musehub/ui/new")
88 assert resp.status_code == 200
89 html = resp.text
90 assert "wizard-form" in html or "new_repo" in html or "Create" in html
91
92
93 @pytest.mark.anyio
94 async def test_new_repo_page_has_owner_input(client: AsyncClient) -> None:
95 """The wizard page references the owner input field."""
96 resp = await client.get("/musehub/ui/new")
97 assert resp.status_code == 200
98 assert "f-owner" in resp.text
99
100
101 @pytest.mark.anyio
102 async def test_new_repo_page_has_visibility_options(client: AsyncClient) -> None:
103 """The wizard page has Public/Private visibility toggle."""
104 resp = await client.get("/musehub/ui/new")
105 assert resp.status_code == 200
106 assert "Public" in resp.text
107 assert "Private" in resp.text
108
109
110 @pytest.mark.anyio
111 async def test_new_repo_page_has_license_options(client: AsyncClient) -> None:
112 """The wizard page includes JS LICENSES constant with the expected license names."""
113 resp = await client.get("/musehub/ui/new")
114 assert resp.status_code == 200
115 assert "CC0" in resp.text
116 assert "CC BY" in resp.text
117 assert "All Rights Reserved" in resp.text
118
119
120 @pytest.mark.anyio
121 async def test_new_repo_page_has_topics_input(client: AsyncClient) -> None:
122 """The wizard page contains the topics tag input container."""
123 resp = await client.get("/musehub/ui/new")
124 assert resp.status_code == 200
125 # SSR template uses tag-input-container + Alpine.js x-ref for the chip input
126 assert "tag-input-container" in resp.text or "topics-container" in resp.text or "topic" in resp.text.lower()
127
128
129 @pytest.mark.anyio
130 async def test_new_repo_page_has_initialize_checkbox(client: AsyncClient) -> None:
131 """The wizard page has the 'Initialize this repository' checkbox."""
132 resp = await client.get("/musehub/ui/new")
133 assert resp.status_code == 200
134 assert "f-initialize" in resp.text or "initialize" in resp.text.lower()
135
136
137 @pytest.mark.anyio
138 async def test_new_repo_page_has_branch_input(client: AsyncClient) -> None:
139 """The wizard page has the default branch name input."""
140 resp = await client.get("/musehub/ui/new")
141 assert resp.status_code == 200
142 assert "f-branch" in resp.text
143
144
145 @pytest.mark.anyio
146 async def test_new_repo_page_has_template_search(client: AsyncClient) -> None:
147 """The wizard page has the template repository search input."""
148 resp = await client.get("/musehub/ui/new")
149 assert resp.status_code == 200
150 assert "template-search-input" in resp.text or "template" in resp.text.lower()
151
152
153 # ---------------------------------------------------------------------------
154 # GET /musehub/ui/new/check — name availability
155 # ---------------------------------------------------------------------------
156
157
158 @pytest.mark.anyio
159 async def test_check_available_returns_true(
160 client: AsyncClient,
161 db_session: AsyncSession,
162 ) -> None:
163 """GET /new/check → available=true when no repo exists with that owner+slug."""
164 resp = await client.get(
165 "/musehub/ui/new/check",
166 params={"owner": "nobody", "slug": "no-such-repo"},
167 )
168 assert resp.status_code == 200
169 assert resp.json()["available"] is True
170
171
172 @pytest.mark.anyio
173 async def test_check_taken_returns_false(
174 client: AsyncClient,
175 db_session: AsyncSession,
176 ) -> None:
177 """GET /new/check → available=false when the owner+slug is already taken."""
178 await _seed_repo(db_session, owner="wizowner", slug="existing-repo")
179 resp = await client.get(
180 "/musehub/ui/new/check",
181 params={"owner": "wizowner", "slug": "existing-repo"},
182 )
183 assert resp.status_code == 200
184 assert resp.json()["available"] is False
185
186
187 @pytest.mark.anyio
188 async def test_check_requires_owner_and_slug(client: AsyncClient) -> None:
189 """GET /new/check without required params returns 422."""
190 resp = await client.get("/musehub/ui/new/check")
191 assert resp.status_code == 422
192
193
194 # ---------------------------------------------------------------------------
195 # POST /musehub/ui/new — repo creation
196 # ---------------------------------------------------------------------------
197
198
199 @pytest.mark.anyio
200 async def test_create_repo_requires_auth(client: AsyncClient) -> None:
201 """POST /musehub/ui/new without Authorization header returns 401 or 403."""
202 resp = await client.post(
203 "/musehub/ui/new",
204 json={
205 "name": "test-repo",
206 "owner": "someowner",
207 "visibility": "private",
208 },
209 )
210 assert resp.status_code in (401, 403)
211
212
213 @pytest.mark.anyio
214 async def test_create_repo_success(
215 client: AsyncClient,
216 db_session: AsyncSession,
217 auth_headers: dict[str, str],
218 ) -> None:
219 """POST /musehub/ui/new with valid body returns 201 and a redirect URL."""
220 resp = await client.post(
221 "/musehub/ui/new",
222 json={
223 "name": "New Composition",
224 "owner": "testowner",
225 "visibility": "public",
226 "description": "A new jazz piece",
227 "tags": [],
228 "topics": ["jazz", "piano"],
229 "initialize": True,
230 "defaultBranch": "main",
231 },
232 headers=auth_headers,
233 )
234 assert resp.status_code == 201
235 data = resp.json()
236 assert "redirect" in data
237 assert "musehub/ui" in data["redirect"]
238
239
240 @pytest.mark.anyio
241 async def test_create_repo_409_on_duplicate(
242 client: AsyncClient,
243 db_session: AsyncSession,
244 auth_headers: dict[str, str],
245 ) -> None:
246 """POST /musehub/ui/new with a duplicate owner+name returns 409."""
247 await _seed_repo(db_session, owner="dupowner", slug="dup-repo")
248 # 'dup-repo' is the slug generated from the name 'dup-repo'
249 resp = await client.post(
250 "/musehub/ui/new",
251 json={
252 "name": "dup-repo",
253 "owner": "dupowner",
254 "visibility": "private",
255 },
256 headers=auth_headers,
257 )
258 assert resp.status_code == 409
259
260
261 @pytest.mark.anyio
262 async def test_create_repo_redirect_url_format(
263 client: AsyncClient,
264 db_session: AsyncSession,
265 auth_headers: dict[str, str],
266 ) -> None:
267 """The redirect URL contains owner/slug path and ?welcome=1 query param."""
268 resp = await client.post(
269 "/musehub/ui/new",
270 json={
271 "name": "redirect-test",
272 "owner": "urlowner",
273 "visibility": "private",
274 },
275 headers=auth_headers,
276 )
277 assert resp.status_code == 201
278 redirect = resp.json()["redirect"]
279 assert "urlowner" in redirect
280 assert "welcome=1" in redirect
281 assert redirect.startswith("/musehub/ui/")
282
283
284 @pytest.mark.anyio
285 async def test_create_repo_private_default(
286 client: AsyncClient,
287 db_session: AsyncSession,
288 auth_headers: dict[str, str],
289 ) -> None:
290 """POST without specifying visibility defaults to 'private'."""
291 resp = await client.post(
292 "/musehub/ui/new",
293 json={
294 "name": "private-default-test",
295 "owner": "privowner",
296 },
297 headers=auth_headers,
298 )
299 assert resp.status_code == 201
300 # Confirm the slug and owner are in the redirect — repo was created.
301 assert "privowner" in resp.json()["redirect"]
302
303
304 @pytest.mark.anyio
305 async def test_create_repo_initializes_repo(
306 client: AsyncClient,
307 db_session: AsyncSession,
308 auth_headers: dict[str, str],
309 ) -> None:
310 """POST with initialize=true creates the repo successfully."""
311 resp = await client.post(
312 "/musehub/ui/new",
313 json={
314 "name": "init-repo-test",
315 "owner": "initowner",
316 "visibility": "public",
317 "initialize": True,
318 "defaultBranch": "trunk",
319 },
320 headers=auth_headers,
321 )
322 assert resp.status_code == 201
323 data = resp.json()
324 assert "repoId" in data
325 assert data["slug"] == "init-repo-test"
326
327
328 @pytest.mark.anyio
329 async def test_create_repo_with_license(
330 client: AsyncClient,
331 db_session: AsyncSession,
332 auth_headers: dict[str, str],
333 ) -> None:
334 """POST with a license value is accepted and reflected in the response."""
335 resp = await client.post(
336 "/musehub/ui/new",
337 json={
338 "name": "licensed-repo",
339 "owner": "licowner",
340 "visibility": "public",
341 "license": "CC BY",
342 },
343 headers=auth_headers,
344 )
345 assert resp.status_code == 201
346
347
348 @pytest.mark.anyio
349 async def test_create_repo_with_topics(
350 client: AsyncClient,
351 db_session: AsyncSession,
352 auth_headers: dict[str, str],
353 ) -> None:
354 """POST with topics results in a 201 and stores tags on the new repo."""
355 resp = await client.post(
356 "/musehub/ui/new",
357 json={
358 "name": "topical-repo",
359 "owner": "topicowner",
360 "visibility": "public",
361 "topics": ["jazz", "piano", "neosoul"],
362 },
363 headers=auth_headers,
364 )
365 assert resp.status_code == 201