mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 02:28:30 +08:00
346 lines
13 KiB
Python
346 lines
13 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import secrets
|
|
from typing import cast
|
|
|
|
import httpx
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from dify_agent.adapters.shell.enterprise import EnterpriseShellProvider
|
|
from dify_agent.adapters.shell.shellctl import ShellctlProvider
|
|
from dify_agent.agent_stub.server.agent_stub_drive import DifyApiAgentStubDriveRequestHandler
|
|
from dify_agent.agent_stub.server.agent_stub_files import DifyApiAgentStubFileRequestHandler
|
|
from dify_agent.agent_stub.server.tokens.agent_stub import AgentStubTokenCodec
|
|
from dify_agent.server.settings import ServerSettings
|
|
|
|
|
|
def _base64url_secret(value: bytes) -> str:
|
|
import base64
|
|
|
|
return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii")
|
|
|
|
|
|
def test_server_settings_reads_shellctl_entrypoint_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_SHELLCTL_ENTRYPOINT", "http://shellctl.example")
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.shellctl_entrypoint == "http://shellctl.example"
|
|
|
|
|
|
def test_server_settings_reads_shellctl_auth_token_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_SHELLCTL_AUTH_TOKEN", "shell-secret")
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.shellctl_auth_token == "shell-secret"
|
|
|
|
|
|
def test_server_settings_reads_shell_home_root_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_SHELL_HOME_ROOT", "/tmp/dify-agent-home/")
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.shell_home_root == "/tmp/dify-agent-home"
|
|
|
|
|
|
def test_server_settings_rejects_relative_shell_home_root(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_SHELL_HOME_ROOT", "relative/path")
|
|
|
|
with pytest.raises(ValidationError, match="DIFY_AGENT_SHELL_HOME_ROOT must be an absolute path"):
|
|
ServerSettings()
|
|
|
|
|
|
def test_server_settings_reads_enterprise_timeouts_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_ENTERPRISE_SANDBOX_GATEWAY_TIMEOUT", "45")
|
|
monkeypatch.setenv("DIFY_AGENT_ENTERPRISE_SANDBOX_PROXY_TIMEOUT", "90")
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.enterprise_sandbox_gateway_timeout == 45
|
|
assert settings.enterprise_sandbox_proxy_timeout == 90
|
|
|
|
|
|
def test_server_settings_defaults_shellctl_auth_token_to_none(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
monkeypatch.delenv("DIFY_AGENT_SHELLCTL_AUTH_TOKEN", raising=False)
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.shellctl_auth_token is None
|
|
|
|
|
|
def test_server_settings_reads_agent_stub_settings_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_STUB_API_BASE_URL", "https://agent.example.com/agent-stub/")
|
|
monkeypatch.setenv("DIFY_AGENT_SERVER_SECRET_KEY", _base64url_secret(secrets.token_bytes(32)))
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.agent_stub_api_base_url == "https://agent.example.com/agent-stub"
|
|
|
|
|
|
def test_server_settings_normalizes_agent_stub_service_root_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_STUB_API_BASE_URL", "https://agent.example.com")
|
|
monkeypatch.setenv("DIFY_AGENT_SERVER_SECRET_KEY", _base64url_secret(secrets.token_bytes(32)))
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.agent_stub_api_base_url == "https://agent.example.com/agent-stub"
|
|
|
|
|
|
def test_server_settings_ignores_obsolete_legacy_settings_namespace(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_SHELL_BACK_PROXY_PUBLIC_URL", "https://agent.example.com/back-proxy/")
|
|
monkeypatch.setenv("DIFY_AGENT_BACK_PROXY_URL", "https://agent.example.com/back-proxy/")
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.agent_stub_api_base_url is None
|
|
|
|
|
|
def test_server_settings_rejects_agent_stub_api_base_url_with_query_or_fragment() -> None:
|
|
secret = _base64url_secret(secrets.token_bytes(32))
|
|
|
|
with pytest.raises(ValidationError, match="query string or fragment"):
|
|
_ = ServerSettings(
|
|
agent_stub_api_base_url="https://agent.example.com/agent-stub?x=1",
|
|
server_secret_key=secret,
|
|
)
|
|
|
|
with pytest.raises(ValidationError, match="query string or fragment"):
|
|
_ = ServerSettings(
|
|
agent_stub_api_base_url="https://agent.example.com/agent-stub#fragment",
|
|
server_secret_key=secret,
|
|
)
|
|
|
|
|
|
def test_server_settings_rejects_agent_stub_api_base_url_with_unexpected_path() -> None:
|
|
with pytest.raises(ValidationError, match="empty or /agent-stub"):
|
|
_ = ServerSettings(
|
|
agent_stub_api_base_url="https://agent.example.com/foo",
|
|
server_secret_key=_base64url_secret(secrets.token_bytes(32)),
|
|
)
|
|
|
|
|
|
def test_server_settings_rejects_public_agent_stub_api_base_url_without_secret_key() -> None:
|
|
with pytest.raises(ValidationError, match="DIFY_AGENT_SERVER_SECRET_KEY"):
|
|
_ = ServerSettings(agent_stub_api_base_url="https://agent.example.com/agent-stub")
|
|
|
|
|
|
def test_server_settings_accepts_grpc_agent_stub_api_base_url_and_bind_override() -> None:
|
|
settings = ServerSettings(
|
|
agent_stub_api_base_url="grpc://agent.example.com:9091",
|
|
agent_stub_grpc_bind_address="0.0.0.0:9191",
|
|
server_secret_key=_base64url_secret(secrets.token_bytes(32)),
|
|
)
|
|
|
|
assert settings.agent_stub_api_base_url == "grpc://agent.example.com:9091"
|
|
assert settings.agent_stub_grpc_bind_address == "0.0.0.0:9191"
|
|
|
|
|
|
def test_server_settings_rejects_grpc_bind_override_without_grpc_url() -> None:
|
|
with pytest.raises(ValidationError, match="grpc://"):
|
|
_ = ServerSettings(
|
|
agent_stub_api_base_url="https://agent.example.com/agent-stub",
|
|
agent_stub_grpc_bind_address="0.0.0.0:9191",
|
|
server_secret_key=_base64url_secret(secrets.token_bytes(32)),
|
|
)
|
|
|
|
|
|
def test_server_settings_rejects_invalid_server_secret_key() -> None:
|
|
with pytest.raises(ValidationError, match="32 decoded bytes"):
|
|
_ = ServerSettings(server_secret_key=_base64url_secret(b"short"))
|
|
|
|
|
|
def test_server_settings_rejects_padded_or_quoted_server_secret_key() -> None:
|
|
secret = _base64url_secret(secrets.token_bytes(32))
|
|
|
|
with pytest.raises(ValidationError, match="unpadded base64url"):
|
|
_ = ServerSettings(server_secret_key=f"{secret}=")
|
|
|
|
with pytest.raises(ValidationError, match="unpadded base64url"):
|
|
_ = ServerSettings(server_secret_key=f'"{secret}"')
|
|
|
|
|
|
def test_server_settings_normalizes_inner_api_url_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_INNER_API_URL", "https://api.example.com/")
|
|
monkeypatch.setenv("DIFY_AGENT_INNER_API_KEY", "inner-secret")
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.inner_api_url == "https://api.example.com"
|
|
assert settings.inner_api_key == "inner-secret"
|
|
|
|
|
|
def test_server_settings_allows_inner_api_url_without_key_until_a_bridge_is_used() -> None:
|
|
settings = ServerSettings(inner_api_key="inner-secret")
|
|
assert settings.inner_api_key == "inner-secret"
|
|
assert settings.inner_api_url == "http://localhost:5001"
|
|
|
|
|
|
def test_server_settings_rejects_inner_api_url_with_query_or_fragment() -> None:
|
|
with pytest.raises(ValidationError, match="query string or fragment"):
|
|
_ = ServerSettings(
|
|
inner_api_url="https://api.example.com?x=1",
|
|
inner_api_key="inner-secret",
|
|
)
|
|
|
|
with pytest.raises(ValidationError, match="query string or fragment"):
|
|
_ = ServerSettings(
|
|
inner_api_url="https://api.example.com#frag",
|
|
inner_api_key="inner-secret",
|
|
)
|
|
|
|
|
|
def test_server_settings_create_agent_stub_token_codec_returns_none_without_secret() -> None:
|
|
assert ServerSettings().create_agent_stub_token_codec() is None
|
|
|
|
|
|
def test_server_settings_create_agent_stub_token_codec_returns_codec_when_secret_is_configured() -> None:
|
|
settings = ServerSettings(server_secret_key=_base64url_secret(secrets.token_bytes(32)))
|
|
|
|
codec = settings.create_agent_stub_token_codec()
|
|
|
|
assert isinstance(codec, AgentStubTokenCodec)
|
|
|
|
|
|
def test_server_settings_create_agent_stub_file_request_handler_returns_none_without_full_settings() -> None:
|
|
assert ServerSettings().create_agent_stub_file_request_handler() is None
|
|
|
|
|
|
def test_server_settings_create_agent_stub_file_request_handler_returns_handler_when_configured() -> None:
|
|
settings = ServerSettings(
|
|
inner_api_url="https://api.example.com",
|
|
inner_api_key="inner-secret",
|
|
)
|
|
|
|
handler = settings.create_agent_stub_file_request_handler()
|
|
|
|
assert isinstance(handler, DifyApiAgentStubFileRequestHandler)
|
|
assert handler.inner_api_url == "https://api.example.com"
|
|
assert handler.inner_api_key == "inner-secret"
|
|
|
|
|
|
def test_server_settings_create_agent_stub_drive_request_handler_returns_none_without_full_settings() -> None:
|
|
assert ServerSettings().create_agent_stub_drive_request_handler() is None
|
|
|
|
|
|
def test_server_settings_create_agent_stub_drive_request_handler_returns_handler_when_configured() -> None:
|
|
settings = ServerSettings(
|
|
inner_api_url="https://api.example.com",
|
|
inner_api_key="inner-secret",
|
|
outbound_http_connect_timeout=11,
|
|
outbound_http_read_timeout=22,
|
|
outbound_http_write_timeout=33,
|
|
outbound_http_pool_timeout=44,
|
|
)
|
|
|
|
handler = settings.create_agent_stub_drive_request_handler()
|
|
|
|
assert isinstance(handler, DifyApiAgentStubDriveRequestHandler)
|
|
assert handler.inner_api_url == "https://api.example.com"
|
|
assert handler.inner_api_key == "inner-secret"
|
|
timeout = cast(httpx.Timeout, handler.timeout)
|
|
assert timeout.connect == 11
|
|
assert timeout.read == 22
|
|
assert timeout.write == 33
|
|
assert timeout.pool == 44
|
|
|
|
|
|
def test_build_shell_provider_returns_none_when_shellctl_entrypoint_is_unset(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
monkeypatch.delenv("DIFY_AGENT_SHELLCTL_ENTRYPOINT", raising=False)
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
assert ServerSettings().build_shell_provider() is None
|
|
|
|
|
|
def test_build_shell_provider_returns_shellctl_provider_when_configured() -> None:
|
|
settings = ServerSettings(
|
|
shell_provider="shellctl",
|
|
shellctl_entrypoint="http://shellctl.example",
|
|
shellctl_auth_token="shell-secret",
|
|
)
|
|
|
|
provider = settings.build_shell_provider()
|
|
|
|
assert isinstance(provider, ShellctlProvider)
|
|
assert provider.entrypoint == "http://shellctl.example"
|
|
assert provider.token == "shell-secret"
|
|
|
|
|
|
def test_build_shell_provider_returns_enterprise_provider_when_selected() -> None:
|
|
settings = ServerSettings(
|
|
shell_provider="enterprise",
|
|
enterprise_sandbox_gateway_endpoint="https://gateway.example",
|
|
enterprise_sandbox_gateway_auth_token="gateway-secret",
|
|
enterprise_sandbox_gateway_timeout=45,
|
|
enterprise_sandbox_proxy_timeout=90,
|
|
)
|
|
|
|
provider = settings.build_shell_provider()
|
|
|
|
assert isinstance(provider, EnterpriseShellProvider)
|
|
assert provider.gateway_endpoint == "https://gateway.example"
|
|
assert provider.auth_token == "gateway-secret"
|
|
assert provider.gateway_timeout == 45
|
|
assert provider.proxy_timeout == 90
|
|
|
|
|
|
def test_build_shell_provider_returns_none_when_enterprise_endpoint_is_unset(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
monkeypatch.delenv("DIFY_AGENT_ENTERPRISE_SANDBOX_GATEWAY_ENDPOINT", raising=False)
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
assert ServerSettings(shell_provider="enterprise").build_shell_provider() is None
|
|
|
|
|
|
def test_build_shell_provider_rejects_blank_shellctl_entrypoint() -> None:
|
|
with pytest.raises(ValidationError, match="shellctl_entrypoint is required"):
|
|
_ = ServerSettings(shell_provider="shellctl", shellctl_entrypoint=" ").build_shell_provider()
|
|
|
|
|
|
def test_server_settings_parses_shell_redact_patterns_json_array(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_SHELL_REDACT_PATTERNS", '["sk-[A-Za-z0-9]+","ghp_[A-Za-z0-9]{36}"]')
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.get_shell_redact_patterns() == ["sk-[A-Za-z0-9]+", "ghp_[A-Za-z0-9]{36}"]
|
|
|
|
|
|
def test_server_settings_shell_redact_patterns_empty_string_yields_empty_list(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_SHELL_REDACT_PATTERNS", "")
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.get_shell_redact_patterns() == []
|
|
|
|
|
|
def test_server_settings_shell_redact_patterns_defaults_to_empty_list(
|
|
monkeypatch: pytest.MonkeyPatch,
|
|
tmp_path: Path,
|
|
) -> None:
|
|
monkeypatch.delenv("DIFY_AGENT_SHELL_REDACT_PATTERNS", raising=False)
|
|
monkeypatch.chdir(tmp_path)
|
|
|
|
settings = ServerSettings()
|
|
|
|
assert settings.get_shell_redact_patterns() == []
|
|
|
|
|
|
def test_server_settings_rejects_non_array_shell_redact_patterns(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setenv("DIFY_AGENT_SHELL_REDACT_PATTERNS", '{"key": "value"}')
|
|
|
|
settings = ServerSettings()
|
|
|
|
with pytest.raises(ValueError, match="must be a JSON array"):
|
|
settings.get_shell_redact_patterns()
|