refactor(inner_api): dep-inject request payloads with @model_validate (#41575)

This commit is contained in:
Eddy ZHANG 2026-09-02 03:40:46 +00:00 committed by GitHub
parent 88c64a8db9
commit 4d9bda7422
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 127 additions and 77 deletions

View File

@ -14,7 +14,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from controllers.common.schema import query_params_from_model, register_schema_model
from controllers.console.wraps import setup_required
from controllers.console.wraps import model_validate, setup_required
from controllers.inner_api import inner_api_ns
from controllers.inner_api.wraps import enterprise_inner_api_only
from extensions.ext_database import db
@ -61,9 +61,9 @@ class EnterpriseAppDSLImport(Resource):
404: "Creator account not found or inactive",
}
)
def post(self, workspace_id: str):
@model_validate(InnerAppDSLImportPayload)
def post(self, args: InnerAppDSLImportPayload, workspace_id: str):
"""Import a DSL into a workspace on behalf of a specified creator."""
args = InnerAppDSLImportPayload.model_validate(inner_api_ns.payload or {})
account = _get_active_account(args.creator_email)
if account is None:

View File

@ -4,7 +4,7 @@ from flask_restx import Resource
from pydantic import BaseModel, Field
from controllers.common.schema import register_schema_model
from controllers.console.wraps import setup_required
from controllers.console.wraps import model_validate, setup_required
from controllers.inner_api import inner_api_ns
from controllers.inner_api.wraps import inner_api_only
from extensions.ext_application_services import application_services
@ -27,8 +27,8 @@ class BaseMail(Resource):
@inner_api_ns.doc("send_inner_mail")
@inner_api_ns.doc(description="Send internal email")
@inner_api_ns.expect(inner_api_ns.models[InnerMailPayload.__name__])
def post(self):
args = InnerMailPayload.model_validate(inner_api_ns.payload or {})
@model_validate(InnerMailPayload)
def post(self, args: InnerMailPayload):
application_services().inner_mail.send(
InnerMailMessage(
recipients=tuple(args.to),

View File

@ -15,7 +15,7 @@ from sqlalchemy import select
from sqlalchemy.orm import Session
from controllers.common.schema import register_schema_model
from controllers.console.wraps import setup_required
from controllers.console.wraps import model_validate, setup_required
from controllers.inner_api import inner_api_ns
from controllers.inner_api.wraps import enterprise_inner_api_only
from core.helper import encrypter
@ -67,8 +67,8 @@ class EnterpriseRuntimeCredentialsResolve(Resource):
},
)
@inner_api_ns.expect(inner_api_ns.models[InnerRuntimeCredentialsResolvePayload.__name__])
def post(self):
args = InnerRuntimeCredentialsResolvePayload.model_validate(inner_api_ns.payload or {})
@model_validate(InnerRuntimeCredentialsResolvePayload)
def post(self, args: InnerRuntimeCredentialsResolvePayload):
if not args.credentials:
return {"credentials": []}, 200

View File

