chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -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",
"¹²¹",
"",
"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",
"¹²¹",
"",
"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",
"¹²¹",
"",
"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",
"¹²¹",
"",
"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&param2=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&param2=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",
"¹²¹",
"",
"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"