refactor(web): dep-inject payloads with @model_validate (#41501)

This commit is contained in:
Eddy ZHANG authored and GitHub committed 2026-08-31 00:50:12 +00:00
1 parent bf64390d7c
commit 8894b09c3b
6 files changed
+27 -40

No files matched your search

+3 -3
View File
@@ -6,6 +6,7 @@ from werkzeug.exceptions import InternalServerError
import services
from controllers.common.controller_schemas import TextToAudioPayload as TextToAudioPayloadBase
from controllers.console.wraps import model_validate
from controllers.web import web_ns
from controllers.web.error import (
AppUnavailableError,
@@ -131,11 +132,10 @@ class TextApi(WebApiResource):
)
# response-contract:ignore provider audio bytes; TODO: model binary audio response if shape is standardized.
@web_ns.response(200, "Success")
def post(self, app_model: App, end_user: EndUser):
@model_validate(TextToAudioPayload)
def post(self, payload: TextToAudioPayload, app_model: App, end_user: EndUser):
"""Convert text to audio"""
try:
payload = TextToAudioPayload.model_validate(web_ns.payload or {})
message_id = payload.message_id
text = payload.text
voice = payload.voice
+3 -3
View File
@@ -8,6 +8,7 @@ from werkzeug.exceptions import NotFound
from controllers.common.controller_schemas import ConversationRenamePayload
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console.wraps import model_validate
from controllers.web import web_ns
from controllers.web.error import NotChatAppError
from controllers.web.wraps import WebApiResource
@@ -153,15 +154,14 @@ class ConversationRenameApi(WebApiResource):
)
@web_ns.response(200, "Conversation renamed successfully", web_ns.models[SimpleConversation.__name__])
@web_ns.expect(web_ns.models[ConversationRenamePayload.__name__])
def post(self, app_model: App, end_user: EndUser, c_id: UUID):
@model_validate(ConversationRenamePayload)
def post(self, payload: ConversationRenamePayload, app_model: App, end_user: EndUser, c_id: UUID):
app_mode = AppMode.value_of(app_model.mode)
if app_mode not in {AppMode.CHAT, AppMode.AGENT_CHAT, AppMode.ADVANCED_CHAT, AppMode.AGENT}:
raise NotChatAppError()
conversation_id = str(c_id)
payload = ConversationRenamePayload.model_validate(web_ns.payload or {})
try:
session = db.session()
conversation = ConversationService.rename(
+3 -2
View File
@@ -9,6 +9,7 @@ from controllers.common.errors import (
RemoteFileUploadError,
UnsupportedFileTypeError,
)
from controllers.console.wraps import model_validate
from core.file import remote_fetcher
from extensions.ext_database import db
from fields.file_fields import FileWithSignedUrl, RemoteFileInfo
@@ -86,7 +87,8 @@ class RemoteFileUploadApi(WebApiResource):
)
@web_ns.response(201, "Remote file uploaded", web_ns.models[FileWithSignedUrl.__name__])
@web_ns.expect(web_ns.models[RemoteFileUploadPayload.__name__])
def post(self, app_model: App, end_user: EndUser):
@model_validate(RemoteFileUploadPayload)
def post(self, payload: RemoteFileUploadPayload, app_model: App, end_user: EndUser):
"""Upload a file from a remote URL.
Downloads a file from the provided remote URL and uploads it
@@ -108,7 +110,6 @@ class RemoteFileUploadApi(WebApiResource):
FileTooLargeError: File exceeds size limit
UnsupportedFileTypeError: File type not supported
"""
payload = RemoteFileUploadPayload.model_validate(web_ns.payload or {})
url = str(payload.url)
try:
@@ -102,10 +102,8 @@ class TestConversationRenameApi:
ConversationRenameApi().post(_completion_app(), _end_user(), uuid4())
@patch("controllers.web.conversation.ConversationService.rename")
@patch("controllers.web.conversation.web_ns")
def test_rename_success(self, mock_ns: MagicMock, mock_rename: MagicMock, app: Flask) -> None:
def test_rename_success(self, mock_rename: MagicMock, app: Flask) -> None:
c_id = uuid4()
mock_ns.payload = {"name": "New Name", "auto_generate": False}
conv = SimpleNamespace(
id=str(c_id),
name="New Name",
@@ -126,10 +124,8 @@ class TestConversationRenameApi:
"controllers.web.conversation.ConversationService.rename",
side_effect=ConversationNotExistsError(),
)
@patch("controllers.web.conversation.web_ns")
def test_rename_not_found(self, mock_ns: MagicMock, mock_rename: MagicMock, app: Flask) -> None:
def test_rename_not_found(self, mock_rename: MagicMock, app: Flask) -> None:
c_id = uuid4()
mock_ns.payload = {"name": "X", "auto_generate": False}
with app.test_request_context(f"/conversations/{c_id}/name", method="POST", json={"name": "X"}):
with pytest.raises(NotFound, match="Conversation Not Exists"):
@@ -146,24 +146,21 @@ class TestAudioApi:
# ---------------------------------------------------------------------------
class TestTextApi:
@patch("controllers.web.audio.AudioService.transcript_tts", return_value="audio-bytes")
@patch("controllers.web.audio.web_ns")
def test_happy_path(self, mock_ns: MagicMock, mock_tts: MagicMock, app: Flask) -> None:
mock_ns.payload = {"text": "hello", "voice": "alloy"}
with app.test_request_context("/text-to-audio", method="POST"):
def test_happy_path(self, mock_tts: MagicMock, app: Flask) -> None:
with app.test_request_context("/text-to-audio", method="POST", json={"text": "hello", "voice": "alloy"}):
result = TextApi().post(_app_model(), _end_user())
assert result == "audio-bytes"
mock_tts.assert_called_once()
@patch("controllers.web.audio.AudioService.transcript_tts", return_value="audio-bytes")
@patch("controllers.web.audio.web_ns")
def test_happy_path_with_message_ref(self, mock_ns: MagicMock, mock_tts: MagicMock, app: Flask) -> None:
def test_happy_path_with_message_ref(self, mock_tts: MagicMock, app: Flask) -> None:
message_id = "550e8400-e29b-41d4-a716-446655440000"
mock_ns.payload = {"text": "hello", "message_id": message_id}
app_model = _app_model()
with app.test_request_context("/text-to-audio", method="POST"):
with app.test_request_context(
"/text-to-audio", method="POST", json={"text": "hello", "message_id": message_id}
):
result = TextApi().post(app_model, _end_user())
assert result == "audio-bytes"
@@ -177,10 +174,7 @@ class TestTextApi:
"controllers.web.audio.AudioService.transcript_tts",
side_effect=InvokeError(description="invoke failed"),
)
@patch("controllers.web.audio.web_ns")
def test_invoke_error_mapped(self, mock_ns: MagicMock, mock_tts: MagicMock, app: Flask) -> None:
mock_ns.payload = {"text": "hello"}
with app.test_request_context("/text-to-audio", method="POST"):
def test_invoke_error_mapped(self, mock_tts: MagicMock, app: Flask) -> None:
with app.test_request_context("/text-to-audio", method="POST", json={"text": "hello"}):
with pytest.raises(CompletionRequestError):
TextApi().post(_app_model(), _end_user())
@@ -134,12 +134,10 @@ class TestRemoteFileUploadApi:
@patch("controllers.web.remote_files.FileService")
@patch("controllers.web.remote_files.helpers.guess_file_info_from_response")
@patch("controllers.web.remote_files.remote_fetcher")
@patch("controllers.web.remote_files.web_ns")
@patch("controllers.web.remote_files.db")
def test_upload_success(
self,
mock_db: MagicMock,
mock_ns: MagicMock,
mock_proxy: MagicMock,
mock_guess: MagicMock,
mock_file_svc_cls: MagicMock,
@@ -148,7 +146,6 @@ class TestRemoteFileUploadApi:
sqlite_engine: Engine,
) -> None:
mock_db.engine = sqlite_engine
mock_ns.payload = {"url": "https://example.com/file.pdf"}
head_resp = MagicMock()
head_resp.status_code = 200
head_resp.content = b"pdf-content"
@@ -164,7 +161,9 @@ class TestRemoteFileUploadApi:
mock_file_svc_cls.return_value.upload_file.return_value = _upload_file()
with app.test_request_context("/remote-files/upload", method="POST"):
with app.test_request_context(
"/remote-files/upload", method="POST", json={"url": "https://example.com/file.pdf"}
):
result, status = RemoteFileUploadApi().post(_app_model(), _end_user())
assert status == 201
@@ -173,16 +172,13 @@ class TestRemoteFileUploadApi:
@patch("controllers.web.remote_files.FileService.is_file_size_within_limit", return_value=False)
@patch("controllers.web.remote_files.helpers.guess_file_info_from_response")
@patch("controllers.web.remote_files.remote_fetcher")
@patch("controllers.web.remote_files.web_ns")
def test_file_too_large(
self,
mock_ns: MagicMock,
mock_proxy: MagicMock,
mock_guess: MagicMock,
mock_size_check: MagicMock,
app: Flask,
) -> None:
mock_ns.payload = {"url": "https://example.com/big.zip"}
head_resp = MagicMock()
head_resp.status_code = 200
mock_proxy.make_request.return_value = head_resp
@@ -190,18 +186,18 @@ class TestRemoteFileUploadApi:
filename="big.zip", extension="zip", mimetype="application/zip", size=999999999
)
with app.test_request_context("/remote-files/upload", method="POST"):
with app.test_request_context(
"/remote-files/upload", method="POST", json={"url": "https://example.com/big.zip"}
):
with pytest.raises(FileTooLargeError):
RemoteFileUploadApi().post(_app_model(), _end_user())
@patch("controllers.web.remote_files.remote_fetcher")
@patch("controllers.web.remote_files.web_ns")
def test_fetch_failure_raises(self, mock_ns: MagicMock, mock_proxy: MagicMock, app: Flask) -> None:
def test_fetch_failure_raises(self, mock_proxy: MagicMock, app: Flask) -> None:
import httpx
mock_ns.payload = {"url": "https://example.com/bad"}
mock_proxy.make_request.side_effect = httpx.RequestError("connection failed")
with app.test_request_context("/remote-files/upload", method="POST"):
with app.test_request_context("/remote-files/upload", method="POST", json={"url": "https://example.com/bad"}):
with pytest.raises(RemoteFileUploadError):
RemoteFileUploadApi().post(_app_model(), _end_user())