test: unit test cases for controllers.files, controllers.mcp and controllers.trigger module (#32057)

This commit is contained in:
rajatagarwal-oss 2026-02-25 12:11:42 +05:30 committed by GitHub
parent 6ff420cd03
commit 212756c315
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 1310 additions and 0 deletions

View File

@ -64,6 +64,10 @@ class ToolFileApi(Resource):
if not stream or not tool_file:
raise NotFound("file is not found")
except NotFound:
raise
except Exception:
raise UnsupportedFileTypeError()

View File

@ -0,0 +1,211 @@
import types
from unittest.mock import patch
import pytest
from werkzeug.exceptions import NotFound
import controllers.files.image_preview as module
def unwrap(func):
while hasattr(func, "__wrapped__"):
func = func.__wrapped__
return func
@pytest.fixture(autouse=True)
def mock_db():
"""
Replace Flask-SQLAlchemy db with a plain object
to avoid touching Flask app context entirely.
"""
fake_db = types.SimpleNamespace(engine=object())
module.db = fake_db
class DummyUploadFile:
def __init__(self, mime_type="text/plain", size=10, name="test.txt", extension="txt"):
self.mime_type = mime_type
self.size = size
self.name = name
self.extension = extension
def fake_request(args: dict):
"""Return a fake request object (NOT a Flask LocalProxy)."""
return types.SimpleNamespace(args=types.SimpleNamespace(to_dict=lambda flat=True: args))
class TestImagePreviewApi:
@patch.object(module, "FileService")
def test_success(self, mock_file_service):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
}
)
generator = iter([b"img"])
mock_file_service.return_value.get_image_preview.return_value = (
generator,
"image/png",
)
api = module.ImagePreviewApi()
get_fn = unwrap(api.get)
response = get_fn("file-id")
assert response.mimetype == "image/png"
@patch.object(module, "FileService")
def test_unsupported_file_type(self, mock_file_service):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
}
)
mock_file_service.return_value.get_image_preview.side_effect = (
module.services.errors.file.UnsupportedFileTypeError()
)
api = module.ImagePreviewApi()
get_fn = unwrap(api.get)
with pytest.raises(module.UnsupportedFileTypeError):
get_fn("file-id")
class TestFilePreviewApi:
@patch.object(module, "enforce_download_for_html")
@patch.object(module, "FileService")
def test_basic_stream(self, mock_file_service, mock_enforce):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": False,
}
)
generator = iter([b"data"])
upload_file = DummyUploadFile(size=100)
mock_file_service.return_value.get_file_generator_by_file_id.return_value = (
generator,
upload_file,
)
api = module.FilePreviewApi()
get_fn = unwrap(api.get)
response = get_fn("file-id")
assert response.mimetype == "text/plain"
assert response.headers["Content-Length"] == "100"
assert "Accept-Ranges" not in response.headers
mock_enforce.assert_called_once()
@patch.object(module, "enforce_download_for_html")
@patch.object(module, "FileService")
def test_as_attachment(self, mock_file_service, mock_enforce):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": True,
}
)
generator = iter([b"data"])
upload_file = DummyUploadFile(
mime_type="application/pdf",
name="doc.pdf",
extension="pdf",
)
mock_file_service.return_value.get_file_generator_by_file_id.return_value = (
generator,
upload_file,
)
api = module.FilePreviewApi()
get_fn = unwrap(api.get)
response = get_fn("file-id")
assert response.headers["Content-Disposition"].startswith("attachment")
assert response.headers["Content-Type"] == "application/octet-stream"
mock_enforce.assert_called_once()
@patch.object(module, "FileService")
def test_unsupported_file_type(self, mock_file_service):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": False,
}
)
mock_file_service.return_value.get_file_generator_by_file_id.side_effect = (
module.services.errors.file.UnsupportedFileTypeError()
)
api = module.FilePreviewApi()
get_fn = unwrap(api.get)
with pytest.raises(module.UnsupportedFileTypeError):
get_fn("file-id")
class TestWorkspaceWebappLogoApi:
@patch.object(module, "FileService")
@patch.object(module.TenantService, "get_custom_config")
def test_success(self, mock_config, mock_file_service):
mock_config.return_value = {"replace_webapp_logo": "logo-id"}
generator = iter([b"logo"])
mock_file_service.return_value.get_public_image_preview.return_value = (
generator,
"image/png",
)
api = module.WorkspaceWebappLogoApi()
get_fn = unwrap(api.get)
response = get_fn("workspace-id")
assert response.mimetype == "image/png"
@patch.object(module.TenantService, "get_custom_config")
def test_logo_not_configured(self, mock_config):
mock_config.return_value = {}
api = module.WorkspaceWebappLogoApi()
get_fn = unwrap(api.get)
with pytest.raises(NotFound):
get_fn("workspace-id")
@patch.object(module, "FileService")
@patch.object(module.TenantService, "get_custom_config")
def test_unsupported_file_type(self, mock_config, mock_file_service):
mock_config.return_value = {"replace_webapp_logo": "logo-id"}
mock_file_service.return_value.get_public_image_preview.side_effect = (
module.services.errors.file.UnsupportedFileTypeError()
)
api = module.WorkspaceWebappLogoApi()
get_fn = unwrap(api.get)
with pytest.raises(module.UnsupportedFileTypeError):
get_fn("workspace-id")

