From ef7c5285a4d8bd0b431eb9b7d3dba21efcc4cdcd Mon Sep 17 00:00:00 2001 From: mesrop2008 Date: Wed, 14 Oct 2026 14:56:08 +0800 Subject: [PATCH 1/3] fix(api): resolve new pyrefly unreachable-except and unwrap diagnostics pyrefly 1.4 flags except clauses that can never match and fails to resolve the signature of methods stacked under setup_required. - Collapse except tuples whose members are already covered by ValueError (pydantic ValidationError, JSONDecodeError, UnicodeDecodeError, binascii.Error, DocumentIndexingError) or by AgentAppGeneratorError. - Annotate the CSRF _unauthorized helper as NoReturn and drop the trailing raise that existed only for the type checker. - Remove dead validation branches guarded by required Literal fields (KnowledgeConfig.indexing_technique, PreProcessingRule.id) and the model_construct-based test that targeted one of them. - Cast decorated controller methods at the test boundary before unwrapping them. Fixes #44038 --- api/controllers/console/app/workflow.py | 3 +-- .../rag_pipeline/rag_pipeline_workflow.py | 4 +-- api/controllers/console/explore/trial.py | 4 +-- api/core/plugin/plugin_service.py | 4 +-- api/core/provider_manager.py | 5 ++-- api/core/workflow/file_reference.py | 2 +- api/libs/oauth.py | 3 +-- api/libs/token.py | 4 +-- .../data_source/credential_repository.py | 4 +-- .../knowledge/document_repository.py | 4 +-- api/services/knowledge/dataset_service.py | 3 --- api/services/knowledge/documents/adapters.py | 4 +-- api/services/tools/tools_transform_service.py | 2 +- .../console/workspace/test_skills.py | 4 +-- .../files/test_plugin_file_upload.py | 21 ++++++++++----- .../inner_api/app/test_file_grants.py | 3 ++- .../services/document_service_validation.py | 27 ------------------- 17 files changed, 37 insertions(+), 64 deletions(-) diff --git a/api/controllers/console/app/workflow.py b/api/controllers/console/app/workflow.py index 255cc684dd060b..8bb2bc14f11527 100644 --- a/api/controllers/console/app/workflow.py +++ b/api/controllers/console/app/workflow.py @@ -12,7 +12,6 @@ ConfigDict, Field, RootModel, - ValidationError, field_validator, model_validator, ) @@ -651,7 +650,7 @@ def post(self, current_user: Account, app_model: App): elif "text/plain" in content_type: try: args_model = SyncDraftWorkflowPayload.model_validate_json(request.data) - except (ValueError, ValidationError): + except ValueError: return {"message": "Invalid JSON data"}, 400 else: abort(415) diff --git a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py index 08532d09cec7b3..b69c67fe8a3158 100644 --- a/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py +++ b/api/controllers/console/datasets/rag_pipeline/rag_pipeline_workflow.py @@ -5,7 +5,7 @@ from flask import abort, request from flask_restx import Resource -from pydantic import BaseModel, Field, RootModel, ValidationError +from pydantic import BaseModel, Field, RootModel from sqlalchemy.orm import Session, sessionmaker from werkzeug.exceptions import BadRequest, Forbidden, InternalServerError, NotFound @@ -248,7 +248,7 @@ def post(self, current_user: Account, pipeline: Pipeline): elif "text/plain" in content_type: try: payload = DraftWorkflowSyncPayload.model_validate_json(request.data) - except (ValueError, ValidationError): + except ValueError: return {"message": "Invalid JSON data"}, 400 else: abort(415) diff --git a/api/controllers/console/explore/trial.py b/api/controllers/console/explore/trial.py index b504885ccba71e..f0a66b544344a9 100644 --- a/api/controllers/console/explore/trial.py +++ b/api/controllers/console/explore/trial.py @@ -64,7 +64,7 @@ from controllers.console.remote_files import RemoteFileUploadPayload, upload_remote_file from controllers.console.wraps import cloud_edition_billing_resource_check, model_validate from controllers.web.error import InvokeRateLimitError as InvokeRateLimitHttpError -from core.app.apps.agent_app.errors import AgentAppGeneratorError, AgentAppNotPublishedError +from core.app.apps.agent_app.errors import AgentAppGeneratorError from core.app.entities.app_invoke_entities import InvokeFrom from core.errors.error import ( AppInvokeQuotaExceededError, @@ -611,7 +611,7 @@ def post(self, req_data: ChatRequest, request_context: RequestContext, trial_app except services.errors.app_model_config.AppModelConfigBrokenError: logger.exception("App model config broken.") raise AppUnavailableError() - except (AgentAppGeneratorError, AgentAppNotPublishedError): + except AgentAppGeneratorError: raise AppUnavailableError() from None except ProviderTokenNotInitError as ex: raise ProviderNotInitializeError(ex.description) diff --git a/api/core/plugin/plugin_service.py b/api/core/plugin/plugin_service.py index d1cd948783dea9..a07fc7b496d78e 100644 --- a/api/core/plugin/plugin_service.py +++ b/api/core/plugin/plugin_service.py @@ -25,7 +25,7 @@ from typing import Literal, Protocol import zstandard -from pydantic import BaseModel, TypeAdapter, ValidationError +from pydantic import BaseModel, TypeAdapter from redis import RedisError from redis.exceptions import LockError from sqlalchemy import delete, select, update @@ -336,7 +336,7 @@ def _load_cached_plugin_model_providers_for_generation( try: providers = cls._get_or_parse_plugin_model_providers_cache_payload(cached_providers) return providers, True - except (TypeError, ValueError, ValidationError): + except (TypeError, ValueError): logger.warning( "Invalid cached plugin model providers for tenant %s; deleting cache key %s.", tenant_id, diff --git a/api/core/provider_manager.py b/api/core/provider_manager.py index b62c7747bb4835..9608016061417f 100644 --- a/api/core/provider_manager.py +++ b/api/core/provider_manager.py @@ -7,7 +7,6 @@ from collections.abc import Callable, Sequence from dataclasses import asdict, dataclass from enum import StrEnum -from json import JSONDecodeError from typing import TYPE_CHECKING, Any, Protocol, Self from pydantic import TypeAdapter, ValidationError @@ -1494,7 +1493,7 @@ def _get_and_decrypt_credentials( try: credentials = _credentials_adapter.validate_json(encrypted_config) - except (ValueError, JSONDecodeError): + except ValueError: return {} # Decrypt secret variables @@ -1796,7 +1795,7 @@ def _to_model_settings( provider_model_credentials = _credentials_adapter.validate_json( load_balancing_model_config.encrypted_config ) - except (ValueError, JSONDecodeError): + except ValueError: continue # Get decoding context for decrypting credentials diff --git a/api/core/workflow/file_reference.py b/api/core/workflow/file_reference.py index 25b4a59bfe1376..c2b3dc677c7806 100644 --- a/api/core/workflow/file_reference.py +++ b/api/core/workflow/file_reference.py @@ -62,7 +62,7 @@ def parse_file_reference(reference: str | None) -> FileReference | None: encoded_payload = reference.removeprefix(_FILE_REFERENCE_PREFIX) try: payload = json.loads(base64.urlsafe_b64decode(encoded_payload.encode())) - except (ValueError, json.JSONDecodeError): + except ValueError: return FileReference(record_id=reference) record_id = payload.get("record_id") diff --git a/api/libs/oauth.py b/api/libs/oauth.py index 79796e5f4c0eba..4d98a2a1517047 100644 --- a/api/libs/oauth.py +++ b/api/libs/oauth.py @@ -1,5 +1,4 @@ import base64 -import binascii import json import logging import urllib.parse @@ -99,7 +98,7 @@ def decode_oauth_state(state: str | None) -> OAuthState: padded_state = state + "=" * (-len(state) % 4) raw_state = base64.urlsafe_b64decode(padded_state.encode("ascii")).decode("utf-8") return OAUTH_STATE_ADAPTER.validate_python(json.loads(raw_state)) - except (binascii.Error, ValueError, UnicodeDecodeError, json.JSONDecodeError, ValidationError): + except ValueError: # Covers binascii.Error, UnicodeDecodeError, JSONDecodeError and ValidationError. return {} diff --git a/api/libs/token.py b/api/libs/token.py index 9fd684fe3058ac..7a955e5fc33086 100644 --- a/api/libs/token.py +++ b/api/libs/token.py @@ -2,6 +2,7 @@ import logging import re from datetime import UTC, datetime, timedelta +from typing import NoReturn from flask import Request from werkzeug.exceptions import Unauthorized @@ -200,7 +201,7 @@ def check_csrf_token(request: Request, user_id: str): if is_admin_api_key_request(request): return - def _unauthorized(): + def _unauthorized() -> NoReturn: raise Unauthorized("CSRF token is missing or invalid.") for pattern in CSRF_WHITE_LIST: @@ -219,7 +220,6 @@ def _unauthorized(): verified = PassportService().verify(csrf_token) except Exception: _unauthorized() - raise # unreachable, but helps the type checker see verified is always bound if verified.get("sub") != user_id: _unauthorized() diff --git a/api/repositories/data_source/credential_repository.py b/api/repositories/data_source/credential_repository.py index 3d818301808293..dc3f86df2244d5 100644 --- a/api/repositories/data_source/credential_repository.py +++ b/api/repositories/data_source/credential_repository.py @@ -3,7 +3,7 @@ from collections.abc import Mapping from typing import cast -from pydantic import TypeAdapter, ValidationError +from pydantic import TypeAdapter from sqlalchemy import select, update from sqlalchemy.engine import CursorResult from sqlalchemy.orm import Session, sessionmaker @@ -23,7 +23,7 @@ def _source_mapping(value: object) -> Mapping[str, object]: if isinstance(value, str): return _CREDENTIALS_ADAPTER.validate_json(value) return _CREDENTIALS_ADAPTER.validate_python(value) - except (TypeError, ValueError, ValidationError): + except (TypeError, ValueError): return {} diff --git a/api/repositories/knowledge/document_repository.py b/api/repositories/knowledge/document_repository.py index d5d9f8724f37d2..726ff40f63b3d1 100644 --- a/api/repositories/knowledge/document_repository.py +++ b/api/repositories/knowledge/document_repository.py @@ -2,7 +2,7 @@ from collections.abc import Mapping -from pydantic import TypeAdapter, ValidationError +from pydantic import TypeAdapter from sqlalchemy import exists, select, update from sqlalchemy.orm import Session, sessionmaker @@ -27,7 +27,7 @@ def _mapping(value: object) -> Mapping[str, object] | None: if isinstance(value, str): return _MAPPING_ADAPTER.validate_json(value) return _MAPPING_ADAPTER.validate_python(value) - except (TypeError, ValueError, ValidationError): + except (TypeError, ValueError): return {} diff --git a/api/services/knowledge/dataset_service.py b/api/services/knowledge/dataset_service.py index bc4ab0bc671343..b21a01b2f1efb8 100644 --- a/api/services/knowledge/dataset_service.py +++ b/api/services/knowledge/dataset_service.py @@ -3213,9 +3213,6 @@ def process_rule_args_validate(cls, knowledge_config: KnowledgeConfig): unique_pre_processing_rule_dicts = {} for pre_processing_rule in knowledge_config.process_rule.rules.pre_processing_rules: - if not pre_processing_rule.id: - raise ValueError("Process rule pre_processing_rules id is required") - if not isinstance(pre_processing_rule.enabled, bool): raise ValueError("Process rule pre_processing_rules enabled is invalid") diff --git a/api/services/knowledge/documents/adapters.py b/api/services/knowledge/documents/adapters.py index e61a702bcf0ac3..f907371de3ed07 100644 --- a/api/services/knowledge/documents/adapters.py +++ b/api/services/knowledge/documents/adapters.py @@ -344,8 +344,6 @@ def _creation_dataset( ) -> DocumentCreationDataset: with self._sessions() as session: dataset = _require_dataset(session, ref) - if not dataset.indexing_technique and not config.indexing_technique: - raise ValueError("indexing_technique is required.") self._actor(session, context) DatasetService.check_doc_form(dataset, config.doc_form, session=session) return DocumentCreationDataset( @@ -542,7 +540,7 @@ def change_status( DocumentService.batch_update_document_status( dataset, [doc.id for doc in documents], action, account, session ) - except (ValueError, DocumentIndexingError) as error: + except ValueError as error: raise DocumentInvalidActionError(str(error)) from error def sync_website(self, ref: DocumentRef) -> None: diff --git a/api/services/tools/tools_transform_service.py b/api/services/tools/tools_transform_service.py index ee5729a0c4cbd4..ecdf78a12cd204 100644 --- a/api/services/tools/tools_transform_service.py +++ b/api/services/tools/tools_transform_service.py @@ -59,7 +59,7 @@ def get_tool_provider_icon_url( parsed = emoji_icon_adapter.validate_json(icon) return {"background": parsed["background"], "content": parsed["content"]} return {"background": icon["background"], "content": icon["content"]} - except (ValueError, ValidationError, KeyError): + except (ValueError, KeyError): return {"background": "#252525", "content": "\ud83d\ude01"} case ToolProviderType.MCP: if isinstance(icon, Mapping): diff --git a/api/tests/unit_tests/controllers/console/workspace/test_skills.py b/api/tests/unit_tests/controllers/console/workspace/test_skills.py index 0ed28c0f3f3ab9..fd91eee9636718 100644 --- a/api/tests/unit_tests/controllers/console/workspace/test_skills.py +++ b/api/tests/unit_tests/controllers/console/workspace/test_skills.py @@ -618,7 +618,7 @@ def test_export_skill_maps_service_error(app: Flask, request_context: RequestCon def test_inner_api_pulls_published_skill_archive(app: Flask) -> None: api = PublishedSkillPullApi() - method = unwrap(api.get) + method = unwrap(cast(Callable[..., Response], api.get)) service, pull_published_archive = _recording_service( "pull_published_archive", result=PublishedSkillArchive( @@ -645,7 +645,7 @@ def test_inner_api_pulls_published_skill_archive(app: Flask) -> None: def test_inner_api_pull_maps_missing_tenant_to_invalid_request(app: Flask) -> None: api = PublishedSkillPullApi() - method = unwrap(api.get) + method = unwrap(cast(Callable[..., tuple[dict[str, str], int]], api.get)) with app.test_request_context("/", method="GET"): payload, status = method(api, "skill-1") diff --git a/api/tests/unit_tests/controllers/files/test_plugin_file_upload.py b/api/tests/unit_tests/controllers/files/test_plugin_file_upload.py index 704afa3087833a..18bd149f97899f 100644 --- a/api/tests/unit_tests/controllers/files/test_plugin_file_upload.py +++ b/api/tests/unit_tests/controllers/files/test_plugin_file_upload.py @@ -2,6 +2,7 @@ import types from collections.abc import Callable from inspect import unwrap +from typing import cast from unittest.mock import MagicMock, patch import pytest @@ -9,6 +10,7 @@ from pydantic import ValidationError import controllers.files.plugin_file_upload as module +from controllers.common.schema import JsonResponseWithStatus from controllers.files import bp as files_blueprint from core.workflow.file_reference import build_file_reference from enums import DeploymentEdition @@ -68,6 +70,11 @@ def _valid_args() -> dict[str, object]: } +def _post_upload() -> JsonResponseWithStatus: + api = module.PluginUploadFileApi() + return unwrap(cast(Callable[..., object], api.post))(api) + + @pytest.fixture def files_app(config_overrides: Callable[..., None]) -> Flask: config_overrides(DEPLOYMENT_EDITION=DeploymentEdition.CLOUD) @@ -96,7 +103,7 @@ def test_upload_returns_the_existing_plugin_file_contract( service = application_services.return_value.plugin_file_uploads service.upload.return_value = _result() - response, status = unwrap(module.PluginUploadFileApi().post)(module.PluginUploadFileApi()) + response, status = _post_upload() assert status == 201 assert response == { @@ -134,7 +141,7 @@ def test_missing_file_has_a_specific_client_error(self, monkeypatch: pytest.Monk monkeypatch.setattr(module, "request", _fake_request(_valid_args())) with pytest.raises(module.NoFileUploadedError): - unwrap(module.PluginUploadFileApi().post)(module.PluginUploadFileApi()) + _post_upload() @pytest.mark.parametrize( ("file", "expected_error"), @@ -152,7 +159,7 @@ def test_invalid_file_metadata_has_a_specific_client_error( monkeypatch.setattr(module, "request", _fake_request(_valid_args(), file=file)) with pytest.raises(expected_error): - unwrap(module.PluginUploadFileApi().post)(module.PluginUploadFileApi()) + _post_upload() @patch.object(module, "application_services") def test_access_denied_is_reported_without_leaking_identity_details( @@ -164,7 +171,7 @@ def test_access_denied_is_reported_without_leaking_identity_details( application_services.return_value.plugin_file_uploads.upload.side_effect = PluginFileUploadAccessDeniedError() with pytest.raises(module.InvalidPluginFileUploadError) as error_info: - unwrap(module.PluginUploadFileApi().post)(module.PluginUploadFileApi()) + _post_upload() assert error_info.value.code == 403 assert error_info.value.error_code == "invalid_plugin_file_upload" @@ -181,7 +188,7 @@ def test_signed_size_limit_is_reported_as_413( ) with pytest.raises(module.FileTooLargeError) as error_info: - unwrap(module.PluginUploadFileApi().post)(module.PluginUploadFileApi()) + _post_upload() assert error_info.value.code == 413 assert error_info.value.__cause__ is not None @@ -196,7 +203,7 @@ def test_unsupported_file_type_is_reported_as_415( application_services.return_value.plugin_file_uploads.upload.side_effect = ServiceUnsupportedFileTypeError() with pytest.raises(module.UnsupportedFileTypeError) as error_info: - unwrap(module.PluginUploadFileApi().post)(module.PluginUploadFileApi()) + _post_upload() assert error_info.value.code == 415 assert error_info.value.__cause__ is not None @@ -212,7 +219,7 @@ def test_unexpected_failure_is_not_relabelled_as_a_client_error( application_services.return_value.plugin_file_uploads.upload.side_effect = storage_error with pytest.raises(OSError) as error_info: - unwrap(module.PluginUploadFileApi().post)(module.PluginUploadFileApi()) + _post_upload() assert error_info.value is storage_error diff --git a/api/tests/unit_tests/controllers/inner_api/app/test_file_grants.py b/api/tests/unit_tests/controllers/inner_api/app/test_file_grants.py index a0b91cc5f1cd57..cd02a34f76fcff 100644 --- a/api/tests/unit_tests/controllers/inner_api/app/test_file_grants.py +++ b/api/tests/unit_tests/controllers/inner_api/app/test_file_grants.py @@ -6,6 +6,7 @@ from collections.abc import Callable, Iterator from pathlib import Path from types import SimpleNamespace +from typing import cast from unittest.mock import patch import jwt @@ -78,7 +79,7 @@ def _mint(app: Flask, payload: dict[str, object]) -> dict[str, object]: with app.test_request_context("/", method="POST", json=payload): with patch(f"{CONTROLLER_MODULE}.inner_api_ns") as mock_ns: mock_ns.payload = payload - return inspect.unwrap(handler.post)(handler) + return inspect.unwrap(cast(Callable[..., object], handler.post))(handler) def _subject_of(response: dict[str, object]) -> str: diff --git a/api/tests/unit_tests/services/document_service_validation.py b/api/tests/unit_tests/services/document_service_validation.py index 8ec2b3efc5087c..19d270ae8b87c2 100644 --- a/api/tests/unit_tests/services/document_service_validation.py +++ b/api/tests/unit_tests/services/document_service_validation.py @@ -1394,33 +1394,6 @@ def test_process_rule_args_validate_missing_pre_processing_rules_error(self): with pytest.raises(ValueError, match="Process rule pre_processing_rules is required"): DocumentService.process_rule_args_validate(knowledge_config) - def test_process_rule_args_validate_missing_pre_processing_rule_id_error(self): - """ - Test error when pre_processing_rule id is missing. - - Verifies that when a pre_processing_rule has no id, a ValueError - is raised. - - This test ensures: - - Missing pre_processing_rule id is rejected - - Error message is clear - - Error type is correct - """ - # Arrange - pre_processing_rules = [ - PreProcessingRule.model_construct(id=None, enabled=True) # Missing id - ] - process_rule = DocumentValidationTestDataFactory.create_process_rule( - mode="custom", pre_processing_rules=pre_processing_rules - ) - knowledge_config = DocumentValidationTestDataFactory.create_knowledge_config(process_rule=process_rule) - - # Mock DatasetProcessRule.MODES - with patch.object(DatasetProcessRule, "MODES", ["automatic", "custom", "hierarchical"]): - # Act & Assert - with pytest.raises(ValueError, match="Process rule pre_processing_rules id is required"): - DocumentService.process_rule_args_validate(knowledge_config) - def test_process_rule_args_validate_invalid_pre_processing_rule_enabled_error(self): """ Test error when pre_processing_rule enabled is not boolean. From 4cbf4b068ad8f713ead8c50bcd40a405fc8964ff Mon Sep 17 00:00:00 2001 From: mesrop2008 Date: Wed, 14 Oct 2026 15:20:09 +0800 Subject: [PATCH 2/3] test(api): cover malformed-input branches touched by the pyrefly fix Add unit tests for the except ValueError paths simplified in the previous commit so the patch is fully covered: - ProviderManager skips load-balancing configs and returns empty provider credentials when the stored JSON is malformed. - parse_file_reference falls back to the raw value for invalid base64 or JSON payloads, and round-trips canonical references. - The datasource credential repository rejects stored documents whose data_source_info cannot be parsed. - Document change_status maps ValueError and DocumentIndexingError to DocumentInvalidActionError. --- .../unit_tests/core/test_provider_manager.py | 79 +++++++++++++++++++ .../core/workflow/test_file_reference.py | 24 ++++++ .../data_source/test_credential_repository.py | 26 ++++++ .../knowledge/test_dataset_adapters.py | 23 ++++++ 4 files changed, 152 insertions(+) create mode 100644 api/tests/unit_tests/core/workflow/test_file_reference.py diff --git a/api/tests/unit_tests/core/test_provider_manager.py b/api/tests/unit_tests/core/test_provider_manager.py index 8f364f37f909c1..d53ff5188f1526 100644 --- a/api/tests/unit_tests/core/test_provider_manager.py +++ b/api/tests/unit_tests/core/test_provider_manager.py @@ -16,6 +16,7 @@ ModelSettings, ProviderQuotaType, ) +from core.helper.model_provider_cache import ProviderCredentialsCacheType from core.hosting_configuration import HostingProvider, TrialHostingQuota from core.plugin.entities.plugin import PluginInstallationSource from core.plugin.entities.plugin_daemon import PluginModelProviderDeclaration @@ -512,6 +513,84 @@ def test__to_model_settings_lb_disabled(mock_provider_entity, provider_db: Sessi assert len(result[0].load_balancing_configs) == 0 +def test__to_model_settings_skips_lb_config_with_malformed_credentials(mock_provider_entity, provider_db: Session): + ps = ProviderModelSetting( + tenant_id="tenant_id", + provider_name="openai", + model_name="gpt-4", + model_type="llm", + enabled=True, + load_balancing_enabled=True, + ) + ps.id = "id" + load_balancing_model_configs = [ + LoadBalancingModelConfig( + tenant_id="tenant_id", + provider_name="openai", + model_name="gpt-4", + model_type="llm", + name="__inherit__", + encrypted_config=None, + enabled=True, + ), + LoadBalancingModelConfig( + tenant_id="tenant_id", + provider_name="openai", + model_name="gpt-4", + model_type="llm", + name="malformed", + encrypted_config="{not-json", + enabled=True, + ), + LoadBalancingModelConfig( + tenant_id="tenant_id", + provider_name="openai", + model_name="gpt-4", + model_type="llm", + name="first", + encrypted_config='{"openai_api_key": "fake_key"}', + enabled=True, + ), + ] + for index, config in enumerate(load_balancing_model_configs, start=1): + config.id = f"id{index}" + provider_model_settings, load_balancing_model_configs = _persist_model_configuration( + provider_db, ps, load_balancing_model_configs + ) + provider_manager = _build_provider_manager() + + with ( + patch("core.helper.model_provider_cache.ProviderCredentialsCache.get", return_value=None), + patch("core.helper.model_provider_cache.ProviderCredentialsCache.set") as cache_set, + patch.object(provider_manager, "_get_decoding_context", return_value=None), + ): + result = provider_manager._to_model_settings( + provider_entity=mock_provider_entity, + provider_model_settings=provider_model_settings, + load_balancing_model_configs=load_balancing_model_configs, + ) + + assert len(result) == 1 + assert [config.name for config in result[0].load_balancing_configs] == ["__inherit__", "first"] + cache_set.assert_called_once_with(credentials={"openai_api_key": "fake_key"}) + + +def test_get_and_decrypt_credentials_returns_empty_dict_for_malformed_config(): + provider_manager = _build_provider_manager() + + with patch("core.helper.model_provider_cache.ProviderCredentialsCache.get", return_value=None): + credentials = provider_manager._get_and_decrypt_credentials( + tenant_id="tenant_id", + record_id="record_id", + encrypted_config="{not-json", + secret_variables=["openai_api_key"], + cache_type=ProviderCredentialsCacheType.PROVIDER, + is_provider=True, + ) + + assert credentials == {} + + def test_get_default_model_uses_first_available_active_model(provider_db: Session): other_tenant_default = TenantDefaultModel( tenant_id="other-tenant", diff --git a/api/tests/unit_tests/core/workflow/test_file_reference.py b/api/tests/unit_tests/core/workflow/test_file_reference.py new file mode 100644 index 00000000000000..db07b4562adcfe --- /dev/null +++ b/api/tests/unit_tests/core/workflow/test_file_reference.py @@ -0,0 +1,24 @@ +import base64 + +import pytest + +from core.workflow.file_reference import FileReference, build_file_reference, parse_file_reference + + +def test_parse_file_reference_round_trips_canonical_reference() -> None: + reference = build_file_reference(record_id="file-1", storage_key="files/file-1.png") + + assert parse_file_reference(reference) == FileReference(record_id="file-1", storage_key="files/file-1.png") + + +@pytest.mark.parametrize( + "encoded_payload", + [ + pytest.param("abc", id="invalid-base64"), + pytest.param(base64.urlsafe_b64encode(b"not json").decode(), id="invalid-json"), + ], +) +def test_parse_file_reference_falls_back_to_raw_value_for_malformed_payload(encoded_payload: str) -> None: + reference = f"dify-file-ref:{encoded_payload}" + + assert parse_file_reference(reference) == FileReference(record_id=reference) diff --git a/api/tests/unit_tests/repositories/data_source/test_credential_repository.py b/api/tests/unit_tests/repositories/data_source/test_credential_repository.py index 23ab5055a5a377..5de8b46ffb6774 100644 --- a/api/tests/unit_tests/repositories/data_source/test_credential_repository.py +++ b/api/tests/unit_tests/repositories/data_source/test_credential_repository.py @@ -494,3 +494,29 @@ def test_get_for_stored_document_rejects_deleted_or_different_stored_credential( ) is None ) + + +def test_get_for_stored_document_rejects_malformed_stored_source_info( + sqlite_session_factory: sessionmaker[Session], +) -> None: + repository = SQLAlchemyDatasourceCredentialRepository(session_factory=sqlite_session_factory) + with sqlite_session_factory.begin() as session: + credential = _provider() + session.add(credential) + session.flush() + dataset, document = _notion_source(workspace_id="workspace-1", credential_id=credential.id) + document.data_source_info = "{not-json" + session.add_all([dataset, document]) + credential_id = credential.id + + assert ( + repository.get_for_stored_document( + workspace_id="workspace-1", + dataset_id="dataset-1", + document_id="document-1", + credential_id=credential_id, + provider="notion_datasource", + plugin_id="langgenius/notion_datasource", + ) + is None + ) diff --git a/api/tests/unit_tests/services/knowledge/test_dataset_adapters.py b/api/tests/unit_tests/services/knowledge/test_dataset_adapters.py index f9fb45e2a0188b..5d9434e2107d73 100644 --- a/api/tests/unit_tests/services/knowledge/test_dataset_adapters.py +++ b/api/tests/unit_tests/services/knowledge/test_dataset_adapters.py @@ -24,10 +24,12 @@ from models.enums import CreatorUserRole, DatasetQuerySource, IndexingStatus, SegmentStatus from repositories.app.console_repository import ConsoleAppRepository from services.enterprise import rbac_service +from services.errors.document import DocumentIndexingError from services.knowledge.dataset_access import DatasetNotFoundError from services.knowledge.datasets.adapters import SQLAlchemyDatasetOperations from services.knowledge.datasets.application import DatasetListFilter from services.knowledge.documents.adapters import SQLAlchemyDocumentOperations +from services.knowledge.documents.application import DocumentInvalidActionError from services.knowledge.entities.document_creation import DocumentIndexingJobs from services.knowledge.entities.knowledge_entities import KnowledgeConfig from services.knowledge.resource_scope import DatasetRef @@ -375,3 +377,24 @@ def test_queries_and_related_apps_materialize_after_session_close( assert related["total"] == 1 assert related["data"][0]["id"] == "app" assert related["data"][0]["mode"] == "chat" + + +@pytest.mark.usefixtures("operations") +@pytest.mark.parametrize( + "error", + [ValueError("Invalid action"), DocumentIndexingError("Document is being indexed")], + ids=["value-error", "document-indexing-error"], +) +def test_change_status_reports_rejected_status_updates_as_invalid_actions( + sqlite_session_factory: sessionmaker[Session], error: ValueError +) -> None: + with ( + patch( + "services.knowledge.documents.adapters.DocumentService.batch_update_document_status", + side_effect=error, + ), + pytest.raises(DocumentInvalidActionError, match=str(error)) as error_info, + ): + SQLAlchemyDocumentOperations(session_factory=sqlite_session_factory).change_status(CONTEXT, REF, [], "enable") + + assert error_info.value.__cause__ is error From 5eed226f14d457df491aa29f83550a667a6bbd55 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 15 Oct 2026 11:55:58 +0000 Subject: [PATCH 3/3] [autofix.ci] apply automated fixes --- api/repositories/data_source/credential_repository.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/repositories/data_source/credential_repository.py b/api/repositories/data_source/credential_repository.py index 65fb0c334c934c..69c689b31fefd6 100644 --- a/api/repositories/data_source/credential_repository.py +++ b/api/repositories/data_source/credential_repository.py @@ -4,7 +4,6 @@ from typing import cast from pydantic import TypeAdapter -from pydantic import TypeAdapter, ValidationError from sqlalchemy import cast as sql_cast from sqlalchemy import select, update from sqlalchemy.engine import CursorResult