diff --git a/api/libs/oauth.py b/api/libs/oauth.py index 81ca07dc9f57b1..d87a16aec0ec88 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 diff --git a/api/libs/token.py b/api/libs/token.py index 068d0440e87985..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: diff --git a/api/tests/helpers/unit/services/document_service_validation.py b/api/tests/helpers/unit/services/document_service_validation.py index 8ec2b3efc5087c..19d270ae8b87c2 100644 --- a/api/tests/helpers/unit/services/document_service_validation.py +++ b/api/tests/helpers/unit/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. 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 8e2109d8858d46..9fa92f571f050c 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) 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 42f8498e93219d..7646e172c206c3 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 diff --git a/api/tests/unit_tests/core/test_provider_manager.py b/api/tests/unit_tests/core/test_provider_manager.py index b0dabc0346f4ee..82b485818565fb 100644 --- a/api/tests/unit_tests/core/test_provider_manager.py +++ b/api/tests/unit_tests/core/test_provider_manager.py @@ -579,6 +579,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_injected_runtime_for_existing_default_record(provider_db: Session): existing_default_model = TenantDefaultModel( tenant_id="tenant-id", 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 f83ea6165cf03e..eda0cca8479b76 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 @@ -572,3 +572,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/datasets/test_adapters.py b/api/tests/unit_tests/services/knowledge/datasets/test_adapters.py index 01fb45f27afefe..54d3553beac1bc 100644 --- a/api/tests/unit_tests/services/knowledge/datasets/test_adapters.py +++ b/api/tests/unit_tests/services/knowledge/datasets/test_adapters.py @@ -33,10 +33,12 @@ from models.model import AppMode 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 @@ -456,3 +458,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