diff --git a/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py b/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py index d08106aa5f2..d639f442d93 100644 --- a/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_ops_trace_api.py @@ -6,6 +6,7 @@ from unittest.mock import MagicMock, PropertyMock, patch import pytest from flask import Flask +from sqlalchemy.orm import Session from werkzeug.exceptions import Forbidden from controllers.common import wraps as common_wraps @@ -15,6 +16,7 @@ from controllers.console.app import ops_trace as ops_trace_module from controllers.console.app import wraps as app_wraps from libs import login as login_lib from models.account import Account, AccountStatus, TenantAccountRole +from models.model import App, AppMode, IconType def _make_account(role: TenantAccountRole) -> Account: @@ -137,11 +139,33 @@ def test_trace_config_mutations_require_rbac_permission( payload: dict[str, object] | None, service_method_name: str, service_result: object, + sqlite_session: Session, ) -> None: app.config.setdefault("RESTX_MASK_HEADER", "X-Fields") account = _make_account(TenantAccountRole.NORMAL) _patch_console_guards(monkeypatch, account, _make_app(), rbac_enabled=True) - monkeypatch.setattr(common_wraps.db, "session", SimpleNamespace(scalar=lambda _stmt: "other-account")) + owned_app = App() + owned_app.id = "app-123" + owned_app.tenant_id = "tenant-123" + owned_app.name = "Trace app" + owned_app.description = "" + owned_app.mode = AppMode.CHAT + owned_app.icon_type = IconType.EMOJI + owned_app.icon = "robot" + owned_app.icon_background = "#ffffff" + owned_app.enable_site = False + owned_app.enable_api = False + owned_app.api_rpm = 0 + owned_app.api_rph = 0 + owned_app.is_demo = False + owned_app.is_public = False + owned_app.is_universal = False + owned_app.max_active_requests = None + owned_app.maintainer = "other-account" + owned_app.use_icon_as_answer_icon = False + sqlite_session.add(owned_app) + sqlite_session.commit() + monkeypatch.setattr(common_wraps.db, "session", sqlite_session) monkeypatch.setattr(common_wraps.RBACService.CheckAccess, "check", MagicMock(return_value=False)) service_mock = MagicMock(return_value=service_result) monkeypatch.setattr(ops_trace_module.OpsService, service_method_name, service_mock) diff --git a/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py b/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py index 14386efda37..848d57c87c0 100644 --- a/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py +++ b/api/tests/unit_tests/controllers/console/app/test_workflow_trigger_api.py @@ -1,14 +1,64 @@ from __future__ import annotations import inspect +import uuid from datetime import UTC, datetime from types import SimpleNamespace -from unittest.mock import MagicMock, PropertyMock, patch +from unittest.mock import PropertyMock, patch +import pytest from flask import Flask +from sqlalchemy import Engine +from sqlalchemy.orm import Session from controllers.console import console_ns from controllers.console.app import workflow_trigger as workflow_trigger_module +from models.base import TypeBase +from models.enums import AppTriggerStatus, AppTriggerType +from models.model import App, AppMode, IconType +from models.trigger import AppTrigger + + +@pytest.fixture +def database_session(sqlite_engine: Engine, monkeypatch: pytest.MonkeyPatch): + models = (App, AppTrigger) + tables = [model.metadata.tables[model.__tablename__] for model in models] + TypeBase.metadata.create_all(sqlite_engine, tables=tables) + monkeypatch.setattr(workflow_trigger_module, "db", SimpleNamespace(engine=sqlite_engine)) + with Session(sqlite_engine, expire_on_commit=False) as session: + yield session + + +def _persist_app_trigger( + session: Session, + *, + tenant_id: str | None = None, + status: AppTriggerStatus = AppTriggerStatus.ENABLED, +) -> tuple[App, AppTrigger]: + tenant_id = tenant_id or str(uuid.uuid4()) + app_model = App( + id=str(uuid.uuid4()), + tenant_id=tenant_id, + name="Workflow App", + mode=AppMode.WORKFLOW, + icon_type=IconType.EMOJI, + icon="workflow", + icon_background="#FFFFFF", + enable_site=False, + enable_api=True, + ) + trigger = AppTrigger( + tenant_id=tenant_id, + app_id=app_model.id, + node_id="node-1", + trigger_type=AppTriggerType.TRIGGER_PLUGIN, + title="Trigger", + provider_name="provider", + status=status, + ) + session.add_all([app_model, trigger]) + session.commit() + return app_model, trigger def test_parser_models_validate(): @@ -23,22 +73,20 @@ def test_parser_models_validate(): def test_workflow_trigger_response_serializes_datetime(): created_at = datetime(2026, 1, 2, 3, 4, 5, tzinfo=UTC) - trigger = SimpleNamespace( - id="trigger-1", - trigger_type="trigger-plugin", + response = workflow_trigger_module.WorkflowTriggerResponse( + id=str(uuid.uuid4()), + trigger_type=AppTriggerType.TRIGGER_PLUGIN, title="Trigger", node_id="node-1", provider_name="provider", icon="https://example.com/icon", - status="enabled", + status=AppTriggerStatus.ENABLED, created_at=created_at, updated_at=created_at, ) - payload = workflow_trigger_module.WorkflowTriggerResponse.model_validate(trigger, from_attributes=True).model_dump( - mode="json" - ) - assert payload["id"] == "trigger-1" + payload = response.model_dump(mode="json") + assert payload["id"] == response.id assert payload["created_at"] == "2026-01-02T03:04:05Z" assert payload["updated_at"] == "2026-01-02T03:04:05Z" @@ -59,51 +107,33 @@ def test_webhook_trigger_response_serializes_datetime(): assert payload["created_at"] == "2026-01-02T03:04:05Z" -def test_app_triggers_get_uses_injected_tenant_id(app: Flask) -> None: - trigger = SimpleNamespace( - id="trigger-1", - trigger_type="trigger-plugin", - title="Trigger", +def test_app_triggers_get_uses_injected_tenant_id(app: Flask, database_session: Session) -> None: + app_model, trigger = _persist_app_trigger(database_session) + other_tenant_trigger = AppTrigger( + tenant_id=str(uuid.uuid4()), + app_id=app_model.id, node_id="node-1", + trigger_type=AppTriggerType.TRIGGER_PLUGIN, + title="Other Tenant Trigger", provider_name="provider", - icon="", - status="enabled", - created_at=None, - updated_at=None, + status=AppTriggerStatus.ENABLED, ) - session = MagicMock() - session.execute.return_value.scalars.return_value.all.return_value = [trigger] + database_session.add(other_tenant_trigger) + database_session.commit() api = workflow_trigger_module.AppTriggersApi() method = inspect.unwrap(api.get) - with ( - app.test_request_context("/"), - patch.object(type(workflow_trigger_module.db), "engine", new_callable=PropertyMock, return_value=MagicMock()), - patch("controllers.console.app.workflow_trigger.sessionmaker") as sessionmaker_mock, - ): - sessionmaker_mock.return_value.begin.return_value.__enter__.return_value = session - response = method(api, "tenant-1", SimpleNamespace(id="app-1")) + with app.test_request_context("/"): + response = method(api, app_model.tenant_id, app_model) - assert response["data"][0]["id"] == "trigger-1" + assert [item["id"] for item in response["data"]] == [trigger.id] assert response["data"][0]["icon"].endswith("/provider/icon") -def test_app_trigger_enable_uses_injected_tenant_id(app: Flask) -> None: - trigger = SimpleNamespace( - id="trigger-1", - trigger_type="trigger-plugin", - title="Trigger", - node_id="node-1", - provider_name="provider", - icon="", - status="disabled", - created_at=None, - updated_at=None, - ) - session = MagicMock() - session.execute.return_value.scalar_one_or_none.return_value = trigger - payload = {"trigger_id": "trigger-1", "enable_trigger": True} +def test_app_trigger_enable_uses_injected_tenant_id(app: Flask, database_session: Session) -> None: + app_model, trigger = _persist_app_trigger(database_session, status=AppTriggerStatus.DISABLED) + payload = {"trigger_id": trigger.id, "enable_trigger": True} api = workflow_trigger_module.AppTriggerEnableApi() method = inspect.unwrap(api.post) @@ -111,11 +141,12 @@ def test_app_trigger_enable_uses_injected_tenant_id(app: Flask) -> None: with ( app.test_request_context("/", json=payload), patch.object(type(console_ns), "payload", new_callable=PropertyMock, return_value=payload), - patch.object(type(workflow_trigger_module.db), "engine", new_callable=PropertyMock, return_value=MagicMock()), - patch("controllers.console.app.workflow_trigger.sessionmaker") as sessionmaker_mock, ): - sessionmaker_mock.return_value.begin.return_value.__enter__.return_value = session - response = method(api, "tenant-1", SimpleNamespace(id="app-1")) + response = method(api, app_model.tenant_id, app_model) - assert response["id"] == "trigger-1" + assert response["id"] == trigger.id assert response["status"] == "enabled" + database_session.expire_all() + persisted_trigger = database_session.get(AppTrigger, trigger.id) + assert persisted_trigger is not None + assert persisted_trigger.status == AppTriggerStatus.ENABLED