gabriel / musehub public
test_musehub_ui_commits_ssr.py python
209 lines 6.8 KB
Raw
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923 fix(issues): use issue number as pagination cursor, not cre… Sonnet 4.6 patch 8 days ago
1 """SSR + HTMX fragment tests for the MuseHub 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 /{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 secrets
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.core.genesis import compute_identity_id, compute_repo_id
33 from musehub.db.musehub_repo_models import MusehubBranch, MusehubCommit, MusehubCommitRef, MusehubRepo
34
35 # ── Constants ──────────────────────────────────────────────────────────────────
36
37 _OWNER = "ssr570owner"
38 _SLUG = "ssr570-commits"
39 _SHA_ALICE = "aa" + "0" * 38
40 _SHA_BOB = "bb" + "0" * 38
41
42
43 # ── Seed helpers ───────────────────────────────────────────────────────────────
44
45
46 async def _seed_repo(db: AsyncSession) -> str:
47 """Seed a public repo and return its repo_id string."""
48 owner_id = compute_identity_id(_OWNER.encode())
49 created_at = datetime.now(tz=timezone.utc)
50 repo = MusehubRepo(
51 repo_id=compute_repo_id(owner_id, _SLUG, "code", created_at.isoformat()),
52 name=_SLUG,
53 owner=_OWNER,
54 slug=_SLUG,
55 visibility="public",
56 owner_user_id=owner_id,
57 created_at=created_at,
58 updated_at=created_at,
59 )
60 db.add(repo)
61 await db.flush()
62 return str(repo.repo_id)
63
64
65 async def _seed_commit(
66 db: AsyncSession,
67 repo_id: str,
68 *,
69 commit_id: str | None = None,
70 author: str = "alice",
71 message: str = "Test commit message",
72 branch: str = "main",
73 timestamp: datetime | None = None,
74 ) -> MusehubCommit:
75 """Seed a commit row and return the ORM object."""
76 cid = commit_id or secrets.token_hex(20)
77 ts = timestamp or datetime.now(timezone.utc)
78 commit = MusehubCommit(
79 commit_id=cid,
80 branch=branch,
81 parent_ids=[],
82 message=message,
83 author=author,
84 timestamp=ts,
85 snapshot_id=None,
86 )
87 db.add(commit)
88 db.add(MusehubCommitRef(repo_id=repo_id, commit_id=cid))
89 await db.flush()
90 return commit
91
92
93 async def _seed_branch(db: AsyncSession, repo_id: str, head_id: str, name: str = "main") -> None:
94 """Seed a branch row."""
95 db.add(MusehubBranch(repo_id=repo_id, name=name, head_commit_id=head_id))
96 await db.flush()
97
98
99 # ── Tests ──────────────────────────────────────────────────────────────────────
100
101
102 async def test_commits_page_renders_commit_message_server_side(
103 client: AsyncClient,
104 db_session: AsyncSession,
105 ) -> None:
106 """Commit message is present in the HTML response — no client JS required."""
107 repo_id = await _seed_repo(db_session)
108 await _seed_commit(
109 db_session, repo_id, message="Bassline groove at 120 BPM feels right"
110 )
111 await db_session.commit()
112
113 response = await client.get(f"/{_OWNER}/{_SLUG}/commits")
114
115 assert response.status_code == 200
116 assert "text/html" in response.headers["content-type"]
117 assert "Bassline groove at 120 BPM feels right" in response.text
118
119
120 async def test_commits_page_filter_form_has_hx_get(
121 client: AsyncClient,
122 db_session: AsyncSession,
123 ) -> None:
124 """The filter form carries hx-get so HTMX intercepts submissions."""
125 repo_id = await _seed_repo(db_session)
126 await _seed_commit(db_session, repo_id)
127 await db_session.commit()
128
129 response = await client.get(f"/{_OWNER}/{_SLUG}/commits")
130
131 assert response.status_code == 200
132 assert "hx-get" in response.text
133
134
135 async def test_commits_page_fragment_on_htmx_request(
136 client: AsyncClient,
137 db_session: AsyncSession,
138 ) -> None:
139 """HX-Request: true returns a bare HTML fragment without the full page shell."""
140 repo_id = await _seed_repo(db_session)
141 await _seed_commit(
142 db_session, repo_id, message="Fragment-only commit row"
143 )
144 await db_session.commit()
145
146 response = await client.get(
147 f"/{_OWNER}/{_SLUG}/commits",
148 headers={"HX-Request": "true"},
149 )
150
151 assert response.status_code == 200
152 # No full-page HTML shell in a fragment response.
153 assert "<html" not in response.text
154 assert "<head" not in response.text
155 # The commit content must still be present.
156 assert "Fragment-only commit row" in response.text
157
158
159 async def test_commits_page_author_filter_narrows_results(
160 client: AsyncClient,
161 db_session: AsyncSession,
162 ) -> None:
163 """?author=alice includes only Alice's commits; Bob's message is absent."""
164 repo_id = await _seed_repo(db_session)
165 await _seed_commit(
166 db_session, repo_id,
167 commit_id=_SHA_ALICE,
168 author="alice",
169 message="Alice lays down the bass",
170 )
171 await _seed_commit(
172 db_session, repo_id,
173 commit_id=_SHA_BOB,
174 author="bob",
175 message="Bob adds a reverb tail",
176 )
177 await db_session.commit()
178
179 response = await client.get(
180 f"/{_OWNER}/{_SLUG}/commits?author=alice"
181 )
182
183 assert response.status_code == 200
184 body = response.text
185 assert "Alice lays down the bass" in body
186 assert "Bob adds a reverb tail" not in body
187
188
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"/{_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
File History 1 commit
sha256:0997d6250ae6476362f6fe2025af7789f46d03df3e9f34356d5e8ee79b201923 fix(issues): use issue number as pagination cursor, not cre… Sonnet 4.6 patch 8 days ago