gabriel / musehub public
test_musehub_ui_commits_ssr.py python
209 lines 6.6 KB
cd448303 Initial extraction of MuseHub from maestro monorepo. Gabriel Cardona <gabriel@tellurstori.com> 7d ago
1 """SSR + HTMX fragment tests for the Muse Hub commits list page — issue #570.
2
3 Validates that commit data is rendered server-side into HTML (no JS required)
4 and that HTMX fragment requests return bare HTML without the full page shell.
5
6 Covers GET /musehub/ui/{owner}/{repo_slug}/commits:
7
8 - test_commits_page_renders_commit_message_server_side
9 Seed a commit; its message appears in the response HTML.
10
11 - test_commits_page_filter_form_has_hx_get
12 The filter form has hx-get attribute pointing at the commits URL.
13
14 - test_commits_page_fragment_on_htmx_request
15 GET with HX-Request: true returns a bare fragment (no <html>/<head> shell).
16
17 - test_commits_page_author_filter_narrows_results
18 ?author=alice shows only Alice's commits; Bob's are absent.
19
20 - test_commits_page_pagination_renders_next
21 More than per_page commits → "Older →" link present in the response.
22 """
23 from __future__ import annotations
24
25 import uuid
26 from datetime import datetime, timezone
27
28 import pytest
29 from httpx import AsyncClient
30 from sqlalchemy.ext.asyncio import AsyncSession
31
32 from musehub.db.musehub_models import MusehubBranch, MusehubCommit, MusehubRepo
33
34 # ── Constants ──────────────────────────────────────────────────────────────────
35
36 _OWNER = "ssr570owner"
37 _SLUG = "ssr570-commits"
38 _SHA_ALICE = "aa" + "0" * 38
39 _SHA_BOB = "bb" + "0" * 38
40
41
42 # ── Seed helpers ───────────────────────────────────────────────────────────────
43
44
45 async def _seed_repo(db: AsyncSession) -> str:
46 """Seed a public repo and return its repo_id string."""
47 repo = MusehubRepo(
48 repo_id=str(uuid.uuid4()),
49 name=_SLUG,
50 owner=_OWNER,
51 slug=_SLUG,
52 visibility="public",
53 owner_user_id=str(uuid.uuid4()),
54 )
55 db.add(repo)
56 await db.flush()
57 return str(repo.repo_id)
58
59
60 async def _seed_commit(
61 db: AsyncSession,
62 repo_id: str,
63 *,
64 commit_id: str | None = None,
65 author: str = "alice",
66 message: str = "Test commit message",
67 branch: str = "main",
68 timestamp: datetime | None = None,
69 ) -> MusehubCommit:
70 """Seed a commit row and return the ORM object."""
71 cid = commit_id or (uuid.uuid4().hex + uuid.uuid4().hex)[:40]
72 ts = timestamp or datetime.now(timezone.utc)
73 commit = MusehubCommit(
74 commit_id=cid,
75 repo_id=repo_id,
76 branch=branch,
77 parent_ids=[],
78 message=message,
79 author=author,
80 timestamp=ts,
81 snapshot_id=None,
82 )
83 db.add(commit)
84 await db.flush()
85 return commit
86
87
88 async def _seed_branch(db: AsyncSession, repo_id: str, head_id: str, name: str = "main") -> None:
89 """Seed a branch row."""
90 db.add(MusehubBranch(repo_id=repo_id, name=name, head_commit_id=head_id))
91 await db.flush()
92
93
94 # ── Tests ──────────────────────────────────────────────────────────────────────
95
96
97 @pytest.mark.anyio
98 async def test_commits_page_renders_commit_message_server_side(
99 client: AsyncClient,
100 db_session: AsyncSession,
101 ) -> None:
102 """Commit message is present in the HTML response — no client JS required."""
103 repo_id = await _seed_repo(db_session)
104 await _seed_commit(
105 db_session, repo_id, message="Bassline groove at 120 BPM feels right"
106 )
107 await db_session.commit()
108
109 response = await client.get(f"/musehub/ui/{_OWNER}/{_SLUG}/commits")
110
111 assert response.status_code == 200
112 assert "text/html" in response.headers["content-type"]
113 assert "Bassline groove at 120 BPM feels right" in response.text
114
115
116 @pytest.mark.anyio
117 async def test_commits_page_filter_form_has_hx_get(
118 client: AsyncClient,
119 db_session: AsyncSession,
120 ) -> None:
121 """The filter form carries hx-get so HTMX intercepts submissions."""
122 repo_id = await _seed_repo(db_session)
123 await _seed_commit(db_session, repo_id)
124 await db_session.commit()
125
126 response = await client.get(f"/musehub/ui/{_OWNER}/{_SLUG}/commits")
127
128 assert response.status_code == 200
129 assert "hx-get" in response.text
130
131
132 @pytest.mark.anyio
133 async def test_commits_page_fragment_on_htmx_request(
134 client: AsyncClient,
135 db_session: AsyncSession,
136 ) -> None:
137 """HX-Request: true returns a bare HTML fragment without the full page shell."""
138 repo_id = await _seed_repo(db_session)
139 await _seed_commit(
140 db_session, repo_id, message="Fragment-only commit row"
141 )
142 await db_session.commit()
143
144 response = await client.get(
145 f"/musehub/ui/{_OWNER}/{_SLUG}/commits",
146 headers={"HX-Request": "true"},
147 )
148
149 assert response.status_code == 200
150 # No full-page HTML shell in a fragment response.
151 assert "<html" not in response.text
152 assert "<head" not in response.text
153 # The commit content must still be present.
154 assert "Fragment-only commit row" in response.text
155
156
157 @pytest.mark.anyio
158 async def test_commits_page_author_filter_narrows_results(
159 client: AsyncClient,
160 db_session: AsyncSession,
161 ) -> None:
162 """?author=alice includes only Alice's commits; Bob's message is absent."""
163 repo_id = await _seed_repo(db_session)
164 await _seed_commit(
165 db_session, repo_id,
166 commit_id=_SHA_ALICE,
167 author="alice",
168 message="Alice lays down the bass",
169 )
170 await _seed_commit(
171 db_session, repo_id,
172 commit_id=_SHA_BOB,
173 author="bob",
174 message="Bob adds a reverb tail",
175 )
176 await db_session.commit()
177
178 response = await client.get(
179 f"/musehub/ui/{_OWNER}/{_SLUG}/commits?author=alice"
180 )
181
182 assert response.status_code == 200
183 body = response.text
184 assert "Alice lays down the bass" in body
185 assert "Bob adds a reverb tail" not in body
186
187
188 @pytest.mark.anyio
189 async def test_commits_page_pagination_renders_next(
190 client: AsyncClient,
191 db_session: AsyncSession,
192 ) -> None:
193 """When total commits exceed per_page, the 'Older →' pagination link appears."""
194 repo_id = await _seed_repo(db_session)
195 # Seed 35 commits — more than the default per_page=30.
196 for i in range(35):
197 cid = f"{i:040x}"
198 await _seed_commit(
199 db_session, repo_id,
200 commit_id=cid,
201 message=f"Commit number {i}",
202 )
203 await db_session.commit()
204
205 response = await client.get(f"/musehub/ui/{_OWNER}/{_SLUG}/commits")
206
207 assert response.status_code == 200
208 # "Older →" appears as an anchor when there is a next page.
209 assert "Older" in response.text