@@ -113,6 +113,141 @@ def test_rejects_bad_signature(client):
113113 assert response .status_code == 401
114114
115115
116+ def _zip_submission_bytes () -> bytes :
117+ stream = io .BytesIO ()
118+ with zipfile .ZipFile (stream , "w" ) as archive :
119+ archive .writestr ("model.py" , VALID_CODE )
120+ return stream .getvalue ()
121+
122+
123+ def _read_submission_row (client : TestClient , submission_id : str ) -> dict :
124+ import anyio
125+
126+ stored = client .app .state .repository
127+
128+ async def read_code ():
129+ async with stored .database .connect () as conn :
130+ rows = await conn .execute_fetchall (
131+ "SELECT code, filename FROM submissions WHERE id=?" , (submission_id ,)
132+ )
133+ return dict (rows [0 ])
134+
135+ return anyio .run (read_code )
136+
137+
138+ def test_public_submission_accepts_raw_zip (client ):
139+ raw = _zip_submission_bytes ()
140+ response = client .post (
141+ "/v1/submissions" ,
142+ content = raw ,
143+ headers = {
144+ ** signed_headers ("secret" , raw ),
145+ "Content-Type" : "application/zip" ,
146+ "X-Submission-Filename" : "project.zip" ,
147+ },
148+ )
149+ assert response .status_code == 200 , response .text
150+ submission_id = response .json ()["id" ]
151+ row = _read_submission_row (client , submission_id )
152+ assert row ["filename" ] == "project.zip"
153+ # Signature contract: bytes consumed by the handler must equal the bytes
154+ # authenticate_miner signed over (Starlette caches request.body()).
155+ assert base64 .b64decode (row ["code" ]) == raw
156+
157+
158+ def test_public_submission_raw_zip_signature_contract (client ):
159+ raw = _zip_submission_bytes ()
160+ good = client .post (
161+ "/v1/submissions" ,
162+ content = raw ,
163+ headers = {** signed_headers ("secret" , raw ), "Content-Type" : "application/zip" },
164+ )
165+ assert good .status_code == 200 , good .text
166+ row = _read_submission_row (client , good .json ()["id" ])
167+ assert row ["filename" ] == "submission.zip"
168+ assert base64 .b64decode (row ["code" ]) == raw
169+
170+ bad = client .post (
171+ "/v1/submissions" ,
172+ content = raw ,
173+ headers = {
174+ "X-Hotkey" : "hk" ,
175+ "X-Signature" : "deadbeef" ,
176+ "X-Nonce" : "n-bad" ,
177+ "X-Timestamp" : signed_headers ("secret" , raw )["X-Timestamp" ],
178+ "Content-Type" : "application/zip" ,
179+ },
180+ )
181+ assert bad .status_code == 401 , bad .text
182+
183+
184+ def test_public_submission_accepts_raw_python_octet_stream (client ):
185+ raw = VALID_CODE .encode ()
186+ response = client .post (
187+ "/v1/submissions" ,
188+ content = raw ,
189+ headers = {
190+ ** signed_headers ("secret" , raw ),
191+ "Content-Type" : "application/octet-stream" ,
192+ "X-Submission-Filename" : "entry.py" ,
193+ },
194+ )
195+ assert response .status_code == 200 , response .text
196+ row = _read_submission_row (client , response .json ()["id" ])
197+ assert row ["filename" ] == "entry.py"
198+ assert base64 .b64decode (row ["code" ]) == raw
199+
200+
201+ def test_public_submission_json_still_accepted (client ):
202+ payload = {"code" : VALID_CODE , "filename" : "model.py" }
203+ body = json .dumps (payload , separators = ("," , ":" )).encode ()
204+ response = client .post (
205+ "/v1/submissions" ,
206+ content = body ,
207+ headers = {** signed_headers ("secret" , body ), "Content-Type" : "application/json" },
208+ )
209+ assert response .status_code == 200 , response .text
210+ assert response .json ()["hotkey" ] == "hk"
211+
212+
213+ def test_public_submission_rejects_traversal_filename (client ):
214+ raw = _zip_submission_bytes ()
215+ response = client .post (
216+ "/v1/submissions" ,
217+ content = raw ,
218+ headers = {
219+ ** signed_headers ("secret" , raw ),
220+ "Content-Type" : "application/zip" ,
221+ "X-Submission-Filename" : "../escape.py" ,
222+ },
223+ )
224+ assert response .status_code == 422 , response .text
225+ assert response .status_code != 500
226+
227+
228+ def test_public_submission_malformed_json_no_500 (client ):
229+ raw = b"{not valid json"
230+ response = client .post (
231+ "/v1/submissions" ,
232+ content = raw ,
233+ headers = {** signed_headers ("secret" , raw ), "Content-Type" : "application/json" },
234+ )
235+ assert response .status_code == 400 , response .text
236+ assert response .status_code != 500
237+
238+
239+ def test_public_submission_oversized_raw_zip_413 (small_cap_client ):
240+ cap = small_cap_client .app .state .settings .max_code_bytes
241+ raw = b"P" * (cap + 1 )
242+ response = small_cap_client .post (
243+ "/v1/submissions" ,
244+ content = raw ,
245+ headers = {** signed_headers ("secret" , raw ), "Content-Type" : "application/zip" },
246+ )
247+ assert response .status_code == 413 , response .text
248+ assert response .json ()["detail" ] == "submission too large"
249+
250+
116251def test_internal_bridge_accepts_raw_zip_submission (client ):
117252 stream = io .BytesIO ()
118253 with zipfile .ZipFile (stream , "w" ) as archive :
0 commit comments