View File

@ -0,0 +1,173 @@
import types
from unittest.mock import patch
import pytest
from werkzeug.exceptions import Forbidden, NotFound
import controllers.files.tool_files as module
def unwrap(func):
while hasattr(func, "__wrapped__"):
func = func.__wrapped__
return func
def fake_request(args: dict):
return types.SimpleNamespace(args=types.SimpleNamespace(to_dict=lambda flat=True: args))
class DummyToolFile:
def __init__(self, mimetype="text/plain", size=10, name="tool.txt"):
self.mimetype = mimetype
self.size = size
self.name = name
@pytest.fixture(autouse=True)
def mock_global_db():
fake_db = types.SimpleNamespace(engine=object())
module.global_db = fake_db
class TestToolFileApi:
@patch.object(module, "verify_tool_file_signature", return_value=True)
@patch.object(module, "ToolFileManager")
def test_success_stream(
self,
mock_tool_file_manager,
mock_verify,
):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": False,
}
)
stream = iter([b"data"])
tool_file = DummyToolFile(size=100)
mock_tool_file_manager.return_value.get_file_generator_by_tool_file_id.return_value = (
stream,
tool_file,
)
api = module.ToolFileApi()
get_fn = unwrap(api.get)
response = get_fn("file-id", "txt")
assert response.mimetype == "text/plain"
assert response.headers["Content-Length"] == "100"
mock_verify.assert_called_once_with(
file_id="file-id",
timestamp="123",
nonce="abc",
sign="sig",
)
@patch.object(module, "verify_tool_file_signature", return_value=True)
@patch.object(module, "ToolFileManager")
def test_as_attachment(
self,
mock_tool_file_manager,
mock_verify,
):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": True,
}
)
stream = iter([b"data"])
tool_file = DummyToolFile(
mimetype="application/pdf",
name="doc.pdf",
)
mock_tool_file_manager.return_value.get_file_generator_by_tool_file_id.return_value = (
stream,
tool_file,
)
api = module.ToolFileApi()
get_fn = unwrap(api.get)
response = get_fn("file-id", "pdf")
assert response.headers["Content-Disposition"].startswith("attachment")
mock_verify.assert_called_once()
@patch.object(module, "verify_tool_file_signature", return_value=False)
def test_invalid_signature(self, mock_verify):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "bad-sig",
"as_attachment": False,
}
)
api = module.ToolFileApi()
get_fn = unwrap(api.get)
with pytest.raises(Forbidden):
get_fn("file-id", "txt")
@patch.object(module, "verify_tool_file_signature", return_value=True)
@patch.object(module, "ToolFileManager")
def test_file_not_found(
self,
mock_tool_file_manager,
mock_verify,
):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": False,
}
)
mock_tool_file_manager.return_value.get_file_generator_by_tool_file_id.return_value = (
None,
None,
)
api = module.ToolFileApi()
get_fn = unwrap(api.get)
with pytest.raises(NotFound):
get_fn("file-id", "txt")
@patch.object(module, "verify_tool_file_signature", return_value=True)
@patch.object(module, "ToolFileManager")
def test_unsupported_file_type(
self,
mock_tool_file_manager,
mock_verify,
):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"as_attachment": False,
}
)
mock_tool_file_manager.return_value.get_file_generator_by_tool_file_id.side_effect = Exception("boom")
api = module.ToolFileApi()
get_fn = unwrap(api.get)
with pytest.raises(module.UnsupportedFileTypeError):
get_fn("file-id", "txt")

