gabriel / musehub public
test_musehub_issues_input_limits.py python
349 lines 13.2 KB
Raw
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor ⚠ breaking 142 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 71 chars (sha256:<64-hex> canonical form)
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_anchor_canonical_prefix_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
179 """sha256:<64-hex> is the canonical commit ID format (71 chars) and must be accepted."""
180 repo_id = await _create_repo(client, auth_headers, "il-commit-canonical")
181 import uuid
182 hex64 = (uuid.uuid4().hex + uuid.uuid4().hex)[:64]
183 canonical = f"sha256:{hex64}"
184 assert len(canonical) == 71
185 r = await client.post(
186 f"/api/repos/{repo_id}/issues",
187 json={"title": "t", "commitAnchors": [canonical]},
188 headers=auth_headers,
189 )
190 assert r.status_code == 201, (
191 f"Canonical sha256:<64-hex> commit anchor (71 chars) was rejected with {r.status_code}: {r.text}. "
192 "The max_length for commit anchors must be 71 to fit the 'sha256:' prefix."
193 )
194
195
196 async def test_issue_commit_anchors_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
197 repo_id = await _create_repo(client, auth_headers, "il-commit-ok")
198 import uuid
199 anchors = [(uuid.uuid4().hex + uuid.uuid4().hex)[:64] for _ in range(50)]
200 r = await client.post(
201 f"/api/repos/{repo_id}/issues",
202 json={"title": "t", "commitAnchors": anchors},
203 headers=auth_headers,
204 )
205 assert r.status_code == 201
206
207
208 async def test_issue_commit_anchors_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
209 repo_id = await _create_repo(client, auth_headers, "il-commit-over")
210 import uuid
211 anchors = [(uuid.uuid4().hex + uuid.uuid4().hex)[:64] for _ in range(51)]
212 r = await client.post(
213 f"/api/repos/{repo_id}/issues",
214 json={"title": "t", "commitAnchors": anchors},
215 headers=auth_headers,
216 )
217 assert r.status_code == 422
218
219
220 async def test_issue_commit_anchor_item_too_long_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
221 repo_id = await _create_repo(client, auth_headers, "il-commit-item")
222 r = await client.post(
223 f"/api/repos/{repo_id}/issues",
224 json={"title": "t", "commitAnchors": ["a" * 72]},
225 headers=auth_headers,
226 )
227 assert r.status_code == 422
228
229
230 # ---------------------------------------------------------------------------
231 # IssueCommentCreate — body
232 # ---------------------------------------------------------------------------
233
234
235 async def test_comment_body_at_limit_accepted(client: AsyncClient, auth_headers: StrDict) -> None:
236 repo_id = await _create_repo(client, auth_headers, "il-cmt-ok")
237 number = await _create_issue(client, auth_headers, repo_id)
238 r = await client.post(
239 f"/api/repos/{repo_id}/issues/{number}/comments",
240 json={"body": "x" * 50_000},
241 headers=auth_headers,
242 )
243 assert r.status_code == 201
244
245
246 async def test_create_comment_returns_single_resource(client: AsyncClient, auth_headers: StrDict) -> None:
247 """POST .../comments must return the created comment as a flat single resource, not a list."""
248 repo_id = await _create_repo(client, auth_headers, "il-cmt-shape")
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": "hello world"},
253 headers=auth_headers,
254 )
255 assert r.status_code == 201
256 data = r.json()
257 # Must be a flat resource — not a list envelope.
258 assert "commentId" in data, f"expected 'commentId' key, got: {list(data.keys())}"
259 assert "comments" not in data, "response must not wrap in list envelope"
260 assert data["body"] == "hello world"
261 assert "author" in data
262 assert "createdAt" in data
263
264
265 async def test_comment_body_over_limit_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
266 repo_id = await _create_repo(client, auth_headers, "il-cmt-over")
267 number = await _create_issue(client, auth_headers, repo_id)
268 r = await client.post(
269 f"/api/repos/{repo_id}/issues/{number}/comments",
270 json={"body": "x" * 50_001},
271 headers=auth_headers,
272 )
273 assert r.status_code == 422
274
275
276 async def test_comment_body_empty_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
277 repo_id = await _create_repo(client, auth_headers, "il-cmt-empty")
278 number = await _create_issue(client, auth_headers, repo_id)
279 r = await client.post(
280 f"/api/repos/{repo_id}/issues/{number}/comments",
281 json={"body": ""},
282 headers=auth_headers,
283 )
284 assert r.status_code == 422
285
286
287 # ---------------------------------------------------------------------------
288 # IssueLabelAssignRequest
289 # ---------------------------------------------------------------------------
290
291
292 async def test_label_assign_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
293 repo_id = await _create_repo(client, auth_headers, "il-lbl-assign")
294 number = await _create_issue(client, auth_headers, repo_id)
295 r = await client.post(
296 f"/api/repos/{repo_id}/issues/{number}/labels",
297 json={"labels": [f"lbl-{i}" for i in range(21)]},
298 headers=auth_headers,
299 )
300 assert r.status_code == 422
301
302
303 async def test_label_assign_item_too_long_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
304 repo_id = await _create_repo(client, auth_headers, "il-lbl-item")
305 number = await _create_issue(client, auth_headers, repo_id)
306 r = await client.post(
307 f"/api/repos/{repo_id}/issues/{number}/labels",
308 json={"labels": ["x" * 101]},
309 headers=auth_headers,
310 )
311 assert r.status_code == 422
312
313
314 # ---------------------------------------------------------------------------
315 # IssueUpdate — same limits apply on PATCH
316 # ---------------------------------------------------------------------------
317
318
319 async def test_update_body_over_limit_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
320 repo_id = await _create_repo(client, auth_headers, "il-upd-body")
321 number = await _create_issue(client, auth_headers, repo_id)
322 r = await client.patch(
323 f"/api/repos/{repo_id}/issues/{number}",
324 json={"body": "x" * 50_001},
325 headers=auth_headers,
326 )
327 assert r.status_code == 422
328
329
330 async def test_update_labels_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
331 repo_id = await _create_repo(client, auth_headers, "il-upd-labels")
332 number = await _create_issue(client, auth_headers, repo_id)
333 r = await client.patch(
334 f"/api/repos/{repo_id}/issues/{number}",
335 json={"labels": [f"l{i}" for i in range(21)]},
336 headers=auth_headers,
337 )
338 assert r.status_code == 422
339
340
341 async def test_update_symbol_anchors_too_many_rejected(client: AsyncClient, auth_headers: StrDict) -> None:
342 repo_id = await _create_repo(client, auth_headers, "il-upd-sym")
343 number = await _create_issue(client, auth_headers, repo_id)
344 r = await client.patch(
345 f"/api/repos/{repo_id}/issues/{number}",
346 json={"symbolAnchors": [f"f.py::S{i}" for i in range(51)]},
347 headers=auth_headers,
348 )
349 assert r.status_code == 422
File History 1 commit
sha256:bb2baaabdd19320bde50cb69d447fd1c1e571467df729be1a23b5e93064b046a feat(intel): standardize headers, gauge icon, velocity card… Sonnet 4.6 minor 142 days ago