Files
dify/api/tests/unit_tests/controllers/web/test_workflow.py
T
林玮 (Jade Lin) febb5f949f Merge remote-tracking branch 'origin/main' into linw/snp-911-end-user-auth
# Conflicts:
#	api/.importlinter
#	api/controllers/console/__init__.py
#	api/controllers/console/workspace/account.py
#	api/core/app/apps/message_based_app_generator.py
#	api/core/app/entities/app_invoke_entities.py
#	api/core/tools/tool_manager.py
#	api/extensions/application_services/installed_app.py
#	api/extensions/ext_application_services.py
#	api/models/__init__.py
#	api/models/workflow.py
#	api/openapi/markdown/console-openapi.md
#	api/openapi/markdown/web-openapi.md
#	api/repositories/account/repository.py
#	api/repositories/api_workflow_run_repository.py
#	api/repositories/app/console_repository.py
#	api/repositories/app_definition_query_repository.py
#	api/services/app_definition_query_service.py
#	api/services/app_generate_service.py
#	api/services/conversation_service.py
#	api/services/message_service.py
#	api/services/web_conversation_service.py
#	api/tasks/app_generate/workflow_execute_task.py
#	api/tests/unit_tests/controllers/console/auth/test_login.py
#	api/tests/unit_tests/controllers/web/test_app.py
#	api/tests/unit_tests/core/app/apps/advanced_chat/test_app_generator.py
#	api/tests/unit_tests/repositories/app/test_console_repository.py
#	api/tests/unit_tests/services/test_app_definition_query_service.py
#	api/tests/unit_tests/services/test_app_generate_service.py
#	api/tests/unit_tests/services/test_message_service.py
#	docker/envs/core-services/api.env.example
#	packages/contracts/generated/api/console/account/types.gen.ts
#	packages/contracts/generated/api/console/account/zod.gen.ts
#	packages/contracts/generated/api/console/apps/errors.gen.ts
#	packages/contracts/generated/api/console/apps/orpc.gen.ts
#	packages/contracts/generated/api/console/errors.gen.ts
#	packages/contracts/generated/api/console/workspaces/errors.gen.ts
#	packages/contracts/generated/api/console/workspaces/orpc.gen.ts
#	packages/contracts/generated/api/web/errors.gen.ts
#	packages/contracts/generated/api/web/orpc.gen.ts
#	packages/contracts/generated/api/web/types.gen.ts
#	packages/contracts/generated/api/web/zod.gen.ts
2026-11-05 14:17:44 +08:00

145 lines
6.0 KiB
Python

