picpocket / test /test_upload_form_limits.py
chawin.chen
fix: 放宽评分接口图片表单字段限制
d590bfb
Raw History Blame Contribute Delete
5.29 kB
"""使用内存 TestClient 验证表单解析,不启动服务器、不导入模型/数据库。"""
import json
import unittest
from unittest.mock import patch
from fastapi import APIRouter, FastAPI, File, Form, HTTPException, Query, UploadFile
from fastapi.testclient import TestClient
from upload_routes import FaceAnalysisFormRoute
class UploadFormLimitsTest(unittest.TestCase):
def setUp(self) -> None:
self.received_files = []
app = FastAPI()
router = APIRouter(prefix="/facescore", route_class=FaceAnalysisFormRoute)
@router.post("/analyze")
@router.post("/app/analyze")
async def analyze(
images: str = Form(None), nickname: str = Form(None),
file: UploadFile = File(None), files: list[UploadFile] = File(None),
model: str = Query("hybrid"), fail: bool = Query(False),
) -> dict:
uploads = ([file] if file is not None else []) + (files or [])
self.received_files.extend(uploads)
if fail:
raise HTTPException(status_code=400, detail="business error")
return {
"images": images, "nickname": nickname, "model": model,
"uploads": [{"name": item.filename, "size": len(await item.read())} for item in uploads],
}
@router.post("/other")
async def other(images: str = Form(...)) -> dict:
return {"size": len(images)}
app.include_router(router)
self.client = TestClient(app)
self.large_images = json.dumps(["A" * (2 * 1024 * 1024)])
def test_two_analysis_routes_accept_large_multipart_base64(self) -> None:
for path in ("/facescore/analyze", "/facescore/app/analyze"):
with self.subTest(path=path):
response = self.client.post(path, params={"model": "deepface"}, files={
"images": (None, self.large_images), "nickname": (None, "测试昵称"),
})
self.assertEqual(200, response.status_code, response.text)
self.assertEqual(self.large_images, response.json()["images"])
self.assertEqual("测试昵称", response.json()["nickname"])
self.assertEqual("deepface", response.json()["model"])
def test_urlencoded_base64_is_preserved(self) -> None:
response = self.client.post("/facescore/analyze", data={"images": self.large_images})
self.assertEqual(200, response.status_code, response.text)
self.assertEqual(self.large_images, response.json()["images"])
def test_other_routes_retain_default_limit(self) -> None:
response = self.client.post("/facescore/other", files={"images": (None, self.large_images)})
self.assertEqual(400, response.status_code)
self.assertIn("maximum size", response.json()["detail"])
def test_configured_limit_rejects_larger_fields_with_413(self) -> None:
with patch("upload_routes.FACE_SCORE_MAX_FORM_FIELD_MB", 1):
for multipart in (True, False):
with self.subTest(multipart=multipart):
kwargs = {"files": {"images": (None, self.large_images)}} if multipart else {
"data": {"images": self.large_images}}
response = self.client.post("/facescore/analyze", **kwargs)
self.assertEqual(413, response.status_code, response.text)
self.assertIn("1MB", response.json()["detail"])
def test_field_at_limit_is_accepted(self) -> None:
with patch("upload_routes.FACE_SCORE_MAX_FORM_FIELD_MB", 1):
response = self.client.post("/facescore/analyze", files={"images": (None, "A" * (1024 * 1024))})
self.assertEqual(200, response.status_code, response.text)
def test_file_uploads_still_work_and_are_closed(self) -> None:
for field, count in (("file", 1), ("files", 2)):
with self.subTest(field=field), patch("upload_routes.FACE_SCORE_MAX_FORM_FIELD_MB", 1):
response = self.client.post("/facescore/analyze", files=[
(field, (f"photo{i}.jpg", b"x" * (2 * 1024 * 1024), "image/jpeg")) for i in range(count)
])
self.assertEqual(200, response.status_code, response.text)
self.assertEqual(count, len(response.json()["uploads"]))
self.assertTrue(all(item["size"] == 2 * 1024 * 1024 for item in response.json()["uploads"]))
self.assertTrue(all(item.file.closed for item in self.received_files))
def test_business_error_is_preserved_and_upload_closed(self) -> None:
response = self.client.post("/facescore/analyze?fail=true", files={"file": ("photo.jpg", b"image")})
self.assertEqual(400, response.status_code)
self.assertEqual("business error", response.json()["detail"])
self.assertTrue(self.received_files[0].file.closed)
def test_invalid_multipart_is_still_rejected(self) -> None:
response = self.client.post("/facescore/analyze", content=b"invalid", headers={"content-type": "multipart/form-data"})
self.assertEqual(400, response.status_code)
self.assertIn("boundary", response.json()["detail"])
if __name__ == "__main__":
unittest.main()