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