gabriel / musehub public
test_musehub_ui_collaborators_ssr.py python
175 lines 6.0 KB
Raw
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 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 # Must match the auth_headers test identity ("testuser") so the owner-only gate
34 # on the collaborators settings page (claims.handle == owner) passes.
35 _OWNER = "testuser"
36 _SLUG = "ssr-collab-repo"
37
38
39 # ---------------------------------------------------------------------------
40 # Helpers
41 # ---------------------------------------------------------------------------
42
43
44 async def _make_repo(db: AsyncSession) -> str:
45 """Seed a minimal public repo and return its repo_id string."""
46 owner_id = compute_identity_id(_OWNER.encode())
47 created_at = datetime.now(tz=timezone.utc)
48 repo = MusehubRepo(
49 repo_id=compute_repo_id(owner_id, _SLUG, "code", created_at.isoformat()),
50 name=_SLUG,
51 owner=_OWNER,
52 slug=_SLUG,
53 visibility="public",
54 owner_user_id=owner_id,
55 created_at=created_at,
56 updated_at=created_at,
57 )
58 db.add(repo)
59 await db.commit()
60 await db.refresh(repo)
61 return str(repo.repo_id)
62
63
64 async def _add_collaborator(
65 db: AsyncSession,
66 repo_id: str,
67 *,
68 user_id: str | None = None,
69 permission: str = "write",
70 invited_by: str | None = None,
71 ) -> MusehubCollaborator:
72 """Seed a collaborator record and return it."""
73 handle = user_id or f"collab-{secrets.token_hex(4)}"
74 identity_id = compute_identity_id(handle.encode())
75 created_at = datetime.now(tz=timezone.utc)
76 collab = MusehubCollaborator(
77 id=compute_collaborator_id(repo_id, identity_id, created_at.isoformat()),
78 repo_id=repo_id,
79 identity_handle=handle,
80 permission=permission,
81 invited_by_handle=invited_by,
82 )
83 db.add(collab)
84 await db.commit()
85 await db.refresh(collab)
86 return collab
87
88
89 # ---------------------------------------------------------------------------
90 # Tests
91 # ---------------------------------------------------------------------------
92
93
94 async def test_collaborators_page_renders_collaborator_server_side(
95 client: AsyncClient,
96 db_session: AsyncSession,
97 auth_headers: dict[str, str],
98 ) -> None:
99 """Seed a collaborator, GET the page, assert user_id is in the HTML body.
100
101 The SSR migration means collaborators must be rendered server-side.
102 This test fails if the handler omits ``collaborators`` from the template
103 context or the template requires a client-side fetch to populate the list.
104 """
105 repo_id = await _make_repo(db_session)
106 known_user_id = secrets.token_hex(16)
107 await _add_collaborator(db_session, repo_id, user_id=known_user_id, permission="write")
108
109 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
110 assert resp.status_code == 200
111 assert known_user_id in resp.text
112
113
114 async def test_collaborators_page_invite_form_has_hx_post(
115 client: AsyncClient,
116 db_session: AsyncSession,
117 auth_headers: dict[str, str],
118 ) -> None:
119 """The invite form uses HTMX ``hx-post`` to call the collaborators API.
120
121 The SSR migration replaces the inline JS inviteCollab() function with an
122 HTMX form that posts directly to the JSON API endpoint.
123 """
124 await _make_repo(db_session)
125 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
126 assert resp.status_code == 200
127 assert "hx-post" in resp.text
128 assert "/collaborators" in resp.text
129
130
131 async def test_collaborators_page_remove_form_has_hx_delete(
132 client: AsyncClient,
133 db_session: AsyncSession,
134 auth_headers: dict[str, str],
135 ) -> None:
136 """Non-owner collaborator rows carry ``hx-delete`` on the remove form.
137
138 The SSR migration replaces the JS removeCollab() function with an HTMX
139 form targeting the collaborators API endpoint for the specific user.
140 """
141 repo_id = await _make_repo(db_session)
142 target_user_id = secrets.token_hex(16)
143 await _add_collaborator(db_session, repo_id, user_id=target_user_id, permission="write")
144
145 resp = await client.get(f"/{_OWNER}/{_SLUG}/settings/collaborators")
146 assert resp.status_code == 200
147 assert "hx-delete" in resp.text
148 assert target_user_id in resp.text
149
150
151 async def test_collaborators_page_htmx_request_returns_fragment(
152 client: AsyncClient,
153 db_session: AsyncSession,
154 auth_headers: dict[str, str],
155 ) -> None:
156 """GET with ``HX-Request: true`` returns only the bare collaborator fragment.
157
158 The fragment must not contain a full HTML document shell (<html>, <head>)
159 — it is swapped directly into ``#collaborator-rows`` by HTMX.
160 """
161 repo_id = await _make_repo(db_session)
162 known_user_id = secrets.token_hex(16)
163 await _add_collaborator(db_session, repo_id, user_id=known_user_id)
164
165 resp = await client.get(
166 f"/{_OWNER}/{_SLUG}/settings/collaborators",
167 headers={"HX-Request": "true"},
168 )
169 assert resp.status_code == 200
170 body = resp.text
171 # Fragment must contain the seeded collaborator
172 assert known_user_id in body
173 # Fragment must NOT be a full HTML document
174 assert "<html" not in body
175 assert "<head" not in body
File History 14 commits
sha256:34035d72cef530c1ab9d6a6f53be18d803dde705fc3157617d70352a96d0747b Merge branch 'fix/wire-push-external-parent-manifest' into dev Human 8 days ago
sha256:fc04e4cae9e1774d6a21b65c45daeed0e6787eb581d13aa1b03bfe9384a34226 Merge branch 'fix/two-column-scroll-layout' into dev Human 8 days ago
sha256:408916fc5973ba59c6e4eebaa80ebdcc801c0a63205651e25009d11548f79454 chore: bump version to 0.2.0.dev2 — nightly.2, matching muse Sonnet 4.6 patch 11 days ago
sha256:d035733f21ccff27735fddebfbbe0ed24565a32a22db8de5885402262671ecd2 chore: bump version to 0.2.0rc15 for musehub#113 fix release Sonnet 4.6 patch 14 days ago
sha256:0032d6cfa33bc3c8367436ad768e7dd0e339b4332153160247da8266cb5fa352 Merge branch 'task/version-tags-phase3-server' into dev Human 17 days ago
sha256:4669620efda9ff41c55bdefd1f7bfe1c239d468428744c84ead9957e5a003a53 merge: rescue snapshot-recovery hardening (c00aa21d) into d… Opus 4.8 minor 29 days ago
sha256:a59da49c4611b970fc4b6ae48678ce4943261c213a07ddbd73ce9201df869b4a fix: remove false-positive proposal_comments index drop fro… Sonnet 4.6 patch 33 days ago
sha256:0a240d6dbff234f07d98a28a4a9a68db702f3f9ff9260196f24219bdb1c0b6f3 feat: render markdown mists as HTML with heading anchor links Sonnet 4.6 patch 34 days ago
sha256:24a7d47486ebc4ebd1832830580e177ec6f877b48dced8c000e198cdec4ce9d6 Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:b9ff931d147e0114a1f17060f415b89ed551c170a91ff226c70437aa5c85f9ee Merge 'task/bump-version-rc12' into 'dev' — proposal: Bump … Human 35 days ago
sha256:d1122d21e73471879b460037b22c0b50fded7c423444a176f248428f75dac39c Merge 'task/fix-issue-pagination-cursor' into 'dev' — propo… Human 35 days ago
sha256:01e18975e73d2b3cd5b6db7929c895bef9aa6e0d4391dc5b2adfc548b41318dd Merge 'feat/adding-debug-logs-to-staging' into 'dev' — prop… Human 35 days ago
sha256:6b1949fc2797ca4c1936a637a4cbfec828ef56cf52398a2e74ca3c4f494e728f fix: use wire_bytes not mpack_bytes_raw in compute_object_b… Sonnet 4.6 patch 48 days ago
sha256:b99f2455dc346966d040133f5203297e6e3ef5803a93728a2c30568d0a0f7583 rename: delta_add → delta_upsert across wire format, models… Sonnet 4.6 patch 50 days ago