mirror of
https://github.com/langgenius/dify.git
synced 2026-08-30 17:11:50 +08:00
test: centralize typed runtime config overrides (#40857)
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
from configs.extra.agent_backend_config import AgentBackendConfig
|
||||
from configs.extra.archive_config import ArchiveStorageConfig
|
||||
from configs.extra.knowledge_fs_config import KnowledgeFSConfig
|
||||
from configs.extra.logstore_config import LogStoreConfig
|
||||
from configs.extra.notion_config import NotionConfig
|
||||
from configs.extra.sentry_config import SentryConfig
|
||||
from configs.extra.turnstile_config import TurnstileConfig
|
||||
@@ -11,6 +12,7 @@ class ExtraServiceConfig(
|
||||
AgentBackendConfig,
|
||||
ArchiveStorageConfig,
|
||||
KnowledgeFSConfig,
|
||||
LogStoreConfig,
|
||||
NotionConfig,
|
||||
SentryConfig,
|
||||
TurnstileConfig,
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
from pydantic_settings import BaseSettings
|
||||
|
||||
|
||||
class LogStoreConfig(BaseSettings):
|
||||
"""Migration controls for repositories backed by Aliyun LogStore."""
|
||||
|
||||
LOGSTORE_DUAL_WRITE_ENABLED: bool = False
|
||||
|
||||
# Keep workflow graphs in LogStore by default. Deployments may disable this
|
||||
# while migrating large graph payloads to another persistence owner.
|
||||
LOGSTORE_ENABLE_PUT_GRAPH_FIELD: bool = True
|
||||
@@ -1,12 +1,12 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from typing import override
|
||||
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from configs import dify_config
|
||||
from core.repositories.factory import WorkflowExecutionRepository
|
||||
from core.repositories.sqlalchemy_workflow_execution_repository import SQLAlchemyWorkflowExecutionRepository
|
||||
from extensions.logstore.aliyun_logstore import AliyunLogStore
|
||||
@@ -71,14 +71,12 @@ class LogstoreWorkflowExecutionRepository(WorkflowExecutionRepository):
|
||||
triggered_from=triggered_from,
|
||||
)
|
||||
|
||||
# Control flag for dual-write (write to both LogStore and SQL database)
|
||||
# Set to True to enable dual-write for safe migration, False to use LogStore only
|
||||
self._enable_dual_write = os.environ.get("LOGSTORE_DUAL_WRITE_ENABLED", "false").lower() == "true"
|
||||
self._enable_dual_write = dify_config.LOGSTORE_DUAL_WRITE_ENABLED
|
||||
|
||||
# Control flag for whether to write the `graph` field to LogStore.
|
||||
# If LOGSTORE_ENABLE_PUT_GRAPH_FIELD is "true", write the full `graph` field;
|
||||
# otherwise write an empty {} instead. Defaults to writing the `graph` field.
|
||||
self._enable_put_graph_field = os.environ.get("LOGSTORE_ENABLE_PUT_GRAPH_FIELD", "true").lower() == "true"
|
||||
self._enable_put_graph_field = dify_config.LOGSTORE_ENABLE_PUT_GRAPH_FIELD
|
||||
|
||||
def _to_logstore_model(self, domain_model: WorkflowExecution) -> list[tuple[str, str]]:
|
||||
"""
|
||||
|
||||
@@ -7,7 +7,6 @@ using Aliyun SLS LogStore with append-only writes and version control.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from collections.abc import Sequence
|
||||
from datetime import datetime
|
||||
@@ -16,6 +15,7 @@ from typing import Any, override
|
||||
from sqlalchemy.engine import Engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from configs import dify_config
|
||||
from core.ops.utils import JSON_DICT_ADAPTER
|
||||
from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository
|
||||
from core.repositories.factory import OrderConfig, WorkflowNodeExecutionRepository
|
||||
@@ -152,9 +152,9 @@ class LogstoreWorkflowNodeExecutionRepository(WorkflowNodeExecutionRepository):
|
||||
triggered_from=triggered_from,
|
||||
)
|
||||
|
||||
# Control flag for dual-write (write to both LogStore and SQL database)
|
||||
# Set to True to enable dual-write for safe migration, False to use LogStore only
|
||||
self._enable_dual_write = os.environ.get("LOGSTORE_DUAL_WRITE_ENABLED", "false").lower() == "true"
|
||||
# Keep the migration switch on the typed application config so callers
|
||||
# and tests share the same validated source.
|
||||
self._enable_dual_write = dify_config.LOGSTORE_DUAL_WRITE_ENABLED
|
||||
|
||||
def _to_logstore_model(self, domain_model: WorkflowNodeExecution) -> Sequence[tuple[str, str]]:
|
||||
logger.debug(
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import pytest
|
||||
|
||||
from configs.extra.logstore_config import LogStoreConfig
|
||||
from tests.unit_tests.configs._isolated_settings import InitSettingsOnly
|
||||
|
||||
|
||||
class _IsolatedLogStoreConfig(InitSettingsOnly, LogStoreConfig):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw_value", "expected"),
|
||||
[
|
||||
pytest.param("true", True, id="enabled"),
|
||||
pytest.param("false", False, id="disabled"),
|
||||
],
|
||||
)
|
||||
def test_logstore_migration_flags_parse_boolean_values(raw_value: str, expected: bool) -> None:
|
||||
config = _IsolatedLogStoreConfig(
|
||||
LOGSTORE_DUAL_WRITE_ENABLED=raw_value,
|
||||
LOGSTORE_ENABLE_PUT_GRAPH_FIELD=raw_value,
|
||||
)
|
||||
|
||||
assert config.LOGSTORE_DUAL_WRITE_ENABLED is expected
|
||||
assert config.LOGSTORE_ENABLE_PUT_GRAPH_FIELD is expected
|
||||
@@ -2,7 +2,6 @@
|
||||
Unit tests for inner_api auth decorators
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
from uuid import NAMESPACE_URL, uuid5
|
||||
|
||||
import pytest
|
||||
@@ -11,7 +10,6 @@ from sqlalchemy import Engine, event
|
||||
from sqlalchemy.orm import Session
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
from configs import dify_config
|
||||
from controllers.inner_api.wraps import (
|
||||
billing_inner_api_only,
|
||||
enterprise_inner_api_only,
|
||||
@@ -23,6 +21,16 @@ from models.enums import EndUserType
|
||||
from models.model import EndUser
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _inner_api_config(config_overrides) -> None:
|
||||
config_overrides(
|
||||
INNER_API=True,
|
||||
INNER_API_KEY="valid_key",
|
||||
PLUGIN_DAEMON_KEY="plugin_key",
|
||||
INNER_API_KEY_FOR_PLUGIN="valid_plugin_key",
|
||||
)
|
||||
|
||||
|
||||
def _stable_uuid(value: str) -> str:
|
||||
return str(uuid5(NAMESPACE_URL, value))
|
||||
|
||||
@@ -40,14 +48,12 @@ class TestBillingInnerApiOnly:
|
||||
|
||||
# Act
|
||||
with app.test_request_context(headers={"X-Inner-Api-Key": "valid_key"}):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
with patch.object(dify_config, "INNER_API_KEY", "valid_key"):
|
||||
result = protected_view()
|
||||
result = protected_view()
|
||||
|
||||
# Assert
|
||||
assert result == "success"
|
||||
|
||||
def test_should_return_404_when_inner_api_disabled(self, app: Flask):
|
||||
def test_should_return_404_when_inner_api_disabled(self, app: Flask, config_overrides):
|
||||
"""Test that 404 is returned when INNER_API is disabled"""
|
||||
|
||||
# Arrange
|
||||
@@ -56,11 +62,11 @@ class TestBillingInnerApiOnly:
|
||||
return "success"
|
||||
|
||||
# Act & Assert
|
||||
config_overrides(INNER_API=False)
|
||||
with app.test_request_context():
|
||||
with patch.object(dify_config, "INNER_API", False):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 404
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 404
|
||||
|
||||
def test_should_return_401_when_api_key_missing(self, app: Flask):
|
||||
"""Test that 401 is returned when X-Inner-Api-Key header is missing"""
|
||||
@@ -72,11 +78,9 @@ class TestBillingInnerApiOnly:
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context(headers={}):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
with patch.object(dify_config, "INNER_API_KEY", "valid_key"):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
|
||||
def test_should_return_401_when_api_key_invalid(self, app: Flask):
|
||||
"""Test that 401 is returned when X-Inner-Api-Key header is invalid"""
|
||||
@@ -88,11 +92,9 @@ class TestBillingInnerApiOnly:
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context(headers={"X-Inner-Api-Key": "invalid_key"}):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
with patch.object(dify_config, "INNER_API_KEY", "valid_key"):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
|
||||
|
||||
class TestEnterpriseInnerApiOnly:
|
||||
@@ -108,14 +110,12 @@ class TestEnterpriseInnerApiOnly:
|
||||
|
||||
# Act
|
||||
with app.test_request_context(headers={"X-Inner-Api-Key": "valid_key"}):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
with patch.object(dify_config, "INNER_API_KEY", "valid_key"):
|
||||
result = protected_view()
|
||||
result = protected_view()
|
||||
|
||||
# Assert
|
||||
assert result == "success"
|
||||
|
||||
def test_should_return_404_when_inner_api_disabled(self, app: Flask):
|
||||
def test_should_return_404_when_inner_api_disabled(self, app: Flask, config_overrides):
|
||||
"""Test that 404 is returned when INNER_API is disabled"""
|
||||
|
||||
# Arrange
|
||||
@@ -124,11 +124,11 @@ class TestEnterpriseInnerApiOnly:
|
||||
return "success"
|
||||
|
||||
# Act & Assert
|
||||
config_overrides(INNER_API=False)
|
||||
with app.test_request_context():
|
||||
with patch.object(dify_config, "INNER_API", False):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 404
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 404
|
||||
|
||||
def test_should_return_401_when_api_key_missing(self, app: Flask):
|
||||
"""Test that 401 is returned when X-Inner-Api-Key header is missing"""
|
||||
@@ -140,11 +140,9 @@ class TestEnterpriseInnerApiOnly:
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context(headers={}):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
with patch.object(dify_config, "INNER_API_KEY", "valid_key"):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
|
||||
def test_should_return_401_when_api_key_invalid(self, app: Flask):
|
||||
"""Test that 401 is returned when X-Inner-Api-Key header is invalid"""
|
||||
@@ -156,11 +154,9 @@ class TestEnterpriseInnerApiOnly:
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context(headers={"X-Inner-Api-Key": "invalid_key"}):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
with patch.object(dify_config, "INNER_API_KEY", "valid_key"):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
|
||||
|
||||
class TestInnerApiOnly:
|
||||
@@ -172,22 +168,20 @@ class TestInnerApiOnly:
|
||||
return "success"
|
||||
|
||||
with app.test_request_context(headers={"X-Inner-Api-Key": "valid_key"}):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
with patch.object(dify_config, "INNER_API_KEY", "valid_key"):
|
||||
result = protected_view()
|
||||
result = protected_view()
|
||||
|
||||
assert result == "success"
|
||||
|
||||
def test_should_return_404_when_inner_api_disabled(self, app: Flask):
|
||||
def test_should_return_404_when_inner_api_disabled(self, app: Flask, config_overrides):
|
||||
@inner_api_only
|
||||
def protected_view():
|
||||
return "success"
|
||||
|
||||
config_overrides(INNER_API=False)
|
||||
with app.test_request_context():
|
||||
with patch.object(dify_config, "INNER_API", False):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 404
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 404
|
||||
|
||||
def test_should_return_401_when_api_key_missing(self, app: Flask):
|
||||
@inner_api_only
|
||||
@@ -195,11 +189,9 @@ class TestInnerApiOnly:
|
||||
return "success"
|
||||
|
||||
with app.test_request_context(headers={}):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
with patch.object(dify_config, "INNER_API_KEY", "valid_key"):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
|
||||
def test_should_return_401_when_api_key_invalid(self, app: Flask):
|
||||
@inner_api_only
|
||||
@@ -207,17 +199,15 @@ class TestInnerApiOnly:
|
||||
return "success"
|
||||
|
||||
with app.test_request_context(headers={"X-Inner-Api-Key": "invalid_key"}):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
with patch.object(dify_config, "INNER_API_KEY", "valid_key"):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 401
|
||||
|
||||
|
||||
class TestEnterpriseInnerApiUserAuth:
|
||||
"""Test enterprise_inner_api_user_auth decorator for HMAC-based user authentication"""
|
||||
|
||||
def test_should_pass_through_when_inner_api_disabled(self, app: Flask):
|
||||
def test_should_pass_through_when_inner_api_disabled(self, app: Flask, config_overrides):
|
||||
"""Test that request passes through when INNER_API is disabled"""
|
||||
|
||||
# Arrange
|
||||
@@ -226,9 +216,9 @@ class TestEnterpriseInnerApiUserAuth:
|
||||
return kwargs.get("user", "no_user")
|
||||
|
||||
# Act
|
||||
config_overrides(INNER_API=False)
|
||||
with app.test_request_context():
|
||||
with patch.object(dify_config, "INNER_API", False):
|
||||
result = protected_view()
|
||||
result = protected_view()
|
||||
|
||||
# Assert
|
||||
assert result == "no_user"
|
||||
@@ -243,8 +233,7 @@ class TestEnterpriseInnerApiUserAuth:
|
||||
|
||||
# Act
|
||||
with app.test_request_context(headers={}):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
result = protected_view()
|
||||
result = protected_view()
|
||||
|
||||
# Assert
|
||||
assert result == "no_user"
|
||||
@@ -259,8 +248,7 @@ class TestEnterpriseInnerApiUserAuth:
|
||||
|
||||
# Act
|
||||
with app.test_request_context(headers={"Authorization": "invalid_format"}):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
result = protected_view()
|
||||
result = protected_view()
|
||||
|
||||
# Assert
|
||||
assert result == "no_user"
|
||||
@@ -281,8 +269,7 @@ class TestEnterpriseInnerApiUserAuth:
|
||||
with app.test_request_context(
|
||||
headers={"Authorization": "Bearer user123:wrong_signature", "X-Inner-Api-Key": "valid_key"}
|
||||
):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
result = protected_view()
|
||||
result = protected_view()
|
||||
finally:
|
||||
event.remove(sqlite_engine, "before_cursor_execute", fail_on_query)
|
||||
|
||||
@@ -321,8 +308,7 @@ class TestEnterpriseInnerApiUserAuth:
|
||||
with app.test_request_context(
|
||||
headers={"Authorization": f"Bearer {user_id}:{valid_signature}", "X-Inner-Api-Key": inner_api_key}
|
||||
):
|
||||
with patch.object(dify_config, "INNER_API", True):
|
||||
result = protected_view()
|
||||
result = protected_view()
|
||||
|
||||
# Assert
|
||||
assert isinstance(result, EndUser)
|
||||
@@ -344,14 +330,12 @@ class TestPluginInnerApiOnly:
|
||||
|
||||
# Act
|
||||
with app.test_request_context(headers={"X-Inner-Api-Key": "valid_plugin_key"}):
|
||||
with patch.object(dify_config, "PLUGIN_DAEMON_KEY", "plugin_key"):
|
||||
with patch.object(dify_config, "INNER_API_KEY_FOR_PLUGIN", "valid_plugin_key"):
|
||||
result = protected_view()
|
||||
result = protected_view()
|
||||
|
||||
# Assert
|
||||
assert result == "success"
|
||||
|
||||
def test_should_return_404_when_plugin_daemon_key_not_set(self, app: Flask):
|
||||
def test_should_return_404_when_plugin_daemon_key_not_set(self, app: Flask, config_overrides):
|
||||
"""Test that 404 is returned when PLUGIN_DAEMON_KEY is not set"""
|
||||
|
||||
# Arrange
|
||||
@@ -360,11 +344,11 @@ class TestPluginInnerApiOnly:
|
||||
return "success"
|
||||
|
||||
# Act & Assert
|
||||
config_overrides(PLUGIN_DAEMON_KEY="")
|
||||
with app.test_request_context():
|
||||
with patch.object(dify_config, "PLUGIN_DAEMON_KEY", ""):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 404
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 404
|
||||
|
||||
def test_should_return_404_when_api_key_invalid(self, app: Flask):
|
||||
"""Test that 404 is returned when X-Inner-Api-Key header is invalid (note: returns 404, not 401)"""
|
||||
@@ -376,8 +360,6 @@ class TestPluginInnerApiOnly:
|
||||
|
||||
# Act & Assert
|
||||
with app.test_request_context(headers={"X-Inner-Api-Key": "invalid_key"}):
|
||||
with patch.object(dify_config, "PLUGIN_DAEMON_KEY", "plugin_key"):
|
||||
with patch.object(dify_config, "INNER_API_KEY_FOR_PLUGIN", "valid_plugin_key"):
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 404
|
||||
with pytest.raises(HTTPException) as exc_info:
|
||||
protected_view()
|
||||
assert exc_info.value.code == 404
|
||||
|
||||
@@ -6,6 +6,12 @@ from collections.abc import Iterator
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _swagger_config(config_overrides) -> None:
|
||||
config_overrides(SWAGGER_UI_ENABLED=True)
|
||||
|
||||
|
||||
USER_PROPERTY_SCHEMA = {
|
||||
"description": (
|
||||
"User identifier, unique within the application. This identifier scopes data access; resources created with "
|
||||
@@ -154,14 +160,11 @@ def test_uuid_path_format_is_derived_from_route_converter():
|
||||
}
|
||||
|
||||
|
||||
def test_openapi_json_endpoints_render(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_openapi_json_endpoints_render():
|
||||
from controllers.console import bp as console_bp
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
from controllers.web import bp as web_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -190,12 +193,9 @@ def test_openapi_json_endpoints_render(monkeypatch: pytest.MonkeyPatch):
|
||||
assert app.config["RESTX_INCLUDE_ALL_MODELS"] is True
|
||||
|
||||
|
||||
def test_service_document_file_routes_document_multipart_form_data(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_service_document_file_routes_document_multipart_form_data():
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -236,12 +236,9 @@ def test_service_document_file_routes_document_multipart_form_data(monkeypatch:
|
||||
assert update_operation["requestBody"]["required"] is False
|
||||
|
||||
|
||||
def test_service_openapi_merges_public_api_reference_descriptions(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_service_openapi_merges_public_api_reference_descriptions():
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -261,12 +258,9 @@ def test_service_openapi_merges_public_api_reference_descriptions(monkeypatch: p
|
||||
assert _parameters_by_name(rename_operation)["c_id"]["description"] == "Conversation ID."
|
||||
|
||||
|
||||
def test_service_document_list_documents_query_params_render(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_service_document_list_documents_query_params_render():
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -280,12 +274,9 @@ def test_service_document_list_documents_query_params_render(monkeypatch: pytest
|
||||
assert params[name]["in"] == "query"
|
||||
|
||||
|
||||
def test_service_openapi_documents_decorator_user_contracts(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_service_openapi_documents_decorator_user_contracts():
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -330,12 +321,9 @@ def test_service_openapi_documents_decorator_user_contracts(monkeypatch: pytest.
|
||||
assert events_params["user"]["required"] is True
|
||||
|
||||
|
||||
def test_service_openapi_documents_app_multipart_contracts(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_service_openapi_documents_app_multipart_contracts():
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -365,12 +353,9 @@ def test_service_openapi_documents_app_multipart_contracts(monkeypatch: pytest.M
|
||||
assert pipeline_schema["required"] == ["file"]
|
||||
|
||||
|
||||
def test_service_openapi_documents_non_json_response_media_types(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_service_openapi_documents_non_json_response_media_types():
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -409,12 +394,9 @@ def test_service_openapi_documents_non_json_response_media_types(monkeypatch: py
|
||||
}
|
||||
|
||||
|
||||
def test_service_openapi_documents_uuid_params_and_deprecated_routes(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_service_openapi_documents_uuid_params_and_deprecated_routes():
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -441,12 +423,9 @@ def test_service_openapi_documents_uuid_params_and_deprecated_routes(monkeypatch
|
||||
assert paths["/datasets/{dataset_id}/documents/{document_id}/update_by_text"]["post"]["deprecated"] is True
|
||||
|
||||
|
||||
def test_service_openapi_documents_path_action_enums(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_service_openapi_documents_path_action_enums():
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -465,12 +444,9 @@ def test_service_openapi_documents_path_action_enums(monkeypatch: pytest.MonkeyP
|
||||
assert metadata_params["action"]["schema"]["enum"] == ["enable", "disable"]
|
||||
|
||||
|
||||
def test_service_openapi_documents_conditional_payload_schemas(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_service_openapi_documents_conditional_payload_schemas():
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -497,12 +473,9 @@ def test_service_openapi_documents_conditional_payload_schemas(monkeypatch: pyte
|
||||
assert without_text_branch["properties"]["text"]["type"] == "null"
|
||||
|
||||
|
||||
def test_service_openapi_does_not_encode_docs_coverage_boundaries(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_service_openapi_does_not_encode_docs_coverage_boundaries():
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -525,12 +498,9 @@ def test_service_openapi_does_not_encode_docs_coverage_boundaries(monkeypatch: p
|
||||
assert paths["/datasets/{dataset_id}/documents/{document_id}/update-by-file"]["post"]["deprecated"] is True
|
||||
|
||||
|
||||
def test_service_openapi_documents_auth_and_compatibility_payloads(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_service_openapi_documents_auth_and_compatibility_payloads():
|
||||
from controllers.service_api import bp as service_api_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -556,12 +526,9 @@ def test_service_openapi_documents_auth_and_compatibility_payloads(monkeypatch:
|
||||
assert tag_ids_schema["required"] == ["tag_ids", "target_id"]
|
||||
|
||||
|
||||
def test_console_account_avatar_query_param_renders_as_query(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_console_account_avatar_query_param_renders_as_query():
|
||||
from controllers.console import bp as console_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -576,12 +543,9 @@ def test_console_account_avatar_query_param_renders_as_query(monkeypatch: pytest
|
||||
assert params["avatar"]["required"] is True
|
||||
|
||||
|
||||
def test_console_account_profile_patch_and_deprecated_aliases(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_console_account_profile_patch_and_deprecated_aliases():
|
||||
from controllers.console import bp as console_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
@@ -610,12 +574,9 @@ def test_console_account_profile_patch_and_deprecated_aliases(monkeypatch: pytes
|
||||
assert paths["/account/avatar"]["get"].get("deprecated") is not True
|
||||
|
||||
|
||||
def test_console_agent_debug_conversation_refresh_has_no_body(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_console_agent_debug_conversation_refresh_has_no_body():
|
||||
from controllers.console import bp as console_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.register_blueprint(console_bp)
|
||||
@@ -627,12 +588,9 @@ def test_console_agent_debug_conversation_refresh_has_no_body(monkeypatch: pytes
|
||||
assert "AgentDebugConversationRefreshPayload" not in payload["components"]["schemas"]
|
||||
|
||||
|
||||
def test_console_member_invite_documents_bad_request_response(monkeypatch: pytest.MonkeyPatch):
|
||||
from configs import dify_config
|
||||
def test_console_member_invite_documents_bad_request_response():
|
||||
from controllers.console import bp as console_bp
|
||||
|
||||
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config["TESTING"] = True
|
||||
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
|
||||
|
||||
@@ -77,13 +77,12 @@ class TestPluginRuntimeExecution:
|
||||
return BasePluginClient()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "test-api-key"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="test-api-key",
|
||||
)
|
||||
|
||||
def test_request_preparation(self, plugin_client, mock_config):
|
||||
"""Test that requests are properly prepared with correct headers and URL."""
|
||||
@@ -182,13 +181,12 @@ class TestPluginRuntimeSandboxIsolation:
|
||||
return BasePluginClient()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "secure-api-key"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="secure-api-key",
|
||||
)
|
||||
|
||||
def test_api_key_authentication(self, plugin_client, mock_config):
|
||||
"""Test that all requests include API key for authentication."""
|
||||
@@ -272,13 +270,13 @@ class TestPluginRuntimeResourceLimits:
|
||||
return BasePluginClient()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration with timeout."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "test-key"),
|
||||
patch("core.plugin.impl.base.plugin_daemon_request_timeout", httpx.Timeout(30.0)),
|
||||
):
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="test-key",
|
||||
)
|
||||
with patch("core.plugin.impl.base.plugin_daemon_request_timeout", httpx.Timeout(30.0)):
|
||||
yield
|
||||
|
||||
def test_timeout_configuration_applied(self, plugin_client, mock_config):
|
||||
@@ -346,13 +344,12 @@ class TestPluginRuntimeErrorHandling:
|
||||
return BasePluginClient()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "test-key"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="test-key",
|
||||
)
|
||||
|
||||
def test_plugin_invoke_rate_limit_error(self, plugin_client, mock_config):
|
||||
"""Test handling of rate limit errors during plugin invocation."""
|
||||
@@ -605,13 +602,12 @@ class TestPluginRuntimeCommunication:
|
||||
return BasePluginClient()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "test-key"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="test-key",
|
||||
)
|
||||
|
||||
def test_request_response_communication(self, plugin_client, mock_config):
|
||||
"""Test basic request/response communication pattern."""
|
||||
@@ -811,13 +807,12 @@ class TestPluginToolManagerIntegration:
|
||||
return PluginToolManager()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "test-key"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="test-key",
|
||||
)
|
||||
|
||||
def test_tool_invocation_success(self, tool_manager, mock_config):
|
||||
"""Test successful tool invocation."""
|
||||
@@ -938,13 +933,12 @@ class TestPluginInstallerIntegration:
|
||||
return PluginInstaller()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "test-key"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="test-key",
|
||||
)
|
||||
|
||||
def test_list_plugins_success(self, installer, mock_config):
|
||||
"""Test successful plugin listing."""
|
||||
@@ -1012,13 +1006,12 @@ class TestPluginRuntimeEdgeCases:
|
||||
return BasePluginClient()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "test-key"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="test-key",
|
||||
)
|
||||
|
||||
def test_malformed_json_response(self, plugin_client, mock_config):
|
||||
"""Test handling of malformed JSON responses."""
|
||||
@@ -1174,13 +1167,12 @@ class TestPluginRuntimeAdvancedScenarios:
|
||||
return BasePluginClient()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "test-key"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="test-key",
|
||||
)
|
||||
|
||||
def test_multiple_sequential_requests(self, plugin_client, mock_config):
|
||||
"""Test multiple sequential requests to the same endpoint."""
|
||||
@@ -1360,13 +1352,12 @@ class TestPluginRuntimeSecurityAndValidation:
|
||||
return BasePluginClient()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "secure-key-123"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="secure-key-123",
|
||||
)
|
||||
|
||||
def test_api_key_header_always_present(self, plugin_client, mock_config):
|
||||
"""Test that API key header is always included in requests."""
|
||||
@@ -1480,13 +1471,12 @@ class TestPluginRuntimePerformanceScenarios:
|
||||
return BasePluginClient()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "test-key"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="test-key",
|
||||
)
|
||||
|
||||
def test_high_volume_streaming(self, plugin_client, mock_config):
|
||||
"""Test streaming with high volume of chunks."""
|
||||
@@ -1598,13 +1588,12 @@ class TestPluginToolManagerAdvanced:
|
||||
return PluginToolManager()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "test-key"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="test-key",
|
||||
)
|
||||
|
||||
def test_tool_invocation_with_complex_parameters(self, tool_manager, mock_config):
|
||||
"""Test tool invocation with complex parameter structures."""
|
||||
@@ -1750,13 +1739,12 @@ class TestPluginInstallerAdvanced:
|
||||
return PluginInstaller()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(self):
|
||||
def mock_config(self, config_overrides):
|
||||
"""Mock plugin daemon configuration."""
|
||||
with (
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_URL", "http://127.0.0.1:5002"),
|
||||
patch("core.plugin.impl.base.dify_config.PLUGIN_DAEMON_KEY", "test-key"),
|
||||
):
|
||||
yield
|
||||
config_overrides(
|
||||
PLUGIN_DAEMON_URL="http://127.0.0.1:5002",
|
||||
PLUGIN_DAEMON_KEY="test-key",
|
||||
)
|
||||
|
||||
def test_upload_plugin_package_success(self, installer, mock_config):
|
||||
"""Test successful plugin package upload."""
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Literal
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
@@ -18,6 +19,16 @@ from core.tools.signature import (
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _signature_config(config_overrides: Callable[..., None]) -> None:
|
||||
config_overrides(
|
||||
SECRET_KEY="unit-secret",
|
||||
FILES_URL="https://files.example.com",
|
||||
INTERNAL_FILES_URL="https://internal.example.com",
|
||||
FILES_ACCESS_TIMEOUT=120,
|
||||
)
|
||||
|
||||
|
||||
def test_bind_file_uri_uses_selected_base_and_preserves_remote_url() -> None:
|
||||
uri = "/files/tools/tool-file-id.png?sign=1"
|
||||
|
||||
@@ -31,7 +42,6 @@ def test_bind_file_uri_uses_selected_base_and_preserves_remote_url() -> None:
|
||||
def test_sign_tool_file_uri_has_no_origin(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x08" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
|
||||
uri = sign_tool_file_uri("tool-file-id", ".png")
|
||||
parsed = urlparse(uri)
|
||||
@@ -45,10 +55,6 @@ def test_sign_tool_file_uri_has_no_origin(monkeypatch: pytest.MonkeyPatch) -> No
|
||||
def test_sign_tool_file_and_verify_roundtrip(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x01" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_URL", "https://files.example.com")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.INTERNAL_FILES_URL", "https://internal.example.com")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_ACCESS_TIMEOUT", 120)
|
||||
|
||||
url = sign_tool_file("tool-file-id", ".png", for_external=False)
|
||||
parsed = urlparse(url)
|
||||
@@ -66,10 +72,6 @@ def test_sign_tool_file_and_verify_roundtrip(monkeypatch: pytest.MonkeyPatch) ->
|
||||
def test_sign_tool_file_for_external_uses_files_url(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x04" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_URL", "https://files.example.com")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.INTERNAL_FILES_URL", "https://internal.example.com")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_ACCESS_TIMEOUT", 120)
|
||||
|
||||
url = sign_tool_file("tool-file-id", ".png", for_external=True)
|
||||
parsed = urlparse(url)
|
||||
@@ -79,13 +81,12 @@ def test_sign_tool_file_for_external_uses_files_url(monkeypatch: pytest.MonkeyPa
|
||||
assert parsed.path == "/files/tools/tool-file-id.png"
|
||||
|
||||
|
||||
def test_verify_tool_file_signature_rejects_invalid_sign(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_verify_tool_file_signature_rejects_invalid_sign(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
) -> None:
|
||||
config_overrides(INTERNAL_FILES_URL="", FILES_ACCESS_TIMEOUT=10)
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x02" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_URL", "https://files.example.com")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.INTERNAL_FILES_URL", "")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_ACCESS_TIMEOUT", 10)
|
||||
|
||||
url = sign_tool_file("tool-file-id", ".txt")
|
||||
parsed = urlparse(url)
|
||||
@@ -97,13 +98,12 @@ def test_verify_tool_file_signature_rejects_invalid_sign(monkeypatch: pytest.Mon
|
||||
assert verify_tool_file_signature("tool-file-id", timestamp, nonce, "bad-signature") is False
|
||||
|
||||
|
||||
def test_verify_tool_file_signature_rejects_expired_signature(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_verify_tool_file_signature_rejects_expired_signature(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
) -> None:
|
||||
config_overrides(INTERNAL_FILES_URL="", FILES_ACCESS_TIMEOUT=10)
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x02" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_URL", "https://files.example.com")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.INTERNAL_FILES_URL", "")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_ACCESS_TIMEOUT", 10)
|
||||
|
||||
url = sign_tool_file("tool-file-id", ".txt")
|
||||
parsed = urlparse(url)
|
||||
@@ -119,9 +119,6 @@ def test_verify_tool_file_signature_rejects_expired_signature(monkeypatch: pytes
|
||||
def test_sign_upload_file_preview_url_uses_files_url(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x03" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_URL", "https://files.example.com")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.INTERNAL_FILES_URL", "https://internal.example.com")
|
||||
|
||||
url = sign_upload_file_preview_url("upload-id", ".png")
|
||||
parsed = urlparse(url)
|
||||
@@ -137,9 +134,6 @@ def test_sign_upload_file_preview_url_uses_files_url(monkeypatch: pytest.MonkeyP
|
||||
def test_sign_upload_file_preview_url_ignores_internal_files_url(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x05" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_URL", "https://files.example.com")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.INTERNAL_FILES_URL", "https://internal.example.com")
|
||||
|
||||
url = sign_upload_file_preview_url("upload-id", ".png")
|
||||
parsed = urlparse(url)
|
||||
@@ -152,11 +146,12 @@ def test_sign_upload_file_preview_url_ignores_internal_files_url(monkeypatch: py
|
||||
assert query["sign"][0]
|
||||
|
||||
|
||||
def test_get_signed_file_uri_for_plugin_and_verify_roundtrip(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_get_signed_file_uri_for_plugin_and_verify_roundtrip(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
) -> None:
|
||||
config_overrides(FILES_ACCESS_TIMEOUT=60)
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x06" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_ACCESS_TIMEOUT", 60)
|
||||
|
||||
uri = get_signed_file_uri_for_plugin(
|
||||
filename="report.pdf",
|
||||
@@ -198,13 +193,13 @@ def test_get_signed_file_uri_for_plugin_and_verify_roundtrip(monkeypatch: pytest
|
||||
)
|
||||
def test_plugin_upload_signature_binds_max_size_without_legacy_payload_ambiguity(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
config_overrides: Callable[..., None],
|
||||
user_from: Literal["account", "end-user"] | None,
|
||||
forged_nonce_suffix: str,
|
||||
) -> None:
|
||||
config_overrides(FILES_ACCESS_TIMEOUT=60)
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x0a" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_ACCESS_TIMEOUT", 60)
|
||||
|
||||
uri = get_signed_file_uri_for_plugin(
|
||||
filename="report.pdf",
|
||||
@@ -233,11 +228,12 @@ def test_plugin_upload_signature_binds_max_size_without_legacy_payload_ambiguity
|
||||
assert verify_plugin_file_signature(**forged) is False
|
||||
|
||||
|
||||
def test_plugin_upload_signature_binds_account_user_from(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_plugin_upload_signature_binds_account_user_from(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
) -> None:
|
||||
config_overrides(FILES_ACCESS_TIMEOUT=60)
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x09" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_ACCESS_TIMEOUT", 60)
|
||||
|
||||
uri = get_signed_file_uri_for_plugin(
|
||||
filename="report.pdf",
|
||||
@@ -263,11 +259,12 @@ def test_plugin_upload_signature_binds_account_user_from(monkeypatch: pytest.Mon
|
||||
assert verify_plugin_file_signature(**signed) is False
|
||||
|
||||
|
||||
def test_verify_plugin_file_signature_rejects_invalid_signatures(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
def test_verify_plugin_file_signature_rejects_invalid_signatures(
|
||||
monkeypatch: pytest.MonkeyPatch, config_overrides: Callable[..., None]
|
||||
) -> None:
|
||||
config_overrides(FILES_ACCESS_TIMEOUT=30)
|
||||
monkeypatch.setattr("core.tools.signature.time.time", lambda: 1700000000)
|
||||
monkeypatch.setattr("core.tools.signature.os.urandom", lambda _: b"\x07" * 16)
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.SECRET_KEY", "unit-secret")
|
||||
monkeypatch.setattr("core.tools.signature.dify_config.FILES_ACCESS_TIMEOUT", 30)
|
||||
|
||||
uri = get_signed_file_uri_for_plugin(
|
||||
filename="report.pdf",
|
||||
|
||||
@@ -10,8 +10,6 @@ Test coverage:
|
||||
- Disabled mode behavior
|
||||
"""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from opentelemetry.trace import StatusCode
|
||||
|
||||
@@ -21,10 +19,14 @@ from graphon.enums import BuiltinNodeTypes
|
||||
from graphon.graph_events import GraphRunAbortedEvent
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _otel_config(config_overrides) -> None:
|
||||
config_overrides(ENABLE_OTEL=True)
|
||||
|
||||
|
||||
class TestObservabilityLayerInitialization:
|
||||
"""Test ObservabilityLayer initialization logic."""
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_initialization_when_otel_enabled(self, tracer_provider_with_memory_exporter):
|
||||
"""Test that layer initializes correctly when OTel is enabled."""
|
||||
@@ -34,10 +36,10 @@ class TestObservabilityLayerInitialization:
|
||||
assert BuiltinNodeTypes.TOOL in layer._parsers
|
||||
assert layer._default_parser is not None
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", False)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_true")
|
||||
def test_initialization_when_instrument_flag_enabled(self, tracer_provider_with_memory_exporter):
|
||||
def test_initialization_when_instrument_flag_enabled(self, tracer_provider_with_memory_exporter, config_overrides):
|
||||
"""Test that layer enables when instrument flag is enabled."""
|
||||
config_overrides(ENABLE_OTEL=False)
|
||||
layer = ObservabilityLayer()
|
||||
assert not layer._is_disabled
|
||||
assert layer._tracer is not None
|
||||
@@ -48,7 +50,6 @@ class TestObservabilityLayerInitialization:
|
||||
class TestObservabilityLayerNodeSpanLifecycle:
|
||||
"""Test node span creation and lifecycle management."""
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_node_span_created_and_ended(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_llm_node
|
||||
@@ -65,7 +66,6 @@ class TestObservabilityLayerNodeSpanLifecycle:
|
||||
assert spans[0].name == mock_llm_node.title
|
||||
assert spans[0].status.status_code == StatusCode.OK
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_node_error_recorded_in_span(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_llm_node
|
||||
@@ -84,7 +84,6 @@ class TestObservabilityLayerNodeSpanLifecycle:
|
||||
assert len(spans[0].events) > 0
|
||||
assert any("exception" in event.name.lower() for event in spans[0].events)
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_node_end_without_start_handled_gracefully(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_llm_node
|
||||
@@ -102,7 +101,6 @@ class TestObservabilityLayerNodeSpanLifecycle:
|
||||
class TestObservabilityLayerParserIntegration:
|
||||
"""Test parser integration for different node types."""
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_default_parser_used_for_regular_node(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_start_node
|
||||
@@ -121,7 +119,6 @@ class TestObservabilityLayerParserIntegration:
|
||||
assert attrs["node.execution_id"] == mock_start_node.execution_id
|
||||
assert attrs["node.type"] == mock_start_node.node_type
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_tool_parser_used_for_tool_node(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_tool_node
|
||||
@@ -140,7 +137,6 @@ class TestObservabilityLayerParserIntegration:
|
||||
assert attrs["gen_ai.tool.name"] == mock_tool_node.title
|
||||
assert attrs["gen_ai.tool.type"] == mock_tool_node._node_data.provider_type.value
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_llm_parser_used_for_llm_node(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_llm_node, mock_result_event
|
||||
@@ -178,7 +174,6 @@ class TestObservabilityLayerParserIntegration:
|
||||
assert attrs["gen_ai.completion"] == "test completion"
|
||||
assert attrs["gen_ai.response.finish_reason"] == "stop"
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_retrieval_parser_used_for_retrieval_node(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_retrieval_node, mock_result_event
|
||||
@@ -206,7 +201,6 @@ class TestObservabilityLayerParserIntegration:
|
||||
assert attrs["retrieval.query"] == "test query"
|
||||
assert "retrieval.document" in attrs
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_result_event_extracts_inputs_and_outputs(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_start_node, mock_result_event
|
||||
@@ -237,7 +231,6 @@ class TestObservabilityLayerParserIntegration:
|
||||
class TestObservabilityLayerGraphLifecycle:
|
||||
"""Test graph lifecycle management."""
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_on_graph_start_clears_contexts(self, tracer_provider_with_memory_exporter, mock_llm_node):
|
||||
"""Test that on_graph_start clears node contexts."""
|
||||
@@ -250,7 +243,6 @@ class TestObservabilityLayerGraphLifecycle:
|
||||
layer.on_graph_start()
|
||||
assert len(layer._node_contexts) == 0
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_on_graph_end_with_no_unfinished_spans(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_llm_node
|
||||
@@ -266,7 +258,6 @@ class TestObservabilityLayerGraphLifecycle:
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 1
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_on_graph_end_with_unfinished_spans_logs_warning(
|
||||
self, tracer_provider_with_memory_exporter, mock_llm_node, caplog
|
||||
@@ -283,7 +274,6 @@ class TestObservabilityLayerGraphLifecycle:
|
||||
assert len(layer._node_contexts) == 0
|
||||
assert "node spans were not properly ended" in caplog.text
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", True)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_graph_aborted_event_records_reason_on_current_span(
|
||||
self, tracer_provider_with_memory_exporter, memory_span_exporter, mock_start_node
|
||||
@@ -308,10 +298,10 @@ class TestObservabilityLayerGraphLifecycle:
|
||||
class TestObservabilityLayerDisabledMode:
|
||||
"""Test behavior when layer is disabled."""
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", False)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_disabled_mode_skips_node_start(self, memory_span_exporter, mock_start_node):
|
||||
def test_disabled_mode_skips_node_start(self, memory_span_exporter, mock_start_node, config_overrides):
|
||||
"""Test that disabled layer doesn't create spans on node start."""
|
||||
config_overrides(ENABLE_OTEL=False)
|
||||
layer = ObservabilityLayer()
|
||||
assert layer._is_disabled
|
||||
|
||||
@@ -322,10 +312,10 @@ class TestObservabilityLayerDisabledMode:
|
||||
spans = memory_span_exporter.get_finished_spans()
|
||||
assert len(spans) == 0
|
||||
|
||||
@patch("core.app.workflow.layers.observability.dify_config.ENABLE_OTEL", False)
|
||||
@pytest.mark.usefixtures("mock_is_instrument_flag_enabled_false")
|
||||
def test_disabled_mode_skips_node_end(self, memory_span_exporter, mock_llm_node):
|
||||
def test_disabled_mode_skips_node_end(self, memory_span_exporter, mock_llm_node, config_overrides):
|
||||
"""Test that disabled layer doesn't process node end."""
|
||||
config_overrides(ENABLE_OTEL=False)
|
||||
layer = ObservabilityLayer()
|
||||
assert layer._is_disabled
|
||||
|
||||
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
from collections.abc import Callable
|
||||
from types import SimpleNamespace
|
||||
from typing import cast
|
||||
from unittest.mock import patch
|
||||
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from extensions.logstore.repositories.logstore_workflow_execution_repository import (
|
||||
LogstoreWorkflowExecutionRepository,
|
||||
)
|
||||
from models.account import Account
|
||||
from models.enums import WorkflowRunTriggeredFrom
|
||||
|
||||
|
||||
def test_repository_uses_typed_logstore_migration_flags(
|
||||
config_overrides: Callable[..., None],
|
||||
sqlite_session_factory: sessionmaker[Session],
|
||||
) -> None:
|
||||
config_overrides(
|
||||
LOGSTORE_DUAL_WRITE_ENABLED=True,
|
||||
LOGSTORE_ENABLE_PUT_GRAPH_FIELD=False,
|
||||
)
|
||||
with (
|
||||
patch("extensions.logstore.repositories.logstore_workflow_execution_repository.AliyunLogStore"),
|
||||
patch(
|
||||
"extensions.logstore.repositories.logstore_workflow_execution_repository."
|
||||
"SQLAlchemyWorkflowExecutionRepository"
|
||||
),
|
||||
):
|
||||
repository = LogstoreWorkflowExecutionRepository(
|
||||
session_factory=sqlite_session_factory,
|
||||
tenant_id="tenant-1",
|
||||
user=cast(Account, SimpleNamespace(id="account-1")),
|
||||
app_id="app-1",
|
||||
triggered_from=WorkflowRunTriggeredFrom.APP_RUN,
|
||||
)
|
||||
|
||||
assert repository._enable_dual_write is True
|
||||
assert repository._enable_put_graph_field is False
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
from collections.abc import Callable
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from sqlalchemy.orm import Session, sessionmaker
|
||||
|
||||
from extensions.logstore.repositories.logstore_workflow_node_execution_repository import (
|
||||
@@ -17,9 +17,9 @@ def _make_account() -> Account:
|
||||
|
||||
|
||||
def test_save_synchronously_writes_sql_when_dual_write_is_disabled(
|
||||
monkeypatch: pytest.MonkeyPatch, sqlite_session_factory: sessionmaker[Session]
|
||||
config_overrides: Callable[..., None], sqlite_session_factory: sessionmaker[Session]
|
||||
) -> None:
|
||||
monkeypatch.delenv("LOGSTORE_DUAL_WRITE_ENABLED", raising=False)
|
||||
config_overrides(LOGSTORE_DUAL_WRITE_ENABLED=False)
|
||||
with (
|
||||
patch("extensions.logstore.repositories.logstore_workflow_node_execution_repository.AliyunLogStore"),
|
||||
patch(
|
||||
|
||||
@@ -41,26 +41,32 @@ def test_extract_access_token():
|
||||
assert extract_webapp_access_token(request) == expected_webapp
|
||||
|
||||
|
||||
def test_real_cookie_name_uses_host_prefix_without_domain(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(token.dify_config, "CONSOLE_WEB_URL", "https://console.example.com", raising=False)
|
||||
monkeypatch.setattr(token.dify_config, "CONSOLE_API_URL", "https://api.example.com", raising=False)
|
||||
monkeypatch.setattr(token.dify_config, "COOKIE_DOMAIN", "", raising=False)
|
||||
def test_real_cookie_name_uses_host_prefix_without_domain(config_overrides):
|
||||
config_overrides(
|
||||
CONSOLE_WEB_URL="https://console.example.com",
|
||||
CONSOLE_API_URL="https://api.example.com",
|
||||
COOKIE_DOMAIN="",
|
||||
)
|
||||
|
||||
assert token._real_cookie_name("csrf_token") == "__Host-csrf_token"
|
||||
|
||||
|
||||
def test_real_cookie_name_without_host_prefix_when_domain_present(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(token.dify_config, "CONSOLE_WEB_URL", "https://console.example.com", raising=False)
|
||||
monkeypatch.setattr(token.dify_config, "CONSOLE_API_URL", "https://api.example.com", raising=False)
|
||||
monkeypatch.setattr(token.dify_config, "COOKIE_DOMAIN", ".example.com", raising=False)
|
||||
def test_real_cookie_name_without_host_prefix_when_domain_present(config_overrides):
|
||||
config_overrides(
|
||||
CONSOLE_WEB_URL="https://console.example.com",
|
||||
CONSOLE_API_URL="https://api.example.com",
|
||||
COOKIE_DOMAIN=".example.com",
|
||||
)
|
||||
|
||||
assert token._real_cookie_name("csrf_token") == "csrf_token"
|
||||
|
||||
|
||||
def test_set_csrf_cookie_includes_domain_when_configured(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(token.dify_config, "CONSOLE_WEB_URL", "https://console.example.com", raising=False)
|
||||
monkeypatch.setattr(token.dify_config, "CONSOLE_API_URL", "https://api.example.com", raising=False)
|
||||
monkeypatch.setattr(token.dify_config, "COOKIE_DOMAIN", ".example.com", raising=False)
|
||||
def test_set_csrf_cookie_includes_domain_when_configured(config_overrides):
|
||||
config_overrides(
|
||||
CONSOLE_WEB_URL="https://console.example.com",
|
||||
CONSOLE_API_URL="https://api.example.com",
|
||||
COOKIE_DOMAIN=".example.com",
|
||||
)
|
||||
|
||||
response = Response()
|
||||
request = MagicMock()
|
||||
@@ -94,12 +100,14 @@ def test_non_whitelisted_path_requires_csrf():
|
||||
token.check_csrf_token(request, "account-1")
|
||||
|
||||
|
||||
def test_admin_api_key_header_bypasses_csrf_when_console_cookie_is_present(monkeypatch: pytest.MonkeyPatch):
|
||||
monkeypatch.setattr(token.dify_config, "ADMIN_API_KEY_ENABLE", True)
|
||||
monkeypatch.setattr(token.dify_config, "ADMIN_API_KEY", "admin-key")
|
||||
monkeypatch.setattr(token.dify_config, "CONSOLE_WEB_URL", "http://console.example.com")
|
||||
monkeypatch.setattr(token.dify_config, "CONSOLE_API_URL", "http://api.example.com")
|
||||
monkeypatch.setattr(token.dify_config, "COOKIE_DOMAIN", "")
|
||||
def test_admin_api_key_header_bypasses_csrf_when_console_cookie_is_present(config_overrides):
|
||||
config_overrides(
|
||||
ADMIN_API_KEY_ENABLE=True,
|
||||
ADMIN_API_KEY="admin-key",
|
||||
CONSOLE_WEB_URL="http://console.example.com",
|
||||
CONSOLE_API_URL="http://api.example.com",
|
||||
COOKIE_DOMAIN="",
|
||||
)
|
||||
request = cast(
|
||||
Request,
|
||||
MockRequest(
|
||||
|
||||
Reference in New Issue
Block a user