gabriel / musehub public
test_musehub_ui_settings.py python
285 lines 11.0 KB
Raw
sha256:f0ce2f5b7a3316126a89512a9f2ab9e4ac3cf2dbd7dae9155812a102138d15b4 test: add AV_19-AV_26 SSR visibility gate tests; fix settin… Sonnet 4.6 patch 8 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 datetime import datetime, timezone
32 from musehub.core.genesis import compute_identity_id, compute_repo_id
33 from musehub.db.musehub_repo_models import MusehubRepo
34
35
36 # ---------------------------------------------------------------------------
37 # Fixtures / helpers
38 # ---------------------------------------------------------------------------
39
40 async def _make_repo(
41 db_session: AsyncSession,
42 owner: str = "settingsowner",
43 slug: str = "settings-repo",
44 visibility: str = "private",
45 ) -> MusehubRepo:
46 """Seed a minimal repo for settings tests and return the ORM row."""
47 created_at = datetime.now(tz=timezone.utc)
48 owner_id = compute_identity_id(owner.encode())
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=visibility,
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 # Happy-path — HTML responses
67 # ---------------------------------------------------------------------------
68
69
70 async def test_settings_page_returns_200(
71 client: AsyncClient,
72 db_session: AsyncSession,
73 ) -> None:
74 """GET /{owner}/{slug}/settings returns HTTP 200."""
75 repo = await _make_repo(db_session)
76 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
77 assert resp.status_code == 200
78
79
80 async def test_settings_page_no_auth_required(
81 client: AsyncClient,
82 db_session: AsyncSession,
83 ) -> None:
84 """The settings HTML shell is publicly accessible without authentication.
85
86 Auth is enforced client-side when writing (PATCH/DELETE), not on the HTML
87 shell itself — consistent with all other MuseHub UI pages.
88 """
89 repo = await _make_repo(db_session, owner="pubowner", slug="pub-repo", visibility="public")
90 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
91 assert resp.status_code == 200
92 assert "text/html" in resp.headers.get("content-type", "")
93
94
95 async def test_settings_page_unknown_repo_404(
96 client: AsyncClient,
97 db_session: AsyncSession,
98 ) -> None:
99 """GET /{owner}/{slug}/settings returns 404 for unknown repos."""
100 resp = await client.get("/ghost-owner/nonexistent-repo/settings")
101 assert resp.status_code == 404
102
103
104 # ---------------------------------------------------------------------------
105 # Content checks — sections and navigation
106 # ---------------------------------------------------------------------------
107
108
109 async def test_settings_page_contains_general_section(
110 client: AsyncClient,
111 db_session: AsyncSession,
112 ) -> None:
113 """Settings page HTML contains the General settings form."""
114 repo = await _make_repo(db_session, owner="genowner", slug="gen-repo")
115 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
116 assert resp.status_code == 200
117 assert "section-general" in resp.text
118
119
120 async def test_settings_page_contains_danger_zone(
121 client: AsyncClient,
122 db_session: AsyncSession,
123 ) -> None:
124 """Settings page HTML contains the Danger Zone section."""
125 repo = await _make_repo(db_session, owner="dangowner", slug="dang-repo")
126 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
127 assert resp.status_code == 200
128 assert "danger" in resp.text.lower()
129 assert "Delete" in resp.text or "delete" in resp.text
130
131
132 async def test_settings_page_contains_merge_section(
133 client: AsyncClient,
134 db_session: AsyncSession,
135 ) -> None:
136 """Settings page HTML contains the Merge settings section."""
137 repo = await _make_repo(db_session, owner="mergeowner", slug="merge-repo")
138 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
139 assert resp.status_code == 200
140 assert "section-merge" in resp.text
141
142
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"/{repo.owner}/{repo.slug}/settings")
150 assert resp.status_code == 200
151 assert "section-collaboration" in resp.text
152
153
154 async def test_settings_page_sidebar_navigation(
155 client: AsyncClient,
156 db_session: AsyncSession,
157 ) -> None:
158 """Settings page HTML contains Alpine.js-powered sidebar navigation links."""
159 repo = await _make_repo(db_session, owner="navowner", slug="nav-repo")
160 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
161 assert resp.status_code == 200
162 html = resp.text
163 assert "settings-nav-link" in html
164 assert "x-on:click" in html or "x-data" in html
165
166
167 async def test_settings_page_section_param(
168 client: AsyncClient,
169 db_session: AsyncSession,
170 ) -> None:
171 """?section=danger pre-selects the danger sidebar section in the template context."""
172 repo = await _make_repo(db_session, owner="secpowner", slug="secp-repo")
173 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?section=danger")
174 assert resp.status_code == 200
175 # The activeSection JS variable should be populated from the context
176 assert "activeSection" in resp.text or "active_section" in resp.text or "danger" in resp.text
177
178
179 # ---------------------------------------------------------------------------
180 # Content negotiation — JSON
181 # ---------------------------------------------------------------------------
182
183
184 async def test_settings_json_response(
185 client: AsyncClient,
186 db_session: AsyncSession,
187 ) -> None:
188 """GET /{owner}/{slug}/settings?format=json returns RepoSettingsResponse."""
189 repo = await _make_repo(db_session, owner="jsonowner", slug="json-repo")
190 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?format=json")
191 assert resp.status_code == 200
192 assert "application/json" in resp.headers.get("content-type", "")
193 data = resp.json()
194 assert "name" in data or "visibility" in data
195
196
197 async def test_settings_json_has_visibility(
198 client: AsyncClient,
199 db_session: AsyncSession,
200 ) -> None:
201 """JSON response includes the ``visibility`` field."""
202 repo = await _make_repo(db_session, owner="visowner", slug="vis-repo", visibility="public")
203 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?format=json")
204 assert resp.status_code == 200
205 data = resp.json()
206 assert data.get("visibility") == "public"
207
208
209 async def test_settings_json_has_merge_flags(
210 client: AsyncClient,
211 db_session: AsyncSession,
212 ) -> None:
213 """JSON response includes merge strategy boolean flags."""
214 repo = await _make_repo(db_session, owner="flagowner", slug="flag-repo")
215 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?format=json")
216 assert resp.status_code == 200
217 data = resp.json()
218 # RepoSettingsResponse uses camelCase via by_alias=True in negotiate_response
219 assert "allowMergeCommit" in data or "allow_merge_commit" in data
220
221
222 # ---------------------------------------------------------------------------
223 # Template content — specific UI elements
224 # ---------------------------------------------------------------------------
225
226
227 async def test_settings_page_topic_tag_input(
228 client: AsyncClient,
229 db_session: AsyncSession,
230 ) -> None:
231 """Settings page includes the topic tag input container."""
232 repo = await _make_repo(db_session, owner="tagowner", slug="tag-repo")
233 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
234 assert resp.status_code == 200
235 assert "topics-container" in resp.text or "tag-input" in resp.text
236
237
238 async def test_settings_page_danger_zone_delete_confirm(
239 client: AsyncClient,
240 db_session: AsyncSession,
241 ) -> None:
242 """Settings page requires typing the full repo name to confirm deletion."""
243 repo = await _make_repo(db_session, owner="delowner", slug="del-repo")
244 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
245 assert resp.status_code == 200
246 assert "confirm-delete-name" in resp.text
247
248
249 async def test_settings_page_danger_zone_transfer(
250 client: AsyncClient,
251 db_session: AsyncSession,
252 ) -> None:
253 """Settings page includes a transfer ownership action."""
254 repo = await _make_repo(db_session, owner="tfrowner", slug="tfr-repo")
255 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
256 assert resp.status_code == 200
257 assert "transfer" in resp.text.lower()
258 assert "modal-transfer" in resp.text
259
260
261 async def test_settings_page_danger_zone_archive(
262 client: AsyncClient,
263 db_session: AsyncSession,
264 ) -> None:
265 """Settings page includes an archive repository action."""
266 repo = await _make_repo(db_session, owner="archowner", slug="arch-repo")
267 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
268 assert resp.status_code == 200
269 assert "archive" in resp.text.lower()
270 assert "modal-archive" in resp.text
271
272
273 async def test_settings_page_uses_owner_slug_base_url(
274 client: AsyncClient,
275 db_session: AsyncSession,
276 ) -> None:
277 """The page injects the owner/slug-based base URL into the JS context, not a UUID.
278
279 Regression guard: all MuseHub UI pages must use ``/{owner}/{slug}``
280 style URLs so breadcrumb links and API calls are human-readable.
281 """
282 repo = await _make_repo(db_session, owner="slugowner", slug="slug-repo")
283 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
284 assert resp.status_code == 200
285 assert f"/{repo.owner}/{repo.slug}" in resp.text
File History 1 commit
sha256:f0ce2f5b7a3316126a89512a9f2ab9e4ac3cf2dbd7dae9155812a102138d15b4 test: add AV_19-AV_26 SSR visibility gate tests; fix settin… Sonnet 4.6 patch 8 days ago