gabriel / musehub public
test_musehub_ui_collaborators_ssr.py python
158 lines 5.2 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 days ago
1 """SSR tests for the MuseHub collaborators settings page (issue #564).
2
3 Covers GET /{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
30 _OWNER = "ssr-owner"
31 _SLUG = "ssr-collab-repo"
32
33
34 # ---------------------------------------------------------------------------
35 # Helpers
36 # ---------------------------------------------------------------------------
37
38
39 async def _make_repo(db: AsyncSession) -> str:
40 """Seed a minimal public repo and return its repo_id string."""
41 repo = MusehubRepo(
42 name=_SLUG,
43 owner=_OWNER,
44 slug=_SLUG,
45 visibility="public",
46 owner_user_id="ssr-owner-uid",
47 )
48 db.add(repo)
49 await db.commit()
50 await db.refresh(repo)
51 return str(repo.repo_id)
52
53
54 async def _add_collaborator(
55 db: AsyncSession,
56 repo_id: str,
57 *,
58 user_id: str | None = None,
59 permission: str = "write",
60 invited_by: str | None = None,
61 ) -> MusehubCollaborator:
62 """Seed a collaborator record and return it."""
63 collab = MusehubCollaborator(
64 id=str(uuid.uuid4()),
65 repo_id=repo_id,
66 identity_handle=user_id or ("collab-" + str(uuid.uuid4())[:8]),
67 permission=permission,
68 invited_by_handle=invited_by,
69 )
70 db.add(collab)
71 await db.commit()
72 await db.refresh(collab)
73 return collab
74
75
76 # ---------------------------------------------------------------------------
77 # Tests
78 # ---------------------------------------------------------------------------
79
80
81 async def test_collaborators_page_renders_collaborator_server_side(
82 client: AsyncClient,
83 db_session: AsyncSession,
84 ) -> None:
85 """Seed a collaborator, GET the page, assert user_id is in the HTML body.
86
87 The SSR migration means collaborators must be rendered server-side.
88 This test fails if the handler omits ``collaborators`` from the template
89 context or the template requires a client-side fetch to populate the list.
90 """
91 repo_id = await _make_repo(db_session)
92 known_user_id = str(uuid.uuid4())
93 await _add_collaborator(db_session, repo_id, user_id=known_user_id, permission="write")
94
95 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
96 assert resp.status_code == 200
97 assert known_user_id in resp.text
98
99
100 async def test_collaborators_page_invite_form_has_hx_post(
101 client: AsyncClient,
102 db_session: AsyncSession,
103 ) -> None:
104 """The invite form uses HTMX ``hx-post`` to call the collaborators API.
105
106 The SSR migration replaces the inline JS inviteCollab() function with an
107 HTMX form that posts directly to the JSON API endpoint.
108 """
109 await _make_repo(db_session)
110 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
111 assert resp.status_code == 200
112 assert "hx-post" in resp.text
113 assert "/collaborators" in resp.text
114
115
116 async def test_collaborators_page_remove_form_has_hx_delete(
117 client: AsyncClient,
118 db_session: AsyncSession,
119 ) -> None:
120 """Non-owner collaborator rows carry ``hx-delete`` on the remove form.
121
122 The SSR migration replaces the JS removeCollab() function with an HTMX
123 form targeting the collaborators API endpoint for the specific user.
124 """
125 repo_id = await _make_repo(db_session)
126 target_user_id = str(uuid.uuid4())
127 await _add_collaborator(db_session, repo_id, user_id=target_user_id, permission="write")
128
129 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
130 assert resp.status_code == 200
131 assert "hx-delete" in resp.text
132 assert target_user_id in resp.text
133
134
135 async def test_collaborators_page_htmx_request_returns_fragment(
136 client: AsyncClient,
137 db_session: AsyncSession,
138 ) -> None:
139 """GET with ``HX-Request: true`` returns only the bare collaborator fragment.
140
141 The fragment must not contain a full HTML document shell (<html>, <head>)
142 — it is swapped directly into ``#collaborator-rows`` by HTMX.
143 """
144 repo_id = await _make_repo(db_session)
145 known_user_id = str(uuid.uuid4())
146 await _add_collaborator(db_session, repo_id, user_id=known_user_id)
147
148 resp = await client.get(
149 f"/{_OWNER}/{_SLUG}/settings/collaborators",
150 headers={"HX-Request": "true"},
151 )
152 assert resp.status_code == 200
153 body = resp.text
154 # Fragment must contain the seeded collaborator
155 assert known_user_id in body
156 # Fragment must NOT be a full HTML document
157 assert "<html" not in body
158 assert "<head" not in body
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago