gabriel / musehub public
test_musehub_ui_forks.py python
279 lines 9.1 KB
9236d457 fix: remove all inline scripts, restore strict script-src CSP (#51) Gabriel Cardona <cgcardona@gmail.com> 23h ago
1 """Tests for MuseHub fork network UI endpoint.
2
3 Covers GET /{owner}/{repo_slug}/forks:
4
5 - test_forks_page_returns_200 — page renders without auth
6 - test_forks_page_no_auth_required — no JWT needed for HTML shell
7 - test_forks_page_has_svg_dag_markup — SVG DAG scaffold present in HTML
8 - test_forks_page_has_legend — divergence colour legend present
9 - test_forks_page_has_compare_button_js"Compare" action JS present
10 - test_forks_page_has_contribute_upstream_js"Contribute upstream" action JS present
11 - test_forks_page_json_response — ?format=json returns ForkNetworkResponse
12 - test_forks_page_json_has_root_and_total — JSON contains root and totalForks fields
13 - test_forks_page_json_children_present — fork children appear in JSON root.children
14 - test_forks_page_json_divergence_computed — divergence_commits field is non-negative int
15 - test_forks_page_unknown_repo_404 — unknown owner/slug → 404
16 - test_forks_page_base_url_in_html — HTML uses owner/slug base URL pattern
17 - test_forks_page_json_empty_repo — repo with no forks returns total_forks=0
18 """
19 from __future__ import annotations
20
21 import pytest
22 from httpx import AsyncClient
23 from sqlalchemy.ext.asyncio import AsyncSession
24
25 from datetime import datetime, timezone
26
27 from musehub.db.musehub_models import MusehubCommit, MusehubFork, MusehubRepo
28
29
30 # ---------------------------------------------------------------------------
31 # Helpers
32 # ---------------------------------------------------------------------------
33
34
35 async def _make_repo(
36 db: AsyncSession,
37 owner: str = "upstream",
38 slug: str = "bass-project",
39 visibility: str = "public",
40 ) -> str:
41 """Seed a public repo and return its repo_id string."""
42 repo = MusehubRepo(
43 name=slug,
44 owner=owner,
45 slug=slug,
46 visibility=visibility,
47 owner_user_id=f"uid-{owner}",
48 )
49 db.add(repo)
50 await db.commit()
51 await db.refresh(repo)
52 return str(repo.repo_id)
53
54
55 async def _make_commit(db: AsyncSession, repo_id: str, sha: str = "abc123", branch: str = "main") -> None:
56 """Seed a single commit into a repo."""
57 commit = MusehubCommit(
58 commit_id=sha,
59 repo_id=repo_id,
60 branch=branch,
61 message="Initial composition",
62 author="upstream",
63 parent_ids=[],
64 timestamp=datetime.now(tz=timezone.utc),
65 )
66 db.add(commit)
67 await db.commit()
68
69
70 async def _make_fork(
71 db: AsyncSession,
72 source_repo_id: str,
73 fork_owner: str = "forker",
74 fork_slug: str = "bass-project",
75 ) -> str:
76 """Seed a fork repo and fork relationship; return fork's repo_id."""
77 fork_repo = MusehubRepo(
78 name=fork_slug,
79 owner=fork_owner,
80 slug=fork_slug,
81 visibility="public",
82 owner_user_id=f"uid-{fork_owner}",
83 description=f"Fork of upstream/{fork_slug}",
84 )
85 db.add(fork_repo)
86 await db.commit()
87 await db.refresh(fork_repo)
88
89 fork_record = MusehubFork(
90 source_repo_id=source_repo_id,
91 fork_repo_id=str(fork_repo.repo_id),
92 forked_by=fork_owner,
93 )
94 db.add(fork_record)
95 await db.commit()
96 return str(fork_repo.repo_id)
97
98
99 # ---------------------------------------------------------------------------
100 # Tests — HTML shell
101 # ---------------------------------------------------------------------------
102
103
104 @pytest.mark.anyio
105 async def test_forks_page_returns_200(
106 client: AsyncClient,
107 db_session: AsyncSession,
108 ) -> None:
109 """GET /{owner}/{slug}/forks returns 200 HTML."""
110 await _make_repo(db_session)
111 response = await client.get("/upstream/bass-project/forks")
112 assert response.status_code == 200
113 assert "text/html" in response.headers["content-type"]
114
115
116 @pytest.mark.anyio
117 async def test_forks_page_no_auth_required(
118 client: AsyncClient,
119 db_session: AsyncSession,
120 ) -> None:
121 """Fork network page is publicly accessible — no JWT needed."""
122 await _make_repo(db_session)
123 response = await client.get("/upstream/bass-project/forks")
124 assert response.status_code == 200
125
126
127 @pytest.mark.anyio
128 async def test_forks_page_has_svg_dag_markup(
129 client: AsyncClient,
130 db_session: AsyncSession,
131 ) -> None:
132 """Page HTML includes an SVG element as the DAG scaffold."""
133 await _make_repo(db_session)
134 response = await client.get("/upstream/bass-project/forks")
135 assert response.status_code == 200
136 body = response.text
137 assert "fork-svg" in body or "fork-canvas" in body
138
139
140 @pytest.mark.anyio
141 async def test_forks_page_has_legend(
142 client: AsyncClient,
143 db_session: AsyncSession,
144 ) -> None:
145 """Page contains a divergence colour legend."""
146 await _make_repo(db_session)
147 response = await client.get("/upstream/bass-project/forks")
148 assert response.status_code == 200
149 body = response.text
150 assert "legend" in body or "In sync" in body or "ahead" in body
151
152
153 @pytest.mark.anyio
154 async def test_forks_page_has_compare_button_js(
155 client: AsyncClient,
156 db_session: AsyncSession,
157 ) -> None:
158 """Fork network page dispatches forks.ts; config passed via page_json data block."""
159 await _make_repo(db_session)
160 response = await client.get("/upstream/bass-project/forks")
161 assert response.status_code == 200
162 assert '"page": "forks"' in response.text
163 assert '"repoId"' in response.text
164
165
166 @pytest.mark.anyio
167 async def test_forks_page_has_contribute_upstream_js(
168 client: AsyncClient,
169 db_session: AsyncSession,
170 ) -> None:
171 """Forks page loads with forkNetwork config; Contribute upstream rendered when forks exist."""
172 await _make_repo(db_session)
173 response = await client.get("/upstream/bass-project/forks")
174 assert response.status_code == 200
175 assert "forkNetwork" in response.text
176
177
178 @pytest.mark.anyio
179 async def test_forks_page_base_url_in_html(
180 client: AsyncClient,
181 db_session: AsyncSession,
182 ) -> None:
183 """HTML uses the owner/slug base URL, not raw repo_id UUIDs."""
184 await _make_repo(db_session)
185 response = await client.get("/upstream/bass-project/forks")
186 assert response.status_code == 200
187 assert "/upstream/bass-project" in response.text
188
189
190 # ---------------------------------------------------------------------------
191 # Tests — JSON path
192 # ---------------------------------------------------------------------------
193
194
195 @pytest.mark.anyio
196 async def test_forks_page_json_response(
197 client: AsyncClient,
198 db_session: AsyncSession,
199 ) -> None:
200 """?format=json returns HTTP 200 with application/json content-type."""
201 await _make_repo(db_session)
202 response = await client.get("/upstream/bass-project/forks?format=json")
203 assert response.status_code == 200
204 assert response.headers["content-type"].startswith("application/json")
205
206
207 @pytest.mark.anyio
208 async def test_forks_page_json_has_root_and_total(
209 client: AsyncClient,
210 db_session: AsyncSession,
211 ) -> None:
212 """JSON response contains root node and totalForks counter."""
213 await _make_repo(db_session)
214 response = await client.get("/upstream/bass-project/forks?format=json")
215 assert response.status_code == 200
216 data = response.json()
217 assert "root" in data
218 assert "totalForks" in data
219
220
221 @pytest.mark.anyio
222 async def test_forks_page_json_children_present(
223 client: AsyncClient,
224 db_session: AsyncSession,
225 ) -> None:
226 """A fork repo appears as a child node in the JSON root.children list."""
227 source_id = await _make_repo(db_session)
228 await _make_fork(db_session, source_id, fork_owner="alice", fork_slug="bass-project")
229 response = await client.get("/upstream/bass-project/forks?format=json")
230 assert response.status_code == 200
231 data = response.json()
232 children = data["root"]["children"]
233 assert len(children) == 1
234 assert children[0]["owner"] == "alice"
235 assert children[0]["repoSlug"] == "bass-project"
236
237
238 @pytest.mark.anyio
239 async def test_forks_page_json_divergence_computed(
240 client: AsyncClient,
241 db_session: AsyncSession,
242 ) -> None:
243 """divergenceCommits is a non-negative integer for each fork child."""
244 source_id = await _make_repo(db_session)
245 fork_id = await _make_fork(db_session, source_id, fork_owner="bob", fork_slug="bass-project")
246 # Add a commit to the fork so divergence > 0
247 await _make_commit(db_session, fork_id, sha="fork-commit-001")
248 response = await client.get("/upstream/bass-project/forks?format=json")
249 assert response.status_code == 200
250 data = response.json()
251 children = data["root"]["children"]
252 assert len(children) == 1
253 div = children[0]["divergenceCommits"]
254 assert isinstance(div, int)
255 assert div >= 0
256
257
258 @pytest.mark.anyio
259 async def test_forks_page_unknown_repo_404(
260 client: AsyncClient,
261 db_session: AsyncSession,
262 ) -> None:
263 """Unknown owner/slug returns 404."""
264 response = await client.get("/nobody/nonexistent/forks")
265 assert response.status_code == 404
266
267
268 @pytest.mark.anyio
269 async def test_forks_page_json_empty_repo(
270 client: AsyncClient,
271 db_session: AsyncSession,
272 ) -> None:
273 """A repo with no forks returns totalForks=0 and empty children list."""
274 await _make_repo(db_session)
275 response = await client.get("/upstream/bass-project/forks?format=json")
276 assert response.status_code == 200
277 data = response.json()
278 assert data["totalForks"] == 0
279 assert data["root"]["children"] == []