View File

@ -0,0 +1,189 @@
import types
from unittest.mock import patch
import pytest
from werkzeug.exceptions import Forbidden
import controllers.files.upload as module
def unwrap(func):
while hasattr(func, "__wrapped__"):
func = func.__wrapped__
return func
def fake_request(args: dict, file=None):
return types.SimpleNamespace(
args=types.SimpleNamespace(to_dict=lambda flat=True: args),
files={"file": file} if file else {},
)
class DummyUser:
def __init__(self, user_id="user-1"):
self.id = user_id
class DummyFile:
def __init__(self, filename="test.txt", mimetype="text/plain", content=b"data"):
self.filename = filename
self.mimetype = mimetype
self._content = content
def read(self):
return self._content
class DummyToolFile:
def __init__(self):
self.id = "file-id"
self.name = "test.txt"
self.size = 10
self.mimetype = "text/plain"
self.original_url = "http://original"
self.user_id = "user-1"
self.tenant_id = "tenant-1"
self.conversation_id = None
self.file_key = "file-key"
class TestPluginUploadFileApi:
@patch.object(module, "verify_plugin_file_signature", return_value=True)
@patch.object(module, "get_user", return_value=DummyUser())
@patch.object(module, "ToolFileManager")
def test_success_upload(
self,
mock_tool_file_manager,
mock_get_user,
mock_verify_signature,
):
dummy_file = DummyFile()
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"tenant_id": "tenant-1",
"user_id": "user-1",
},
file=dummy_file,
)
tool_file_manager_instance = mock_tool_file_manager.return_value
tool_file_manager_instance.create_file_by_raw.return_value = DummyToolFile()
mock_tool_file_manager.sign_file.return_value = "signed-url"
api = module.PluginUploadFileApi()
post_fn = unwrap(api.post)
result, status_code = post_fn(api)
assert status_code == 201
assert result["id"] == "file-id"
assert result["preview_url"] == "signed-url"
def test_missing_file(self):
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"tenant_id": "tenant-1",
"user_id": "user-1",
}
)
api = module.PluginUploadFileApi()
post_fn = unwrap(api.post)
with pytest.raises(Forbidden):
post_fn(api)
@patch.object(module, "get_user", return_value=DummyUser())
@patch.object(module, "verify_plugin_file_signature", return_value=False)
def test_invalid_signature(self, mock_verify, mock_get_user):
dummy_file = DummyFile()
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "bad",
"tenant_id": "tenant-1",
"user_id": "user-1",
},
file=dummy_file,
)
api = module.PluginUploadFileApi()
post_fn = unwrap(api.post)
with pytest.raises(Forbidden):
post_fn(api)
@patch.object(module, "get_user", return_value=DummyUser())
@patch.object(module, "verify_plugin_file_signature", return_value=True)
@patch.object(module, "ToolFileManager")
def test_file_too_large(
self,
mock_tool_file_manager,
mock_verify,
mock_get_user,
):
dummy_file = DummyFile()
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"tenant_id": "tenant-1",
"user_id": "user-1",
},
file=dummy_file,
)
mock_tool_file_manager.return_value.create_file_by_raw.side_effect = (
module.services.errors.file.FileTooLargeError("too large")
)
api = module.PluginUploadFileApi()
post_fn = unwrap(api.post)
with pytest.raises(module.FileTooLargeError):
post_fn(api)
@patch.object(module, "get_user", return_value=DummyUser())
@patch.object(module, "verify_plugin_file_signature", return_value=True)
@patch.object(module, "ToolFileManager")
def test_unsupported_file_type(
self,
mock_tool_file_manager,
mock_verify,
mock_get_user,
):
dummy_file = DummyFile()
module.request = fake_request(
{
"timestamp": "123",
"nonce": "abc",
"sign": "sig",
"tenant_id": "tenant-1",
"user_id": "user-1",
},
file=dummy_file,
)
mock_tool_file_manager.return_value.create_file_by_raw.side_effect = (
module.services.errors.file.UnsupportedFileTypeError()
)
api = module.PluginUploadFileApi()
post_fn = unwrap(api.post)
with pytest.raises(module.UnsupportedFileTypeError):
post_fn(api)

