gabriel / musehub public
test_musehub_ui_team.py python
255 lines 8.3 KB
c0f0b481 release: merge dev → main (#5) Gabriel Cardona <cgcardona@gmail.com> 5d 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 a JWT.
8 - test_collaborators_settings_page_no_auth_required
9 The page is accessible without a Bearer token.
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 musehub.db.musehub_collaborator_models import MusehubCollaborator
39 from musehub.db.musehub_models import MusehubRepo
40
41 pytestmark = pytest.mark.anyio
42
43 _skip_template = pytest.mark.skip(
44 reason="musehub/fragments/collaborator_rows.html template not yet implemented"
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 repo = MusehubRepo(
59 name=_SLUG,
60 owner=_OWNER,
61 slug=_SLUG,
62 visibility="private",
63 owner_user_id="owner-user-id",
64 )
65 db_session.add(repo)
66 await db_session.commit()
67 await db_session.refresh(repo)
68 return str(repo.repo_id)
69
70
71 async def _add_collaborator(
72 db_session: AsyncSession,
73 repo_id: str,
74 user_id: str,
75 permission: str = "write",
76 invited_by: str | None = None,
77 ) -> MusehubCollaborator:
78 """Seed a collaborator record and return it."""
79 collab = MusehubCollaborator(
80 id=str(uuid.uuid4()),
81 repo_id=repo_id,
82 user_id=user_id,
83 permission=permission,
84 invited_by=invited_by,
85 )
86 db_session.add(collab)
87 await db_session.commit()
88 await db_session.refresh(collab)
89 return collab
90
91
92 # ---------------------------------------------------------------------------
93 # Tests
94 # ---------------------------------------------------------------------------
95
96
97 @_skip_template
98 async def test_collaborators_settings_page_returns_200(
99 client: AsyncClient,
100 db_session: AsyncSession,
101 ) -> None:
102 """GET /{owner}/{slug}/settings/collaborators returns 200 HTML."""
103 await _make_repo(db_session)
104 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
105 assert resp.status_code == 200
106 assert "text/html" in resp.headers["content-type"]
107
108
109 @_skip_template
110 async def test_collaborators_settings_page_no_auth_required(
111 client: AsyncClient,
112 db_session: AsyncSession,
113 ) -> None:
114 """The HTML shell is accessible without a Bearer token.
115
116 Auth is enforced client-side; the server must not demand a JWT to
117 render the page shell.
118 """
119 await _make_repo(db_session)
120 resp = await client.get(
121 f"/{_OWNER}/{_SLUG}/settings/collaborators",
122 headers={}, # explicit: no Authorization header
123 )
124 assert resp.status_code == 200
125
126
127 async def test_collaborators_settings_page_unknown_repo_404(
128 client: AsyncClient,
129 db_session: AsyncSession,
130 ) -> None:
131 """Unknown owner/slug combination returns 404."""
132 resp = await client.get("/nobody/nonexistent-repo/settings/collaborators")
133 assert resp.status_code == 404
134
135
136 @_skip_template
137 async def test_collaborators_settings_page_has_invite_form_htmx(
138 client: AsyncClient,
139 db_session: AsyncSession,
140 ) -> None:
141 """The page embeds the invite form with hx-post for HTMX submission."""
142 await _make_repo(db_session)
143 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
144 assert resp.status_code == 200
145 assert "hx-post" in resp.text
146
147
148 @_skip_template
149 async def test_collaborators_settings_page_has_permission_badges(
150 client: AsyncClient,
151 db_session: AsyncSession,
152 ) -> None:
153 """The page renders colour-coded permission badge CSS classes server-side."""
154 await _make_repo(db_session)
155 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
156 assert resp.status_code == 200
157 body = resp.text
158 assert "badge-perm-read" in body
159 assert "badge-perm-write" in body
160 assert "badge-perm-admin" in body
161 assert "badge-perm-owner" in body
162
163
164 @_skip_template
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 @_skip_template
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=str(uuid.uuid4()), 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 = str(uuid.uuid4())
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["userId"] == collab_uid
227 assert collab["permission"] == "write"
228
229
230 @_skip_template
231 async def test_collaborators_settings_page_has_settings_tabs(
232 client: AsyncClient,
233 db_session: AsyncSession,
234 ) -> None:
235 """The page includes the settings tab navigation bar."""
236 await _make_repo(db_session)
237 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
238 assert resp.status_code == 200
239 body = resp.text
240 assert "settings-tabs" in body
241 assert "Collaborators" in body
242
243
244 @_skip_template
245 async def test_collaborators_settings_page_has_invite_form_fields(
246 client: AsyncClient,
247 db_session: AsyncSession,
248 ) -> None:
249 """The invite form has user_id and permission input fields rendered server-side."""
250 await _make_repo(db_session)
251 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
252 assert resp.status_code == 200
253 body = resp.text
254 assert 'name="user_id"' in body
255 assert 'name="permission"' in body