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