View File

@ -0,0 +1,508 @@
import types
from unittest.mock import MagicMock, patch
import pytest
from flask import Response
from pydantic import ValidationError
import controllers.mcp.mcp as module
def unwrap(func):
while hasattr(func, "__wrapped__"):
func = func.__wrapped__
return func
@pytest.fixture(autouse=True)
def mock_db():
module.db = types.SimpleNamespace(engine=object())
@pytest.fixture
def fake_session():
session = MagicMock()
session.__enter__.return_value = session
session.__exit__.return_value = False
return session
@pytest.fixture(autouse=True)
def mock_session(fake_session):
module.Session = MagicMock(return_value=fake_session)
@pytest.fixture(autouse=True)
def mock_mcp_ns():
fake_ns = types.SimpleNamespace()
fake_ns.payload = None
fake_ns.models = {}
module.mcp_ns = fake_ns
def fake_payload(data):
module.mcp_ns.payload = data
class DummyServer:
def __init__(self, status, app_id="app-1", tenant_id="tenant-1", server_id="srv-1"):
self.status = status
self.app_id = app_id
self.tenant_id = tenant_id
self.id = server_id
class DummyApp:
def __init__(self, mode, workflow=None, app_model_config=None):
self.id = "app-1"
self.tenant_id = "tenant-1"
self.mode = mode
self.workflow = workflow
self.app_model_config = app_model_config
class DummyWorkflow:
def user_input_form(self, to_old_structure=False):
return []
class DummyConfig:
def to_dict(self):
return {"user_input_form": []}
class DummyResult:
def model_dump(self, **kwargs):
return {"jsonrpc": "2.0", "result": "ok", "id": 1}
class TestMCPAppApi:
@patch.object(module, "handle_mcp_request", return_value=DummyResult())
def test_success_request(self, mock_handle):
fake_payload(
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0"},
},
}
)
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.ADVANCED_CHAT,
workflow=DummyWorkflow(),
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
post_fn = unwrap(api.post)
response = post_fn("server-1")
assert isinstance(response, Response)
mock_handle.assert_called_once()
def test_notification_initialized(self):
fake_payload(
{
"jsonrpc": "2.0",
"method": "notifications/initialized",
"params": {},
}
)
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.ADVANCED_CHAT,
workflow=DummyWorkflow(),
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
post_fn = unwrap(api.post)
response = post_fn("server-1")
assert response.status_code == 202
def test_invalid_notification_method(self):
fake_payload(
{
"jsonrpc": "2.0",
"method": "notifications/invalid",
"params": {},
}
)
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.ADVANCED_CHAT,
workflow=DummyWorkflow(),
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
post_fn = unwrap(api.post)
with pytest.raises(module.MCPRequestError):
post_fn("server-1")
def test_inactive_server(self):
fake_payload(
{
"jsonrpc": "2.0",
"method": "test",
"id": 1,
"params": {},
}
)
server = DummyServer(status="inactive")
app = DummyApp(
mode=module.AppMode.ADVANCED_CHAT,
workflow=DummyWorkflow(),
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
post_fn = unwrap(api.post)
with pytest.raises(module.MCPRequestError):
post_fn("server-1")
def test_invalid_payload(self):
fake_payload({"invalid": "data"})
api = module.MCPAppApi()
post_fn = unwrap(api.post)
with pytest.raises(ValidationError):
post_fn("server-1")
def test_missing_request_id(self):
fake_payload(
{
"jsonrpc": "2.0",
"method": "test",
"params": {},
}
)
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.WORKFLOW,
workflow=DummyWorkflow(),
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
post_fn = unwrap(api.post)
with pytest.raises(module.MCPRequestError):
post_fn("server-1")
def test_server_not_found(self):
"""Test when MCP server doesn't exist"""
fake_payload(
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0"},
},
}
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(
side_effect=module.MCPRequestError(module.mcp_types.INVALID_REQUEST, "Server Not Found")
)
post_fn = unwrap(api.post)
with pytest.raises(module.MCPRequestError) as exc_info:
post_fn("server-1")
assert "Server Not Found" in str(exc_info.value)
def test_app_not_found(self):
"""Test when app associated with server doesn't exist"""
fake_payload(
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0"},
},
}
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(
side_effect=module.MCPRequestError(module.mcp_types.INVALID_REQUEST, "App Not Found")
)
post_fn = unwrap(api.post)
with pytest.raises(module.MCPRequestError) as exc_info:
post_fn("server-1")
assert "App Not Found" in str(exc_info.value)
def test_app_unavailable_no_workflow(self):
"""Test when app has no workflow (ADVANCED_CHAT mode)"""
fake_payload(
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0"},
},
}
)
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.ADVANCED_CHAT,
workflow=None, # No workflow
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
post_fn = unwrap(api.post)
with pytest.raises(module.MCPRequestError) as exc_info:
post_fn("server-1")
assert "App is unavailable" in str(exc_info.value)
def test_app_unavailable_no_model_config(self):
"""Test when app has no model config (chat mode)"""
fake_payload(
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0"},
},
}
)
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.CHAT,
app_model_config=None, # No model config
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
post_fn = unwrap(api.post)
with pytest.raises(module.MCPRequestError) as exc_info:
post_fn("server-1")
assert "App is unavailable" in str(exc_info.value)
@patch.object(module, "handle_mcp_request", return_value=None)
def test_mcp_request_no_response(self, mock_handle):
"""Test when handle_mcp_request returns None"""
fake_payload(
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0"},
},
}
)
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.ADVANCED_CHAT,
workflow=DummyWorkflow(),
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
post_fn = unwrap(api.post)
with pytest.raises(module.MCPRequestError) as exc_info:
post_fn("server-1")
assert "No response generated" in str(exc_info.value)
def test_workflow_mode_with_user_input_form(self):
"""Test WORKFLOW mode app with user input form"""
fake_payload(
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0"},
},
}
)
class WorkflowWithForm:
def user_input_form(self, to_old_structure=False):
return [{"text-input": {"variable": "test_var", "label": "Test"}}]
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.WORKFLOW,
workflow=WorkflowWithForm(),
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
with patch.object(module, "handle_mcp_request", return_value=DummyResult()):
post_fn = unwrap(api.post)
response = post_fn("server-1")
assert isinstance(response, Response)
def test_chat_mode_with_model_config(self):
"""Test CHAT mode app with model config"""
fake_payload(
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0"},
},
}
)
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.CHAT,
app_model_config=DummyConfig(),
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
with patch.object(module, "handle_mcp_request", return_value=DummyResult()):
post_fn = unwrap(api.post)
response = post_fn("server-1")
assert isinstance(response, Response)
def test_invalid_mcp_request_format(self):
"""Test invalid MCP request that doesn't match any type"""
fake_payload(
{
"jsonrpc": "2.0",
"method": "invalid_method_xyz",
"id": 1,
"params": {},
}
)
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.ADVANCED_CHAT,
workflow=DummyWorkflow(),
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
post_fn = unwrap(api.post)
with pytest.raises(module.MCPRequestError) as exc_info:
post_fn("server-1")
assert "Invalid MCP request" in str(exc_info.value)
def test_server_found_successfully(self):
"""Test successful server and app retrieval"""
api = module.MCPAppApi()
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.ADVANCED_CHAT,
workflow=DummyWorkflow(),
)
session = MagicMock()
session.query().where().first.side_effect = [server, app]
result_server, result_app = api._get_mcp_server_and_app("server-1", session)
assert result_server == server
assert result_app == app
def test_validate_server_status_active(self):
"""Test successful server status validation"""
api = module.MCPAppApi()
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
# Should not raise an exception
api._validate_server_status(server)
def test_convert_user_input_form_empty(self):
"""Test converting empty user input form"""
api = module.MCPAppApi()
result = api._convert_user_input_form([])
assert result == []
def test_invalid_user_input_form_validation(self):
"""Test invalid user input form that fails validation"""
fake_payload(
{
"jsonrpc": "2.0",
"method": "initialize",
"id": 1,
"params": {
"protocolVersion": "2024-11-05",
"capabilities": {},
"clientInfo": {"name": "test-client", "version": "1.0"},
},
}
)
class WorkflowWithBadForm:
def user_input_form(self, to_old_structure=False):
# Invalid type that will fail validation
return [{"invalid-type": {"variable": "test_var"}}]
server = DummyServer(status=module.AppMCPServerStatus.ACTIVE)
app = DummyApp(
mode=module.AppMode.WORKFLOW,
workflow=WorkflowWithBadForm(),
)
api = module.MCPAppApi()
api._get_mcp_server_and_app = MagicMock(return_value=(server, app))
post_fn = unwrap(api.post)
with pytest.raises(module.MCPRequestError) as exc_info:
post_fn("server-1")
assert "Invalid user_input_form" in str(exc_info.value)

