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