-
Notifications
You must be signed in to change notification settings - Fork 4.6k
Expand file tree
/
Copy pathtest_step_class_loading.py
More file actions
192 lines (136 loc) · 8.36 KB
/
test_step_class_loading.py
File metadata and controls
192 lines (136 loc) · 8.36 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
# Copyright (c) Microsoft. All rights reserved.
import pytest
from semantic_kernel.exceptions.process_exceptions import ProcessInvalidConfigurationException
from semantic_kernel.processes.kernel_process.kernel_process_step import KernelProcessStep
from semantic_kernel.processes.step_utils import get_step_class_from_qualified_name
class MockValidStep(KernelProcessStep):
"""A valid KernelProcessStep subclass for testing."""
pass
class NotAStep:
"""A class that is NOT a KernelProcessStep subclass."""
pass
def test_valid_step_class_loads_successfully():
"""Test that a valid KernelProcessStep subclass loads correctly."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
result = get_step_class_from_qualified_name(
full_name,
allowed_module_prefixes=[MockValidStep.__module__],
)
assert result is MockValidStep
assert issubclass(result, KernelProcessStep)
def test_invalid_format_no_dots_raises_exception():
"""Test that a class name without dots raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Invalid step class name format"):
get_step_class_from_qualified_name("NoDots")
def test_empty_module_name_raises_exception():
"""Test that empty module name (e.g., '.ClassName') raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Module name and class name cannot be empty"):
get_step_class_from_qualified_name(".ClassName")
def test_empty_class_name_raises_exception():
"""Test that empty class name (e.g., 'module.') raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Module name and class name cannot be empty"):
get_step_class_from_qualified_name("module.")
def test_empty_string_raises_exception():
"""Test that an empty string raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Invalid step class name format"):
get_step_class_from_qualified_name("")
def test_none_like_empty_raises_exception():
"""Test that None-like values raise ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Invalid step class name format"):
get_step_class_from_qualified_name(None) # type: ignore
def test_nonexistent_module_raises_exception():
"""Test that a non-existent module raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="Unable to import module"):
get_step_class_from_qualified_name("nonexistent_module_xyz123.SomeClass", allowed_module_prefixes=None)
def test_nonexistent_class_in_valid_module_raises_exception():
"""Test that a non-existent class in a valid module raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="not found in module"):
get_step_class_from_qualified_name("semantic_kernel.kernel.NonExistentClass12345")
def test_non_class_attribute_raises_exception():
"""Test that loading a non-class attribute (function) raises ProcessInvalidConfigurationException."""
with pytest.raises(ProcessInvalidConfigurationException, match="is not a class type"):
get_step_class_from_qualified_name("semantic_kernel.processes.step_utils.get_fully_qualified_name")
def test_non_step_class_raises_exception():
"""Test that a class not inheriting from KernelProcessStep raises exception."""
full_name = f"{NotAStep.__module__}.{NotAStep.__name__}"
with pytest.raises(ProcessInvalidConfigurationException, match="must be a subclass of KernelProcessStep"):
get_step_class_from_qualified_name(full_name, allowed_module_prefixes=[NotAStep.__module__])
def test_builtin_class_raises_exception():
"""Test that built-in classes like str raise exception (bypassing prefix check to test subclass validation)."""
with pytest.raises(ProcessInvalidConfigurationException, match="must be a subclass of KernelProcessStep"):
get_step_class_from_qualified_name("builtins.str", allowed_module_prefixes=None)
def test_os_system_prevented():
"""Test that os.system is prevented (bypassing prefix check to test type validation)."""
with pytest.raises(ProcessInvalidConfigurationException, match="is not a class type"):
get_step_class_from_qualified_name("os.system", allowed_module_prefixes=None)
def test_arbitrary_class_prevented():
"""Test that arbitrary classes like subprocess.Popen are prevented (bypassing prefix check)."""
with pytest.raises(ProcessInvalidConfigurationException, match="must be a subclass of KernelProcessStep"):
get_step_class_from_qualified_name("subprocess.Popen", allowed_module_prefixes=None)
def test_kernel_class_prevented():
"""Test that even internal SK classes that aren't steps are prevented."""
with pytest.raises(ProcessInvalidConfigurationException, match="must be a subclass of KernelProcessStep"):
get_step_class_from_qualified_name("semantic_kernel.kernel.Kernel")
# Module allowlist tests
def test_allowlist_permits_valid_module():
"""Test that a valid step class from an allowed module loads successfully."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
# Allow the test module (use actual module name from __module__)
module_prefix = MockValidStep.__module__
result = get_step_class_from_qualified_name(
full_name,
allowed_module_prefixes=[module_prefix],
)
assert result is MockValidStep
def test_allowlist_blocks_disallowed_module():
"""Test that a module not in the allowlist is blocked BEFORE import."""
with pytest.raises(ProcessInvalidConfigurationException, match="is not in the allowed module prefixes"):
get_step_class_from_qualified_name(
"os.path",
allowed_module_prefixes=["semantic_kernel.", "myapp."],
)
def test_allowlist_blocks_dangerous_module():
"""Test that dangerous modules are blocked when allowlist is provided."""
with pytest.raises(ProcessInvalidConfigurationException, match="is not in the allowed module prefixes"):
get_step_class_from_qualified_name(
"subprocess.Popen",
allowed_module_prefixes=["semantic_kernel."],
)
def test_empty_allowlist_allows_all():
"""Test that an empty allowlist allows any module."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
result = get_step_class_from_qualified_name(full_name, allowed_module_prefixes=[])
assert result is MockValidStep
def test_none_allowlist_allows_all():
"""Test that None allowlist allows any module (explicit opt-out)."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
result = get_step_class_from_qualified_name(full_name, allowed_module_prefixes=None)
assert result is MockValidStep
def test_default_allowlist_blocks_non_sk_modules():
"""Test that the default allowlist only permits semantic_kernel modules."""
with pytest.raises(ProcessInvalidConfigurationException, match="is not in the allowed module prefixes"):
get_step_class_from_qualified_name("subprocess.Popen")
def test_default_allowlist_permits_sk_modules():
"""Test that the default allowlist permits semantic_kernel modules."""
full_name = "semantic_kernel.processes.kernel_process.kernel_process_step.KernelProcessStep"
result = get_step_class_from_qualified_name(full_name)
assert result is KernelProcessStep
def test_allowlist_prefix_matching():
"""Test that allowlist uses prefix matching correctly."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
# Use a prefix of the actual module name
module_prefix = MockValidStep.__module__[:4] # First 4 chars as prefix
result = get_step_class_from_qualified_name(
full_name,
allowed_module_prefixes=[module_prefix],
)
assert result is MockValidStep
def test_allowlist_multiple_prefixes():
"""Test that multiple allowed prefixes work correctly."""
full_name = f"{MockValidStep.__module__}.{MockValidStep.__name__}"
module_prefix = MockValidStep.__module__
result = get_step_class_from_qualified_name(
full_name,
allowed_module_prefixes=["semantic_kernel.", "myapp.", module_prefix],
)
assert result is MockValidStep