gabriel / musehub public
test_musehub_ui_settings.py python
295 lines 11.2 KB
e6fad116 Remove all Stori, Maestro, and AgentCeption references; rebrand to Muse VCS Gabriel Cardona <gabriel@tellurstori.com> 6d ago
1 """Tests for Muse Hub repo settings page.
2
3 Covers the new ``GET /musehub/ui/{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 JWT
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 @pytest.mark.anyio
64 async def test_settings_page_returns_200(
65 client: AsyncClient,
66 db_session: AsyncSession,
67 ) -> None:
68 """GET /musehub/ui/{owner}/{slug}/settings returns HTTP 200."""
69 repo = await _make_repo(db_session)
70 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
71 assert resp.status_code == 200
72
73
74 @pytest.mark.anyio
75 async def test_settings_page_no_auth_required(
76 client: AsyncClient,
77 db_session: AsyncSession,
78 ) -> None:
79 """The settings HTML shell is publicly accessible without a JWT.
80
81 Auth is enforced client-side when writing (PATCH/DELETE), not on the HTML
82 shell itself — consistent with all other MuseHub UI pages.
83 """
84 repo = await _make_repo(db_session, owner="pubowner", slug="pub-repo", visibility="public")
85 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
86 assert resp.status_code == 200
87 assert "text/html" in resp.headers.get("content-type", "")
88
89
90 @pytest.mark.anyio
91 async def test_settings_page_unknown_repo_404(
92 client: AsyncClient,
93 db_session: AsyncSession,
94 ) -> None:
95 """GET /musehub/ui/{owner}/{slug}/settings returns 404 for unknown repos."""
96 resp = await client.get("/musehub/ui/ghost-owner/nonexistent-repo/settings")
97 assert resp.status_code == 404
98
99
100 # ---------------------------------------------------------------------------
101 # Content checks — sections and navigation
102 # ---------------------------------------------------------------------------
103
104
105 @pytest.mark.anyio
106 async def test_settings_page_contains_general_section(
107 client: AsyncClient,
108 db_session: AsyncSession,
109 ) -> None:
110 """Settings page HTML contains the General settings form."""
111 repo = await _make_repo(db_session, owner="genowner", slug="gen-repo")
112 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
113 assert resp.status_code == 200
114 assert "section-general" in resp.text
115
116
117 @pytest.mark.anyio
118 async def test_settings_page_contains_danger_zone(
119 client: AsyncClient,
120 db_session: AsyncSession,
121 ) -> None:
122 """Settings page HTML contains the Danger Zone section."""
123 repo = await _make_repo(db_session, owner="dangowner", slug="dang-repo")
124 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
125 assert resp.status_code == 200
126 assert "danger" in resp.text.lower()
127 assert "Delete" in resp.text or "delete" in resp.text
128
129
130 @pytest.mark.anyio
131 async def test_settings_page_contains_merge_section(
132 client: AsyncClient,
133 db_session: AsyncSession,
134 ) -> None:
135 """Settings page HTML contains the Merge settings section."""
136 repo = await _make_repo(db_session, owner="mergeowner", slug="merge-repo")
137 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
138 assert resp.status_code == 200
139 assert "section-merge" in resp.text
140
141
142 @pytest.mark.anyio
143 async def test_settings_page_contains_collaboration(
144 client: AsyncClient,
145 db_session: AsyncSession,
146 ) -> None:
147 """Settings page HTML contains the Collaboration section."""
148 repo = await _make_repo(db_session, owner="collabowner", slug="collab-repo")
149 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
150 assert resp.status_code == 200
151 assert "section-collaboration" in resp.text
152
153
154 @pytest.mark.anyio
155 async def test_settings_page_sidebar_navigation(
156 client: AsyncClient,
157 db_session: AsyncSession,
158 ) -> None:
159 """Settings page HTML contains Alpine.js-powered sidebar navigation links."""
160 repo = await _make_repo(db_session, owner="navowner", slug="nav-repo")
161 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
162 assert resp.status_code == 200
163 html = resp.text
164 assert "settings-nav-link" in html
165 assert "x-on:click" in html or "x-data" in html
166
167
168 @pytest.mark.anyio
169 async def test_settings_page_section_param(
170 client: AsyncClient,
171 db_session: AsyncSession,
172 ) -> None:
173 """?section=danger pre-selects the danger sidebar section in the template context."""
174 repo = await _make_repo(db_session, owner="secpowner", slug="secp-repo")
175 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings?section=danger")
176 assert resp.status_code == 200
177 # The activeSection JS variable should be populated from the context
178 assert "activeSection" in resp.text or "active_section" in resp.text or "danger" in resp.text
179
180
181 # ---------------------------------------------------------------------------
182 # Content negotiation — JSON
183 # ---------------------------------------------------------------------------
184
185
186 @pytest.mark.anyio
187 async def test_settings_json_response(
188 client: AsyncClient,
189 db_session: AsyncSession,
190 ) -> None:
191 """GET /musehub/ui/{owner}/{slug}/settings?format=json returns RepoSettingsResponse."""
192 repo = await _make_repo(db_session, owner="jsonowner", slug="json-repo")
193 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings?format=json")
194 assert resp.status_code == 200
195 assert "application/json" in resp.headers.get("content-type", "")
196 data = resp.json()
197 assert "name" in data or "visibility" in data
198
199
200 @pytest.mark.anyio
201 async def test_settings_json_has_visibility(
202 client: AsyncClient,
203 db_session: AsyncSession,
204 ) -> None:
205 """JSON response includes the ``visibility`` field."""
206 repo = await _make_repo(db_session, owner="visowner", slug="vis-repo", visibility="public")
207 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings?format=json")
208 assert resp.status_code == 200
209 data = resp.json()
210 assert data.get("visibility") == "public"
211
212
213 @pytest.mark.anyio
214 async def test_settings_json_has_merge_flags(
215 client: AsyncClient,
216 db_session: AsyncSession,
217 ) -> None:
218 """JSON response includes merge strategy boolean flags."""
219 repo = await _make_repo(db_session, owner="flagowner", slug="flag-repo")
220 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings?format=json")
221 assert resp.status_code == 200
222 data = resp.json()
223 # RepoSettingsResponse uses camelCase via by_alias=True in negotiate_response
224 assert "allowMergeCommit" in data or "allow_merge_commit" in data
225
226
227 # ---------------------------------------------------------------------------
228 # Template content — specific UI elements
229 # ---------------------------------------------------------------------------
230
231
232 @pytest.mark.anyio
233 async def test_settings_page_topic_tag_input(
234 client: AsyncClient,
235 db_session: AsyncSession,
236 ) -> None:
237 """Settings page includes the topic tag input container."""
238 repo = await _make_repo(db_session, owner="tagowner", slug="tag-repo")
239 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
240 assert resp.status_code == 200
241 assert "topics-container" in resp.text or "tag-input" in resp.text
242
243
244 @pytest.mark.anyio
245 async def test_settings_page_danger_zone_delete_confirm(
246 client: AsyncClient,
247 db_session: AsyncSession,
248 ) -> None:
249 """Settings page requires typing the full repo name to confirm deletion."""
250 repo = await _make_repo(db_session, owner="delowner", slug="del-repo")
251 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
252 assert resp.status_code == 200
253 assert "confirm-delete-name" in resp.text
254
255
256 @pytest.mark.anyio
257 async def test_settings_page_danger_zone_transfer(
258 client: AsyncClient,
259 db_session: AsyncSession,
260 ) -> None:
261 """Settings page includes a transfer ownership action."""
262 repo = await _make_repo(db_session, owner="tfrowner", slug="tfr-repo")
263 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
264 assert resp.status_code == 200
265 assert "transfer" in resp.text.lower()
266 assert "modal-transfer" in resp.text
267
268
269 @pytest.mark.anyio
270 async def test_settings_page_danger_zone_archive(
271 client: AsyncClient,
272 db_session: AsyncSession,
273 ) -> None:
274 """Settings page includes an archive repository action."""
275 repo = await _make_repo(db_session, owner="archowner", slug="arch-repo")
276 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
277 assert resp.status_code == 200
278 assert "archive" in resp.text.lower()
279 assert "modal-archive" in resp.text
280
281
282 @pytest.mark.anyio
283 async def test_settings_page_uses_owner_slug_base_url(
284 client: AsyncClient,
285 db_session: AsyncSession,
286 ) -> None:
287 """The page injects the owner/slug-based base URL into the JS context, not a UUID.
288
289 Regression guard: all MuseHub UI pages must use ``/musehub/ui/{owner}/{slug}``
290 style URLs so breadcrumb links and API calls are human-readable.
291 """
292 repo = await _make_repo(db_session, owner="slugowner", slug="slug-repo")
293 resp = await client.get(f"/musehub/ui/{repo.owner}/{repo.slug}/settings")
294 assert resp.status_code == 200
295 assert f"/musehub/ui/{repo.owner}/{repo.slug}" in resp.text