gabriel / musehub public
test_musehub_ui_collaborators_ssr.py python
162 lines 5.4 KB
c2319918 fix(ci): resolve all test failures blocking PR #3 Gabriel Cardona <gabriel@tellurstori.com> 6d ago
1 """SSR tests for the MuseHub collaborators settings page (issue #564).
2
3 Covers GET /musehub/ui/{owner}/{repo_slug}/settings/collaborators after SSR migration:
4
5 - test_collaborators_page_renders_collaborator_server_side
6 Seed a collaborator, GET the page, assert the user_id appears in the HTML body
7 — confirming server-side render rather than client-side JS fetch.
8
9 - test_collaborators_page_invite_form_has_hx_post
10 The invite form carries ``hx-post`` pointing to the collaborators API.
11
12 - test_collaborators_page_remove_form_has_hx_delete
13 Each non-owner collaborator row's remove form carries ``hx-delete``.
14
15 - test_collaborators_page_htmx_request_returns_fragment
16 GET with ``HX-Request: true`` returns only the bare fragment (no <html> wrapper).
17 """
18 from __future__ import annotations
19
20 import uuid
21
22 import pytest
23 from httpx import AsyncClient
24 from sqlalchemy.ext.asyncio import AsyncSession
25
26 from musehub.db.musehub_collaborator_models import MusehubCollaborator
27 from musehub.db.musehub_models import MusehubRepo
28
29 pytestmark = [
30 pytest.mark.anyio,
31 pytest.mark.skip(reason="musehub/fragments/collaborator_rows.html template not yet implemented"),
32 ]
33
34 _OWNER = "ssr-owner"
35 _SLUG = "ssr-collab-repo"
36
37
38 # ---------------------------------------------------------------------------
39 # Helpers
40 # ---------------------------------------------------------------------------
41
42
43 async def _make_repo(db: AsyncSession) -> str:
44 """Seed a minimal public repo and return its repo_id string."""
45 repo = MusehubRepo(
46 name=_SLUG,
47 owner=_OWNER,
48 slug=_SLUG,
49 visibility="public",
50 owner_user_id="ssr-owner-uid",
51 )
52 db.add(repo)
53 await db.commit()
54 await db.refresh(repo)
55 return str(repo.repo_id)
56
57
58 async def _add_collaborator(
59 db: AsyncSession,
60 repo_id: str,
61 *,
62 user_id: str | None = None,
63 permission: str = "write",
64 invited_by: str | None = None,
65 ) -> MusehubCollaborator:
66 """Seed a collaborator record and return it."""
67 collab = MusehubCollaborator(
68 id=str(uuid.uuid4()),
69 repo_id=repo_id,
70 user_id=user_id or str(uuid.uuid4()),
71 permission=permission,
72 invited_by=invited_by,
73 )
74 db.add(collab)
75 await db.commit()
76 await db.refresh(collab)
77 return collab
78
79
80 # ---------------------------------------------------------------------------
81 # Tests
82 # ---------------------------------------------------------------------------
83
84
85 async def test_collaborators_page_renders_collaborator_server_side(
86 client: AsyncClient,
87 db_session: AsyncSession,
88 ) -> None:
89 """Seed a collaborator, GET the page, assert user_id is in the HTML body.
90
91 The SSR migration means collaborators must be rendered server-side.
92 This test fails if the handler omits ``collaborators`` from the template
93 context or the template requires a client-side fetch to populate the list.
94 """
95 repo_id = await _make_repo(db_session)
96 known_user_id = str(uuid.uuid4())
97 await _add_collaborator(db_session, repo_id, user_id=known_user_id, permission="write")
98
99 resp = await client.get(f"/musehub/ui/{_OWNER}/{_SLUG}/settings/collaborators")
100 assert resp.status_code == 200
101 assert known_user_id in resp.text
102
103
104 async def test_collaborators_page_invite_form_has_hx_post(
105 client: AsyncClient,
106 db_session: AsyncSession,
107 ) -> None:
108 """The invite form uses HTMX ``hx-post`` to call the collaborators API.
109
110 The SSR migration replaces the inline JS inviteCollab() function with an
111 HTMX form that posts directly to the JSON API endpoint.
112 """
113 await _make_repo(db_session)
114 resp = await client.get(f"/musehub/ui/{_OWNER}/{_SLUG}/settings/collaborators")
115 assert resp.status_code == 200
116 assert "hx-post" in resp.text
117 assert "/collaborators" in resp.text
118
119
120 async def test_collaborators_page_remove_form_has_hx_delete(
121 client: AsyncClient,
122 db_session: AsyncSession,
123 ) -> None:
124 """Non-owner collaborator rows carry ``hx-delete`` on the remove form.
125
126 The SSR migration replaces the JS removeCollab() function with an HTMX
127 form targeting the collaborators API endpoint for the specific user.
128 """
129 repo_id = await _make_repo(db_session)
130 target_user_id = str(uuid.uuid4())
131 await _add_collaborator(db_session, repo_id, user_id=target_user_id, permission="write")
132
133 resp = await client.get(f"/musehub/ui/{_OWNER}/{_SLUG}/settings/collaborators")
134 assert resp.status_code == 200
135 assert "hx-delete" in resp.text
136 assert target_user_id in resp.text
137
138
139 async def test_collaborators_page_htmx_request_returns_fragment(
140 client: AsyncClient,
141 db_session: AsyncSession,
142 ) -> None:
143 """GET with ``HX-Request: true`` returns only the bare collaborator fragment.
144
145 The fragment must not contain a full HTML document shell (<html>, <head>)
146 — it is swapped directly into ``#collaborator-rows`` by HTMX.
147 """
148 repo_id = await _make_repo(db_session)
149 known_user_id = str(uuid.uuid4())
150 await _add_collaborator(db_session, repo_id, user_id=known_user_id)
151
152 resp = await client.get(
153 f"/musehub/ui/{_OWNER}/{_SLUG}/settings/collaborators",
154 headers={"HX-Request": "true"},
155 )
156 assert resp.status_code == 200
157 body = resp.text
158 # Fragment must contain the seeded collaborator
159 assert known_user_id in body
160 # Fragment must NOT be a full HTML document
161 assert "<html" not in body
162 assert "<head" not in body