From c013fd952ae6b53475b9db902dc2aee6978fb7c4 Mon Sep 17 00:00:00 2001 From: Janardan S Kavia Date: Thu, 18 Jun 2026 12:05:34 -0400 Subject: [PATCH] fix: warn on empty value and decrypt failure in get_all variable listing --- .../langflow/services/variable/service.py | 9 ++- .../unit/services/variable/test_service.py | 63 ++++++++++++++++++- 2 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/backend/base/langflow/services/variable/service.py b/src/backend/base/langflow/services/variable/service.py index 41a7e51ea8..f19b62ce25 100644 --- a/src/backend/base/langflow/services/variable/service.py +++ b/src/backend/base/langflow/services/variable/service.py @@ -238,9 +238,16 @@ class DatabaseVariableService(VariableService, Service): for variable in variables: value = None if variable.type == GENERIC_TYPE: + if not variable.value: + await logger.awarning("Variable '%s' has no stored value — skipping.", variable.name) + continue value = auth_utils.decrypt_api_key(variable.value) if not value: - # If decryption fails (likely due to encryption by different key), skip this variable + await logger.awarning( + "Variable '%s' could not be decrypted — likely encrypted with a different " + "LANGFLOW_SECRET_KEY. Skipping.", + variable.name, + ) continue # Model validate will set value to None if credential type diff --git a/src/backend/tests/unit/services/variable/test_service.py b/src/backend/tests/unit/services/variable/test_service.py index 517e770ffc..de5922e615 100644 --- a/src/backend/tests/unit/services/variable/test_service.py +++ b/src/backend/tests/unit/services/variable/test_service.py @@ -1,6 +1,6 @@ import secrets from datetime import datetime -from unittest.mock import patch +from unittest.mock import AsyncMock, MagicMock, patch from uuid import uuid4 import pytest @@ -394,6 +394,67 @@ async def test_update_generic_variable_with_fernet_signature_fails(service, sess await service.update_variable(user_id, "TEST_VAR", "gAAAAABthis-looks-like-encrypted", session=session) +async def test_get_all__empty_value_warns_and_skips(service, session: AsyncSession): + """get_all warns and skips GENERIC variables whose stored value is None or empty.""" + user_id = uuid4() + + # Create a normal generic variable, then manually blank its value to simulate a bad DB row. + var = await service.create_variable(user_id, "EMPTY_VAR", "initial", type_=GENERIC_TYPE, session=session) + var.value = "" + session.add(var) + await session.flush() + + mock_logger = MagicMock() + mock_logger.awarning = AsyncMock() + with patch("langflow.services.variable.service.logger", mock_logger): + result = await service.get_all(user_id, session=session) + + # Variable should be excluded from results. + assert not any(v.name == "EMPTY_VAR" for v in result) + # Warning must name the variable and mention empty value. + warning_calls = [str(c) for c in mock_logger.awarning.call_args_list] + assert any("EMPTY_VAR" in c for c in warning_calls) + assert any("no stored value" in c for c in warning_calls) + + +async def test_get_all__decrypt_failure_warns_and_skips(service, session: AsyncSession): + """get_all warns with a key-mismatch message when decrypt returns empty for a non-empty value.""" + user_id = uuid4() + + await service.create_variable(user_id, "MY_VAR", "real_value", type_=GENERIC_TYPE, session=session) + + mock_logger = MagicMock() + mock_logger.awarning = AsyncMock() + # Simulate decrypt returning "" (key mismatch) without touching the stored value. + with ( + patch("langflow.services.variable.service.auth_utils.decrypt_api_key", return_value=""), + patch("langflow.services.variable.service.logger", mock_logger), + ): + result = await service.get_all(user_id, session=session) + + # Variable should be excluded from results. + assert not any(v.name == "MY_VAR" for v in result) + # Warning must name the variable and mention SECRET_KEY. + warning_calls = [str(c) for c in mock_logger.awarning.call_args_list] + assert any("MY_VAR" in c for c in warning_calls) + assert any("SECRET_KEY" in c for c in warning_calls) + + +async def test_get_all__healthy_generic_variable_included(service, session: AsyncSession): + """get_all includes GENERIC variables that decrypt successfully — no warnings emitted.""" + user_id = uuid4() + + await service.create_variable(user_id, "GOOD_VAR", "good_value", type_=GENERIC_TYPE, session=session) + + mock_logger = MagicMock() + mock_logger.awarning = AsyncMock() + with patch("langflow.services.variable.service.logger", mock_logger): + result = await service.get_all(user_id, session=session) + + assert any(v.name == "GOOD_VAR" for v in result) + mock_logger.awarning.assert_not_called() + + async def test_create_credential_variable_with_fernet_signature_succeeds(service, session: AsyncSession): """Test that CREDENTIAL variables can have values that look like Fernet tokens (they get encrypted anyway).""" user_id = uuid4()