gabriel / musehub public
test_musehub_ui_settings.py python
278 lines 10.7 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 days ago
1 """Tests for MuseHub repo settings page.
2
3 Covers the new ``GET /{owner}/{repo_slug}/settings`` endpoint
4 implemented in ``musehub/api/routes/musehub/ui_settings.py``.
5
6 Test matrix:
7 - test_settings_page_returns_200 — happy-path HTML response
8 - test_settings_page_no_auth_required — HTML shell needs no auth
9 - test_settings_page_unknown_repo_404 — unknown owner/slug → 404
10 - test_settings_page_contains_general_section — General settings form present
11 - test_settings_page_contains_danger_zone — Danger Zone section present
12 - test_settings_page_contains_merge_section — Merge settings section present
13 - test_settings_page_contains_collaboration — Collaboration section present
14 - test_settings_page_sidebar_navigation — Sidebar nav links present
15 - test_settings_page_section_param — ?section= pre-selects sidebar section
16 - test_settings_json_response — ?format=json returns RepoSettingsResponse fields
17 - test_settings_json_has_visibility — JSON includes visibility field
18 - test_settings_json_has_merge_flags — JSON includes merge strategy flags
19 - test_settings_page_topic_tag_input — tag input container present in template
20 - test_settings_page_danger_zone_delete_confirm — delete confirmation pattern present
21 - test_settings_page_danger_zone_transfer — transfer ownership action present
22 - test_settings_page_danger_zone_archive — archive action present
23 - test_settings_page_uses_owner_slug_base_url — base URL uses owner/slug not UUID
24 """
25 from __future__ import annotations
26
27 import pytest
28 from httpx import AsyncClient
29 from sqlalchemy.ext.asyncio import AsyncSession
30
31 from musehub.db.musehub_models import MusehubRepo
32
33
34 # ---------------------------------------------------------------------------
35 # Fixtures / helpers
36 # ---------------------------------------------------------------------------
37
38 async def _make_repo(
39 db_session: AsyncSession,
40 owner: str = "settingsowner",
41 slug: str = "settings-repo",
42 visibility: str = "private",
43 ) -> MusehubRepo:
44 """Seed a minimal repo for settings tests and return the ORM row."""
45 repo = MusehubRepo(
46 name=slug,
47 owner=owner,
48 slug=slug,
49 visibility=visibility,
50 owner_user_id="settings-owner-uid",
51 )
52 db_session.add(repo)
53 await db_session.commit()
54 await db_session.refresh(repo)
55 return repo
56
57
58 # ---------------------------------------------------------------------------
59 # Happy-path — HTML responses
60 # ---------------------------------------------------------------------------
61
62
63 async def test_settings_page_returns_200(
64 client: AsyncClient,
65 db_session: AsyncSession,
66 ) -> None:
67 """GET /{owner}/{slug}/settings returns HTTP 200."""
68 repo = await _make_repo(db_session)
69 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
70 assert resp.status_code == 200
71
72
73 async def test_settings_page_no_auth_required(
74 client: AsyncClient,
75 db_session: AsyncSession,
76 ) -> None:
77 """The settings HTML shell is publicly accessible without authentication.
78
79 Auth is enforced client-side when writing (PATCH/DELETE), not on the HTML
80 shell itself — consistent with all other MuseHub UI pages.
81 """
82 repo = await _make_repo(db_session, owner="pubowner", slug="pub-repo", visibility="public")
83 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
84 assert resp.status_code == 200
85 assert "text/html" in resp.headers.get("content-type", "")
86
87
88 async def test_settings_page_unknown_repo_404(
89 client: AsyncClient,
90 db_session: AsyncSession,
91 ) -> None:
92 """GET /{owner}/{slug}/settings returns 404 for unknown repos."""
93 resp = await client.get("/ghost-owner/nonexistent-repo/settings")
94 assert resp.status_code == 404
95
96
97 # ---------------------------------------------------------------------------
98 # Content checks — sections and navigation
99 # ---------------------------------------------------------------------------
100
101
102 async def test_settings_page_contains_general_section(
103 client: AsyncClient,
104 db_session: AsyncSession,
105 ) -> None:
106 """Settings page HTML contains the General settings form."""
107 repo = await _make_repo(db_session, owner="genowner", slug="gen-repo")
108 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
109 assert resp.status_code == 200
110 assert "section-general" in resp.text
111
112
113 async def test_settings_page_contains_danger_zone(
114 client: AsyncClient,
115 db_session: AsyncSession,
116 ) -> None:
117 """Settings page HTML contains the Danger Zone section."""
118 repo = await _make_repo(db_session, owner="dangowner", slug="dang-repo")
119 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
120 assert resp.status_code == 200
121 assert "danger" in resp.text.lower()
122 assert "Delete" in resp.text or "delete" in resp.text
123
124
125 async def test_settings_page_contains_merge_section(
126 client: AsyncClient,
127 db_session: AsyncSession,
128 ) -> None:
129 """Settings page HTML contains the Merge settings section."""
130 repo = await _make_repo(db_session, owner="mergeowner", slug="merge-repo")
131 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
132 assert resp.status_code == 200
133 assert "section-merge" in resp.text
134
135
136 async def test_settings_page_contains_collaboration(
137 client: AsyncClient,
138 db_session: AsyncSession,
139 ) -> None:
140 """Settings page HTML contains the Collaboration section."""
141 repo = await _make_repo(db_session, owner="collabowner", slug="collab-repo")
142 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
143 assert resp.status_code == 200
144 assert "section-collaboration" in resp.text
145
146
147 async def test_settings_page_sidebar_navigation(
148 client: AsyncClient,
149 db_session: AsyncSession,
150 ) -> None:
151 """Settings page HTML contains Alpine.js-powered sidebar navigation links."""
152 repo = await _make_repo(db_session, owner="navowner", slug="nav-repo")
153 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
154 assert resp.status_code == 200
155 html = resp.text
156 assert "settings-nav-link" in html
157 assert "x-on:click" in html or "x-data" in html
158
159
160 async def test_settings_page_section_param(
161 client: AsyncClient,
162 db_session: AsyncSession,
163 ) -> None:
164 """?section=danger pre-selects the danger sidebar section in the template context."""
165 repo = await _make_repo(db_session, owner="secpowner", slug="secp-repo")
166 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?section=danger")
167 assert resp.status_code == 200
168 # The activeSection JS variable should be populated from the context
169 assert "activeSection" in resp.text or "active_section" in resp.text or "danger" in resp.text
170
171
172 # ---------------------------------------------------------------------------
173 # Content negotiation — JSON
174 # ---------------------------------------------------------------------------
175
176
177 async def test_settings_json_response(
178 client: AsyncClient,
179 db_session: AsyncSession,
180 ) -> None:
181 """GET /{owner}/{slug}/settings?format=json returns RepoSettingsResponse."""
182 repo = await _make_repo(db_session, owner="jsonowner", slug="json-repo")
183 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?format=json")
184 assert resp.status_code == 200
185 assert "application/json" in resp.headers.get("content-type", "")
186 data = resp.json()
187 assert "name" in data or "visibility" in data
188
189
190 async def test_settings_json_has_visibility(
191 client: AsyncClient,
192 db_session: AsyncSession,
193 ) -> None:
194 """JSON response includes the ``visibility`` field."""
195 repo = await _make_repo(db_session, owner="visowner", slug="vis-repo", visibility="public")
196 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?format=json")
197 assert resp.status_code == 200
198 data = resp.json()
199 assert data.get("visibility") == "public"
200
201
202 async def test_settings_json_has_merge_flags(
203 client: AsyncClient,
204 db_session: AsyncSession,
205 ) -> None:
206 """JSON response includes merge strategy boolean flags."""
207 repo = await _make_repo(db_session, owner="flagowner", slug="flag-repo")
208 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?format=json")
209 assert resp.status_code == 200
210 data = resp.json()
211 # RepoSettingsResponse uses camelCase via by_alias=True in negotiate_response
212 assert "allowMergeCommit" in data or "allow_merge_commit" in data
213
214
215 # ---------------------------------------------------------------------------
216 # Template content — specific UI elements
217 # ---------------------------------------------------------------------------
218
219
220 async def test_settings_page_topic_tag_input(
221 client: AsyncClient,
222 db_session: AsyncSession,
223 ) -> None:
224 """Settings page includes the topic tag input container."""
225 repo = await _make_repo(db_session, owner="tagowner", slug="tag-repo")
226 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
227 assert resp.status_code == 200
228 assert "topics-container" in resp.text or "tag-input" in resp.text
229
230
231 async def test_settings_page_danger_zone_delete_confirm(
232 client: AsyncClient,
233 db_session: AsyncSession,
234 ) -> None:
235 """Settings page requires typing the full repo name to confirm deletion."""
236 repo = await _make_repo(db_session, owner="delowner", slug="del-repo")
237 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
238 assert resp.status_code == 200
239 assert "confirm-delete-name" in resp.text
240
241
242 async def test_settings_page_danger_zone_transfer(
243 client: AsyncClient,
244 db_session: AsyncSession,
245 ) -> None:
246 """Settings page includes a transfer ownership action."""
247 repo = await _make_repo(db_session, owner="tfrowner", slug="tfr-repo")
248 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
249 assert resp.status_code == 200
250 assert "transfer" in resp.text.lower()
251 assert "modal-transfer" in resp.text
252
253
254 async def test_settings_page_danger_zone_archive(
255 client: AsyncClient,
256 db_session: AsyncSession,
257 ) -> None:
258 """Settings page includes an archive repository action."""
259 repo = await _make_repo(db_session, owner="archowner", slug="arch-repo")
260 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
261 assert resp.status_code == 200
262 assert "archive" in resp.text.lower()
263 assert "modal-archive" in resp.text
264
265
266 async def test_settings_page_uses_owner_slug_base_url(
267 client: AsyncClient,
268 db_session: AsyncSession,
269 ) -> None:
270 """The page injects the owner/slug-based base URL into the JS context, not a UUID.
271
272 Regression guard: all MuseHub UI pages must use ``/{owner}/{slug}``
273 style URLs so breadcrumb links and API calls are human-readable.
274 """
275 repo = await _make_repo(db_session, owner="slugowner", slug="slug-repo")
276 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
277 assert resp.status_code == 200
278 assert f"/{repo.owner}/{repo.slug}" in resp.text
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago