gabriel / musehub public
test_musehub_ui_settings.py python
320 lines 11.9 KB
Raw
sha256:f0ce2f5b7a3316126a89512a9f2ab9e4ac3cf2dbd7dae9155812a102138d15b4 test: add AV_19-AV_26 SSR visibility gate tests; fix settin… Sonnet 4.6 patch 1 day 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_identity_models import MusehubIdentity
34 from musehub.db.musehub_repo_models import MusehubRepo
35
36 # Must match the handle injected by the auth_headers fixture (conftest._TEST_HANDLE).
37 _OWNER = "testuser"
38
39
40 # ---------------------------------------------------------------------------
41 # Fixtures / helpers
42 # ---------------------------------------------------------------------------
43
44 async def _make_repo(
45 db_session: AsyncSession,
46 owner: str = _OWNER,
47 slug: str = "settings-repo",
48 visibility: str = "private",
49 ) -> MusehubRepo:
50 """Seed a minimal repo for settings tests and return the ORM row."""
51 created_at = datetime.now(tz=timezone.utc)
52 owner_id = compute_identity_id(owner.encode())
53 repo = MusehubRepo(
54 repo_id=compute_repo_id(owner_id, slug, "code", created_at.isoformat()),
55 name=slug,
56 owner=owner,
57 slug=slug,
58 visibility=visibility,
59 owner_user_id=owner_id,
60 created_at=created_at,
61 updated_at=created_at,
62 )
63 db_session.add(repo)
64 await db_session.commit()
65 await db_session.refresh(repo)
66 return repo
67
68
69 # ---------------------------------------------------------------------------
70 # Happy-path — HTML responses
71 # ---------------------------------------------------------------------------
72
73
74 async def test_settings_page_returns_200(
75 client: AsyncClient,
76 db_session: AsyncSession,
77 test_user: MusehubIdentity,
78 auth_headers: dict[str, str],
79 ) -> None:
80 """GET /{owner}/{slug}/settings returns HTTP 200 for the authenticated owner."""
81 repo = await _make_repo(db_session)
82 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
83 assert resp.status_code == 200
84
85
86 async def test_settings_page_requires_auth(
87 client: AsyncClient,
88 db_session: AsyncSession,
89 ) -> None:
90 """The settings page requires a valid MSign token — unauthenticated returns 401.
91
92 Settings exposes sensitive repo configuration and is owner-only. Auth is
93 enforced server-side via require_valid_token before any repo lookup.
94 """
95 repo = await _make_repo(db_session, owner="pubowner", slug="pub-req-auth", visibility="public")
96 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
97 assert resp.status_code == 401
98
99
100 async def test_settings_page_unknown_repo_404(
101 client: AsyncClient,
102 db_session: AsyncSession,
103 test_user: MusehubIdentity,
104 auth_headers: dict[str, str],
105 ) -> None:
106 """Authenticated owner GET on an unknown slug returns 404."""
107 resp = await client.get(f"/{_OWNER}/nonexistent-repo-settings-404/settings")
108 assert resp.status_code == 404
109
110
111 # ---------------------------------------------------------------------------
112 # Content checks — sections and navigation
113 # ---------------------------------------------------------------------------
114
115
116 async def test_settings_page_contains_general_section(
117 client: AsyncClient,
118 db_session: AsyncSession,
119 test_user: MusehubIdentity,
120 auth_headers: dict[str, str],
121 ) -> None:
122 """Settings page HTML contains the General settings form."""
123 repo = await _make_repo(db_session, slug="gen-repo")
124 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
125 assert resp.status_code == 200
126 assert "section-general" in resp.text
127
128
129 async def test_settings_page_contains_danger_zone(
130 client: AsyncClient,
131 db_session: AsyncSession,
132 test_user: MusehubIdentity,
133 auth_headers: dict[str, str],
134 ) -> None:
135 """Settings page HTML contains the Danger Zone section."""
136 repo = await _make_repo(db_session, slug="dang-repo")
137 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
138 assert resp.status_code == 200
139 assert "danger" in resp.text.lower()
140 assert "Delete" in resp.text or "delete" in resp.text
141
142
143 async def test_settings_page_contains_merge_section(
144 client: AsyncClient,
145 db_session: AsyncSession,
146 test_user: MusehubIdentity,
147 auth_headers: dict[str, str],
148 ) -> None:
149 """Settings page HTML contains the Merge settings section."""
150 repo = await _make_repo(db_session, slug="merge-repo")
151 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
152 assert resp.status_code == 200
153 assert "section-merge" in resp.text
154
155
156 async def test_settings_page_contains_collaboration(
157 client: AsyncClient,
158 db_session: AsyncSession,
159 test_user: MusehubIdentity,
160 auth_headers: dict[str, str],
161 ) -> None:
162 """Settings page HTML contains the Collaboration section."""
163 repo = await _make_repo(db_session, slug="collab-repo")
164 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
165 assert resp.status_code == 200
166 assert "section-collaboration" in resp.text
167
168
169 async def test_settings_page_sidebar_navigation(
170 client: AsyncClient,
171 db_session: AsyncSession,
172 test_user: MusehubIdentity,
173 auth_headers: dict[str, str],
174 ) -> None:
175 """Settings page HTML contains Alpine.js-powered sidebar navigation links."""
176 repo = await _make_repo(db_session, slug="nav-repo")
177 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
178 assert resp.status_code == 200
179 html = resp.text
180 assert "settings-nav-link" in html
181 assert "x-on:click" in html or "x-data" in html
182
183
184 async def test_settings_page_section_param(
185 client: AsyncClient,
186 db_session: AsyncSession,
187 test_user: MusehubIdentity,
188 auth_headers: dict[str, str],
189 ) -> None:
190 """?section=danger pre-selects the danger sidebar section in the template context."""
191 repo = await _make_repo(db_session, slug="secp-repo")
192 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?section=danger")
193 assert resp.status_code == 200
194 # The activeSection JS variable should be populated from the context
195 assert "activeSection" in resp.text or "active_section" in resp.text or "danger" in resp.text
196
197
198 # ---------------------------------------------------------------------------
199 # Content negotiation — JSON
200 # ---------------------------------------------------------------------------
201
202
203 async def test_settings_json_response(
204 client: AsyncClient,
205 db_session: AsyncSession,
206 test_user: MusehubIdentity,
207 auth_headers: dict[str, str],
208 ) -> None:
209 """GET /{owner}/{slug}/settings?format=json returns RepoSettingsResponse."""
210 repo = await _make_repo(db_session, slug="json-repo")
211 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?format=json")
212 assert resp.status_code == 200
213 assert "application/json" in resp.headers.get("content-type", "")
214 data = resp.json()
215 assert "name" in data or "visibility" in data
216
217
218 async def test_settings_json_has_visibility(
219 client: AsyncClient,
220 db_session: AsyncSession,
221 test_user: MusehubIdentity,
222 auth_headers: dict[str, str],
223 ) -> None:
224 """JSON response includes the ``visibility`` field."""
225 repo = await _make_repo(db_session, slug="vis-repo", visibility="public")
226 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?format=json")
227 assert resp.status_code == 200
228 data = resp.json()
229 assert data.get("visibility") == "public"
230
231
232 async def test_settings_json_has_merge_flags(
233 client: AsyncClient,
234 db_session: AsyncSession,
235 test_user: MusehubIdentity,
236 auth_headers: dict[str, str],
237 ) -> None:
238 """JSON response includes merge strategy boolean flags."""
239 repo = await _make_repo(db_session, slug="flag-repo")
240 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings?format=json")
241 assert resp.status_code == 200
242 data = resp.json()
243 # RepoSettingsResponse uses camelCase via by_alias=True in negotiate_response
244 assert "allowMergeCommit" in data or "allow_merge_commit" in data
245
246
247 # ---------------------------------------------------------------------------
248 # Template content — specific UI elements
249 # ---------------------------------------------------------------------------
250
251
252 async def test_settings_page_topic_tag_input(
253 client: AsyncClient,
254 db_session: AsyncSession,
255 test_user: MusehubIdentity,
256 auth_headers: dict[str, str],
257 ) -> None:
258 """Settings page includes the topic tag input container."""
259 repo = await _make_repo(db_session, slug="tag-repo")
260 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
261 assert resp.status_code == 200
262 assert "topics-container" in resp.text or "tag-input" in resp.text
263
264
265 async def test_settings_page_danger_zone_delete_confirm(
266 client: AsyncClient,
267 db_session: AsyncSession,
268 test_user: MusehubIdentity,
269 auth_headers: dict[str, str],
270 ) -> None:
271 """Settings page requires typing the full repo name to confirm deletion."""
272 repo = await _make_repo(db_session, slug="del-repo")
273 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
274 assert resp.status_code == 200
275 assert "confirm-delete-name" in resp.text
276
277
278 async def test_settings_page_danger_zone_transfer(
279 client: AsyncClient,
280 db_session: AsyncSession,
281 test_user: MusehubIdentity,
282 auth_headers: dict[str, str],
283 ) -> None:
284 """Settings page includes a transfer ownership action."""
285 repo = await _make_repo(db_session, slug="tfr-repo")
286 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
287 assert resp.status_code == 200
288 assert "transfer" in resp.text.lower()
289 assert "modal-transfer" in resp.text
290
291
292 async def test_settings_page_danger_zone_archive(
293 client: AsyncClient,
294 db_session: AsyncSession,
295 test_user: MusehubIdentity,
296 auth_headers: dict[str, str],
297 ) -> None:
298 """Settings page includes an archive repository action."""
299 repo = await _make_repo(db_session, slug="arch-repo")
300 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
301 assert resp.status_code == 200
302 assert "archive" in resp.text.lower()
303 assert "modal-archive" in resp.text
304
305
306 async def test_settings_page_uses_owner_slug_base_url(
307 client: AsyncClient,
308 db_session: AsyncSession,
309 test_user: MusehubIdentity,
310 auth_headers: dict[str, str],
311 ) -> None:
312 """The page injects the owner/slug-based base URL into the JS context, not a UUID.
313
314 Regression guard: all MuseHub UI pages must use ``/{owner}/{slug}``
315 style URLs so breadcrumb links and API calls are human-readable.
316 """
317 repo = await _make_repo(db_session, slug="slug-repo")
318 resp = await client.get(f"/{repo.owner}/{repo.slug}/settings")
319 assert resp.status_code == 200
320 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 1 day ago