@ -16,6 +16,7 @@ from pydantic import ValidationError
from sqlalchemy import event
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session, scoped_session, sessionmaker
from werkzeug.exceptions import UnprocessableEntity
from controllers.inner_api.app import dsl as dsl_module
from controllers.inner_api.app.dsl import (
@ -29,6 +30,7 @@ from models.account import AccountStatus, TenantAccountRole
from models.model import AppMode, IconType
from services.app_dsl_service import Import, ImportStatus
from services.errors.app import IsDraftWorkflowError, WorkflowNotFoundError
from tests.unit_tests.config_override import config_overrides_context
def _persist_app(session: Session) -> App:
@ -185,13 +187,12 @@ class TestEnterpriseAppDSLImport:
self._mock_dsl.import_app.return_value = self._make_import_result(ImportStatus.COMPLETED)
unwrapped = inspect.unwrap(api_instance.post)
with app.test_request_context():
with patch("controllers.inner_api.app.dsl.inner_api_ns") as mock_ns:
mock_ns.payload = {
"yaml_content": "version: 0.6.0\n",
"creator_email": "user@example.com",
}
result = unwrapped(api_instance, workspace_id="ws-123")
payload = {
"yaml_content": "version: 0.6.0\n",
"creator_email": "user@example.com",
}
with app.test_request_context(json=payload):
result = unwrapped(api_instance, InnerAppDSLImportPayload.model_validate(payload), workspace_id="ws-123")
body, status_code = result
assert status_code == 200
@ -208,10 +209,11 @@ class TestEnterpriseAppDSLImport:
self._mock_dsl.import_app.return_value = self._make_import_result(ImportStatus.PENDING)
unwrapped = inspect.unwrap(api_instance.post)
with app.test_request_context():
with patch("controllers.inner_api.app.dsl.inner_api_ns") as mock_ns:
mock_ns.payload = {"yaml_content": "test", "creator_email": "u@e.com"}
body, status_code = unwrapped(api_instance, workspace_id="ws-123")
payload = {"yaml_content": "test", "creator_email": "u@e.com"}
with app.test_request_context(json=payload):
body, status_code = unwrapped(
api_instance, InnerAppDSLImportPayload.model_validate(payload), workspace_id="ws-123"
)
assert status_code == 202
assert body["status"] == "pending"
@ -225,10 +227,11 @@ class TestEnterpriseAppDSLImport:
self._mock_dsl.import_app.return_value = self._make_import_result(ImportStatus.FAILED)
unwrapped = inspect.unwrap(api_instance.post)
with app.test_request_context():
with patch("controllers.inner_api.app.dsl.inner_api_ns") as mock_ns:
mock_ns.payload = {"yaml_content": "test", "creator_email": "u@e.com"}
body, status_code = unwrapped(api_instance, workspace_id="ws-123")
payload = {"yaml_content": "test", "creator_email": "u@e.com"}
with app.test_request_context(json=payload):
body, status_code = unwrapped(
api_instance, InnerAppDSLImportPayload.model_validate(payload), workspace_id="ws-123"
)
assert status_code == 400
assert body["status"] == "failed"
@ -239,10 +242,9 @@ class TestEnterpriseAppDSLImport:
mock_get_account.return_value = None
unwrapped = inspect.unwrap(api_instance.post)
with app.test_request_context():
with patch("controllers.inner_api.app.dsl.inner_api_ns") as mock_ns:
mock_ns.payload = {"yaml_content": "test", "creator_email": "missing@e.com"}
result = unwrapped(api_instance, workspace_id="ws-123")
payload = {"yaml_content": "test", "creator_email": "missing@e.com"}
with app.test_request_context(json=payload):
result = unwrapped(api_instance, InnerAppDSLImportPayload.model_validate(payload), workspace_id="ws-123")
body, status_code = result
assert status_code == 404
@ -485,3 +487,22 @@ class TestEnterpriseAppDSLExport:
body, status_code = result
assert status_code == 404
assert "app not found" in body["message"]
class TestModelValidateDecorator:
"""The handler tests above unwrap the view, so this is what covers the decorator."""
def test_invalid_body_is_rejected_before_the_handler_runs(self, app: Flask) -> None:
api_instance = EnterpriseAppDSLImport()
with (
patch("controllers.console.wraps._is_setup_completed", return_value=True),
config_overrides_context(INNER_API=True, INNER_API_KEY="inner-api-key"),
app.test_request_context(
method="POST",
json={},
headers={"X-Inner-Api-Key": "inner-api-key"},
),
pytest.raises(UnprocessableEntity),
):
api_instance.post(workspace_id="ws-123")

View File

@ -8,7 +8,7 @@ from unittest.mock import MagicMock, patch
import pytest
from flask import Flask
from pydantic import ValidationError
from werkzeug.exceptions import NotFound
from werkzeug.exceptions import NotFound, UnprocessableEntity
from controllers.inner_api.mail import BaseMail, BillingMail, EnterpriseMail, InnerMailPayload
from controllers.inner_api.wraps import InnerApiUnauthorizedError
@ -96,16 +96,31 @@ class TestBaseMail:
services = SimpleNamespace(inner_mail=mail_service)
with (
app.test_request_context(),
patch("controllers.inner_api.mail.inner_api_ns") as namespace,
app.test_request_context(method="POST", json=payload),
patch("controllers.inner_api.mail.application_services", return_value=services),
):
namespace.payload = payload
result = unwrap(resource_type.post)(resource_type())
assert result == ({"message": "success"}, 200)
mail_service.send.assert_called_once_with(expected)
@pytest.mark.parametrize("resource_type", [EnterpriseMail, BillingMail])
def test_invalid_body_is_rejected_before_the_application_service_runs(
self, resource_type: type[BaseMail], app: Flask
) -> None:
"""`super().post()` relies on the decorator to supply the payload, so this covers it."""
mail_service = MagicMock()
services = SimpleNamespace(inner_mail=mail_service)
with (
app.test_request_context(method="POST", json={}),
patch("controllers.inner_api.mail.application_services", return_value=services),
pytest.raises(UnprocessableEntity),
):
unwrap(resource_type.post)(resource_type())
mail_service.send.assert_not_called()
def test_disabled_inner_api_returns_not_found_before_setup(app: Flask, config_overrides: Callable[..., None]) -> None:
config_overrides(INNER_API=False)

View File

@ -8,6 +8,7 @@ import pytest
from flask import Flask
from sqlalchemy.engine import Engine
from sqlalchemy.orm import Session
from werkzeug.exceptions import UnprocessableEntity
from controllers.inner_api.runtime_credentials import (
EnterpriseRuntimeCredentialsResolve,
@ -15,6 +16,7 @@ from controllers.inner_api.runtime_credentials import (
)
from models.provider import ProviderCredential
from models.tools import BuiltinToolProvider
from tests.unit_tests.config_override import config_overrides_context
def test_runtime_credentials_payload_accepts_items():
@ -73,19 +75,18 @@ def test_runtime_model_credentials_resolve_returns_decrypted_values(
handler = EnterpriseRuntimeCredentialsResolve()
unwrapped = inspect.unwrap(handler.post)
with app.test_request_context():
with patch("controllers.inner_api.runtime_credentials.inner_api_ns") as mock_ns:
mock_ns.payload = {
"tenant_id": "tenant-1",
"credentials": [
{
"credential_id": "credential-1",
"provider": "langgenius/openai/openai",
"kind": "model",
}
],
payload = {
"tenant_id": "tenant-1",
"credentials": [
{
"credential_id": "credential-1",
"provider": "langgenius/openai/openai",
"kind": "model",
}
body, status_code = unwrapped(handler)
],
}
with app.test_request_context(json=payload):
body, status_code = unwrapped(handler, InnerRuntimeCredentialsResolvePayload.model_validate(payload))
assert status_code == 200
assert body["credentials"][0]["kind"] == "model"
@ -104,13 +105,12 @@ def test_runtime_model_credentials_resolve_rejects_unknown_provider(mock_provide
handler = EnterpriseRuntimeCredentialsResolve()
unwrapped = inspect.unwrap(handler.post)
with app.test_request_context():
with patch("controllers.inner_api.runtime_credentials.inner_api_ns") as mock_ns:
mock_ns.payload = {
"tenant_id": "tenant-1",
"credentials": [{"credential_id": "credential-1", "provider": "missing", "kind": "model"}],
}
body, status_code = unwrapped(handler)
payload = {
"tenant_id": "tenant-1",
"credentials": [{"credential_id": "credential-1", "provider": "missing", "kind": "model"}],
}
with app.test_request_context(json=payload):
body, status_code = unwrapped(handler, InnerRuntimeCredentialsResolvePayload.model_validate(payload))
assert status_code == 404
assert "provider" in body["message"]
@ -152,19 +152,18 @@ def test_runtime_tool_credentials_resolve_returns_decrypted_values(
handler = EnterpriseRuntimeCredentialsResolve()
unwrapped = inspect.unwrap(handler.post)
with app.test_request_context():
with patch("controllers.inner_api.runtime_credentials.inner_api_ns") as mock_ns:
mock_ns.payload = {
"tenant_id": "tenant-1",
"credentials": [
{
"credential_id": "credential-1",
"provider": "langgenius/tavily/tavily",
"kind": "tool",
}
],
payload = {
"tenant_id": "tenant-1",
"credentials": [
{
"credential_id": "credential-1",
"provider": "langgenius/tavily/tavily",
"kind": "tool",
}
body, status_code = unwrapped(handler)
],
}
with app.test_request_context(json=payload):
body, status_code = unwrapped(handler, InnerRuntimeCredentialsResolvePayload.model_validate(payload))
assert status_code == 200
assert body["credentials"][0]["kind"] == "tool"
@ -201,13 +200,12 @@ def test_runtime_tool_credentials_resolve_rejects_unknown_credential(
handler = EnterpriseRuntimeCredentialsResolve()
unwrapped = inspect.unwrap(handler.post)
with app.test_request_context():
with patch("controllers.inner_api.runtime_credentials.inner_api_ns") as mock_ns:
mock_ns.payload = {
"tenant_id": "tenant-1",
"credentials": [{"credential_id": "missing", "provider": "langgenius/tavily/tavily", "kind": "tool"}],
}
body, status_code = unwrapped(handler)
payload = {
"tenant_id": "tenant-1",
"credentials": [{"credential_id": "missing", "provider": "langgenius/tavily/tavily", "kind": "tool"}],
}
with app.test_request_context(json=payload):
body, status_code = unwrapped(handler, InnerRuntimeCredentialsResolvePayload.model_validate(payload))
assert status_code == 404
assert "credential" in body["message"]
@ -216,13 +214,29 @@ def test_runtime_tool_credentials_resolve_rejects_unknown_credential(
def test_runtime_credentials_resolve_rejects_unknown_kind(app: Flask):
handler = EnterpriseRuntimeCredentialsResolve()
unwrapped = inspect.unwrap(handler.post)
with app.test_request_context():
with patch("controllers.inner_api.runtime_credentials.inner_api_ns") as mock_ns:
mock_ns.payload = {
"tenant_id": "tenant-1",
"credentials": [{"credential_id": "credential-1", "provider": "x", "kind": "secret"}],
}
body, status_code = unwrapped(handler)
payload = {
"tenant_id": "tenant-1",
"credentials": [{"credential_id": "credential-1", "provider": "x", "kind": "secret"}],
}
with app.test_request_context(json=payload):
body, status_code = unwrapped(handler, InnerRuntimeCredentialsResolvePayload.model_validate(payload))
assert status_code == 400
assert "kind" in body["message"]
def test_invalid_body_is_rejected_before_the_handler_runs(app: Flask) -> None:
"""The tests above unwrap the view, so this is what covers the decorator."""
handler = EnterpriseRuntimeCredentialsResolve()
with (
patch("controllers.console.wraps._is_setup_completed", return_value=True),
config_overrides_context(INNER_API=True, INNER_API_KEY="inner-api-key"),
app.test_request_context(
method="POST",
json={},
headers={"X-Inner-Api-Key": "inner-api-key"},
),
pytest.raises(UnprocessableEntity),
):
handler.post()