gabriel / musehub public
test_musehub_ui_forks.py python
278 lines 9.0 KB
c0f0b481 release: merge dev → main (#5) Gabriel Cardona <cgcardona@gmail.com> 5d 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 """Page JavaScript includes Compare action."""
159 await _make_repo(db_session)
160 response = await client.get("/upstream/bass-project/forks")
161 assert response.status_code == 200
162 assert "Compare" in response.text
163
164
165 @pytest.mark.anyio
166 async def test_forks_page_has_contribute_upstream_js(
167 client: AsyncClient,
168 db_session: AsyncSession,
169 ) -> None:
170 """Page JavaScript includes Contribute upstream action."""
171 await _make_repo(db_session)
172 response = await client.get("/upstream/bass-project/forks")
173 assert response.status_code == 200
174 assert "Contribute upstream" in response.text or "contribute" in response.text.lower()
175
176
177 @pytest.mark.anyio
178 async def test_forks_page_base_url_in_html(
179 client: AsyncClient,
180 db_session: AsyncSession,
181 ) -> None:
182 """HTML uses the owner/slug base URL, not raw repo_id UUIDs."""
183 await _make_repo(db_session)
184 response = await client.get("/upstream/bass-project/forks")
185 assert response.status_code == 200
186 assert "/upstream/bass-project" in response.text
187
188
189 # ---------------------------------------------------------------------------
190 # Tests — JSON path
191 # ---------------------------------------------------------------------------
192
193
194 @pytest.mark.anyio
195 async def test_forks_page_json_response(
196 client: AsyncClient,
197 db_session: AsyncSession,
198 ) -> None:
199 """?format=json returns HTTP 200 with application/json content-type."""
200 await _make_repo(db_session)
201 response = await client.get("/upstream/bass-project/forks?format=json")
202 assert response.status_code == 200
203 assert response.headers["content-type"].startswith("application/json")
204
205
206 @pytest.mark.anyio
207 async def test_forks_page_json_has_root_and_total(
208 client: AsyncClient,
209 db_session: AsyncSession,
210 ) -> None:
211 """JSON response contains root node and totalForks counter."""
212 await _make_repo(db_session)
213 response = await client.get("/upstream/bass-project/forks?format=json")
214 assert response.status_code == 200
215 data = response.json()
216 assert "root" in data
217 assert "totalForks" in data
218
219
220 @pytest.mark.anyio
221 async def test_forks_page_json_children_present(
222 client: AsyncClient,
223 db_session: AsyncSession,
224 ) -> None:
225 """A fork repo appears as a child node in the JSON root.children list."""
226 source_id = await _make_repo(db_session)
227 await _make_fork(db_session, source_id, fork_owner="alice", fork_slug="bass-project")
228 response = await client.get("/upstream/bass-project/forks?format=json")
229 assert response.status_code == 200
230 data = response.json()
231 children = data["root"]["children"]
232 assert len(children) == 1
233 assert children[0]["owner"] == "alice"
234 assert children[0]["repoSlug"] == "bass-project"
235
236
237 @pytest.mark.anyio
238 async def test_forks_page_json_divergence_computed(
239 client: AsyncClient,
240 db_session: AsyncSession,
241 ) -> None:
242 """divergenceCommits is a non-negative integer for each fork child."""
243 source_id = await _make_repo(db_session)
244 fork_id = await _make_fork(db_session, source_id, fork_owner="bob", fork_slug="bass-project")
245 # Add a commit to the fork so divergence > 0
246 await _make_commit(db_session, fork_id, sha="fork-commit-001")
247 response = await client.get("/upstream/bass-project/forks?format=json")
248 assert response.status_code == 200
249 data = response.json()
250 children = data["root"]["children"]
251 assert len(children) == 1
252 div = children[0]["divergenceCommits"]
253 assert isinstance(div, int)
254 assert div >= 0
255
256
257 @pytest.mark.anyio
258 async def test_forks_page_unknown_repo_404(
259 client: AsyncClient,
260 db_session: AsyncSession,
261 ) -> None:
262 """Unknown owner/slug returns 404."""
263 response = await client.get("/nobody/nonexistent/forks")
264 assert response.status_code == 404
265
266
267 @pytest.mark.anyio
268 async def test_forks_page_json_empty_repo(
269 client: AsyncClient,
270 db_session: AsyncSession,
271 ) -> None:
272 """A repo with no forks returns totalForks=0 and empty children list."""
273 await _make_repo(db_session)
274 response = await client.get("/upstream/bass-project/forks?format=json")
275 assert response.status_code == 200
276 data = response.json()
277 assert data["totalForks"] == 0
278 assert data["root"]["children"] == []