gabriel / musehub public
test_musehub_api_contracts.py python
337 lines 10.7 KB
Raw
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago
1 """Deep API contract tests for core MuseHub endpoints.
2
3 This file addresses the shallow-assertion gap: many existing tests only
4 assert on status codes. Here we verify complete response bodies, field
5 types, and envelope structure for the most critical endpoints — repos,
6 commits, branches, issues, and explore.
7
8 Uses ``tests.factories`` for clean, declarative data setup.
9 All tests are module-level async functions (not class-based) to ensure
10 pytest-asyncio fixture injection works correctly.
11 """
12 from __future__ import annotations
13
14 import pytest
15 from httpx import AsyncClient
16 from sqlalchemy.ext.asyncio import AsyncSession
17
18 from tests.factories import create_repo, create_branch, create_commit
19 from musehub.muse_contracts.json_types import StrDict
20
21
22 # ---------------------------------------------------------------------------
23 # Repo CRUD response contracts
24 # ---------------------------------------------------------------------------
25
26 @pytest.mark.anyio
27 async def test_create_repo_response_shape(
28 client: AsyncClient,
29 auth_headers: StrDict,
30 db_session: AsyncSession,
31 ) -> None:
32 """POST /repos returns all required fields with correct types."""
33 resp = await client.post(
34 "/api/repos",
35 json={"name": "contract-test", "owner": "tester", "visibility": "public"},
36 headers=auth_headers,
37 )
38 assert resp.status_code == 201
39 body = resp.json()
40
41 for key in ("repoId", "name", "owner", "slug", "visibility", "ownerUserId",
42 "cloneUrl", "createdAt"):
43 assert key in body, f"Missing field: {key}"
44 assert isinstance(body[key], str), f"Field {key} should be a string"
45
46 assert body["name"] == "contract-test"
47 assert body["owner"] == "tester"
48 assert body["visibility"] == "public"
49 assert body["slug"] == "contract-test"
50 assert isinstance(body["tags"], list)
51
52
53 @pytest.mark.anyio
54 async def test_get_repo_response_shape(
55 client: AsyncClient,
56 auth_headers: StrDict,
57 db_session: AsyncSession,
58 ) -> None:
59 """GET /repos/{id} returns all expected fields."""
60 create = await client.post(
61 "/api/repos",
62 json={"name": "get-shape-test", "owner": "tester"},
63 headers=auth_headers,
64 )
65 repo_id = create.json()["repoId"]
66
67 resp = await client.get(f"/api/repos/{repo_id}", headers=auth_headers)
68 assert resp.status_code == 200
69 body = resp.json()
70
71 assert body["repoId"] == repo_id
72 assert body["name"] == "get-shape-test"
73 assert isinstance(body["tags"], list)
74 assert isinstance(body["createdAt"], str)
75
76
77 @pytest.mark.anyio
78 async def test_update_repo_settings_returns_updated_fields(
79 client: AsyncClient,
80 auth_headers: StrDict,
81 db_session: AsyncSession,
82 ) -> None:
83 """PATCH /repos/{id}/settings returns the updated repo fields."""
84 create = await client.post(
85 "/api/repos",
86 json={"name": "patch-test-repo", "owner": "testuser"},
87 headers=auth_headers,
88 )
89 repo_id = create.json()["repoId"]
90
91 patch_resp = await client.patch(
92 f"/api/repos/{repo_id}/settings",
93 json={"description": "Updated description", "visibility": "public"},
94 headers=auth_headers,
95 )
96 assert patch_resp.status_code == 200
97 body = patch_resp.json()
98 assert body["description"] == "Updated description"
99 assert body["visibility"] == "public"
100 assert "name" in body # RepoSettingsResponse fields
101
102
103 # ---------------------------------------------------------------------------
104 # Branch response contracts
105 # ---------------------------------------------------------------------------
106
107 @pytest.mark.anyio
108 async def test_list_branches_envelope(
109 client: AsyncClient,
110 auth_headers: StrDict,
111 db_session: AsyncSession,
112 ) -> None:
113 """GET /repos/{id}/branches returns a 'branches' list with name fields."""
114 repo = await create_repo(db_session, owner="brancher", slug="branch-contract")
115 await create_branch(db_session, repo_id=str(repo.repo_id), name="main")
116 await create_branch(db_session, repo_id=str(repo.repo_id), name="feature-x")
117
118 resp = await client.get(
119 f"/api/repos/{repo.repo_id}/branches",
120 headers=auth_headers,
121 )
122 assert resp.status_code == 200
123 body = resp.json()
124
125 assert "branches" in body
126 assert isinstance(body["branches"], list)
127 assert len(body["branches"]) == 2
128 for branch in body["branches"]:
129 assert "name" in branch
130 assert isinstance(branch["name"], str)
131
132
133 @pytest.mark.anyio
134 async def test_branch_names_are_correct(
135 client: AsyncClient,
136 auth_headers: StrDict,
137 db_session: AsyncSession,
138 ) -> None:
139 """Branch names returned by the API match what was inserted."""
140 repo = await create_repo(db_session, owner="brancher2", slug="branch-names")
141 await create_branch(db_session, repo_id=str(repo.repo_id), name="develop")
142
143 resp = await client.get(
144 f"/api/repos/{repo.repo_id}/branches",
145 headers=auth_headers,
146 )
147 names = [b["name"] for b in resp.json()["branches"]]
148 assert "develop" in names
149
150
151 # ---------------------------------------------------------------------------
152 # Commit response contracts
153 # ---------------------------------------------------------------------------
154
155 @pytest.mark.anyio
156 async def test_list_commits_envelope(
157 client: AsyncClient,
158 auth_headers: StrDict,
159 db_session: AsyncSession,
160 ) -> None:
161 """GET /repos/{id}/commits returns a 'commits' list envelope."""
162 repo = await create_repo(db_session, owner="committer", slug="commit-contract")
163 await create_commit(db_session, str(repo.repo_id), message="init: first commit")
164 await create_commit(db_session, str(repo.repo_id), message="feat: second commit")
165
166 resp = await client.get(
167 f"/api/repos/{repo.repo_id}/commits",
168 headers=auth_headers,
169 )
170 assert resp.status_code == 200
171 body = resp.json()
172
173 assert "commits" in body
174 assert isinstance(body["commits"], list)
175 assert len(body["commits"]) == 2
176
177
178 @pytest.mark.anyio
179 async def test_commit_fields(
180 client: AsyncClient,
181 auth_headers: StrDict,
182 db_session: AsyncSession,
183 ) -> None:
184 """Each commit object has message, author, branch, commitId, timestamp, parentIds."""
185 repo = await create_repo(db_session, owner="committer2", slug="commit-fields")
186 await create_commit(
187 db_session, str(repo.repo_id),
188 message="feat: piano track",
189 author="mozart",
190 branch="main",
191 )
192
193 resp = await client.get(
194 f"/api/repos/{repo.repo_id}/commits",
195 headers=auth_headers,
196 )
197 commits = resp.json()["commits"]
198 assert len(commits) == 1
199 c = commits[0]
200
201 assert c["message"] == "feat: piano track"
202 assert c["author"] == "mozart"
203 assert c["branch"] == "main"
204 assert "commitId" in c
205 assert "timestamp" in c
206 assert isinstance(c["parentIds"], list)
207
208
209 # ---------------------------------------------------------------------------
210 # Issue response contracts
211 # ---------------------------------------------------------------------------
212
213 @pytest.mark.anyio
214 async def test_create_issue_response_shape(
215 client: AsyncClient,
216 auth_headers: StrDict,
217 db_session: AsyncSession,
218 ) -> None:
219 """POST /repos/{id}/issues returns title, body, status, number, createdAt."""
220 create_repo_resp = await client.post(
221 "/api/repos",
222 json={"name": "issue-contract-repo", "owner": "testuser"},
223 headers=auth_headers,
224 )
225 repo_id = create_repo_resp.json()["repoId"]
226
227 resp = await client.post(
228 f"/api/repos/{repo_id}/issues",
229 json={"title": "Bug: tempo drift", "body": "The tempo drifts by 3 BPM"},
230 headers=auth_headers,
231 )
232 assert resp.status_code == 201
233 body = resp.json()
234
235 assert body["title"] == "Bug: tempo drift"
236 assert body["body"] == "The tempo drifts by 3 BPM"
237 assert body["state"] == "open"
238 assert "number" in body
239 assert isinstance(body["number"], int)
240 assert "createdAt" in body
241
242
243 @pytest.mark.anyio
244 async def test_list_issues_returns_open_issues(
245 client: AsyncClient,
246 auth_headers: StrDict,
247 db_session: AsyncSession,
248 ) -> None:
249 """GET /repos/{id}/issues returns issues envelope with status=open."""
250 create_repo_resp = await client.post(
251 "/api/repos",
252 json={"name": "issue-list-contract", "owner": "testuser"},
253 headers=auth_headers,
254 )
255 repo_id = create_repo_resp.json()["repoId"]
256
257 for i in range(3):
258 await client.post(
259 f"/api/repos/{repo_id}/issues",
260 json={"title": f"Issue {i}"},
261 headers=auth_headers,
262 )
263
264 resp = await client.get(
265 f"/api/repos/{repo_id}/issues",
266 headers=auth_headers,
267 )
268 assert resp.status_code == 200
269 body = resp.json()
270
271 assert "issues" in body
272 assert len(body["issues"]) == 3
273 for issue in body["issues"]:
274 assert issue["state"] == "open"
275 assert "title" in issue
276 assert "number" in issue
277
278
279 @pytest.mark.anyio
280 async def test_close_issue_changes_status(
281 client: AsyncClient,
282 auth_headers: StrDict,
283 db_session: AsyncSession,
284 ) -> None:
285 """POST /repos/{id}/issues/{n}/close sets status to 'closed'."""
286 create_repo_resp = await client.post(
287 "/api/repos",
288 json={"name": "close-issue-contract", "owner": "testuser"},
289 headers=auth_headers,
290 )
291 repo_id = create_repo_resp.json()["repoId"]
292
293 issue_resp = await client.post(
294 f"/api/repos/{repo_id}/issues",
295 json={"title": "Close me"},
296 headers=auth_headers,
297 )
298 number = issue_resp.json()["number"]
299
300 close_resp = await client.post(
301 f"/api/repos/{repo_id}/issues/{number}/close",
302 headers=auth_headers,
303 )
304 assert close_resp.status_code == 200
305 assert close_resp.json()["state"] == "closed"
306
307
308 # ---------------------------------------------------------------------------
309 # Explore / discover
310 # ---------------------------------------------------------------------------
311
312 @pytest.mark.anyio
313 async def test_explore_returns_public_repos(
314 client: AsyncClient,
315 auth_headers: StrDict,
316 db_session: AsyncSession,
317 ) -> None:
318 """GET /repos/explore returns public repos and excludes private ones."""
319 await client.post(
320 "/api/repos",
321 json={"name": "explore-public", "owner": "explorer", "visibility": "public"},
322 headers=auth_headers,
323 )
324 await client.post(
325 "/api/repos",
326 json={"name": "explore-private", "owner": "explorer", "visibility": "private"},
327 headers=auth_headers,
328 )
329
330 resp = await client.get("/api/discover/repos")
331 assert resp.status_code == 200
332 body = resp.json()
333 repos = body if isinstance(body, list) else body.get("repos", body.get("items", []))
334
335 slugs = [r.get("slug", "") for r in repos]
336 assert "explore-public" in slugs
337 assert "explore-private" not in slugs
File History 1 commit
sha256:a10adeeb7a0169cb9900f9806ed7a973047258abb6283724fe55e8eb68ff3f0a init: musehub initial commit Human 171 days ago