gabriel / musehub public
test_musehub_ui_new_repo.py python
280 lines 8.8 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 155 days 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 authentication
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 from musehub.types.json_types import StrDict
32
33
34 # ---------------------------------------------------------------------------
35 # Helpers
36 # ---------------------------------------------------------------------------
37
38 async def _seed_repo(
39 db_session: AsyncSession,
40 owner: str = "wizowner",
41 slug: str = "existing-repo",
42 ) -> MusehubRepo:
43 """Seed a repo with a known owner/slug for uniqueness-check tests."""
44 repo = MusehubRepo(
45 name=slug,
46 owner=owner,
47 slug=slug,
48 visibility="public",
49 owner_user_id="seed-uid",
50 )
51 db_session.add(repo)
52 await db_session.commit()
53 await db_session.refresh(repo)
54 return repo
55
56
57 # ---------------------------------------------------------------------------
58 # GET /new — redirect (repos require a domain context)
59 # ---------------------------------------------------------------------------
60
61
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 async def test_new_repo_page_redirect_no_auth_required(client: AsyncClient) -> None:
74 """The redirect from /new does not require authentication."""
75 resp = await client.get("/new", follow_redirects=False)
76 assert resp.status_code == 302
77
78
79 # ---------------------------------------------------------------------------
80 # GET /new/check — name availability
81 # ---------------------------------------------------------------------------
82
83
84 async def test_check_available_returns_true(
85 client: AsyncClient,
86 db_session: AsyncSession,
87 ) -> None:
88 """GET /new/check → available=true when no repo exists with that owner+slug."""
89 resp = await client.get(
90 "/new/check",
91 params={"owner": "nobody", "slug": "no-such-repo"},
92 )
93 assert resp.status_code == 200
94 assert resp.json()["available"] is True
95
96
97 async def test_check_taken_returns_false(
98 client: AsyncClient,
99 db_session: AsyncSession,
100 ) -> None:
101 """GET /new/check → available=false when the owner+slug is already taken."""
102 await _seed_repo(db_session, owner="wizowner", slug="existing-repo")
103 resp = await client.get(
104 "/new/check",
105 params={"owner": "wizowner", "slug": "existing-repo"},
106 )
107 assert resp.status_code == 200
108 assert resp.json()["available"] is False
109
110
111 async def test_check_requires_owner_and_slug(client: AsyncClient) -> None:
112 """GET /new/check without required params returns 422."""
113 resp = await client.get("/new/check")
114 assert resp.status_code == 422
115
116
117 # ---------------------------------------------------------------------------
118 # POST /new — repo creation
119 # ---------------------------------------------------------------------------
120
121
122 async def test_create_repo_requires_auth(client: AsyncClient) -> None:
123 """POST /new without Authorization header returns 401 or 403."""
124 resp = await client.post(
125 "/new",
126 json={
127 "name": "test-repo",
128 "owner": "someowner",
129 "visibility": "private",
130 },
131 )
132 assert resp.status_code in (401, 403)
133
134
135 async def test_create_repo_success(
136 client: AsyncClient,
137 db_session: AsyncSession,
138 auth_headers: StrDict,
139 ) -> None:
140 """POST /new with valid body returns 201 and a redirect URL."""
141 resp = await client.post(
142 "/new",
143 json={
144 "name": "New Composition",
145 "owner": "testowner",
146 "visibility": "public",
147 "description": "A new jazz piece",
148 "tags": [],
149 "topics": ["jazz", "piano"],
150 "initialize": True,
151 "defaultBranch": "main",
152 },
153 headers=auth_headers,
154 )
155 assert resp.status_code == 201
156 data = resp.json()
157 assert "redirect" in data
158 assert "welcome=1" in data["redirect"]
159
160
161 async def test_create_repo_409_on_duplicate(
162 client: AsyncClient,
163 db_session: AsyncSession,
164 auth_headers: StrDict,
165 ) -> None:
166 """POST /new with a duplicate owner+name returns 409."""
167 await _seed_repo(db_session, owner="dupowner", slug="dup-repo")
168 # 'dup-repo' is the slug generated from the name 'dup-repo'
169 resp = await client.post(
170 "/new",
171 json={
172 "name": "dup-repo",
173 "owner": "dupowner",
174 "visibility": "private",
175 },
176 headers=auth_headers,
177 )
178 assert resp.status_code == 409
179
180
181 async def test_create_repo_redirect_url_format(
182 client: AsyncClient,
183 db_session: AsyncSession,
184 auth_headers: StrDict,
185 ) -> None:
186 """The redirect URL contains owner/slug path and ?welcome=1 query param."""
187 resp = await client.post(
188 "/new",
189 json={
190 "name": "redirect-test",
191 "owner": "urlowner",
192 "visibility": "private",
193 },
194 headers=auth_headers,
195 )
196 assert resp.status_code == 201
197 redirect = resp.json()["redirect"]
198 assert "urlowner" in redirect
199 assert "welcome=1" in redirect
200 assert redirect.startswith("/")
201
202
203 async def test_create_repo_private_default(
204 client: AsyncClient,
205 db_session: AsyncSession,
206 auth_headers: StrDict,
207 ) -> None:
208 """POST without specifying visibility defaults to 'private'."""
209 resp = await client.post(
210 "/new",
211 json={
212 "name": "private-default-test",
213 "owner": "privowner",
214 },
215 headers=auth_headers,
216 )
217 assert resp.status_code == 201
218 # Confirm the slug and owner are in the redirect — repo was created.
219 assert "privowner" in resp.json()["redirect"]
220
221
222 async def test_create_repo_initializes_repo(
223 client: AsyncClient,
224 db_session: AsyncSession,
225 auth_headers: StrDict,
226 ) -> None:
227 """POST with initialize=true creates the repo successfully."""
228 resp = await client.post(
229 "/new",
230 json={
231 "name": "init-repo-test",
232 "owner": "initowner",
233 "visibility": "public",
234 "initialize": True,
235 "defaultBranch": "trunk",
236 },
237 headers=auth_headers,
238 )
239 assert resp.status_code == 201
240 data = resp.json()
241 assert "repoId" in data
242 assert data["slug"] == "init-repo-test"
243
244
245 async def test_create_repo_with_license(
246 client: AsyncClient,
247 db_session: AsyncSession,
248 auth_headers: StrDict,
249 ) -> None:
250 """POST with a license value is accepted and reflected in the response."""
251 resp = await client.post(
252 "/new",
253 json={
254 "name": "licensed-repo",
255 "owner": "licowner",
256 "visibility": "public",
257 "license": "CC BY",
258 },
259 headers=auth_headers,
260 )
261 assert resp.status_code == 201
262
263
264 async def test_create_repo_with_topics(
265 client: AsyncClient,
266 db_session: AsyncSession,
267 auth_headers: StrDict,
268 ) -> None:
269 """POST with topics results in a 201 and stores tags on the new repo."""
270 resp = await client.post(
271 "/new",
272 json={
273 "name": "topical-repo",
274 "owner": "topicowner",
275 "visibility": "public",
276 "topics": ["jazz", "piano", "neosoul"],
277 },
278 headers=auth_headers,
279 )
280 assert resp.status_code == 201
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 155 days ago