"""Unit tests for controllers.web.workflow endpoints."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
from flask import Flask
from controllers.web.error import (
NotWorkflowAppError,
ProviderNotInitializeError,
ProviderQuotaExceededError,
TriggerWorkflowServiceModeUnavailableError,
)
from controllers.web.workflow import WorkflowRunApi, WorkflowTaskStopApi
from core.errors.error import ProviderTokenNotInitError, QuotaExceededError
from models.model import App, AppMode, EndUser
from services.errors.app import (
TriggerWorkflowServiceModeUnavailableError as TriggerWorkflowServiceModeUnavailableServiceError,
)
from tests.helpers.unit.model_factories import make_end_user
def _workflow_app() -> App:
return App(id="app-1", tenant_id="tenant-1", mode=AppMode.WORKFLOW)
def _chat_app() -> App:
return App(id="app-1", tenant_id="tenant-1", mode=AppMode.CHAT)
def _end_user() -> EndUser:
return make_end_user(end_user_id="eu-1")
# ---------------------------------------------------------------------------
# WorkflowRunApi
# ---------------------------------------------------------------------------
class TestWorkflowRunApi:
def test_wrong_mode_raises(self, app: Flask) -> None:
with app.test_request_context("/workflows/run", method="POST"):
with pytest.raises(NotWorkflowAppError):
WorkflowRunApi().post(_chat_app(), _end_user())
@patch("controllers.web.workflow.helper.compact_generate_response", return_value={"result": "ok"})
@patch("controllers.web.workflow.AppGenerateService.generate")
@patch("controllers.web.workflow.web_ns")
def test_happy_path(self, mock_ns: MagicMock, mock_gen: MagicMock, mock_compact: MagicMock, app: Flask) -> None:
mock_ns.payload = {"inputs": {"key": "val"}}
mock_gen.return_value = "response"
with app.test_request_context("/workflows/run", method="POST"):
result = WorkflowRunApi().post(_workflow_app(), _end_user())
assert result == {"result": "ok"}
@patch(
"controllers.web.workflow.AppGenerateService.generate",
side_effect=ProviderTokenNotInitError(description="not init"),
)
@patch("controllers.web.workflow.web_ns")
def test_provider_not_init(self, mock_ns: MagicMock, mock_gen: MagicMock, app: Flask) -> None:
empty_inputs: dict[str, object] = {}
mock_ns.payload = {"inputs": empty_inputs}
with app.test_request_context("/workflows/run", method="POST"):
with pytest.raises(ProviderNotInitializeError):
WorkflowRunApi().post(_workflow_app(), _end_user())
@patch(
"controllers.web.workflow.AppGenerateService.generate",
side_effect=TriggerWorkflowServiceModeUnavailableServiceError(),
)
@patch("controllers.web.workflow.web_ns")
def test_trigger_workflow_returns_stable_unavailable_error(
self,
mock_ns: MagicMock,
mock_gen: MagicMock,
app: Flask,
) -> None:
empty_inputs: dict[str, object] = {}
mock_ns.payload = {"inputs": empty_inputs}
with app.test_request_context("/workflows/run", method="POST"):
with pytest.raises(TriggerWorkflowServiceModeUnavailableError) as exc_info:
WorkflowRunApi().post(_workflow_app(), _end_user())
assert exc_info.value.code == 403
assert exc_info.value.error_code == "trigger_workflow_service_mode_unavailable"
@patch(
"controllers.web.workflow.AppGenerateService.generate",
side_effect=QuotaExceededError(),
)
@patch("controllers.web.workflow.web_ns")
def test_quota_exceeded(self, mock_ns: MagicMock, mock_gen: MagicMock, app: Flask) -> None:
empty_inputs: dict[str, object] = {}
mock_ns.payload = {"inputs": empty_inputs}
with app.test_request_context("/workflows/run", method="POST"):
with pytest.raises(ProviderQuotaExceededError):
WorkflowRunApi().post(_workflow_app(), _end_user())
# ---------------------------------------------------------------------------
# WorkflowTaskStopApi
# ---------------------------------------------------------------------------
class TestWorkflowTaskStopApi:
def test_wrong_mode_raises(self, app: Flask) -> None:
with app.test_request_context("/workflows/tasks/task-1/stop", method="POST"):
with pytest.raises(NotWorkflowAppError):
WorkflowTaskStopApi().post(_chat_app(), _end_user(), "task-1")
@patch("controllers.web.workflow.AppTaskService.stop_workflow_task")
def test_stop_passes_app_scope(self, stop: MagicMock, app: Flask) -> None:
with app.test_request_context("/workflows/tasks/task-1/stop", method="POST"):
result = WorkflowTaskStopApi().post(_workflow_app(), _end_user(), "task-1")
assert result == {"result": "success"}
stop.assert_called_once_with(
tenant_id="tenant-1", app_id="app-1", task_id="task-1", app_mode=AppMode.WORKFLOW, owner=None
)
@pytest.mark.parametrize("failure", ["busy", "resources_changed"])
def test_workflow_admission_failures_return_retryable_conflicts(
app: Flask, monkeypatch: pytest.MonkeyPatch, failure: str
) -> None:
from controllers.web.workflow import AppGenerateService
from libs.exception import BaseHTTPException
from repositories.entities.web_execution import EndUserBusyError, WebExecutionResourcesChangedError
error = EndUserBusyError() if failure == "busy" else WebExecutionResourcesChangedError()
def reject(*_args: object, **_kwargs: object) -> None:
raise error
monkeypatch.setattr(AppGenerateService, "generate", reject)
with app.test_request_context("/workflows/run", method="POST", json={"inputs": {}}):
with pytest.raises(BaseHTTPException) as caught:
WorkflowRunApi().post(_workflow_app(), _end_user())
assert caught.value.code == 409
assert caught.value.error_code == ("end_user_busy" if failure == "busy" else "webapp_resources_changed")