gabriel / musehub public
test_musehub_ui_new_repo.py python
288 lines 9.1 KB
Raw
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923 fix(issues): use issue number as pagination cursor, not cre… Sonnet 4.6 patch 8 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 datetime import datetime, timezone
31
32 from musehub.core.genesis import compute_identity_id, compute_repo_id
33 from musehub.db.musehub_repo_models import MusehubRepo
34 from musehub.types.json_types import StrDict
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41 async def _seed_repo(
42 db_session: AsyncSession,
43 owner: str = "wizowner",
44 slug: str = "existing-repo",
45 ) -> MusehubRepo:
46 """Seed a repo with a known owner/slug for uniqueness-check tests."""
47 owner_id = compute_identity_id(owner.encode())
48 created_at = datetime.now(tz=timezone.utc)
49 repo = MusehubRepo(
50 repo_id=compute_repo_id(owner_id, slug, "code", created_at.isoformat()),
51 name=slug,
52 owner=owner,
53 slug=slug,
54 visibility="public",
55 owner_user_id=owner_id,
56 created_at=created_at,
57 updated_at=created_at,
58 )
59 db_session.add(repo)
60 await db_session.commit()
61 await db_session.refresh(repo)
62 return repo
63
64
65 # ---------------------------------------------------------------------------
66 # GET /new — redirect (repos require a domain context)
67 # ---------------------------------------------------------------------------
68
69
70 async def test_new_repo_page_redirects_to_domains(client: AsyncClient) -> None:
71 """GET /new → 302 redirect to /domains.
72
73 Repository creation is now domain-scoped; the standalone /new wizard
74 no longer exists. Users are directed to pick a domain first.
75 """
76 resp = await client.get("/new", follow_redirects=False)
77 assert resp.status_code == 302
78 assert resp.headers["location"].endswith("/domains")
79
80
81 async def test_new_repo_page_redirect_no_auth_required(client: AsyncClient) -> None:
82 """The redirect from /new does not require authentication."""
83 resp = await client.get("/new", follow_redirects=False)
84 assert resp.status_code == 302
85
86
87 # ---------------------------------------------------------------------------
88 # GET /new/check — name availability
89 # ---------------------------------------------------------------------------
90
91
92 async def test_check_available_returns_true(
93 client: AsyncClient,
94 db_session: AsyncSession,
95 ) -> None:
96 """GET /new/check → available=true when no repo exists with that owner+slug."""
97 resp = await client.get(
98 "/new/check",
99 params={"owner": "nobody", "slug": "no-such-repo"},
100 )
101 assert resp.status_code == 200
102 assert resp.json()["available"] is True
103
104
105 async def test_check_taken_returns_false(
106 client: AsyncClient,
107 db_session: AsyncSession,
108 ) -> None:
109 """GET /new/check → available=false when the owner+slug is already taken."""
110 await _seed_repo(db_session, owner="wizowner", slug="existing-repo")
111 resp = await client.get(
112 "/new/check",
113 params={"owner": "wizowner", "slug": "existing-repo"},
114 )
115 assert resp.status_code == 200
116 assert resp.json()["available"] is False
117
118
119 async def test_check_requires_owner_and_slug(client: AsyncClient) -> None:
120 """GET /new/check without required params returns 422."""
121 resp = await client.get("/new/check")
122 assert resp.status_code == 422
123
124
125 # ---------------------------------------------------------------------------
126 # POST /new — repo creation
127 # ---------------------------------------------------------------------------
128
129
130 async def test_create_repo_requires_auth(client: AsyncClient) -> None:
131 """POST /new without Authorization header returns 401 or 403."""
132 resp = await client.post(
133 "/new",
134 json={
135 "name": "test-repo",
136 "owner": "someowner",
137 "visibility": "private",
138 },
139 )
140 assert resp.status_code in (401, 403)
141
142
143 async def test_create_repo_success(
144 client: AsyncClient,
145 db_session: AsyncSession,
146 auth_headers: StrDict,
147 ) -> None:
148 """POST /new with valid body returns 201 and a redirect URL."""
149 resp = await client.post(
150 "/new",
151 json={
152 "name": "New Composition",
153 "owner": "testowner",
154 "visibility": "public",
155 "description": "A new jazz piece",
156 "tags": [],
157 "topics": ["jazz", "piano"],
158 "initialize": True,
159 "defaultBranch": "main",
160 },
161 headers=auth_headers,
162 )
163 assert resp.status_code == 201
164 data = resp.json()
165 assert "redirect" in data
166 assert "welcome=1" in data["redirect"]
167
168
169 async def test_create_repo_409_on_duplicate(
170 client: AsyncClient,
171 db_session: AsyncSession,
172 auth_headers: StrDict,
173 ) -> None:
174 """POST /new with a duplicate owner+name returns 409."""
175 await _seed_repo(db_session, owner="dupowner", slug="dup-repo")
176 # 'dup-repo' is the slug generated from the name 'dup-repo'
177 resp = await client.post(
178 "/new",
179 json={
180 "name": "dup-repo",
181 "owner": "dupowner",
182 "visibility": "private",
183 },
184 headers=auth_headers,
185 )
186 assert resp.status_code == 409
187
188
189 async def test_create_repo_redirect_url_format(
190 client: AsyncClient,
191 db_session: AsyncSession,
192 auth_headers: StrDict,
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 async def test_create_repo_private_default(
212 client: AsyncClient,
213 db_session: AsyncSession,
214 auth_headers: StrDict,
215 ) -> None:
216 """POST without specifying visibility defaults to 'private'."""
217 resp = await client.post(
218 "/new",
219 json={
220 "name": "private-default-test",
221 "owner": "privowner",
222 },
223 headers=auth_headers,
224 )
225 assert resp.status_code == 201
226 # Confirm the slug and owner are in the redirect — repo was created.
227 assert "privowner" in resp.json()["redirect"]
228
229
230 async def test_create_repo_initializes_repo(
231 client: AsyncClient,
232 db_session: AsyncSession,
233 auth_headers: StrDict,
234 ) -> None:
235 """POST with initialize=true creates the repo successfully."""
236 resp = await client.post(
237 "/new",
238 json={
239 "name": "init-repo-test",
240 "owner": "initowner",
241 "visibility": "public",
242 "initialize": True,
243 "defaultBranch": "trunk",
244 },
245 headers=auth_headers,
246 )
247 assert resp.status_code == 201
248 data = resp.json()
249 assert "repoId" in data
250 assert data["slug"] == "init-repo-test"
251
252
253 async def test_create_repo_with_license(
254 client: AsyncClient,
255 db_session: AsyncSession,
256 auth_headers: StrDict,
257 ) -> None:
258 """POST with a license value is accepted and reflected in the response."""
259 resp = await client.post(
260 "/new",
261 json={
262 "name": "licensed-repo",
263 "owner": "licowner",
264 "visibility": "public",
265 "license": "CC BY",
266 },
267 headers=auth_headers,
268 )
269 assert resp.status_code == 201
270
271
272 async def test_create_repo_with_topics(
273 client: AsyncClient,
274 db_session: AsyncSession,
275 auth_headers: StrDict,
276 ) -> None:
277 """POST with topics results in a 201 and stores tags on the new repo."""
278 resp = await client.post(
279 "/new",
280 json={
281 "name": "topical-repo",
282 "owner": "topicowner",
283 "visibility": "public",
284 "topics": ["jazz", "piano", "neosoul"],
285 },
286 headers=auth_headers,
287 )
288 assert resp.status_code == 201
File History 1 commit
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923 fix(issues): use issue number as pagination cursor, not cre… Sonnet 4.6 patch 8 days ago