gabriel / musehub public
test_musehub_ui_new_repo.py python
292 lines 9.0 KB
d4eb1c39 Theme overhaul: domains, new-repo, MCP docs, copy icons; legacy CSS rem… Gabriel Cardona <cgcardona@gmail.com> 3d ago
1 """Tests for the MuseHub new-repo creation wizard.
2
3 Covers ``musehub/api/routes/musehub/ui_new_repo.py``:
4
5 GET /new — redirects to /domains (repos require a domain context)
6 POST /new — create repo (JSON body, auth required)
7 GET /new/check — name availability check
8
9 Test matrix:
10 test_new_repo_page_redirects_to_domains — GET /new → 302 to /domains
11 test_new_repo_page_redirect_no_auth_required — redirect happens without a JWT
12 test_check_available_returns_true — GET /new/check → available=true
13 test_check_taken_returns_false — GET /new/check → available=false
14 test_check_requires_owner_and_slug — GET /new/check → 422 when missing params
15 test_create_repo_requires_auth — POST without token → 401/403
16 test_create_repo_success — POST with valid body → 201 + redirect
17 test_create_repo_409_on_duplicate — POST duplicate → 409
18 test_create_repo_redirect_url_format — redirect URL contains /{owner}/{slug}?welcome=1
19 test_create_repo_private_default — POST without visibility → defaults to private
20 test_create_repo_initializes_repo — POST with initialize=true creates the repo
21 test_create_repo_with_license — POST with license field stored correctly
22 test_create_repo_with_topics — POST with topics stored as tags
23 """
24 from __future__ import annotations
25
26 import pytest
27 from httpx import AsyncClient
28 from sqlalchemy.ext.asyncio import AsyncSession
29
30 from musehub.db.musehub_models import MusehubRepo
31
32
33 # ---------------------------------------------------------------------------
34 # Helpers
35 # ---------------------------------------------------------------------------
36
37 async def _seed_repo(
38 db_session: AsyncSession,
39 owner: str = "wizowner",
40 slug: str = "existing-repo",
41 ) -> MusehubRepo:
42 """Seed a repo with a known owner/slug for uniqueness-check tests."""
43 repo = MusehubRepo(
44 name=slug,
45 owner=owner,
46 slug=slug,
47 visibility="public",
48 owner_user_id="seed-uid",
49 )
50 db_session.add(repo)
51 await db_session.commit()
52 await db_session.refresh(repo)
53 return repo
54
55
56 # ---------------------------------------------------------------------------
57 # GET /new — redirect (repos require a domain context)
58 # ---------------------------------------------------------------------------
59
60
61 @pytest.mark.anyio
62 async def test_new_repo_page_redirects_to_domains(client: AsyncClient) -> None:
63 """GET /new → 302 redirect to /domains.
64
65 Repository creation is now domain-scoped; the standalone /new wizard
66 no longer exists. Users are directed to pick a domain first.
67 """
68 resp = await client.get("/new", follow_redirects=False)
69 assert resp.status_code == 302
70 assert resp.headers["location"].endswith("/domains")
71
72
73 @pytest.mark.anyio
74 async def test_new_repo_page_redirect_no_auth_required(client: AsyncClient) -> None:
75 """The redirect from /new does not require authentication."""
76 resp = await client.get("/new", follow_redirects=False)
77 assert resp.status_code == 302
78
79
80 # ---------------------------------------------------------------------------
81 # GET /new/check — name availability
82 # ---------------------------------------------------------------------------
83
84
85 @pytest.mark.anyio
86 async def test_check_available_returns_true(
87 client: AsyncClient,
88 db_session: AsyncSession,
89 ) -> None:
90 """GET /new/check → available=true when no repo exists with that owner+slug."""
91 resp = await client.get(
92 "/new/check",
93 params={"owner": "nobody", "slug": "no-such-repo"},
94 )
95 assert resp.status_code == 200
96 assert resp.json()["available"] is True
97
98
99 @pytest.mark.anyio
100 async def test_check_taken_returns_false(
101 client: AsyncClient,
102 db_session: AsyncSession,
103 ) -> None:
104 """GET /new/check → available=false when the owner+slug is already taken."""
105 await _seed_repo(db_session, owner="wizowner", slug="existing-repo")
106 resp = await client.get(
107 "/new/check",
108 params={"owner": "wizowner", "slug": "existing-repo"},
109 )
110 assert resp.status_code == 200
111 assert resp.json()["available"] is False
112
113
114 @pytest.mark.anyio
115 async def test_check_requires_owner_and_slug(client: AsyncClient) -> None:
116 """GET /new/check without required params returns 422."""
117 resp = await client.get("/new/check")
118 assert resp.status_code == 422
119
120
121 # ---------------------------------------------------------------------------
122 # POST /new — repo creation
123 # ---------------------------------------------------------------------------
124
125
126 @pytest.mark.anyio
127 async def test_create_repo_requires_auth(client: AsyncClient) -> None:
128 """POST /new without Authorization header returns 401 or 403."""
129 resp = await client.post(
130 "/new",
131 json={
132 "name": "test-repo",
133 "owner": "someowner",
134 "visibility": "private",
135 },
136 )
137 assert resp.status_code in (401, 403)
138
139
140 @pytest.mark.anyio
141 async def test_create_repo_success(
142 client: AsyncClient,
143 db_session: AsyncSession,
144 auth_headers: dict[str, str],
145 ) -> None:
146 """POST /new with valid body returns 201 and a redirect URL."""
147 resp = await client.post(
148 "/new",
149 json={
150 "name": "New Composition",
151 "owner": "testowner",
152 "visibility": "public",
153 "description": "A new jazz piece",
154 "tags": [],
155 "topics": ["jazz", "piano"],
156 "initialize": True,
157 "defaultBranch": "main",
158 },
159 headers=auth_headers,
160 )
161 assert resp.status_code == 201
162 data = resp.json()
163 assert "redirect" in data
164 assert "welcome=1" in data["redirect"]
165
166
167 @pytest.mark.anyio
168 async def test_create_repo_409_on_duplicate(
169 client: AsyncClient,
170 db_session: AsyncSession,
171 auth_headers: dict[str, str],
172 ) -> None:
173 """POST /new with a duplicate owner+name returns 409."""
174 await _seed_repo(db_session, owner="dupowner", slug="dup-repo")
175 # 'dup-repo' is the slug generated from the name 'dup-repo'
176 resp = await client.post(
177 "/new",
178 json={
179 "name": "dup-repo",
180 "owner": "dupowner",
181 "visibility": "private",
182 },
183 headers=auth_headers,
184 )
185 assert resp.status_code == 409
186
187
188 @pytest.mark.anyio
189 async def test_create_repo_redirect_url_format(
190 client: AsyncClient,
191 db_session: AsyncSession,
192 auth_headers: dict[str, str],
193 ) -> None:
194 """The redirect URL contains owner/slug path and ?welcome=1 query param."""
195 resp = await client.post(
196 "/new",
197 json={
198 "name": "redirect-test",
199 "owner": "urlowner",
200 "visibility": "private",
201 },
202 headers=auth_headers,
203 )
204 assert resp.status_code == 201
205 redirect = resp.json()["redirect"]
206 assert "urlowner" in redirect
207 assert "welcome=1" in redirect
208 assert redirect.startswith("/")
209
210
211 @pytest.mark.anyio
212 async def test_create_repo_private_default(
213 client: AsyncClient,
214 db_session: AsyncSession,
215 auth_headers: dict[str, str],
216 ) -> None:
217 """POST without specifying visibility defaults to 'private'."""
218 resp = await client.post(
219 "/new",
220 json={
221 "name": "private-default-test",
222 "owner": "privowner",
223 },
224 headers=auth_headers,
225 )
226 assert resp.status_code == 201
227 # Confirm the slug and owner are in the redirect — repo was created.
228 assert "privowner" in resp.json()["redirect"]
229
230
231 @pytest.mark.anyio
232 async def test_create_repo_initializes_repo(
233 client: AsyncClient,
234 db_session: AsyncSession,
235 auth_headers: dict[str, str],
236 ) -> None:
237 """POST with initialize=true creates the repo successfully."""
238 resp = await client.post(
239 "/new",
240 json={
241 "name": "init-repo-test",
242 "owner": "initowner",
243 "visibility": "public",
244 "initialize": True,
245 "defaultBranch": "trunk",
246 },
247 headers=auth_headers,
248 )
249 assert resp.status_code == 201
250 data = resp.json()
251 assert "repoId" in data
252 assert data["slug"] == "init-repo-test"
253
254
255 @pytest.mark.anyio
256 async def test_create_repo_with_license(
257 client: AsyncClient,
258 db_session: AsyncSession,
259 auth_headers: dict[str, str],
260 ) -> None:
261 """POST with a license value is accepted and reflected in the response."""
262 resp = await client.post(
263 "/new",
264 json={
265 "name": "licensed-repo",
266 "owner": "licowner",
267 "visibility": "public",
268 "license": "CC BY",
269 },
270 headers=auth_headers,
271 )
272 assert resp.status_code == 201
273
274
275 @pytest.mark.anyio
276 async def test_create_repo_with_topics(
277 client: AsyncClient,
278 db_session: AsyncSession,
279 auth_headers: dict[str, str],
280 ) -> None:
281 """POST with topics results in a 201 and stores tags on the new repo."""
282 resp = await client.post(
283 "/new",
284 json={
285 "name": "topical-repo",
286 "owner": "topicowner",
287 "visibility": "public",
288 "topics": ["jazz", "piano", "neosoul"],
289 },
290 headers=auth_headers,
291 )
292 assert resp.status_code == 201