gabriel / musehub public
test_musehub_ui_team.py python
253 lines 8.6 KB
Raw
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor ⚠ breaking 1 day ago
1 """Tests for the MuseHub collaborators/team management UI page (SSR).
2
3 Covers — GET /{owner}/{repo_slug}/settings/collaborators
4
5 Test index:
6 - test_collaborators_settings_page_returns_200
7 GET the settings/collaborators page returns 200 HTML without authentication.
8 - test_collaborators_settings_page_no_auth_required
9 The page is accessible without an Authorization header.
10 - test_collaborators_settings_page_unknown_repo_404
11 Unknown owner/slug combination returns 404.
12 - test_collaborators_settings_page_has_invite_form_htmx
13 The page embeds the invite form with hx-post attribute.
14 - test_collaborators_settings_page_has_permission_badges
15 The page renders colour-coded permission badge CSS classes.
16 - test_collaborators_settings_page_has_owner_crown_badge
17 The page marks owner permission with a crown emoji (👑).
18 - test_collaborators_settings_page_has_remove_button_htmx
19 Each non-owner row has an hx-delete remove form.
20 - test_collaborators_settings_json_response_empty
21 ?format=json returns CollaboratorListResponse with empty list for new repo.
22 - test_collaborators_settings_json_response_with_collaborators
23 ?format=json returns collaborators seeded in the DB.
24 - test_collaborators_settings_page_has_settings_tabs
25 The page includes the settings tab navigation bar.
26 - test_collaborators_settings_page_has_invite_form_fields
27 The invite form contains user_id and permission input fields.
28 """
29 from __future__ import annotations
30
31 import secrets
32
33 import pytest
34 import pytest_asyncio
35 from httpx import AsyncClient
36 from sqlalchemy.ext.asyncio import AsyncSession
37
38 from datetime import datetime, timezone
39
40 from muse.core.types import now_utc_iso
41 from musehub.core.genesis import compute_collaborator_id, compute_identity_id, compute_repo_id
42 from musehub.db.musehub_collaborator_models import MusehubCollaborator
43 from musehub.db.musehub_repo_models import MusehubRepo
44
45
46
47
48 # ---------------------------------------------------------------------------
49 # Helpers
50 # ---------------------------------------------------------------------------
51
52 _OWNER = "testuser"
53 _SLUG = "collab-test-repo"
54
55
56 async def _make_repo(db_session: AsyncSession) -> str:
57 """Seed a minimal repo for collaborator tests and return its repo_id."""
58 owner_id = compute_identity_id(_OWNER.encode())
59 created_at = datetime.now(tz=timezone.utc)
60 repo = MusehubRepo(
61 repo_id=compute_repo_id(owner_id, _SLUG, "code", created_at.isoformat()),
62 name=_SLUG,
63 owner=_OWNER,
64 slug=_SLUG,
65 visibility="private",
66 owner_user_id=owner_id,
67 created_at=created_at,
68 updated_at=created_at,
69 )
70 db_session.add(repo)
71 await db_session.commit()
72 await db_session.refresh(repo)
73 return str(repo.repo_id)
74
75
76 async def _add_collaborator(
77 db_session: AsyncSession,
78 repo_id: str,
79 user_id: str,
80 permission: str = "write",
81 invited_by: str | None = None,
82 ) -> MusehubCollaborator:
83 """Seed a collaborator record and return it."""
84 identity_id = compute_identity_id(user_id.encode())
85 collab = MusehubCollaborator(
86 id=compute_collaborator_id(repo_id, identity_id, now_utc_iso()),
87 repo_id=repo_id,
88 identity_handle=user_id,
89 permission=permission,
90 invited_by_handle=invited_by,
91 )
92 db_session.add(collab)
93 await db_session.commit()
94 await db_session.refresh(collab)
95 return collab
96
97
98 # ---------------------------------------------------------------------------
99 # Tests
100 # ---------------------------------------------------------------------------
101
102
103 async def test_collaborators_settings_page_returns_200(
104 client: AsyncClient,
105 db_session: AsyncSession,
106 ) -> None:
107 """GET /{owner}/{slug}/settings/collaborators returns 200 HTML."""
108 await _make_repo(db_session)
109 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
110 assert resp.status_code == 200
111 assert "text/html" in resp.headers["content-type"]
112
113
114 async def test_collaborators_settings_page_no_auth_required(
115 client: AsyncClient,
116 db_session: AsyncSession,
117 ) -> None:
118 """The HTML shell is accessible without an Authorization header.
119
120 Auth is enforced client-side; the server must not demand MSign auth to
121 render the page shell.
122 """
123 await _make_repo(db_session)
124 resp = await client.get(
125 f"/{_OWNER}/{_SLUG}/settings/collaborators",
126 headers={}, # explicit: no Authorization header
127 )
128 assert resp.status_code == 200
129
130
131 async def test_collaborators_settings_page_unknown_repo_404(
132 client: AsyncClient,
133 db_session: AsyncSession,
134 ) -> None:
135 """Unknown owner/slug combination returns 404."""
136 resp = await client.get("/nobody/nonexistent-repo/settings/collaborators")
137 assert resp.status_code == 404
138
139
140 async def test_collaborators_settings_page_has_invite_form_htmx(
141 client: AsyncClient,
142 db_session: AsyncSession,
143 ) -> None:
144 """The page embeds the invite form with hx-post for HTMX submission."""
145 await _make_repo(db_session)
146 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
147 assert resp.status_code == 200
148 assert "hx-post" in resp.text
149
150
151 async def test_collaborators_settings_page_has_permission_badges(
152 client: AsyncClient,
153 db_session: AsyncSession,
154 ) -> None:
155 """The page renders colour-coded permission badge CSS classes server-side."""
156 await _make_repo(db_session)
157 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
158 assert resp.status_code == 200
159 body = resp.text
160 assert "badge-perm-read" in body
161 assert "badge-perm-write" in body
162 assert "badge-perm-admin" in body
163 assert "badge-perm-owner" in body
164
165
166 async def test_collaborators_settings_page_has_owner_crown_badge(
167 client: AsyncClient,
168 db_session: AsyncSession,
169 ) -> None:
170 """The page marks owner permission with a crown emoji (👑)."""
171 await _make_repo(db_session)
172 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
173 assert resp.status_code == 200
174 assert "👑" in resp.text
175
176
177 async def test_collaborators_settings_page_has_remove_button_htmx(
178 client: AsyncClient,
179 db_session: AsyncSession,
180 ) -> None:
181 """Non-owner collaborator rows carry hx-delete on the remove form."""
182 repo_id = await _make_repo(db_session)
183 await _add_collaborator(db_session, repo_id, user_id=f"collab-{secrets.token_hex(4)}", permission="write")
184 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
185 assert resp.status_code == 200
186 assert "hx-delete" in resp.text
187
188
189 async def test_collaborators_settings_json_response_empty(
190 client: AsyncClient,
191 db_session: AsyncSession,
192 ) -> None:
193 """?format=json returns CollaboratorListResponse with empty list for a new repo."""
194 await _make_repo(db_session)
195 resp = await client.get(
196 f"/{_OWNER}/{_SLUG}/settings/collaborators?format=json"
197 )
198 assert resp.status_code == 200
199 data = resp.json()
200 assert "collaborators" in data
201 assert "total" in data
202 assert data["total"] == 0
203 assert data["collaborators"] == []
204
205
206 async def test_collaborators_settings_json_response_with_collaborators(
207 client: AsyncClient,
208 db_session: AsyncSession,
209 ) -> None:
210 """?format=json returns collaborators seeded in the DB."""
211 repo_id = await _make_repo(db_session)
212 collab_uid = secrets.token_hex(16)
213 await _add_collaborator(
214 db_session, repo_id, user_id=collab_uid, permission="write", invited_by="owner-user-id"
215 )
216
217 resp = await client.get(
218 f"/{_OWNER}/{_SLUG}/settings/collaborators?format=json"
219 )
220 assert resp.status_code == 200
221 data = resp.json()
222 assert data["total"] == 1
223 assert len(data["collaborators"]) == 1
224 collab = data["collaborators"][0]
225 # camelCase keys (Pydantic by_alias=True via negotiate_response)
226 assert collab["handle"] == collab_uid
227 assert collab["permission"] == "write"
228
229
230 async def test_collaborators_settings_page_has_settings_tabs(
231 client: AsyncClient,
232 db_session: AsyncSession,
233 ) -> None:
234 """The page includes the settings tab navigation bar."""
235 await _make_repo(db_session)
236 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
237 assert resp.status_code == 200
238 body = resp.text
239 assert "settings-tabs" in body
240 assert "Collaborators" in body
241
242
243 async def test_collaborators_settings_page_has_invite_form_fields(
244 client: AsyncClient,
245 db_session: AsyncSession,
246 ) -> None:
247 """The invite form has user_id and permission input fields rendered server-side."""
248 await _make_repo(db_session)
249 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
250 assert resp.status_code == 200
251 body = resp.text
252 assert 'name="user_id"' in body
253 assert 'name="permission"' in body
File History 1 commit
sha256:3ff9c9863a9891bdcde71b4a43228f66d0493e38b7cc1d09fe9eb7de774046b2 feat: add repair-commit wire endpoint (API parity with repa… Opus 4.8 minor 1 day ago