View File

@ -0,0 +1,73 @@
from unittest.mock import patch
import pytest
from werkzeug.exceptions import NotFound
import controllers.trigger.trigger as module
@pytest.fixture(autouse=True)
def mock_request():
module.request = object()
@pytest.fixture(autouse=True)
def mock_jsonify():
module.jsonify = lambda payload: payload
VALID_UUID = "123e4567-e89b-42d3-a456-426614174000"
INVALID_UUID = "not-a-uuid"
class TestTriggerEndpoint:
def test_invalid_uuid(self):
with pytest.raises(NotFound):
module.trigger_endpoint(INVALID_UUID)
@patch.object(module.TriggerService, "process_endpoint")
@patch.object(module.TriggerSubscriptionBuilderService, "process_builder_validation_endpoint")
def test_first_handler_returns_response(self, mock_builder, mock_trigger):
mock_trigger.return_value = ("ok", 200)
mock_builder.return_value = None
response = module.trigger_endpoint(VALID_UUID)
assert response == ("ok", 200)
mock_builder.assert_not_called()
@patch.object(module.TriggerService, "process_endpoint")
@patch.object(module.TriggerSubscriptionBuilderService, "process_builder_validation_endpoint")
def test_second_handler_returns_response(self, mock_builder, mock_trigger):
mock_trigger.return_value = None
mock_builder.return_value = ("ok", 200)
response = module.trigger_endpoint(VALID_UUID)
assert response == ("ok", 200)
@patch.object(module.TriggerService, "process_endpoint")
@patch.object(module.TriggerSubscriptionBuilderService, "process_builder_validation_endpoint")
def test_no_handler_returns_response(self, mock_builder, mock_trigger):
mock_trigger.return_value = None
mock_builder.return_value = None
response, status = module.trigger_endpoint(VALID_UUID)
assert status == 404
assert response["error"] == "Endpoint not found"
@patch.object(module.TriggerService, "process_endpoint", side_effect=ValueError("bad input"))
def test_value_error(self, mock_trigger):
response, status = module.trigger_endpoint(VALID_UUID)
assert status == 400
assert response["error"] == "Endpoint processing failed"
assert response["message"] == "bad input"
@patch.object(module.TriggerService, "process_endpoint", side_effect=Exception("boom"))
def test_unexpected_exception(self, mock_trigger):
response, status = module.trigger_endpoint(VALID_UUID)
assert status == 500
assert response["error"] == "Internal server error"

