mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 02:43:49 +08:00
refactor(inner_api): dep-inject request payloads with @model_validate (#41575)
This commit is contained in:
parent
88c64a8db9
commit
4d9bda7422
@ -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:
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user