gabriel / musehub public
test_musehub_ui_collaborators_ssr.py python
169 lines 5.7 KB
Raw
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923 fix(issues): use issue number as pagination cursor, not cre… Sonnet 4.6 patch 8 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 secrets
21
22 import pytest
23 from httpx import AsyncClient
24 from sqlalchemy.ext.asyncio import AsyncSession
25
26 from datetime import datetime, timezone
27
28 from musehub.core.genesis import compute_collaborator_id, compute_identity_id, compute_repo_id
29 from musehub.db.musehub_collaborator_models import MusehubCollaborator
30 from musehub.db.musehub_repo_models import MusehubRepo
31
32
33 _OWNER = "ssr-owner"
34 _SLUG = "ssr-collab-repo"
35
36
37 # ---------------------------------------------------------------------------
38 # Helpers
39 # ---------------------------------------------------------------------------
40
41
42 async def _make_repo(db: AsyncSession) -> str:
43 """Seed a minimal public repo and return its repo_id string."""
44 owner_id = compute_identity_id(_OWNER.encode())
45 created_at = datetime.now(tz=timezone.utc)
46 repo = MusehubRepo(
47 repo_id=compute_repo_id(owner_id, _SLUG, "code", created_at.isoformat()),
48 name=_SLUG,
49 owner=_OWNER,
50 slug=_SLUG,
51 visibility="public",
52 owner_user_id=owner_id,
53 created_at=created_at,
54 updated_at=created_at,
55 )
56 db.add(repo)
57 await db.commit()
58 await db.refresh(repo)
59 return str(repo.repo_id)
60
61
62 async def _add_collaborator(
63 db: AsyncSession,
64 repo_id: str,
65 *,
66 user_id: str | None = None,
67 permission: str = "write",
68 invited_by: str | None = None,
69 ) -> MusehubCollaborator:
70 """Seed a collaborator record and return it."""
71 handle = user_id or f"collab-{secrets.token_hex(4)}"
72 identity_id = compute_identity_id(handle.encode())
73 created_at = datetime.now(tz=timezone.utc)
74 collab = MusehubCollaborator(
75 id=compute_collaborator_id(repo_id, identity_id, created_at.isoformat()),
76 repo_id=repo_id,
77 identity_handle=handle,
78 permission=permission,
79 invited_by_handle=invited_by,
80 )
81 db.add(collab)
82 await db.commit()
83 await db.refresh(collab)
84 return collab
85
86
87 # ---------------------------------------------------------------------------
88 # Tests
89 # ---------------------------------------------------------------------------
90
91
92 async def test_collaborators_page_renders_collaborator_server_side(
93 client: AsyncClient,
94 db_session: AsyncSession,
95 ) -> None:
96 """Seed a collaborator, GET the page, assert user_id is in the HTML body.
97
98 The SSR migration means collaborators must be rendered server-side.
99 This test fails if the handler omits ``collaborators`` from the template
100 context or the template requires a client-side fetch to populate the list.
101 """
102 repo_id = await _make_repo(db_session)
103 known_user_id = secrets.token_hex(16)
104 await _add_collaborator(db_session, repo_id, user_id=known_user_id, permission="write")
105
106 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
107 assert resp.status_code == 200
108 assert known_user_id in resp.text
109
110
111 async def test_collaborators_page_invite_form_has_hx_post(
112 client: AsyncClient,
113 db_session: AsyncSession,
114 ) -> None:
115 """The invite form uses HTMX ``hx-post`` to call the collaborators API.
116
117 The SSR migration replaces the inline JS inviteCollab() function with an
118 HTMX form that posts directly to the JSON API endpoint.
119 """
120 await _make_repo(db_session)
121 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
122 assert resp.status_code == 200
123 assert "hx-post" in resp.text
124 assert "/collaborators" in resp.text
125
126
127 async def test_collaborators_page_remove_form_has_hx_delete(
128 client: AsyncClient,
129 db_session: AsyncSession,
130 ) -> None:
131 """Non-owner collaborator rows carry ``hx-delete`` on the remove form.
132
133 The SSR migration replaces the JS removeCollab() function with an HTMX
134 form targeting the collaborators API endpoint for the specific user.
135 """
136 repo_id = await _make_repo(db_session)
137 target_user_id = secrets.token_hex(16)
138 await _add_collaborator(db_session, repo_id, user_id=target_user_id, permission="write")
139
140 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
141 assert resp.status_code == 200
142 assert "hx-delete" in resp.text
143 assert target_user_id in resp.text
144
145
146 async def test_collaborators_page_htmx_request_returns_fragment(
147 client: AsyncClient,
148 db_session: AsyncSession,
149 ) -> None:
150 """GET with ``HX-Request: true`` returns only the bare collaborator fragment.
151
152 The fragment must not contain a full HTML document shell (<html>, <head>)
153 — it is swapped directly into ``#collaborator-rows`` by HTMX.
154 """
155 repo_id = await _make_repo(db_session)
156 known_user_id = secrets.token_hex(16)
157 await _add_collaborator(db_session, repo_id, user_id=known_user_id)
158
159 resp = await client.get(
160 f"/{_OWNER}/{_SLUG}/settings/collaborators",
161 headers={"HX-Request": "true"},
162 )
163 assert resp.status_code == 200
164 body = resp.text
165 # Fragment must contain the seeded collaborator
166 assert known_user_id in body
167 # Fragment must NOT be a full HTML document
168 assert "<html" not in body
169 assert "<head" not in body
File History 1 commit
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923 fix(issues): use issue number as pagination cursor, not cre… Sonnet 4.6 patch 8 days ago