diff --git a/api/core/helper/credential_utils.py b/api/core/helper/credential_utils.py index b5f68039d7b..e8f3ba0a547 100644 --- a/api/core/helper/credential_utils.py +++ b/api/core/helper/credential_utils.py @@ -10,18 +10,21 @@ def is_credential_exists(credential_id: str, credential_type: "PluginCredentialT """ Check if the credential still exists in the database. + Uses the configured SQLAlchemy session factory instead of Flask-SQLAlchemy's + ``db.engine`` because workflow graph node construction may run without an + active Flask application context. + :param credential_id: The credential ID to check :param credential_type: The type of credential (MODEL or TOOL) :return: True if credential exists, False otherwise """ from sqlalchemy import select - from sqlalchemy.orm import Session - from extensions.ext_database import db + from core.db import session_factory from models.provider import ProviderCredential, ProviderModelCredential from models.tools import BuiltinToolProvider - with Session(db.engine) as session: + with session_factory.create_session() as session: if credential_type == PluginCredentialType.MODEL: # Check both pre-defined and custom model credentials using a single UNION query stmt = ( diff --git a/api/tests/unit_tests/core/helper/test_credential_utils.py b/api/tests/unit_tests/core/helper/test_credential_utils.py index dd10f81b02a..3311bbb006a 100644 --- a/api/tests/unit_tests/core/helper/test_credential_utils.py +++ b/api/tests/unit_tests/core/helper/test_credential_utils.py @@ -114,10 +114,11 @@ def test_is_credential_exists_by_type( scalar_result: str | None, expected: bool, ) -> None: - mocker.patch("extensions.ext_database.db", new=SimpleNamespace(engine=object())) - session_cls = mocker.patch("sqlalchemy.orm.Session") - session = session_cls.return_value.__enter__.return_value + session = mocker.MagicMock() session.scalar.return_value = scalar_result + session_context = mocker.MagicMock() + session_context.__enter__.return_value = session + mocker.patch("core.db.session_factory.create_session", return_value=session_context) result = is_credential_exists("cred-1", credential_type) @@ -128,11 +129,33 @@ def test_is_credential_exists_by_type( def test_is_credential_exists_returns_false_for_unknown_type( mocker: MockerFixture, ) -> None: - mocker.patch("extensions.ext_database.db", new=SimpleNamespace(engine=object())) - session_cls = mocker.patch("sqlalchemy.orm.Session") - session = session_cls.return_value.__enter__.return_value + session = mocker.MagicMock() + session_context = mocker.MagicMock() + session_context.__enter__.return_value = session + mocker.patch("core.db.session_factory.create_session", return_value=session_context) result = is_credential_exists("cred-1", cast(PluginCredentialType, "unknown")) assert result is False session.scalar.assert_not_called() + + +def test_is_credential_exists_uses_configured_session_factory_without_flask_app_context( + mocker: MockerFixture, +) -> None: + class RaisingDB: + @property + def engine(self): + raise RuntimeError("Working outside of application context.") + + session = mocker.MagicMock() + session.scalar.return_value = "model-credential" + session_context = mocker.MagicMock() + session_context.__enter__.return_value = session + create_session = mocker.patch("core.db.session_factory.create_session", return_value=session_context) + mocker.patch("extensions.ext_database.db", new=RaisingDB()) + + result = is_credential_exists("cred-1", PluginCredentialType.MODEL) + + assert result is True + create_session.assert_called_once_with()