gabriel / musehub public
test_musehub_issues_input_limits.py python
331 lines 12.4 KB
Raw
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ breaking 156 days ago
1 """Tests for input size limits on issue and comment write endpoints.
2
3 Verifies that Pydantic validation enforces the goldilocks limits designed for
4 agent-swarm use while preventing abuse:
5
6 body: max 50,000 chars (bumped from 10k for agent-written issues)
7 title: max 500 chars
8 labels: max 20 items, each max 100 chars
9 symbol_anchors: max 50 items, each max 500 chars
10 commit_anchors: max 50 items, each max 64 chars
11 comment body: max 50,000 chars
12
13 All limits are enforced at the Pydantic layer (422 before the DB is touched).
14 """
15 from __future__ import annotations
16
17 import pytest
18 from httpx import AsyncClient
19
20 from musehub.types.json_types import StrDict
21
22
23 # ---------------------------------------------------------------------------
24 # Helpers
25 # ---------------------------------------------------------------------------
26
27
28 async def _create_repo(client: AsyncClient, headers: StrDict, name: str) -> str:
29 r = await client.post("/api/repos", json={"name": name, "owner": "testuser"}, headers=headers)
30 assert r.status_code == 201
31 return r.json()["repoId"]
32
33
34 async def _create_issue(client: AsyncClient, headers: StrDict, repo_id: str, **kwargs: str | int | bool | None) -> int:
35 payload = {"title": "baseline", "body": "", **kwargs}
36 r = await client.post(f"/api/repos/{repo_id}/issues", json=payload, headers=headers)
37 assert r.status_code == 201
38 return r.json()["number"]
39
40
41 # ---------------------------------------------------------------------------
42 # IssueCreate — body
43 # ---------------------------------------------------------------------------
44
45
46 async def test_issue_body_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
47 repo_id = await _create_repo(client, auth_headers, "il-body-ok")
48 r = await client.post(
49 f"/api/repos/{repo_id}/issues",
50 json={"title": "t", "body": "x" * 50_000},
51 headers=auth_headers,
52 )
53 assert r.status_code == 201
54
55
56 async def test_issue_body_over_limit_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
57 repo_id = await _create_repo(client, auth_headers, "il-body-over")
58 r = await client.post(
59 f"/api/repos/{repo_id}/issues",
60 json={"title": "t", "body": "x" * 50_001},
61 headers=auth_headers,
62 )
63 assert r.status_code == 422
64
65
66 # ---------------------------------------------------------------------------
67 # IssueCreate — title
68 # ---------------------------------------------------------------------------
69
70
71 async def test_issue_title_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
72 repo_id = await _create_repo(client, auth_headers, "il-title-ok")
73 r = await client.post(
74 f"/api/repos/{repo_id}/issues",
75 json={"title": "t" * 500},
76 headers=auth_headers,
77 )
78 assert r.status_code == 201
79
80
81 async def test_issue_title_over_limit_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
82 repo_id = await _create_repo(client, auth_headers, "il-title-over")
83 r = await client.post(
84 f"/api/repos/{repo_id}/issues",
85 json={"title": "t" * 501},
86 headers=auth_headers,
87 )
88 assert r.status_code == 422
89
90
91 # ---------------------------------------------------------------------------
92 # IssueCreate — labels
93 # ---------------------------------------------------------------------------
94
95
96 async def test_issue_labels_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
97 repo_id = await _create_repo(client, auth_headers, "il-labels-ok")
98 r = await client.post(
99 f"/api/repos/{repo_id}/issues",
100 json={"title": "t", "labels": [f"label-{i}" for i in range(20)]},
101 headers=auth_headers,
102 )
103 assert r.status_code == 201
104
105
106 async def test_issue_labels_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
107 repo_id = await _create_repo(client, auth_headers, "il-labels-over")
108 r = await client.post(
109 f"/api/repos/{repo_id}/issues",
110 json={"title": "t", "labels": [f"label-{i}" for i in range(21)]},
111 headers=auth_headers,
112 )
113 assert r.status_code == 422
114
115
116 async def test_issue_label_item_too_long_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
117 repo_id = await _create_repo(client, auth_headers, "il-label-item")
118 r = await client.post(
119 f"/api/repos/{repo_id}/issues",
120 json={"title": "t", "labels": ["x" * 101]},
121 headers=auth_headers,
122 )
123 assert r.status_code == 422
124
125
126 async def test_issue_label_item_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
127 repo_id = await _create_repo(client, auth_headers, "il-label-item-ok")
128 r = await client.post(
129 f"/api/repos/{repo_id}/issues",
130 json={"title": "t", "labels": ["x" * 100]},
131 headers=auth_headers,
132 )
133 assert r.status_code == 201
134
135
136 # ---------------------------------------------------------------------------
137 # IssueCreate — symbol_anchors
138 # ---------------------------------------------------------------------------
139
140
141 async def test_issue_symbol_anchors_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
142 repo_id = await _create_repo(client, auth_headers, "il-sym-ok")
143 anchors = [f"path/to/file.py::Symbol{i}" for i in range(50)]
144 r = await client.post(
145 f"/api/repos/{repo_id}/issues",
146 json={"title": "t", "symbolAnchors": anchors},
147 headers=auth_headers,
148 )
149 assert r.status_code == 201
150
151
152 async def test_issue_symbol_anchors_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
153 repo_id = await _create_repo(client, auth_headers, "il-sym-over")
154 anchors = [f"path/to/file.py::Symbol{i}" for i in range(51)]
155 r = await client.post(
156 f"/api/repos/{repo_id}/issues",
157 json={"title": "t", "symbolAnchors": anchors},
158 headers=auth_headers,
159 )
160 assert r.status_code == 422
161
162
163 async def test_issue_symbol_anchor_item_too_long_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
164 repo_id = await _create_repo(client, auth_headers, "il-sym-item")
165 r = await client.post(
166 f"/api/repos/{repo_id}/issues",
167 json={"title": "t", "symbolAnchors": ["x" * 501]},
168 headers=auth_headers,
169 )
170 assert r.status_code == 422
171
172
173 # ---------------------------------------------------------------------------
174 # IssueCreate — commit_anchors
175 # ---------------------------------------------------------------------------
176
177
178 async def test_issue_commit_anchors_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
179 repo_id = await _create_repo(client, auth_headers, "il-commit-ok")
180 import uuid
181 anchors = [(uuid.uuid4().hex + uuid.uuid4().hex)[:64] for _ in range(50)]
182 r = await client.post(
183 f"/api/repos/{repo_id}/issues",
184 json={"title": "t", "commitAnchors": anchors},
185 headers=auth_headers,
186 )
187 assert r.status_code == 201
188
189
190 async def test_issue_commit_anchors_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
191 repo_id = await _create_repo(client, auth_headers, "il-commit-over")
192 import uuid
193 anchors = [(uuid.uuid4().hex + uuid.uuid4().hex)[:64] for _ in range(51)]
194 r = await client.post(
195 f"/api/repos/{repo_id}/issues",
196 json={"title": "t", "commitAnchors": anchors},
197 headers=auth_headers,
198 )
199 assert r.status_code == 422
200
201
202 async def test_issue_commit_anchor_item_too_long_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
203 repo_id = await _create_repo(client, auth_headers, "il-commit-item")
204 r = await client.post(
205 f"/api/repos/{repo_id}/issues",
206 json={"title": "t", "commitAnchors": ["a" * 65]},
207 headers=auth_headers,
208 )
209 assert r.status_code == 422
210
211
212 # ---------------------------------------------------------------------------
213 # IssueCommentCreate — body
214 # ---------------------------------------------------------------------------
215
216
217 async def test_comment_body_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
218 repo_id = await _create_repo(client, auth_headers, "il-cmt-ok")
219 number = await _create_issue(client, auth_headers, repo_id)
220 r = await client.post(
221 f"/api/repos/{repo_id}/issues/{number}/comments",
222 json={"body": "x" * 50_000},
223 headers=auth_headers,
224 )
225 assert r.status_code == 201
226
227
228 async def test_create_comment_returns_single_resource(client: AsyncClient, auth_headers: StrDict) -> None:
229 """POST .../comments must return the created comment as a flat single resource, not a list."""
230 repo_id = await _create_repo(client, auth_headers, "il-cmt-shape")
231 number = await _create_issue(client, auth_headers, repo_id)
232 r = await client.post(
233 f"/api/repos/{repo_id}/issues/{number}/comments",
234 json={"body": "hello world"},
235 headers=auth_headers,
236 )
237 assert r.status_code == 201
238 data = r.json()
239 # Must be a flat resource — not a list envelope.
240 assert "commentId" in data, f"expected 'commentId' key, got: {list(data.keys())}"
241 assert "comments" not in data, "response must not wrap in list envelope"
242 assert data["body"] == "hello world"
243 assert "author" in data
244 assert "createdAt" in data
245
246
247 async def test_comment_body_over_limit_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
248 repo_id = await _create_repo(client, auth_headers, "il-cmt-over")
249 number = await _create_issue(client, auth_headers, repo_id)
250 r = await client.post(
251 f"/api/repos/{repo_id}/issues/{number}/comments",
252 json={"body": "x" * 50_001},
253 headers=auth_headers,
254 )
255 assert r.status_code == 422
256
257
258 async def test_comment_body_empty_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
259 repo_id = await _create_repo(client, auth_headers, "il-cmt-empty")
260 number = await _create_issue(client, auth_headers, repo_id)
261 r = await client.post(
262 f"/api/repos/{repo_id}/issues/{number}/comments",
263 json={"body": ""},
264 headers=auth_headers,
265 )
266 assert r.status_code == 422
267
268
269 # ---------------------------------------------------------------------------
270 # IssueLabelAssignRequest
271 # ---------------------------------------------------------------------------
272
273
274 async def test_label_assign_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
275 repo_id = await _create_repo(client, auth_headers, "il-lbl-assign")
276 number = await _create_issue(client, auth_headers, repo_id)
277 r = await client.post(
278 f"/api/repos/{repo_id}/issues/{number}/labels",
279 json={"labels": [f"lbl-{i}" for i in range(21)]},
280 headers=auth_headers,
281 )
282 assert r.status_code == 422
283
284
285 async def test_label_assign_item_too_long_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
286 repo_id = await _create_repo(client, auth_headers, "il-lbl-item")
287 number = await _create_issue(client, auth_headers, repo_id)
288 r = await client.post(
289 f"/api/repos/{repo_id}/issues/{number}/labels",
290 json={"labels": ["x" * 101]},
291 headers=auth_headers,
292 )
293 assert r.status_code == 422
294
295
296 # ---------------------------------------------------------------------------
297 # IssueUpdate — same limits apply on PATCH
298 # ---------------------------------------------------------------------------
299
300
301 async def test_update_body_over_limit_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
302 repo_id = await _create_repo(client, auth_headers, "il-upd-body")
303 number = await _create_issue(client, auth_headers, repo_id)
304 r = await client.patch(
305 f"/api/repos/{repo_id}/issues/{number}",
306 json={"body": "x" * 50_001},
307 headers=auth_headers,
308 )
309 assert r.status_code == 422
310
311
312 async def test_update_labels_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
313 repo_id = await _create_repo(client, auth_headers, "il-upd-labels")
314 number = await _create_issue(client, auth_headers, repo_id)
315 r = await client.patch(
316 f"/api/repos/{repo_id}/issues/{number}",
317 json={"labels": [f"l{i}" for i in range(21)]},
318 headers=auth_headers,
319 )
320 assert r.status_code == 422
321
322
323 async def test_update_symbol_anchors_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
324 repo_id = await _create_repo(client, auth_headers, "il-upd-sym")
325 number = await _create_issue(client, auth_headers, repo_id)
326 r = await client.patch(
327 f"/api/repos/{repo_id}/issues/{number}",
328 json={"symbolAnchors": [f"f.py::S{i}" for i in range(51)]},
329 headers=auth_headers,
330 )
331 assert r.status_code == 422
File History 1 commit
sha256:9590cee1e0ccd6c76528f005b95d634d80f5019f0dcb7c371e149adc31d1fb65 refactor: enforce gRPC framing on all MWP wire traffic Sonnet 4.6 minor ⚠ 156 days ago