chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from pytest import fixture
|
||||
|
||||
|
||||
@fixture
|
||||
def aca_python_sessions_unit_test_env(monkeypatch, exclude_list, override_env_param_dict):
|
||||
"""Fixture to set environment variables for ACA Python Unit Tests."""
|
||||
if exclude_list is None:
|
||||
exclude_list = []
|
||||
|
||||
if override_env_param_dict is None:
|
||||
override_env_param_dict = {}
|
||||
|
||||
env_vars = {
|
||||
"ACA_POOL_MANAGEMENT_ENDPOINT": "https://test.endpoint/",
|
||||
}
|
||||
|
||||
env_vars.update(override_env_param_dict)
|
||||
|
||||
for key, value in env_vars.items():
|
||||
if key not in exclude_list:
|
||||
monkeypatch.setenv(key, value)
|
||||
else:
|
||||
monkeypatch.delenv(key, raising=False)
|
||||
|
||||
return env_vars
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
|
||||
from semantic_kernel.core_plugins.conversation_summary_plugin import ConversationSummaryPlugin
|
||||
from semantic_kernel.prompt_template.prompt_template_config import PromptTemplateConfig
|
||||
|
||||
|
||||
def test_conversation_summary_plugin():
|
||||
config = PromptTemplateConfig(name="test", description="test")
|
||||
plugin = ConversationSummaryPlugin(config)
|
||||
assert plugin._summarizeConversationFunction is not None
|
||||
assert plugin.return_key == "summary"
|
||||
|
||||
|
||||
def test_conversation_summary_plugin_with_deprecated_value(kernel):
|
||||
config = PromptTemplateConfig(name="test", description="test")
|
||||
plugin = ConversationSummaryPlugin(config, kernel=kernel)
|
||||
assert plugin._summarizeConversationFunction is not None
|
||||
assert plugin.return_key == "summary"
|
||||
@@ -0,0 +1,95 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.core_plugins.crew_ai.crew_ai_enterprise import CrewAIEnterprise
|
||||
from semantic_kernel.core_plugins.crew_ai.crew_ai_models import CrewAIEnterpriseKickoffState, CrewAIStatusResponse
|
||||
from semantic_kernel.exceptions.function_exceptions import PluginInitializationError
|
||||
from semantic_kernel.functions.kernel_parameter_metadata import KernelParameterMetadata
|
||||
from semantic_kernel.functions.kernel_plugin import KernelPlugin
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def crew_ai_enterprise():
|
||||
return CrewAIEnterprise(endpoint="https://test.com", auth_token="FakeToken")
|
||||
|
||||
|
||||
def test_it_can_be_instantiated(crew_ai_enterprise):
|
||||
assert crew_ai_enterprise is not None
|
||||
|
||||
|
||||
def test_create_kernel_plugin(crew_ai_enterprise):
|
||||
plugin = crew_ai_enterprise.create_kernel_plugin(
|
||||
name="test_plugin",
|
||||
description="Test plugin",
|
||||
parameters=[KernelParameterMetadata(name="param1")],
|
||||
)
|
||||
assert isinstance(plugin, KernelPlugin)
|
||||
assert "kickoff" in plugin.functions
|
||||
assert "kickoff_and_wait" in plugin.functions
|
||||
assert "get_status" in plugin.functions
|
||||
assert "wait_for_completion" in plugin.functions
|
||||
|
||||
|
||||
@patch("semantic_kernel.core_plugins.crew_ai.crew_ai_enterprise.CrewAIEnterpriseClient.kickoff")
|
||||
async def test_kickoff(mock_kickoff, crew_ai_enterprise):
|
||||
mock_kickoff.return_value.kickoff_id = "123"
|
||||
kickoff_id = await crew_ai_enterprise.kickoff(inputs={"param1": "value"})
|
||||
assert kickoff_id == "123"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"state",
|
||||
[
|
||||
CrewAIEnterpriseKickoffState.Pending,
|
||||
CrewAIEnterpriseKickoffState.Started,
|
||||
CrewAIEnterpriseKickoffState.Running,
|
||||
CrewAIEnterpriseKickoffState.Success,
|
||||
CrewAIEnterpriseKickoffState.Failed,
|
||||
CrewAIEnterpriseKickoffState.Failure,
|
||||
CrewAIEnterpriseKickoffState.Not_Found,
|
||||
],
|
||||
)
|
||||
@patch("semantic_kernel.core_plugins.crew_ai.crew_ai_enterprise.CrewAIEnterpriseClient.get_status")
|
||||
async def test_get_crew_kickoff_status(mock_get_status, crew_ai_enterprise, state):
|
||||
mock_get_status.return_value = CrewAIStatusResponse(state=state.value)
|
||||
status_response = await crew_ai_enterprise.get_crew_kickoff_status(kickoff_id="123")
|
||||
assert status_response.state == state
|
||||
|
||||
|
||||
@patch("semantic_kernel.core_plugins.crew_ai.crew_ai_enterprise.CrewAIEnterpriseClient.get_status")
|
||||
async def test_wait_for_crew_completion(mock_get_status, crew_ai_enterprise):
|
||||
mock_get_status.side_effect = [
|
||||
CrewAIStatusResponse(state=CrewAIEnterpriseKickoffState.Pending),
|
||||
CrewAIStatusResponse(state=CrewAIEnterpriseKickoffState.Success, result="result"),
|
||||
]
|
||||
result = await crew_ai_enterprise.wait_for_crew_completion(kickoff_id="123")
|
||||
assert result == "result"
|
||||
|
||||
|
||||
def test_build_arguments(crew_ai_enterprise):
|
||||
parameters = [KernelParameterMetadata(name="param1")]
|
||||
arguments = {"param1": "value"}
|
||||
args = crew_ai_enterprise._build_arguments(parameters, arguments)
|
||||
assert args["param1"] == "value"
|
||||
|
||||
|
||||
def test_build_arguments_missing_param(crew_ai_enterprise):
|
||||
parameters = [KernelParameterMetadata(name="param1")]
|
||||
arguments = {}
|
||||
with pytest.raises(PluginInitializationError):
|
||||
crew_ai_enterprise._build_arguments(parameters, arguments)
|
||||
|
||||
|
||||
@patch("semantic_kernel.core_plugins.crew_ai.crew_ai_enterprise.CrewAIEnterpriseClient.__aenter__")
|
||||
async def test_aenter(mock_aenter, crew_ai_enterprise):
|
||||
await crew_ai_enterprise.__aenter__()
|
||||
mock_aenter.assert_called_once()
|
||||
|
||||
|
||||
@patch("semantic_kernel.core_plugins.crew_ai.crew_ai_enterprise.CrewAIEnterpriseClient.__aexit__")
|
||||
async def test_aexit(mock_aexit, crew_ai_enterprise):
|
||||
await crew_ai_enterprise.__aexit__()
|
||||
mock_aexit.assert_called_once()
|
||||
@@ -0,0 +1,425 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel import Kernel
|
||||
from semantic_kernel.core_plugins.http_plugin import HttpPlugin
|
||||
from semantic_kernel.exceptions import FunctionExecutionException
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
|
||||
|
||||
async def test_it_can_be_instantiated():
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
assert plugin is not None
|
||||
|
||||
|
||||
async def test_it_can_be_instantiated_with_allowed_domains():
|
||||
plugin = HttpPlugin(allowed_domains={"example.com", "api.example.com"})
|
||||
assert plugin is not None
|
||||
assert plugin.allowed_domains == {"example.com", "api.example.com"}
|
||||
|
||||
|
||||
async def test_it_can_be_imported():
|
||||
kernel = Kernel()
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
kernel.add_plugin(plugin, "http")
|
||||
assert kernel.get_plugin(plugin_name="http") is not None
|
||||
assert kernel.get_plugin(plugin_name="http").name == "http"
|
||||
assert kernel.get_function(plugin_name="http", function_name="getAsync") is not None
|
||||
assert kernel.get_function(plugin_name="http", function_name="postAsync") is not None
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.get")
|
||||
async def test_get(mock_get):
|
||||
mock_get.return_value.__aenter__.return_value.text.return_value = "Hello"
|
||||
mock_get.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
response = await plugin.get("https://example.org/get")
|
||||
assert response == "Hello"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["get", "post", "put", "delete"])
|
||||
async def test_fail_no_url(method):
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
with pytest.raises(FunctionExecutionException):
|
||||
await getattr(plugin, method)(url="")
|
||||
|
||||
|
||||
async def test_get_none_url():
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
with pytest.raises(FunctionExecutionException):
|
||||
await plugin.get(None)
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.post")
|
||||
async def test_post(mock_post):
|
||||
mock_post.return_value.__aenter__.return_value.text.return_value = "Hello World !"
|
||||
mock_post.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
arguments = KernelArguments(url="https://example.org/post", body="{message: 'Hello, world!'}")
|
||||
response = await plugin.post(**arguments)
|
||||
assert response == "Hello World !"
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.post")
|
||||
async def test_post_nobody(mock_post):
|
||||
mock_post.return_value.__aenter__.return_value.text.return_value = "Hello World !"
|
||||
mock_post.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
arguments = KernelArguments(url="https://example.org/post")
|
||||
response = await plugin.post(**arguments)
|
||||
assert response == "Hello World !"
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.put")
|
||||
async def test_put(mock_put):
|
||||
mock_put.return_value.__aenter__.return_value.text.return_value = "Hello World !"
|
||||
mock_put.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
arguments = KernelArguments(url="https://example.org/put", body="{message: 'Hello, world!'}")
|
||||
response = await plugin.put(**arguments)
|
||||
assert response == "Hello World !"
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.put")
|
||||
async def test_put_nobody(mock_put):
|
||||
mock_put.return_value.__aenter__.return_value.text.return_value = "Hello World !"
|
||||
mock_put.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
arguments = KernelArguments(url="https://example.org/put")
|
||||
response = await plugin.put(**arguments)
|
||||
assert response == "Hello World !"
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.delete")
|
||||
async def test_delete(mock_delete):
|
||||
mock_delete.return_value.__aenter__.return_value.text.return_value = "Hello World !"
|
||||
mock_delete.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
arguments = KernelArguments(url="https://example.org/delete")
|
||||
response = await plugin.delete(**arguments)
|
||||
assert response == "Hello World !"
|
||||
|
||||
|
||||
# Tests for allowed_domains functionality
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["get", "post", "put", "delete"])
|
||||
async def test_throws_for_disallowed_domain(method):
|
||||
"""Test that all HTTP methods throw an exception when the domain is not in the allowed list."""
|
||||
plugin = HttpPlugin(allowed_domains={"example.com"})
|
||||
disallowed_url = "https://notexample.com/path"
|
||||
|
||||
with pytest.raises(FunctionExecutionException, match="Sending requests to the provided location is not allowed"):
|
||||
if method in ["post", "put"]:
|
||||
await getattr(plugin, method)(url=disallowed_url, body={"key": "value"})
|
||||
else:
|
||||
await getattr(plugin, method)(url=disallowed_url)
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.get")
|
||||
async def test_get_allowed_domain(mock_get):
|
||||
"""Test that GET request succeeds when the domain is in the allowed list."""
|
||||
mock_get.return_value.__aenter__.return_value.text.return_value = "Hello"
|
||||
mock_get.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allowed_domains={"example.org"})
|
||||
response = await plugin.get("https://example.org/get")
|
||||
assert response == "Hello"
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.post")
|
||||
async def test_post_allowed_domain(mock_post):
|
||||
"""Test that POST request succeeds when the domain is in the allowed list."""
|
||||
mock_post.return_value.__aenter__.return_value.text.return_value = "Hello"
|
||||
mock_post.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allowed_domains={"example.org"})
|
||||
response = await plugin.post("https://example.org/post", body={"key": "value"})
|
||||
assert response == "Hello"
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.put")
|
||||
async def test_put_allowed_domain(mock_put):
|
||||
"""Test that PUT request succeeds when the domain is in the allowed list."""
|
||||
mock_put.return_value.__aenter__.return_value.text.return_value = "Hello"
|
||||
mock_put.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allowed_domains={"example.org"})
|
||||
response = await plugin.put("https://example.org/put", body={"key": "value"})
|
||||
assert response == "Hello"
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.delete")
|
||||
async def test_delete_allowed_domain(mock_delete):
|
||||
"""Test that DELETE request succeeds when the domain is in the allowed list."""
|
||||
mock_delete.return_value.__aenter__.return_value.text.return_value = "Hello"
|
||||
mock_delete.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allowed_domains={"example.org"})
|
||||
response = await plugin.delete("https://example.org/delete")
|
||||
assert response == "Hello"
|
||||
|
||||
|
||||
async def test_allowed_domains_case_insensitive():
|
||||
"""Test that domain matching is case-insensitive."""
|
||||
plugin = HttpPlugin(allowed_domains={"EXAMPLE.COM"})
|
||||
# Should not raise - case insensitive
|
||||
assert plugin._is_uri_allowed("https://example.com/path") is True
|
||||
assert plugin._is_uri_allowed("https://EXAMPLE.COM/path") is True
|
||||
assert plugin._is_uri_allowed("https://Example.Com/path") is True
|
||||
|
||||
|
||||
async def test_allow_all_domains_allows_all():
|
||||
"""Test that when allow_all_domains is True, all domains are allowed."""
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
assert plugin._is_uri_allowed("https://any-domain.com/path") is True
|
||||
assert plugin._is_uri_allowed("https://another-domain.org/path") is True
|
||||
|
||||
|
||||
async def test_allowed_domains_multiple_domains():
|
||||
"""Test that multiple allowed domains work correctly."""
|
||||
plugin = HttpPlugin(allowed_domains={"example.com", "api.example.com", "test.org"})
|
||||
assert plugin._is_uri_allowed("https://example.com/path") is True
|
||||
assert plugin._is_uri_allowed("https://api.example.com/path") is True
|
||||
assert plugin._is_uri_allowed("https://test.org/path") is True
|
||||
assert plugin._is_uri_allowed("https://notallowed.com/path") is False
|
||||
|
||||
|
||||
async def test_allowed_domains_with_port():
|
||||
"""Test that non-standard ports are rejected by default, even on an allowed domain."""
|
||||
plugin = HttpPlugin(allowed_domains={"example.com"})
|
||||
# Standard ports on an allowed domain are permitted.
|
||||
assert plugin._is_uri_allowed("https://example.com/path") is True
|
||||
assert plugin._is_uri_allowed("https://example.com:443/path") is True
|
||||
assert plugin._is_uri_allowed("http://example.com:80/path") is True
|
||||
# Non-standard ports are rejected even though the host is allowed (SSRF hardening).
|
||||
assert plugin._is_uri_allowed("https://example.com:8080/path") is False
|
||||
assert plugin._is_uri_allowed("https://example.com:9200/_cat/indices") is False
|
||||
assert plugin._is_uri_allowed("https://example.com:6379/") is False
|
||||
|
||||
|
||||
async def test_allowed_ports_custom():
|
||||
"""Test that custom allowed_ports permit additional ports."""
|
||||
plugin = HttpPlugin(allowed_domains={"example.com"}, allowed_ports={443, 8443})
|
||||
assert plugin._is_uri_allowed("https://example.com:8443/path") is True
|
||||
assert plugin._is_uri_allowed("https://example.com/path") is True
|
||||
# A port not in the custom set is still rejected.
|
||||
assert plugin._is_uri_allowed("https://example.com:9200/path") is False
|
||||
|
||||
|
||||
async def test_allow_all_domains_ignores_port():
|
||||
"""Test that port validation is skipped when allow_all_domains is True."""
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
assert plugin._is_uri_allowed("https://any-domain.com:9200/path") is True
|
||||
assert plugin._is_uri_allowed("https://any-domain.com:6379/path") is True
|
||||
|
||||
|
||||
async def test_malformed_port_rejected():
|
||||
"""Test that a malformed/out-of-range port is rejected."""
|
||||
plugin = HttpPlugin(allowed_domains={"example.com"})
|
||||
assert plugin._is_uri_allowed("https://example.com:99999/path") is False
|
||||
|
||||
|
||||
async def test_malformed_port_rejected_with_allow_all_domains():
|
||||
"""Test that a malformed/out-of-range port is rejected even when allow_all_domains is True."""
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
assert plugin._is_uri_allowed("https://example.com:99999/path") is False
|
||||
|
||||
|
||||
async def test_allowed_domains_subdomain_not_matched():
|
||||
"""Test that subdomains are not automatically matched."""
|
||||
plugin = HttpPlugin(allowed_domains={"example.com"})
|
||||
# subdomain should not match unless explicitly allowed
|
||||
assert plugin._is_uri_allowed("https://sub.example.com/path") is False
|
||||
|
||||
|
||||
async def test_allowed_domains_exact_subdomain_match():
|
||||
"""Test that exact subdomain matching works."""
|
||||
plugin = HttpPlugin(allowed_domains={"sub.example.com"})
|
||||
assert plugin._is_uri_allowed("https://sub.example.com/path") is True
|
||||
assert plugin._is_uri_allowed("https://example.com/path") is False
|
||||
assert plugin._is_uri_allowed("https://other.example.com/path") is False
|
||||
|
||||
|
||||
# Security regression tests
|
||||
|
||||
|
||||
async def test_default_constructor_denies_all():
|
||||
"""Test that default HttpPlugin() denies all requests (issue 115285)."""
|
||||
plugin = HttpPlugin()
|
||||
assert plugin._is_uri_allowed("https://example.com/path") is False
|
||||
assert plugin._is_uri_allowed("https://any-domain.com/path") is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("method", ["get", "post", "put", "delete"])
|
||||
async def test_default_constructor_blocks_requests(method):
|
||||
"""Test that default HttpPlugin() blocks all HTTP methods (issue 115285)."""
|
||||
plugin = HttpPlugin()
|
||||
with pytest.raises(FunctionExecutionException, match="Sending requests to the provided location is not allowed"):
|
||||
if method in ["post", "put"]:
|
||||
await getattr(plugin, method)(url="https://example.com/path", body={"key": "value"})
|
||||
else:
|
||||
await getattr(plugin, method)(url="https://example.com/path")
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.get")
|
||||
async def test_allow_all_domains_flag(mock_get):
|
||||
"""Test that allow_all_domains=True permits requests to any domain."""
|
||||
mock_get.return_value.__aenter__.return_value.text.return_value = "OK"
|
||||
mock_get.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
response = await plugin.get("https://any-domain.com/path")
|
||||
assert response == "OK"
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.get")
|
||||
async def test_redirects_disabled_with_allowed_domains(mock_get):
|
||||
"""Test that redirects are disabled when allowed_domains is set (issue 115048)."""
|
||||
mock_get.return_value.__aenter__.return_value.text.return_value = "OK"
|
||||
mock_get.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allowed_domains={"example.com"})
|
||||
await plugin.get("https://example.com/path")
|
||||
|
||||
_, kwargs = mock_get.call_args
|
||||
assert kwargs["allow_redirects"] is False
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.post")
|
||||
async def test_redirects_disabled_for_post_with_allowed_domains(mock_post):
|
||||
"""Test that redirects are disabled for POST when allowed_domains is set."""
|
||||
mock_post.return_value.__aenter__.return_value.text.return_value = "OK"
|
||||
mock_post.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allowed_domains={"example.com"})
|
||||
await plugin.post("https://example.com/path", body={"key": "value"})
|
||||
|
||||
_, kwargs = mock_post.call_args
|
||||
assert kwargs["allow_redirects"] is False
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.put")
|
||||
async def test_redirects_disabled_for_put_with_allowed_domains(mock_put):
|
||||
"""Test that redirects are disabled for PUT when allowed_domains is set."""
|
||||
mock_put.return_value.__aenter__.return_value.text.return_value = "OK"
|
||||
mock_put.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allowed_domains={"example.com"})
|
||||
await plugin.put("https://example.com/path", body={"key": "value"})
|
||||
|
||||
_, kwargs = mock_put.call_args
|
||||
assert kwargs["allow_redirects"] is False
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.delete")
|
||||
async def test_redirects_disabled_for_delete_with_allowed_domains(mock_delete):
|
||||
"""Test that redirects are disabled for DELETE when allowed_domains is set."""
|
||||
mock_delete.return_value.__aenter__.return_value.text.return_value = "OK"
|
||||
mock_delete.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allowed_domains={"example.com"})
|
||||
await plugin.delete("https://example.com/path")
|
||||
|
||||
_, kwargs = mock_delete.call_args
|
||||
assert kwargs["allow_redirects"] is False
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.get")
|
||||
async def test_redirects_allowed_with_allow_all_domains(mock_get):
|
||||
"""Test that redirects are still allowed when allow_all_domains is True."""
|
||||
mock_get.return_value.__aenter__.return_value.text.return_value = "OK"
|
||||
mock_get.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
await plugin.get("https://example.com/path")
|
||||
|
||||
_, kwargs = mock_get.call_args
|
||||
assert kwargs["allow_redirects"] is True
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.post")
|
||||
async def test_redirects_allowed_for_post_with_allow_all_domains(mock_post):
|
||||
"""Test that redirects are allowed for POST when allow_all_domains is True."""
|
||||
mock_post.return_value.__aenter__.return_value.text.return_value = "OK"
|
||||
mock_post.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
await plugin.post("https://example.com/path", body={"key": "value"})
|
||||
|
||||
_, kwargs = mock_post.call_args
|
||||
assert kwargs["allow_redirects"] is True
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.put")
|
||||
async def test_redirects_allowed_for_put_with_allow_all_domains(mock_put):
|
||||
"""Test that redirects are allowed for PUT when allow_all_domains is True."""
|
||||
mock_put.return_value.__aenter__.return_value.text.return_value = "OK"
|
||||
mock_put.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
await plugin.put("https://example.com/path", body={"key": "value"})
|
||||
|
||||
_, kwargs = mock_put.call_args
|
||||
assert kwargs["allow_redirects"] is True
|
||||
|
||||
|
||||
@patch("aiohttp.ClientSession.delete")
|
||||
async def test_redirects_allowed_for_delete_with_allow_all_domains(mock_delete):
|
||||
"""Test that redirects are allowed for DELETE when allow_all_domains is True."""
|
||||
mock_delete.return_value.__aenter__.return_value.text.return_value = "OK"
|
||||
mock_delete.return_value.__aenter__.return_value.status = 200
|
||||
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
await plugin.delete("https://example.com/path")
|
||||
|
||||
_, kwargs = mock_delete.call_args
|
||||
assert kwargs["allow_redirects"] is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scheme", ["file", "ftp", "gopher", "data"])
|
||||
async def test_disallowed_schemes_blocked(scheme):
|
||||
"""Test that non-HTTP schemes are blocked."""
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
assert plugin._is_uri_allowed(f"{scheme}://example.com/path") is False
|
||||
|
||||
|
||||
@pytest.mark.parametrize("scheme", ["file", "ftp", "gopher"])
|
||||
@pytest.mark.parametrize("method", ["get", "post", "put", "delete"])
|
||||
async def test_disallowed_schemes_blocked_all_methods(scheme, method):
|
||||
"""Test that non-HTTP schemes are blocked for all HTTP methods."""
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
with pytest.raises(FunctionExecutionException, match="Sending requests to the provided location is not allowed"):
|
||||
if method in ["post", "put"]:
|
||||
await getattr(plugin, method)(url=f"{scheme}://example.com/path", body={"key": "value"})
|
||||
else:
|
||||
await getattr(plugin, method)(url=f"{scheme}://example.com/path")
|
||||
|
||||
|
||||
async def test_http_scheme_allowed():
|
||||
"""Test that both http and https schemes are allowed."""
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
assert plugin._is_uri_allowed("http://example.com/path") is True
|
||||
assert plugin._is_uri_allowed("https://example.com/path") is True
|
||||
|
||||
|
||||
async def test_empty_hostname_rejected():
|
||||
"""Test that URLs with empty hostnames are rejected."""
|
||||
plugin = HttpPlugin(allow_all_domains=True)
|
||||
assert plugin._is_uri_allowed("http://") is False
|
||||
assert plugin._is_uri_allowed("https://") is False
|
||||
|
||||
|
||||
async def test_allow_all_domains_with_allowed_domains_allows_redirects():
|
||||
"""Test that redirects are allowed when both allow_all_domains and allowed_domains are set."""
|
||||
plugin = HttpPlugin(allowed_domains={"example.com"}, allow_all_domains=True)
|
||||
assert plugin._is_uri_allowed("https://any-domain.com/path") is True
|
||||
@@ -0,0 +1,175 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel import Kernel
|
||||
from semantic_kernel.core_plugins.math_plugin import MathPlugin
|
||||
from semantic_kernel.functions.kernel_arguments import KernelArguments
|
||||
|
||||
|
||||
def test_can_be_instantiated():
|
||||
plugin = MathPlugin()
|
||||
assert plugin is not None
|
||||
|
||||
|
||||
def test_can_be_imported():
|
||||
kernel = Kernel()
|
||||
kernel.add_plugin(MathPlugin(), "math")
|
||||
assert kernel.get_plugin(plugin_name="math") is not None
|
||||
assert kernel.get_plugin(plugin_name="math").name == "math"
|
||||
assert kernel.get_function(plugin_name="math", function_name="Add") is not None
|
||||
assert kernel.get_function(plugin_name="math", function_name="Subtract") is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"initial_value, amount, expected_result",
|
||||
[
|
||||
(10, 10, 20),
|
||||
(0, 10, 10),
|
||||
(0, -10, -10),
|
||||
(10, 0, 10),
|
||||
(-1, 10, 9),
|
||||
(-10, 10, 0),
|
||||
(-192, 13, -179),
|
||||
(-192, -13, -205),
|
||||
],
|
||||
)
|
||||
def test_add_when_valid_parameters_should_succeed(initial_value, amount, expected_result):
|
||||
# Arrange
|
||||
plugin = MathPlugin()
|
||||
arguments = KernelArguments(input=initial_value, amount=amount)
|
||||
|
||||
# Act
|
||||
result = plugin.add(**arguments)
|
||||
|
||||
# Assert
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"initial_value, amount, expected_result",
|
||||
[
|
||||
(10, 10, 0),
|
||||
(0, 10, -10),
|
||||
(10, 0, 10),
|
||||
(100, -10, 110),
|
||||
(100, 102, -2),
|
||||
(-1, 10, -11),
|
||||
(-10, 10, -20),
|
||||
(-192, -13, -179),
|
||||
],
|
||||
)
|
||||
def test_subtract_when_valid_parameters_should_succeed(initial_value, amount, expected_result):
|
||||
# Arrange
|
||||
plugin = MathPlugin()
|
||||
arguments = KernelArguments(input=initial_value, amount=amount)
|
||||
|
||||
# Act
|
||||
result = plugin.subtract(**arguments)
|
||||
|
||||
# Assert
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"initial_value",
|
||||
[
|
||||
"$0",
|
||||
"one hundred",
|
||||
"20..,,2,1",
|
||||
".2,2.1",
|
||||
"0.1.0",
|
||||
"00-099",
|
||||
"¹²¹",
|
||||
"2²",
|
||||
"zero",
|
||||
"-100 units",
|
||||
"1 banana",
|
||||
],
|
||||
)
|
||||
def test_add_when_invalid_initial_value_should_throw(initial_value):
|
||||
# Arrange
|
||||
plugin = MathPlugin()
|
||||
arguments = KernelArguments(input=initial_value, amount=1)
|
||||
|
||||
# Act
|
||||
with pytest.raises(ValueError):
|
||||
plugin.add(**arguments)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"amount",
|
||||
[
|
||||
"$0",
|
||||
"one hundred",
|
||||
"20..,,2,1",
|
||||
".2,2.1",
|
||||
"0.1.0",
|
||||
"00-099",
|
||||
"¹²¹",
|
||||
"2²",
|
||||
"zero",
|
||||
"-100 units",
|
||||
"1 banana",
|
||||
],
|
||||
)
|
||||
def test_add_when_invalid_amount_should_throw(amount):
|
||||
# Arrange
|
||||
plugin = MathPlugin()
|
||||
arguments = KernelArguments(input=1, amount=amount)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(ValueError):
|
||||
plugin.add(**arguments)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"initial_value",
|
||||
[
|
||||
"$0",
|
||||
"one hundred",
|
||||
"20..,,2,1",
|
||||
".2,2.1",
|
||||
"0.1.0",
|
||||
"00-099",
|
||||
"¹²¹",
|
||||
"2²",
|
||||
"zero",
|
||||
"-100 units",
|
||||
"1 banana",
|
||||
],
|
||||
)
|
||||
def test_subtract_when_invalid_initial_value_should_throw(initial_value):
|
||||
# Arrange
|
||||
plugin = MathPlugin()
|
||||
arguments = KernelArguments(input=initial_value, amount=1)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(ValueError):
|
||||
plugin.subtract(**arguments)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"amount",
|
||||
[
|
||||
"$0",
|
||||
"one hundred",
|
||||
"20..,,2,1",
|
||||
".2,2.1",
|
||||
"0.1.0",
|
||||
"00-099",
|
||||
"¹²¹",
|
||||
"2²",
|
||||
"zero",
|
||||
"-100 units",
|
||||
"1 banana",
|
||||
],
|
||||
)
|
||||
def test_subtract_when_invalid_amount_should_throw(amount):
|
||||
# Arrange
|
||||
plugin = MathPlugin()
|
||||
arguments = KernelArguments(input=1, amount=amount)
|
||||
|
||||
# Act / Assert
|
||||
with pytest.raises(ValueError):
|
||||
plugin.subtract(**arguments)
|
||||
@@ -0,0 +1,913 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import mock_open, patch
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from httpx import HTTPStatusError
|
||||
|
||||
from semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin import (
|
||||
SESSIONS_API_VERSION,
|
||||
SessionsPythonTool,
|
||||
)
|
||||
from semantic_kernel.core_plugins.sessions_python_tool.sessions_remote_file_metadata import SessionsRemoteFileMetadata
|
||||
from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException, FunctionInitializationError
|
||||
from semantic_kernel.kernel import Kernel
|
||||
|
||||
|
||||
def auth_callback_test():
|
||||
return "sample_token"
|
||||
|
||||
|
||||
def test_it_can_be_instantiated(aca_python_sessions_unit_test_env):
|
||||
plugin = SessionsPythonTool(auth_callback=auth_callback_test)
|
||||
assert plugin is not None
|
||||
|
||||
|
||||
def test_validate_endpoint(aca_python_sessions_unit_test_env):
|
||||
plugin = SessionsPythonTool(auth_callback=auth_callback_test)
|
||||
assert plugin is not None
|
||||
assert str(plugin.pool_management_endpoint) == aca_python_sessions_unit_test_env["ACA_POOL_MANAGEMENT_ENDPOINT"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"base_url, endpoint, params, expected_url",
|
||||
[
|
||||
(
|
||||
"http://example.com",
|
||||
"api/resource",
|
||||
{"param1": "value1", "param2": "value2"},
|
||||
f"http://example.com/api/resource?param1=value1¶m2=value2&api-version={SESSIONS_API_VERSION}",
|
||||
),
|
||||
(
|
||||
"http://example.com/",
|
||||
"api/resource",
|
||||
{"param1": "value1"},
|
||||
f"http://example.com/api/resource?param1=value1&api-version={SESSIONS_API_VERSION}",
|
||||
),
|
||||
(
|
||||
"http://example.com",
|
||||
"api/resource/",
|
||||
{"param1": "value1", "param2": "value2"},
|
||||
f"http://example.com/api/resource?param1=value1¶m2=value2&api-version={SESSIONS_API_VERSION}",
|
||||
),
|
||||
(
|
||||
"http://example.com/",
|
||||
"api/resource/",
|
||||
{"param1": "value1"},
|
||||
f"http://example.com/api/resource?param1=value1&api-version={SESSIONS_API_VERSION}",
|
||||
),
|
||||
(
|
||||
"http://example.com",
|
||||
"api/resource",
|
||||
{},
|
||||
f"http://example.com/api/resource?api-version={SESSIONS_API_VERSION}",
|
||||
),
|
||||
(
|
||||
"http://example.com/",
|
||||
"api/resource",
|
||||
{},
|
||||
f"http://example.com/api/resource?api-version={SESSIONS_API_VERSION}",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_build_url_with_version(base_url, endpoint, params, expected_url, aca_python_sessions_unit_test_env):
|
||||
plugin = SessionsPythonTool(auth_callback=auth_callback_test)
|
||||
result = plugin._build_url_with_version(base_url, endpoint, params)
|
||||
assert result == expected_url
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"override_env_param_dict",
|
||||
[
|
||||
{"ACA_POOL_MANAGEMENT_ENDPOINT": "https://test.endpoint/python/execute/"},
|
||||
{"ACA_POOL_MANAGEMENT_ENDPOINT": "https://test.endpoint/python/execute"},
|
||||
],
|
||||
indirect=True,
|
||||
)
|
||||
def test_validate_endpoint_with_execute(aca_python_sessions_unit_test_env):
|
||||
plugin = SessionsPythonTool(auth_callback=auth_callback_test)
|
||||
assert plugin is not None
|
||||
assert "/python/execute" not in str(plugin.pool_management_endpoint)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"override_env_param_dict",
|
||||
[{"ACA_POOL_MANAGEMENT_ENDPOINT": "https://test.endpoint"}],
|
||||
indirect=True,
|
||||
)
|
||||
def test_validate_endpoint_no_final_slash(aca_python_sessions_unit_test_env):
|
||||
plugin = SessionsPythonTool(auth_callback=auth_callback_test)
|
||||
assert plugin is not None
|
||||
assert str(plugin.pool_management_endpoint) == "https://test.endpoint/"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("exclude_list", [["ACA_POOL_MANAGEMENT_ENDPOINT"]], indirect=True)
|
||||
def test_validate_settings_fail(aca_python_sessions_unit_test_env):
|
||||
with pytest.raises(FunctionInitializationError):
|
||||
SessionsPythonTool(
|
||||
auth_callback=auth_callback_test,
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
|
||||
def test_it_can_be_imported(kernel: Kernel, aca_python_sessions_unit_test_env):
|
||||
plugin = SessionsPythonTool(auth_callback=auth_callback_test)
|
||||
assert kernel.add_plugin(plugin=plugin, plugin_name="PythonCodeInterpreter")
|
||||
assert kernel.get_plugin(plugin_name="PythonCodeInterpreter") is not None
|
||||
assert kernel.get_plugin(plugin_name="PythonCodeInterpreter").name == "PythonCodeInterpreter"
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.post")
|
||||
async def test_call_to_container_succeeds(mock_post, aca_python_sessions_unit_test_env):
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
):
|
||||
mock_request = httpx.Request(method="POST", url="https://example.com/code/execute/")
|
||||
|
||||
mock_response = httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"$id": "1",
|
||||
"properties": {
|
||||
"$id": "2",
|
||||
"status": "Success",
|
||||
"stdout": "",
|
||||
"stderr": "",
|
||||
"result": "even_numbers = [2 * i for i in range(1, 11)]\\nprint(even_numbers)",
|
||||
"executionTimeInMilliseconds": 12,
|
||||
},
|
||||
},
|
||||
request=mock_request,
|
||||
)
|
||||
|
||||
mock_post.return_value = await async_return(mock_response)
|
||||
|
||||
plugin = SessionsPythonTool(auth_callback=auth_callback_test)
|
||||
result = await plugin.execute_code("print('hello world')")
|
||||
|
||||
assert result is not None
|
||||
mock_post.assert_awaited_once()
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.post")
|
||||
async def test_call_to_container_fails_raises_exception(mock_post, aca_python_sessions_unit_test_env):
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
):
|
||||
mock_request = httpx.Request(method="POST", url="https://example.com/code/execute/")
|
||||
|
||||
mock_response = httpx.Response(status_code=500, request=mock_request)
|
||||
|
||||
mock_post.return_value = await async_return(mock_response)
|
||||
|
||||
plugin = SessionsPythonTool(auth_callback=auth_callback_test)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
_ = await plugin.execute_code("print('hello world')")
|
||||
|
||||
|
||||
async def test_empty_call_to_container_fails_raises_exception(aca_python_sessions_unit_test_env):
|
||||
plugin = SessionsPythonTool(auth_callback=auth_callback_test)
|
||||
with pytest.raises(FunctionExecutionException):
|
||||
await plugin.execute_code(code="")
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.get")
|
||||
@patch("httpx.AsyncClient.post")
|
||||
async def test_upload_file_with_local_path(mock_post, mock_get, aca_python_sessions_unit_test_env, tmp_path):
|
||||
"""Test upload_file when providing a local file path."""
|
||||
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
# Create a real file in a temp directory for the test
|
||||
test_file = tmp_path / "hello.py"
|
||||
test_file.write_bytes(b"file data")
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
):
|
||||
mock_request = httpx.Request(method="POST", url="https://example.com/files/upload?identifier=None")
|
||||
mock_response = httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"$id": "1",
|
||||
"value": [],
|
||||
},
|
||||
request=mock_request,
|
||||
)
|
||||
mock_post.return_value = await async_return(mock_response)
|
||||
|
||||
mock_get_request = httpx.Request(method="GET", url="https://example.com/files?identifier=None")
|
||||
mock_get_response = httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"$id": "1",
|
||||
"value": [
|
||||
{
|
||||
"$id": "2",
|
||||
"properties": {
|
||||
"$id": "3",
|
||||
"filename": "hello.py",
|
||||
"size": 123,
|
||||
"lastModifiedTime": "2024-07-02T19:29:23.4369699Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
request=mock_get_request,
|
||||
)
|
||||
mock_get.return_value = await async_return(mock_get_response)
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
env_file_path="test.env",
|
||||
enable_dangerous_file_uploads=True,
|
||||
allowed_upload_directories={str(tmp_path)},
|
||||
)
|
||||
|
||||
result = await plugin.upload_file(local_file_path=str(test_file), remote_file_path="hello.py")
|
||||
assert result.filename == "hello.py"
|
||||
assert result.size_in_bytes == 123
|
||||
assert result.full_path == "/mnt/data/hello.py"
|
||||
mock_post.assert_awaited_once()
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.get")
|
||||
@patch("httpx.AsyncClient.post")
|
||||
async def test_upload_file_with_local_path_and_no_remote(
|
||||
mock_post, mock_get, aca_python_sessions_unit_test_env, tmp_path
|
||||
):
|
||||
"""Test upload_file when providing a local file path."""
|
||||
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
# Create a real file in a temp directory for the test
|
||||
test_file = tmp_path / "hello.py"
|
||||
test_file.write_bytes(b"file data")
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
):
|
||||
mock_post_request = httpx.Request(method="POST", url="https://example.com/files/upload?identifier=None")
|
||||
mock_post_response = httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"$id": "1",
|
||||
"value": [],
|
||||
},
|
||||
request=mock_post_request,
|
||||
)
|
||||
mock_post.return_value = await async_return(mock_post_response)
|
||||
|
||||
mock_get_request = httpx.Request(method="GET", url="https://example.com/files?identifier=None")
|
||||
mock_get_response = httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"$id": "1",
|
||||
"value": [
|
||||
{
|
||||
"$id": "2",
|
||||
"properties": {
|
||||
"$id": "3",
|
||||
"filename": "hello.py",
|
||||
"size": 123,
|
||||
"lastModifiedTime": "2024-07-02T19:29:23.4369699Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
request=mock_get_request,
|
||||
)
|
||||
mock_get.return_value = await async_return(mock_get_response)
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
env_file_path="test.env",
|
||||
enable_dangerous_file_uploads=True,
|
||||
allowed_upload_directories={str(tmp_path)},
|
||||
)
|
||||
|
||||
result = await plugin.upload_file(local_file_path=str(test_file))
|
||||
assert result.filename == "hello.py"
|
||||
assert result.size_in_bytes == 123
|
||||
mock_post.assert_awaited_once()
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.post")
|
||||
async def test_upload_file_throws_exception(mock_post, aca_python_sessions_unit_test_env, tmp_path):
|
||||
"""Test throwing exception during file upload."""
|
||||
|
||||
async def async_raise_http_error(*args, **kwargs):
|
||||
mock_request = httpx.Request(method="POST", url="https://example.com/files/upload")
|
||||
mock_response = httpx.Response(status_code=500, request=mock_request)
|
||||
raise HTTPStatusError("Server Error", request=mock_request, response=mock_response)
|
||||
|
||||
# Create a real file in a temp directory for the test
|
||||
test_file = tmp_path / "hello.py"
|
||||
test_file.write_bytes(b"file data")
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
):
|
||||
mock_post.side_effect = async_raise_http_error
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
env_file_path="test.env",
|
||||
enable_dangerous_file_uploads=True,
|
||||
allowed_upload_directories={str(tmp_path)},
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
FunctionExecutionException, match="Upload failed with status code 500 and error: Internal Server Error"
|
||||
):
|
||||
await plugin.upload_file(local_file_path=str(test_file))
|
||||
mock_post.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_remote_file_path, expected_remote_file_path",
|
||||
[
|
||||
("uploaded_test.txt", "/mnt/data/uploaded_test.txt"),
|
||||
("/mnt/data/input.py", "/mnt/data/input.py"),
|
||||
],
|
||||
)
|
||||
@patch("httpx.AsyncClient.get")
|
||||
@patch("httpx.AsyncClient.post")
|
||||
async def test_upload_file_with_buffer(
|
||||
mock_post,
|
||||
mock_get,
|
||||
input_remote_file_path,
|
||||
expected_remote_file_path,
|
||||
aca_python_sessions_unit_test_env,
|
||||
tmp_path,
|
||||
):
|
||||
"""Test upload_file when providing file data as a BufferedReader."""
|
||||
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
# Create a real file in a temp directory for the test
|
||||
test_file = tmp_path / "file.py"
|
||||
test_file.write_text("print('hello, world~')")
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
):
|
||||
mock_request = httpx.Request(method="POST", url="https://example.com/files/upload?identifier=None")
|
||||
mock_response = httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"$id": "1",
|
||||
"value": [],
|
||||
},
|
||||
request=mock_request,
|
||||
)
|
||||
mock_post.return_value = await async_return(mock_response)
|
||||
|
||||
mock_get_request = httpx.Request(method="GET", url="https://example.com/files?identifier=None")
|
||||
mock_get_response = httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"$id": "1",
|
||||
"value": [
|
||||
{
|
||||
"$id": "2",
|
||||
"properties": {
|
||||
"$id": "3",
|
||||
"filename": expected_remote_file_path,
|
||||
"size": 456,
|
||||
"lastModifiedTime": "2024-07-02T19:29:23.4369699Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
request=mock_get_request,
|
||||
)
|
||||
mock_get.return_value = await async_return(mock_get_response)
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
enable_dangerous_file_uploads=True,
|
||||
allowed_upload_directories={str(tmp_path)},
|
||||
)
|
||||
|
||||
result = await plugin.upload_file(local_file_path=str(test_file), remote_file_path=input_remote_file_path)
|
||||
assert result.filename == expected_remote_file_path
|
||||
assert result.size_in_bytes == 456
|
||||
mock_post.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_upload_file_fail_with_no_local_path(aca_python_sessions_unit_test_env):
|
||||
"""Test upload_file when not providing a local file path throws an exception."""
|
||||
|
||||
plugin = SessionsPythonTool(auth_callback=lambda: "sample_token")
|
||||
with pytest.raises(FunctionExecutionException):
|
||||
await plugin.upload_file(
|
||||
local_file_path=None,
|
||||
remote_file_path="uploaded_test.txt",
|
||||
)
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.get")
|
||||
async def test_list_files(mock_get, aca_python_sessions_unit_test_env):
|
||||
"""Test list_files function."""
|
||||
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
):
|
||||
mock_request = httpx.Request(method="GET", url="https://example.com/files?identifier=None")
|
||||
|
||||
mock_response = httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"$id": "1",
|
||||
"value": [
|
||||
{
|
||||
"$id": "2",
|
||||
"properties": {
|
||||
"$id": "3",
|
||||
"filename": "hello.py",
|
||||
"size": 123,
|
||||
"lastModifiedTime": "2024-07-02T19:29:23.4369699Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
"$id": "4",
|
||||
"properties": {
|
||||
"$id": "5",
|
||||
"filename": "world.py",
|
||||
"size": 456,
|
||||
"lastModifiedTime": "2024-07-02T19:29:38.1329088Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
request=mock_request,
|
||||
)
|
||||
mock_get.return_value = await async_return(mock_response)
|
||||
|
||||
plugin = SessionsPythonTool(auth_callback=lambda: "sample_token")
|
||||
|
||||
files = await plugin.list_files()
|
||||
assert len(files) == 2
|
||||
assert files[0].filename == "hello.py"
|
||||
assert files[0].size_in_bytes == 123
|
||||
assert files[1].filename == "world.py"
|
||||
assert files[1].size_in_bytes == 456
|
||||
mock_get.assert_awaited_once()
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.get")
|
||||
async def test_list_files_throws_exception(mock_get, aca_python_sessions_unit_test_env):
|
||||
"""Test throwing exception during list files."""
|
||||
|
||||
async def async_raise_http_error(*args, **kwargs):
|
||||
mock_request = httpx.Request(method="GET", url="https://example.com/files?identifier=None")
|
||||
mock_response = httpx.Response(status_code=500, request=mock_request)
|
||||
raise HTTPStatusError("Server Error", request=mock_request, response=mock_response)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
),
|
||||
):
|
||||
mock_get.side_effect = async_raise_http_error
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
FunctionExecutionException, match="List files failed with status code 500 and error: Internal Server Error"
|
||||
):
|
||||
await plugin.list_files()
|
||||
mock_get.assert_awaited_once()
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.get")
|
||||
async def test_download_file_to_local(mock_get, aca_python_sessions_unit_test_env, tmp_path):
|
||||
"""Test download_file when saving to a local file path."""
|
||||
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
async def mock_auth_callback():
|
||||
return "test_token"
|
||||
|
||||
local_file = tmp_path / "local_test.txt"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
),
|
||||
patch("builtins.open", mock_open()) as mock_file,
|
||||
):
|
||||
mock_request = httpx.Request(
|
||||
method="GET",
|
||||
url="https://example.com/python/files/content/remote_text.txt?identifier=None&filename=remote_test.txt",
|
||||
)
|
||||
|
||||
mock_response = httpx.Response(status_code=200, content=b"file data", request=mock_request)
|
||||
mock_get.return_value = await async_return(mock_response)
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=mock_auth_callback,
|
||||
env_file_path="test.env",
|
||||
enable_dangerous_file_uploads=True,
|
||||
allowed_download_directories={str(tmp_path)},
|
||||
)
|
||||
|
||||
await plugin.download_file(remote_file_name="remote_test.txt", local_file_path=str(local_file))
|
||||
mock_get.assert_awaited_once()
|
||||
# Path is canonicalized via os.path.realpath()
|
||||
mock_file.assert_called_once_with(str(local_file), "wb")
|
||||
mock_file().write.assert_called_once_with(b"file data")
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.get")
|
||||
async def test_download_file_to_buffer(mock_get, aca_python_sessions_unit_test_env):
|
||||
"""Test download_file when returning as a BufferedReader."""
|
||||
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
async def mock_auth_callback():
|
||||
return "test_token"
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
):
|
||||
mock_request = httpx.Request(
|
||||
method="GET",
|
||||
url="https://example.com/files/content/remote_test.txt?identifier=None&filename=remote_test.txt",
|
||||
)
|
||||
|
||||
mock_response = httpx.Response(status_code=200, content=b"file data", request=mock_request)
|
||||
mock_get.return_value = await async_return(mock_response)
|
||||
|
||||
plugin = SessionsPythonTool(auth_callback=mock_auth_callback)
|
||||
|
||||
buffer = await plugin.download_file(remote_file_name="remote_test.txt")
|
||||
assert buffer is not None
|
||||
assert buffer.read() == b"file data"
|
||||
mock_get.assert_awaited_once()
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.get")
|
||||
async def test_download_file_throws_exception(mock_get, aca_python_sessions_unit_test_env):
|
||||
"""Test throwing exception during download file."""
|
||||
|
||||
async def async_raise_http_error(*args, **kwargs):
|
||||
mock_request = httpx.Request(
|
||||
method="GET", url="https://example.com/files/content/remote_test.txt?identifier=None"
|
||||
)
|
||||
mock_response = httpx.Response(status_code=500, request=mock_request)
|
||||
raise HTTPStatusError("Server Error", request=mock_request, response=mock_response)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
),
|
||||
):
|
||||
mock_get.side_effect = async_raise_http_error
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
env_file_path="test.env",
|
||||
)
|
||||
|
||||
with pytest.raises(
|
||||
FunctionExecutionException, match="Download failed with status code 500 and error: Internal Server Error"
|
||||
):
|
||||
await plugin.download_file(remote_file_name="remote_test.txt")
|
||||
mock_get.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_code, expected_output",
|
||||
[
|
||||
# Basic whitespace removal
|
||||
(" print('hello') ", "print('hello')"),
|
||||
(" \n `print('hello')` ", "print('hello')"),
|
||||
("` print('hello')`", "print('hello')"),
|
||||
# Removal of 'python' keyword
|
||||
(" python print('hello') ", "print('hello')"),
|
||||
(" Python print('hello') ", "print('hello')"),
|
||||
("` python print('hello')` ", "print('hello')"),
|
||||
("`Python print('hello')`", "print('hello')"),
|
||||
# Mixed usage
|
||||
(" ` python print('hello')` ", "print('hello')"),
|
||||
(" `python print('hello') `", "print('hello')"),
|
||||
# Code without any issues
|
||||
("print('hello')", "print('hello')"),
|
||||
# Empty code
|
||||
("", ""),
|
||||
("` `", ""),
|
||||
(" ", ""),
|
||||
],
|
||||
)
|
||||
def test_sanitize_input(input_code, expected_output, aca_python_sessions_unit_test_env):
|
||||
"""Test the `_sanitize_input` function with various inputs."""
|
||||
plugin = SessionsPythonTool(auth_callback=lambda: "sample_token")
|
||||
sanitized_code = plugin._sanitize_input(input_code)
|
||||
assert sanitized_code == expected_output
|
||||
|
||||
|
||||
async def test_auth_token(aca_python_sessions_unit_test_env):
|
||||
async def token_cb():
|
||||
return "sample_token"
|
||||
|
||||
plugin = SessionsPythonTool(auth_callback=token_cb)
|
||||
assert await plugin._ensure_auth_token() == "sample_token"
|
||||
|
||||
|
||||
async def test_auth_token_fail(aca_python_sessions_unit_test_env):
|
||||
async def token_cb():
|
||||
raise ValueError("Could not get token.")
|
||||
|
||||
plugin = SessionsPythonTool(auth_callback=token_cb)
|
||||
with pytest.raises(
|
||||
FunctionExecutionException, match="Failed to retrieve the client auth token with message: Could not get token."
|
||||
):
|
||||
await plugin._ensure_auth_token()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"filename, expected_full_path",
|
||||
[
|
||||
("/mnt/data/testfile.txt", "/mnt/data/testfile.txt"),
|
||||
("testfile.txt", "/mnt/data/testfile.txt"),
|
||||
],
|
||||
)
|
||||
def test_full_path(filename, expected_full_path):
|
||||
metadata = SessionsRemoteFileMetadata(filename=filename, size_in_bytes=123)
|
||||
assert metadata.full_path == expected_full_path
|
||||
|
||||
|
||||
# region Tests for File Operations
|
||||
|
||||
|
||||
async def test_upload_file_denied_when_no_allowed_directories(aca_python_sessions_unit_test_env):
|
||||
"""Test that upload_file raises an exception when enable_dangerous_file_uploads is False."""
|
||||
plugin = SessionsPythonTool(auth_callback=lambda: "sample_token")
|
||||
|
||||
with pytest.raises(FunctionExecutionException, match="File upload is disabled"):
|
||||
await plugin.upload_file(local_file_path="/some/path/file.txt")
|
||||
|
||||
|
||||
async def test_upload_file_denied_when_flag_true_but_no_directories(aca_python_sessions_unit_test_env):
|
||||
"""Test that upload_file raises an exception when flag is True but directories not configured."""
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
enable_dangerous_file_uploads=True,
|
||||
)
|
||||
|
||||
with pytest.raises(FunctionExecutionException, match="allowed_upload_directories"):
|
||||
await plugin.upload_file(local_file_path="/some/path/file.txt")
|
||||
|
||||
|
||||
async def test_upload_file_denied_outside_allowed_directories(aca_python_sessions_unit_test_env, tmp_path):
|
||||
"""Test that upload_file raises an exception when path is outside allowed directories."""
|
||||
allowed_dir = tmp_path / "allowed"
|
||||
allowed_dir.mkdir()
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
enable_dangerous_file_uploads=True,
|
||||
allowed_upload_directories={str(allowed_dir)},
|
||||
)
|
||||
|
||||
with pytest.raises(FunctionExecutionException, match="Access denied"):
|
||||
await plugin.upload_file(local_file_path="/etc/passwd")
|
||||
|
||||
|
||||
async def test_upload_file_path_traversal_blocked(aca_python_sessions_unit_test_env, tmp_path):
|
||||
"""Test that path traversal attacks are blocked."""
|
||||
allowed_dir = tmp_path / "allowed"
|
||||
allowed_dir.mkdir()
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
enable_dangerous_file_uploads=True,
|
||||
allowed_upload_directories={str(allowed_dir)},
|
||||
)
|
||||
|
||||
# Attempt path traversal
|
||||
traversal_path = str(allowed_dir / ".." / ".." / "etc" / "passwd")
|
||||
|
||||
with pytest.raises(FunctionExecutionException, match="Access denied"):
|
||||
await plugin.upload_file(local_file_path=traversal_path)
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.get")
|
||||
@patch("httpx.AsyncClient.post")
|
||||
async def test_upload_file_succeeds_within_allowed_directory(
|
||||
mock_post, mock_get, aca_python_sessions_unit_test_env, tmp_path
|
||||
):
|
||||
"""Test that upload_file succeeds when path is within allowed directories."""
|
||||
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
allowed_dir = tmp_path / "allowed"
|
||||
allowed_dir.mkdir()
|
||||
test_file = allowed_dir / "test.txt"
|
||||
test_file.write_text("test content")
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
):
|
||||
mock_request = httpx.Request(method="POST", url="https://example.com/files/upload?identifier=None")
|
||||
mock_response = httpx.Response(
|
||||
status_code=200,
|
||||
json={"$id": "1", "value": []},
|
||||
request=mock_request,
|
||||
)
|
||||
mock_post.return_value = await async_return(mock_response)
|
||||
|
||||
mock_get_request = httpx.Request(method="GET", url="https://example.com/files?identifier=None")
|
||||
mock_get_response = httpx.Response(
|
||||
status_code=200,
|
||||
json={
|
||||
"$id": "1",
|
||||
"value": [
|
||||
{
|
||||
"$id": "2",
|
||||
"properties": {
|
||||
"$id": "3",
|
||||
"filename": "test.txt",
|
||||
"size": 12,
|
||||
"lastModifiedTime": "2024-07-02T19:29:23.4369699Z",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
request=mock_get_request,
|
||||
)
|
||||
mock_get.return_value = await async_return(mock_get_response)
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
enable_dangerous_file_uploads=True,
|
||||
allowed_upload_directories={str(allowed_dir)},
|
||||
)
|
||||
|
||||
result = await plugin.upload_file(local_file_path=str(test_file))
|
||||
assert result.filename == "test.txt"
|
||||
mock_post.assert_awaited_once()
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.get")
|
||||
async def test_download_file_works_without_allowed_directories(mock_get, aca_python_sessions_unit_test_env, tmp_path):
|
||||
"""Test that download_file works without restrictions when allowed_download_directories is None."""
|
||||
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
local_file = tmp_path / "downloaded.txt"
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
):
|
||||
mock_request = httpx.Request(method="GET", url="https://example.com/files/content/remote.txt")
|
||||
mock_response = httpx.Response(status_code=200, content=b"file data", request=mock_request)
|
||||
mock_get.return_value = await async_return(mock_response)
|
||||
|
||||
# No allowed_download_directories configured - should work (permissive by default)
|
||||
plugin = SessionsPythonTool(auth_callback=lambda: "sample_token")
|
||||
|
||||
await plugin.download_file(remote_file_name="remote.txt", local_file_path=str(local_file))
|
||||
assert local_file.read_bytes() == b"file data"
|
||||
mock_get.assert_awaited_once()
|
||||
|
||||
|
||||
async def test_download_file_denied_outside_allowed_directories(aca_python_sessions_unit_test_env, tmp_path):
|
||||
"""Test that download_file raises an exception when path is outside allowed directories."""
|
||||
allowed_dir = tmp_path / "allowed"
|
||||
allowed_dir.mkdir()
|
||||
outside_dir = tmp_path / "outside"
|
||||
outside_dir.mkdir()
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
allowed_download_directories={str(allowed_dir)},
|
||||
)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
),
|
||||
patch("httpx.AsyncClient.get") as mock_get,
|
||||
):
|
||||
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
mock_request = httpx.Request(method="GET", url="https://example.com/files/content/remote.txt")
|
||||
mock_response = httpx.Response(status_code=200, content=b"file data", request=mock_request)
|
||||
mock_get.return_value = await async_return(mock_response)
|
||||
|
||||
with pytest.raises(FunctionExecutionException, match="Access denied"):
|
||||
await plugin.download_file(
|
||||
remote_file_name="remote.txt",
|
||||
local_file_path=str(outside_dir / "output.txt"),
|
||||
)
|
||||
|
||||
|
||||
@patch("httpx.AsyncClient.get")
|
||||
async def test_download_file_succeeds_within_allowed_directory(mock_get, aca_python_sessions_unit_test_env, tmp_path):
|
||||
"""Test that download_file succeeds when path is within allowed directories."""
|
||||
|
||||
async def async_return(result):
|
||||
return result
|
||||
|
||||
allowed_dir = tmp_path / "allowed"
|
||||
allowed_dir.mkdir()
|
||||
local_file = allowed_dir / "downloaded.txt"
|
||||
|
||||
with patch(
|
||||
"semantic_kernel.core_plugins.sessions_python_tool.sessions_python_plugin.SessionsPythonTool._ensure_auth_token",
|
||||
return_value="test_token",
|
||||
):
|
||||
mock_request = httpx.Request(method="GET", url="https://example.com/files/content/remote.txt")
|
||||
mock_response = httpx.Response(status_code=200, content=b"file data", request=mock_request)
|
||||
mock_get.return_value = await async_return(mock_response)
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
allowed_download_directories={str(allowed_dir)},
|
||||
)
|
||||
|
||||
await plugin.download_file(remote_file_name="remote.txt", local_file_path=str(local_file))
|
||||
assert local_file.read_bytes() == b"file data"
|
||||
mock_get.assert_awaited_once()
|
||||
|
||||
|
||||
def test_allowed_directories_accepts_list(aca_python_sessions_unit_test_env):
|
||||
"""Test that allowed directories can be passed as a list and are converted to a set."""
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
allowed_upload_directories=["/path/one", "/path/two"],
|
||||
allowed_download_directories=["/path/three"],
|
||||
)
|
||||
|
||||
assert plugin.allowed_upload_directories == {"/path/one", "/path/two"}
|
||||
assert plugin.allowed_download_directories == {"/path/three"}
|
||||
|
||||
|
||||
async def test_empty_set_denies_all_uploads(aca_python_sessions_unit_test_env, tmp_path):
|
||||
"""Test that an empty set for allowed_upload_directories denies all uploads."""
|
||||
test_file = tmp_path / "test.txt"
|
||||
test_file.write_text("content")
|
||||
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
enable_dangerous_file_uploads=True,
|
||||
allowed_upload_directories=set(), # Empty set - deny all
|
||||
)
|
||||
|
||||
with pytest.raises(FunctionExecutionException, match="Access denied"):
|
||||
await plugin.upload_file(local_file_path=str(test_file))
|
||||
|
||||
|
||||
def test_empty_strings_filtered_from_allowed_directories(aca_python_sessions_unit_test_env):
|
||||
"""Test that empty strings are filtered out from allowed directories."""
|
||||
plugin = SessionsPythonTool(
|
||||
auth_callback=lambda: "sample_token",
|
||||
allowed_upload_directories=["", "/valid/path", ""],
|
||||
allowed_download_directories=["", ""],
|
||||
)
|
||||
|
||||
assert plugin.allowed_upload_directories == {"/valid/path"}
|
||||
assert plugin.allowed_download_directories == set() # All empty strings filtered out
|
||||
|
||||
|
||||
# endregion
|
||||
@@ -0,0 +1,48 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from semantic_kernel import Kernel
|
||||
from semantic_kernel.core_plugins.text_plugin import TextPlugin
|
||||
|
||||
|
||||
def test_can_be_instantiated():
|
||||
assert TextPlugin()
|
||||
|
||||
|
||||
def test_can_be_imported(kernel: Kernel):
|
||||
kernel.add_plugin(TextPlugin(), "text_plugin")
|
||||
assert not kernel.get_function(plugin_name="text_plugin", function_name="trim").is_prompt
|
||||
|
||||
|
||||
def test_can_be_imported_with_name(kernel: Kernel):
|
||||
kernel.add_plugin(TextPlugin(), "text")
|
||||
assert not kernel.get_function(plugin_name="text", function_name="trim").is_prompt
|
||||
|
||||
|
||||
def test_can_trim():
|
||||
text_plugin = TextPlugin()
|
||||
result = text_plugin.trim(" hello world ")
|
||||
assert result == "hello world"
|
||||
|
||||
|
||||
def test_can_trim_start():
|
||||
text_plugin = TextPlugin()
|
||||
result = text_plugin.trim_start(" hello world ")
|
||||
assert result == "hello world "
|
||||
|
||||
|
||||
def test_can_trim_end():
|
||||
text_plugin = TextPlugin()
|
||||
result = text_plugin.trim_end(" hello world ")
|
||||
assert result == " hello world"
|
||||
|
||||
|
||||
def test_can_lower():
|
||||
text_plugin = TextPlugin()
|
||||
result = text_plugin.lowercase(" HELLO WORLD ")
|
||||
assert result == " hello world "
|
||||
|
||||
|
||||
def test_can_upper():
|
||||
text_plugin = TextPlugin()
|
||||
result = text_plugin.uppercase(" hello world ")
|
||||
assert result == " HELLO WORLD "
|
||||
@@ -0,0 +1,186 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
import datetime
|
||||
from unittest import mock
|
||||
|
||||
from pytest import raises
|
||||
|
||||
import semantic_kernel as sk
|
||||
from semantic_kernel.core_plugins.time_plugin import TimePlugin
|
||||
from semantic_kernel.exceptions.function_exceptions import FunctionExecutionException
|
||||
|
||||
test_mock_now = datetime.datetime(2031, 1, 12, 12, 24, 56, tzinfo=datetime.timezone.utc)
|
||||
test_mock_today = datetime.date(2031, 1, 12)
|
||||
|
||||
|
||||
def test_can_be_instantiated():
|
||||
assert TimePlugin()
|
||||
|
||||
|
||||
def test_can_be_imported():
|
||||
kernel = sk.Kernel()
|
||||
kernel.add_plugin(TimePlugin(), "time")
|
||||
assert kernel.get_plugin(plugin_name="time") is not None
|
||||
assert kernel.get_plugin(plugin_name="time").name == "time"
|
||||
assert kernel.get_function(plugin_name="time", function_name="now") is not None
|
||||
|
||||
|
||||
def test_today():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.today() == "Sunday, 12 January, 2031"
|
||||
|
||||
|
||||
def test_date():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.date() == "Sunday, 12 January, 2031"
|
||||
|
||||
|
||||
def test_isodate():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.date", wraps=datetime.date) as dt:
|
||||
dt.today.return_value = test_mock_today
|
||||
assert plugin.iso_date() == "2031-01-12"
|
||||
|
||||
|
||||
def test_now():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.now() == "Sunday, January 12, 2031 12:24 PM"
|
||||
|
||||
|
||||
def test_days_ago():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.date", wraps=datetime.date) as dt:
|
||||
dt.today.return_value = test_mock_today
|
||||
assert plugin.days_ago(1) == "Saturday, 11 January, 2031"
|
||||
|
||||
|
||||
def test_date_matching_last_day_name():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.date", wraps=datetime.date) as dt:
|
||||
dt.today.return_value = test_mock_today
|
||||
assert plugin.date_matching_last_day_name("Friday") == "Friday, 10 January, 2031"
|
||||
|
||||
|
||||
def test_date_matching_last_day_name_fail():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.date", wraps=datetime.date) as dt:
|
||||
dt.today.return_value = test_mock_today
|
||||
with raises(FunctionExecutionException):
|
||||
plugin.date_matching_last_day_name("Non-existing-day")
|
||||
|
||||
|
||||
def test_utc_now():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.utcnow.return_value = test_mock_now
|
||||
assert plugin.utc_now() == "Sunday, January 12, 2031 12:24 PM"
|
||||
|
||||
|
||||
def test_time():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.time() == "12:24:56 PM"
|
||||
|
||||
|
||||
def test_year():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.year() == "2031"
|
||||
|
||||
|
||||
def test_month():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.month() == "January"
|
||||
|
||||
|
||||
def test_month_number():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.month_number() == "01"
|
||||
|
||||
|
||||
def test_day():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.day() == "12"
|
||||
|
||||
|
||||
def test_day_of_week():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.day_of_week() == "Sunday"
|
||||
|
||||
|
||||
def test_hour():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.hour() == "12 PM"
|
||||
|
||||
|
||||
def test_hour_number():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.hour_number() == "12"
|
||||
|
||||
|
||||
def test_minute():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.minute() == "24"
|
||||
|
||||
|
||||
def test_second():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.second() == "56"
|
||||
|
||||
|
||||
def test_time_zone_offset():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.time_zone_offset() == "+0000"
|
||||
|
||||
|
||||
def test_time_zone_name():
|
||||
plugin = TimePlugin()
|
||||
|
||||
with mock.patch("datetime.datetime", wraps=datetime.datetime) as dt:
|
||||
dt.now.return_value = test_mock_now
|
||||
assert plugin.time_zone_name() == "UTC"
|
||||
@@ -0,0 +1,60 @@
|
||||
# Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from semantic_kernel.core_plugins.wait_plugin import WaitPlugin
|
||||
from semantic_kernel.exceptions import FunctionExecutionException
|
||||
|
||||
test_data_good = [
|
||||
0,
|
||||
1.0,
|
||||
-2,
|
||||
"0",
|
||||
"1",
|
||||
"2.1",
|
||||
"0.1",
|
||||
"0.01",
|
||||
"0.001",
|
||||
"0.0001",
|
||||
"-0.0001",
|
||||
]
|
||||
|
||||
test_data_bad = [
|
||||
"$0",
|
||||
"one hundred",
|
||||
"20..,,2,1",
|
||||
".2,2.1",
|
||||
"0.1.0",
|
||||
"00-099",
|
||||
"¹²¹",
|
||||
"2²",
|
||||
"zero",
|
||||
"-100 seconds",
|
||||
"1 second",
|
||||
]
|
||||
|
||||
|
||||
def test_can_be_instantiated():
|
||||
plugin = WaitPlugin()
|
||||
assert plugin is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("wait_time", test_data_good)
|
||||
async def test_wait_valid_params(wait_time):
|
||||
plugin = WaitPlugin()
|
||||
with patch("asyncio.sleep") as patched_sleep:
|
||||
await plugin.wait(wait_time)
|
||||
|
||||
patched_sleep.assert_called_once_with(abs(float(wait_time)))
|
||||
|
||||
|
||||
@pytest.mark.parametrize("wait_time", test_data_bad)
|
||||
async def test_wait_invalid_params(wait_time):
|
||||
plugin = WaitPlugin()
|
||||
|
||||
with pytest.raises(FunctionExecutionException) as exc_info:
|
||||
await plugin.wait("wait_time")
|
||||
|
||||
assert exc_info.value.args[0] == "seconds text must be a number"
|
||||
Reference in New Issue
Block a user