mirror of
https://github.com/langgenius/dify.git
synced 2026-11-11 05:41:09 +08:00
refactor(api): enable strict typing for core/libs/services test files (#44815)
Co-authored-by: Felix Shi <204130665+shi-felix@users.noreply.github.com> Co-authored-by: Asuka Minato <i@asukaminato.eu.org>
This commit is contained in:
10 files changed
+74
-57
No files matched your search
+11
-6
@@ -1,19 +1,24 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from core.app.features.hosting_moderation.hosting_moderation import HostingModerationFeature
|
||||
from core.app.features.hosting_moderation.hosting_moderation import (
|
||||
HostingModerationFeature,
|
||||
)
|
||||
from graphon.model_runtime.entities.message_entities import PromptMessage, UserPromptMessage
|
||||
|
||||
|
||||
class TestHostingModerationFeature:
|
||||
def test_check_aggregates_text_and_calls_moderation(self):
|
||||
def test_check_aggregates_text_and_calls_moderation(self) -> None:
|
||||
application_generate_entity = Mock()
|
||||
application_generate_entity.model_conf = {"model": "mock"}
|
||||
application_generate_entity.app_config = SimpleNamespace(tenant_id="tenant-1")
|
||||
|
||||
prompt_messages = [
|
||||
SimpleNamespace(content="hello"),
|
||||
SimpleNamespace(content=123),
|
||||
SimpleNamespace(content="world"),
|
||||
# The middle message bypasses validation on purpose: `content` is not
|
||||
# always a string at runtime, and the feature must skip those entries.
|
||||
prompt_messages: list[PromptMessage] = [
|
||||
UserPromptMessage(role="user", content="hello"),
|
||||
UserPromptMessage.model_construct(role="user", content=123),
|
||||
UserPromptMessage(role="user", content="world"),
|
||||
]
|
||||
|
||||
with patch("core.app.features.hosting_moderation.hosting_moderation.moderation.check_moderation") as mock_check:
|
||||
|
||||
@@ -4,7 +4,7 @@ from core.extension.extensible import ExtensionModule
|
||||
from core.external_data_tool.factory import ExternalDataToolFactory
|
||||
|
||||
|
||||
def test_external_data_tool_factory_init():
|
||||
def test_external_data_tool_factory_init() -> None:
|
||||
with patch("core.external_data_tool.factory.code_based_extension") as mock_code_based_extension:
|
||||
mock_extension_class = MagicMock()
|
||||
mock_code_based_extension.extension_class.return_value = mock_extension_class
|
||||
@@ -23,7 +23,7 @@ def test_external_data_tool_factory_init():
|
||||
)
|
||||
|
||||
|
||||
def test_external_data_tool_factory_validate_config():
|
||||
def test_external_data_tool_factory_validate_config() -> None:
|
||||
with patch("core.external_data_tool.factory.code_based_extension") as mock_code_based_extension:
|
||||
mock_extension_class = MagicMock()
|
||||
mock_code_based_extension.extension_class.return_value = mock_extension_class
|
||||
@@ -38,7 +38,7 @@ def test_external_data_tool_factory_validate_config():
|
||||
mock_extension_class.validate_config.assert_called_once_with(tenant_id, config)
|
||||
|
||||
|
||||
def test_external_data_tool_factory_query():
|
||||
def test_external_data_tool_factory_query() -> None:
|
||||
with patch("core.external_data_tool.factory.code_based_extension") as mock_code_based_extension:
|
||||
mock_extension_class = MagicMock()
|
||||
mock_extension_instance = MagicMock()
|
||||
|
||||
@@ -6,7 +6,7 @@ from core.plugin.impl.asset import PluginAssetManager
|
||||
|
||||
|
||||
class TestPluginAssetManager:
|
||||
def test_fetch_asset_success(self, mocker: MockerFixture):
|
||||
def test_fetch_asset_success(self, mocker: MockerFixture) -> None:
|
||||
manager = PluginAssetManager()
|
||||
response = httpx.Response(status_code=200, content=b"asset-bytes")
|
||||
request_mock = mocker.patch.object(manager, "_request", return_value=response)
|
||||
@@ -16,14 +16,18 @@ class TestPluginAssetManager:
|
||||
assert result == b"asset-bytes"
|
||||
request_mock.assert_called_once_with(method="GET", path="plugin/tenant-1/asset/asset-1")
|
||||
|
||||
def test_fetch_asset_not_found_raises(self, mocker: MockerFixture):
|
||||
def test_fetch_asset_not_found_raises(self, mocker: MockerFixture) -> None:
|
||||
manager = PluginAssetManager()
|
||||
mocker.patch.object(manager, "_request", return_value=httpx.Response(status_code=404, content=b""))
|
||||
mocker.patch.object(
|
||||
manager,
|
||||
"_request",
|
||||
return_value=httpx.Response(status_code=404, content=b""),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="can not found asset asset-1"):
|
||||
manager.fetch_asset("tenant-1", "asset-1")
|
||||
|
||||
def test_extract_asset_success(self, mocker: MockerFixture):
|
||||
def test_extract_asset_success(self, mocker: MockerFixture) -> None:
|
||||
manager = PluginAssetManager()
|
||||
response = httpx.Response(status_code=200, content=b"file-content")
|
||||
request_mock = mocker.patch.object(manager, "_request", return_value=response)
|
||||
@@ -34,12 +38,19 @@ class TestPluginAssetManager:
|
||||
request_mock.assert_called_once_with(
|
||||
method="GET",
|
||||
path="plugin/tenant-1/extract-asset/",
|
||||
params={"plugin_unique_identifier": "org/plugin:1", "file_path": "README.md"},
|
||||
params={
|
||||
"plugin_unique_identifier": "org/plugin:1",
|
||||
"file_path": "README.md",
|
||||
},
|
||||
)
|
||||
|
||||
def test_extract_asset_not_found_raises(self, mocker: MockerFixture):
|
||||
def test_extract_asset_not_found_raises(self, mocker: MockerFixture) -> None:
|
||||
manager = PluginAssetManager()
|
||||
mocker.patch.object(manager, "_request", return_value=httpx.Response(status_code=404, content=b""))
|
||||
mocker.patch.object(
|
||||
manager,
|
||||
"_request",
|
||||
return_value=httpx.Response(status_code=404, content=b""),
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="can not found asset org/plugin:1, 404"):
|
||||
manager.extract_asset("tenant-1", "org/plugin:1", "README.md")
|
||||
@@ -7,13 +7,13 @@ from core.plugin.impl.exc import PluginDaemonError, PluginInvokeError
|
||||
|
||||
|
||||
class TestPluginImplExceptions:
|
||||
def test_plugin_daemon_error_str_contains_request_id(self, mocker: MockerFixture):
|
||||
def test_plugin_daemon_error_str_contains_request_id(self, mocker: MockerFixture) -> None:
|
||||
mocker.patch("core.plugin.impl.exc.get_request_id", return_value="req-123")
|
||||
error = PluginDaemonError("bad")
|
||||
|
||||
assert str(error) == "req_id: req-123 PluginDaemonError: bad"
|
||||
|
||||
def test_plugin_invoke_error_with_json_payload(self):
|
||||
def test_plugin_invoke_error_with_json_payload(self) -> None:
|
||||
err = PluginInvokeError(json.dumps({"error_type": "RateLimit", "message": "too many"}))
|
||||
|
||||
assert err.get_error_type() == "RateLimit"
|
||||
@@ -23,7 +23,7 @@ class TestPluginImplExceptions:
|
||||
assert "RateLimit" in friendly
|
||||
assert "too many" in friendly
|
||||
|
||||
def test_plugin_invoke_error_invalid_json_and_fallback(self, mocker: MockerFixture):
|
||||
def test_plugin_invoke_error_invalid_json_and_fallback(self, mocker: MockerFixture) -> None:
|
||||
err = PluginInvokeError("plain text")
|
||||
|
||||
assert err._get_error_object() == {}
|
||||
@@ -34,7 +34,7 @@ class TestPluginImplExceptions:
|
||||
err2 = PluginInvokeError("plain text")
|
||||
assert err2.get_error_message() == "plain text"
|
||||
|
||||
def test_plugin_invoke_error_get_error_object_handles_adapter_exception(self, mocker: MockerFixture):
|
||||
def test_plugin_invoke_error_get_error_object_handles_adapter_exception(self, mocker: MockerFixture) -> None:
|
||||
adapter = mocker.patch.object(exc_module, "TypeAdapter")
|
||||
adapter.return_value.validate_json.side_effect = RuntimeError("invalid")
|
||||
|
||||
|
||||
@@ -21,41 +21,41 @@ def _make_schema(name: str, field_type: str = "secret-input") -> ProviderConfig:
|
||||
|
||||
|
||||
class TestMaskedCredentials:
|
||||
def test_short_secret_fully_masked(self):
|
||||
def test_short_secret_fully_masked(self) -> None:
|
||||
schema = [_make_schema("key", "secret-input")]
|
||||
result = masked_credentials(schema, {"key": "ab"})
|
||||
assert result["key"] == "**"
|
||||
|
||||
def test_long_secret_partially_masked(self):
|
||||
def test_long_secret_partially_masked(self) -> None:
|
||||
schema = [_make_schema("key", "secret-input")]
|
||||
result = masked_credentials(schema, {"key": "abcdef"})
|
||||
assert result["key"].startswith("ab")
|
||||
assert result["key"].endswith("ef")
|
||||
assert "**" in result["key"]
|
||||
|
||||
def test_non_secret_field_unchanged(self):
|
||||
def test_non_secret_field_unchanged(self) -> None:
|
||||
schema = [_make_schema("host", "text-input")]
|
||||
result = masked_credentials(schema, {"host": "example.com"})
|
||||
assert result["host"] == "example.com"
|
||||
|
||||
def test_unknown_key_passes_through(self):
|
||||
def test_unknown_key_passes_through(self) -> None:
|
||||
result = masked_credentials([], {"unknown": "value"})
|
||||
assert result["unknown"] == "value"
|
||||
|
||||
|
||||
class TestCacheKeyGeneration:
|
||||
def test_credentials_cache_key_contains_ids(self):
|
||||
def test_credentials_cache_key_contains_ids(self) -> None:
|
||||
cache = TriggerProviderCredentialsCache(tenant_id="t1", provider_id="p1", credential_id="c1")
|
||||
assert "t1" in cache.cache_key
|
||||
assert "p1" in cache.cache_key
|
||||
assert "c1" in cache.cache_key
|
||||
|
||||
def test_oauth_client_cache_key_contains_ids(self):
|
||||
def test_oauth_client_cache_key_contains_ids(self) -> None:
|
||||
cache = TriggerProviderOAuthClientParamsCache(tenant_id="t1", provider_id="p1")
|
||||
assert "t1" in cache.cache_key
|
||||
assert "p1" in cache.cache_key
|
||||
|
||||
def test_properties_cache_key_contains_ids(self):
|
||||
def test_properties_cache_key_contains_ids(self) -> None:
|
||||
cache = TriggerProviderPropertiesCache(tenant_id="t1", provider_id="p1", subscription_id="s1")
|
||||
assert "t1" in cache.cache_key
|
||||
assert "p1" in cache.cache_key
|
||||
|
||||
@@ -24,25 +24,25 @@ _GRAPH = {
|
||||
}
|
||||
|
||||
|
||||
def test_upstream_node_ids_collects_all_ancestors_excluding_ghosts():
|
||||
def test_upstream_node_ids_collects_all_ancestors_excluding_ghosts() -> None:
|
||||
topology = WorkflowGraphTopology.from_graph(_GRAPH)
|
||||
assert topology.upstream_node_ids("agent") == {"start", "llm-1", "llm-2"}
|
||||
|
||||
|
||||
def test_upstream_node_ids_differ_per_target_node():
|
||||
def test_upstream_node_ids_differ_per_target_node() -> None:
|
||||
topology = WorkflowGraphTopology.from_graph(_GRAPH)
|
||||
assert topology.upstream_node_ids("llm-1") == {"start"}
|
||||
assert topology.upstream_node_ids("end") == {"start", "llm-1", "llm-2", "agent"}
|
||||
assert topology.upstream_node_ids("start") == set()
|
||||
|
||||
|
||||
def test_is_upstream_kept_for_publish_validation():
|
||||
def test_is_upstream_kept_for_publish_validation() -> None:
|
||||
topology = WorkflowGraphTopology.from_graph(_GRAPH)
|
||||
assert topology.is_upstream(source_node_id="start", target_node_id="end")
|
||||
assert not topology.is_upstream(source_node_id="end", target_node_id="start")
|
||||
|
||||
|
||||
def test_cycle_safe():
|
||||
def test_cycle_safe() -> None:
|
||||
graph = {
|
||||
"nodes": [{"id": "a"}, {"id": "b"}],
|
||||
"edges": [{"source": "a", "target": "b"}, {"source": "b", "target": "a"}],
|
||||
|
||||
@@ -8,27 +8,27 @@ from libs.custom_inputs import time_duration
|
||||
class TestTimeDuration:
|
||||
"""Test time_duration input validator."""
|
||||
|
||||
def test_valid_days(self):
|
||||
def test_valid_days(self) -> None:
|
||||
"""Test valid days format."""
|
||||
result = time_duration("7d")
|
||||
assert result == "7d"
|
||||
|
||||
def test_valid_hours(self):
|
||||
def test_valid_hours(self) -> None:
|
||||
"""Test valid hours format."""
|
||||
result = time_duration("4h")
|
||||
assert result == "4h"
|
||||
|
||||
def test_valid_minutes(self):
|
||||
def test_valid_minutes(self) -> None:
|
||||
"""Test valid minutes format."""
|
||||
result = time_duration("30m")
|
||||
assert result == "30m"
|
||||
|
||||
def test_valid_seconds(self):
|
||||
def test_valid_seconds(self) -> None:
|
||||
"""Test valid seconds format."""
|
||||
result = time_duration("30s")
|
||||
assert result == "30s"
|
||||
|
||||
def test_uppercase_conversion(self):
|
||||
def test_uppercase_conversion(self) -> None:
|
||||
"""Test uppercase units are converted to lowercase."""
|
||||
result = time_duration("7D")
|
||||
assert result == "7d"
|
||||
@@ -36,12 +36,12 @@ class TestTimeDuration:
|
||||
result = time_duration("4H")
|
||||
assert result == "4h"
|
||||
|
||||
def test_invalid_format_no_unit(self):
|
||||
def test_invalid_format_no_unit(self) -> None:
|
||||
"""Test invalid format without unit."""
|
||||
with pytest.raises(ValueError, match="Invalid time duration format"):
|
||||
time_duration("7")
|
||||
|
||||
def test_invalid_format_wrong_unit(self):
|
||||
def test_invalid_format_wrong_unit(self) -> None:
|
||||
"""Test invalid format with wrong unit."""
|
||||
with pytest.raises(ValueError, match="Invalid time duration format"):
|
||||
time_duration("7days")
|
||||
@@ -49,7 +49,7 @@ class TestTimeDuration:
|
||||
with pytest.raises(ValueError, match="Invalid time duration format"):
|
||||
time_duration("7x")
|
||||
|
||||
def test_invalid_format_no_number(self):
|
||||
def test_invalid_format_no_number(self) -> None:
|
||||
"""Test invalid format without number."""
|
||||
with pytest.raises(ValueError, match="Invalid time duration format"):
|
||||
time_duration("d")
|
||||
@@ -57,12 +57,12 @@ class TestTimeDuration:
|
||||
with pytest.raises(ValueError, match="Invalid time duration format"):
|
||||
time_duration("abc")
|
||||
|
||||
def test_empty_string(self):
|
||||
def test_empty_string(self) -> None:
|
||||
"""Test empty string."""
|
||||
with pytest.raises(ValueError, match="Time duration cannot be empty"):
|
||||
time_duration("")
|
||||
|
||||
def test_none(self):
|
||||
def test_none(self) -> None:
|
||||
"""Test None value."""
|
||||
with pytest.raises(ValueError, match="Time duration cannot be empty"):
|
||||
time_duration(None)
|
||||
time_duration(None) # type: ignore[arg-type]
|
||||
@@ -186,7 +186,6 @@ project-excludes = [
|
||||
"core/app/entities/test_app_invoke_entities.py",
|
||||
"core/app/features/rate_limiting/test_rate_limit.py",
|
||||
"core/app/features/annotation_reply/test_annotation_reply.py",
|
||||
"core/app/features/hosting_moderation/test_hosting_moderation.py",
|
||||
"core/app/layers/test_conversation_variable_persist_layer.py",
|
||||
"core/app/layers/test_pause_state_persist_layer.py",
|
||||
"core/app/layers/test_timeslice_layer.py",
|
||||
@@ -208,7 +207,6 @@ project-excludes = [
|
||||
"core/entities/test_mcp_provider.py",
|
||||
"core/entities/test_provider_configuration.py",
|
||||
"core/extension/test_api_based_extension_requestor.py",
|
||||
"core/external_data_tool/test_factory.py",
|
||||
"core/helper/code_executor/test_code_executor.py",
|
||||
"core/helper/test_ssrf_proxy.py",
|
||||
"core/llm_generator/output_parser/test_structured_output.py",
|
||||
@@ -231,11 +229,9 @@ project-excludes = [
|
||||
"core/ops/entities/test_trace_entity.py",
|
||||
"core/ops/test_utils.py",
|
||||
"core/plugin/impl/test_agent.py",
|
||||
"core/plugin/impl/test_asset.py",
|
||||
"core/plugin/impl/test_base.py",
|
||||
"core/plugin/impl/test_datasource.py",
|
||||
"core/plugin/impl/test_endpoint.py",
|
||||
"core/plugin/impl/test_exc.py",
|
||||
"core/plugin/impl/test_model.py",
|
||||
"core/plugin/impl/test_oauth.py",
|
||||
"core/plugin/impl/test_trigger.py",
|
||||
@@ -300,7 +296,6 @@ project-excludes = [
|
||||
"core/tools/utils/test_web_reader_tool.py",
|
||||
"core/tools/workflow_as_tool/test_provider.py",
|
||||
"core/tools/workflow_as_tool/test_tool.py",
|
||||
"core/trigger/utils/test_encryption.py",
|
||||
"context/test_execution_context.py",
|
||||
"context/test_flask_app_context.py",
|
||||
"../e2e_tests/contracts/core/workflow/entities/test_private_workflow_pause.py",
|
||||
@@ -348,7 +343,6 @@ project-excludes = [
|
||||
"core/workflow/nodes/trigger_webhook/test_entities.py",
|
||||
"core/workflow/nodes/trigger_webhook/test_node.py",
|
||||
"core/workflow/nodes/human_input/test_pause_reason.py",
|
||||
"core/workflow/test_graph_topology.py",
|
||||
"core/workflow/nodes/human_input/test_callback.py",
|
||||
"core/workflow/test_human_input_forms.py",
|
||||
"../e2e_tests/contracts/core/workflow/test_node_mapping_bootstrap.py",
|
||||
@@ -384,7 +378,6 @@ project-excludes = [
|
||||
"libs/broadcast_channel/redis/test_streams_channel.py",
|
||||
"libs/test_archive_storage.py",
|
||||
"libs/test_schedule_utils.py",
|
||||
"libs/test_custom_inputs.py",
|
||||
"libs/test_datetime_utils.py",
|
||||
"libs/test_email_i18n.py",
|
||||
"libs/test_external_api.py",
|
||||
@@ -420,7 +413,6 @@ project-excludes = [
|
||||
"../helpers/unit/services/hit_service.py",
|
||||
"services/plugin/test_plugin_parameter_service.py",
|
||||
"services/errors/test_plugin.py",
|
||||
"services/rag_pipeline/pipeline_template/test_pipeline_template_base.py",
|
||||
"services/rag_pipeline/test_pipeline_generate_service.py",
|
||||
"services/rag_pipeline/test_rag_pipeline_dsl_service.py",
|
||||
"services/rag_pipeline/test_rag_pipeline.py",
|
||||
@@ -469,7 +461,6 @@ project-excludes = [
|
||||
"services/test_workflow_node_execution_trace_service.py",
|
||||
"services/test_workflow_run_service.py",
|
||||
"services/test_workflow_service.py",
|
||||
"services/tools/test_api_tools_manage_service.py",
|
||||
"services/tools/test_builtin_tools_manage_service.py",
|
||||
"services/tools/test_tools_transform_service.py",
|
||||
"services/tools/test_tools_manage_service.py",
|
||||
|
||||
+13
-5
@@ -1,15 +1,21 @@
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from services.rag_pipeline.pipeline_template.pipeline_template_base import PipelineTemplateRetrievalBase
|
||||
from services.rag_pipeline.pipeline_template.pipeline_template_base import (
|
||||
PipelineTemplateRetrievalBase,
|
||||
)
|
||||
|
||||
|
||||
class DummyRetrieval(PipelineTemplateRetrievalBase):
|
||||
def get_pipeline_templates(self, language: str, *, session) -> dict:
|
||||
del session
|
||||
def get_pipeline_templates(
|
||||
self, language: str, current_tenant_id: str | None = None, *, session: Session
|
||||
) -> dict[str, object]:
|
||||
del current_tenant_id, session
|
||||
return {"language": language}
|
||||
|
||||
def get_pipeline_template_detail(self, template_id: str, current_tenant_id: str, *, session) -> dict | None:
|
||||
def get_pipeline_template_detail(
|
||||
self, template_id: str, current_tenant_id: str, *, session: Session
|
||||
) -> dict[str, object] | None:
|
||||
del current_tenant_id, session
|
||||
return {"id": template_id}
|
||||
|
||||
@@ -18,7 +24,9 @@ class DummyRetrieval(PipelineTemplateRetrievalBase):
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sqlite_session", [()], indirect=True)
|
||||
def test_pipeline_template_retrieval_base_concrete_implementation(sqlite_session: Session) -> None:
|
||||
def test_pipeline_template_retrieval_base_concrete_implementation(
|
||||
sqlite_session: Session,
|
||||
) -> None:
|
||||
retrieval = DummyRetrieval()
|
||||
|
||||
assert retrieval.get_pipeline_templates("en-US", session=sqlite_session) == {"language": "en-US"}
|
||||
|
||||
@@ -5,7 +5,9 @@ import pytest
|
||||
from services.tools.api_tools_manage_service import ApiToolManageService
|
||||
|
||||
|
||||
def test_get_api_tool_provider_remote_schema_uses_ssrf_proxy_get(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_get_api_tool_provider_remote_schema_uses_ssrf_proxy_get(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
schema = """
|
||||
{
|
||||
"openapi": "3.0.0",
|
||||
|
||||
Reference in new issue
Block a user