View File

@ -0,0 +1,152 @@
import types
from unittest.mock import patch
import pytest
from werkzeug.exceptions import NotFound, RequestEntityTooLarge
import controllers.trigger.webhook as module
@pytest.fixture(autouse=True)
def mock_request():
module.request = types.SimpleNamespace(
method="POST",
headers={"x-test": "1"},
args={"a": "b"},
)
@pytest.fixture(autouse=True)
def mock_jsonify():
module.jsonify = lambda payload: payload
class DummyWebhookTrigger:
webhook_id = "wh-1"
tenant_id = "tenant-1"
app_id = "app-1"
node_id = "node-1"
class TestPrepareWebhookExecution:
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow")
@patch.object(module.WebhookService, "extract_and_validate_webhook_data")
def test_prepare_success(self, mock_extract, mock_get):
mock_get.return_value = ("trigger", "workflow", "node_config")
mock_extract.return_value = {"data": "ok"}
result = module._prepare_webhook_execution("wh-1")
assert result == ("trigger", "workflow", "node_config", {"data": "ok"}, None)
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow")
@patch.object(module.WebhookService, "extract_and_validate_webhook_data", side_effect=ValueError("bad"))
def test_prepare_validation_error(self, mock_extract, mock_get):
mock_get.return_value = ("trigger", "workflow", "node_config")
trigger, workflow, node_config, webhook_data, error = module._prepare_webhook_execution("wh-1")
assert error == "bad"
assert webhook_data["method"] == "POST"
class TestHandleWebhook:
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow")
@patch.object(module.WebhookService, "extract_and_validate_webhook_data")
@patch.object(module.WebhookService, "trigger_workflow_execution")
@patch.object(module.WebhookService, "generate_webhook_response")
def test_success(
self,
mock_generate,
mock_trigger,
mock_extract,
mock_get,
):
mock_get.return_value = (DummyWebhookTrigger(), "workflow", "node_config")
mock_extract.return_value = {"input": "x"}
mock_generate.return_value = ({"ok": True}, 200)
response, status = module.handle_webhook("wh-1")
assert status == 200
assert response["ok"] is True
mock_trigger.assert_called_once()
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow")
@patch.object(module.WebhookService, "extract_and_validate_webhook_data", side_effect=ValueError("bad"))
def test_bad_request(self, mock_extract, mock_get):
mock_get.return_value = (DummyWebhookTrigger(), "workflow", "node_config")
response, status = module.handle_webhook("wh-1")
assert status == 400
assert response["error"] == "Bad Request"
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow", side_effect=ValueError("missing"))
def test_value_error_not_found(self, mock_get):
with pytest.raises(NotFound):
module.handle_webhook("wh-1")
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow", side_effect=RequestEntityTooLarge())
def test_request_entity_too_large(self, mock_get):
with pytest.raises(RequestEntityTooLarge):
module.handle_webhook("wh-1")
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow", side_effect=Exception("boom"))
def test_internal_error(self, mock_get):
response, status = module.handle_webhook("wh-1")
assert status == 500
assert response["error"] == "Internal server error"
class TestHandleWebhookDebug:
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow")
@patch.object(module.WebhookService, "extract_and_validate_webhook_data")
@patch.object(module.WebhookService, "build_workflow_inputs", return_value={"x": 1})
@patch.object(module.TriggerDebugEventBus, "dispatch")
@patch.object(module.WebhookService, "generate_webhook_response")
def test_debug_success(
self,
mock_generate,
mock_dispatch,
mock_build_inputs,
mock_extract,
mock_get,
):
mock_get.return_value = (DummyWebhookTrigger(), None, "node_config")
mock_extract.return_value = {"method": "POST"}
mock_generate.return_value = ({"ok": True}, 200)
response, status = module.handle_webhook_debug("wh-1")
assert status == 200
assert response["ok"] is True
mock_dispatch.assert_called_once()
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow")
@patch.object(module.WebhookService, "extract_and_validate_webhook_data", side_effect=ValueError("bad"))
def test_debug_bad_request(self, mock_extract, mock_get):
mock_get.return_value = (DummyWebhookTrigger(), None, "node_config")
response, status = module.handle_webhook_debug("wh-1")
assert status == 400
assert response["error"] == "Bad Request"
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow", side_effect=ValueError("missing"))
def test_debug_not_found(self, mock_get):
with pytest.raises(NotFound):
module.handle_webhook_debug("wh-1")
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow", side_effect=RequestEntityTooLarge())
def test_debug_request_entity_too_large(self, mock_get):
with pytest.raises(RequestEntityTooLarge):
module.handle_webhook_debug("wh-1")
@patch.object(module.WebhookService, "get_webhook_trigger_and_workflow", side_effect=Exception("boom"))
def test_debug_internal_error(self, mock_get):
response, status = module.handle_webhook_debug("wh-1")
assert status == 500
assert response["error"] == "Internal server error"