Skip to content

Commit a707d97

Browse files
Update UnitTest (#28)
* Update UnitTest
1 parent 7c70262 commit a707d97

File tree

6 files changed

+1165
-817
lines changed

6 files changed

+1165
-817
lines changed

src/alibaba_cloud_ops_mcp_server/tools/api_tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ def _create_function_schemas(service, api, api_meta):
9797
required = schema.get('required', False)
9898

9999
# 只有在service为ecs时,才对特定参数进行特殊处理
100-
if service.lower() == 'ecs' and name in ECS_LIST_PARAMETERS:
100+
if service.lower() == 'ecs' and name in ECS_LIST_PARAMETERS and type_ == 'string':
101101
python_type = list
102102
else:
103103
python_type = type_map.get(type_, str)

tests/alibabacloud/test_api_meta_client.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,140 @@ def test_get_apis_in_service_no_apis(mock_get):
153153
mock_get.return_value.json.return_value = {}
154154
with pytest.raises(KeyError):
155155
api_meta_client.ApiMetaClient.get_apis_in_service('ecs', '2014-05-26')
156+
157+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
158+
def test_get_api_parameters_schema_not_dict(mock_get):
159+
# get_api_meta返回的schema不是dict
160+
api_meta = {
161+
'parameters': [
162+
{'name': 'foo', 'in': 'query', 'schema': None},
163+
{'name': 'bar', 'in': 'query', 'schema': 'notadict'}
164+
]
165+
}
166+
with patch.object(api_meta_client.ApiMetaClient, 'get_api_meta', return_value=(api_meta, '2014-05-26')):
167+
params = api_meta_client.ApiMetaClient.get_api_parameters('ecs', 'DescribeInstances')
168+
# 两个参数都应该被返回
169+
assert 'foo' in params
170+
assert 'bar' in params
171+
172+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
173+
def test_get_apis_in_service_normal(mock_get):
174+
"""测试get_apis_in_service方法正常返回API列表"""
175+
mock_get.return_value.json.return_value = {"apis": {"DescribeInstances": {}, "StartInstance": {}}}
176+
apis = api_meta_client.ApiMetaClient.get_apis_in_service('ecs', '2014-05-26')
177+
assert set(apis) == {"DescribeInstances", "StartInstance"}
178+
assert len(apis) == 2
179+
180+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_service_version', return_value='2014-05-26')
181+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=(None, None))
182+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
183+
def test_get_api_meta_service_none_exception(mock_pop_api, mock_get_std, mock_get_ver):
184+
"""测试get_api_meta方法中service_standard为None时抛出异常"""
185+
with pytest.raises(Exception) as e:
186+
api_meta_client.ApiMetaClient.get_api_meta('ecs', 'DescribeInstances')
187+
assert 'InvalidServiceName' in str(e.value)
188+
189+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_service_version', return_value='2014-05-26')
190+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=('ecs', None))
191+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
192+
def test_get_api_meta_api_none_exception(mock_pop_api, mock_get_std, mock_get_ver):
193+
"""测试get_api_meta方法中api_standard为None时抛出异常"""
194+
with pytest.raises(Exception) as e:
195+
api_meta_client.ApiMetaClient.get_api_meta('ecs', 'DescribeInstances')
196+
assert 'InvalidAPIName' in str(e.value)
197+
198+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_api_meta')
199+
def test_get_api_parameters_schema_not_dict_more_cases(mock_get_meta):
200+
"""测试get_api_parameters中更多非dict类型的schema"""
201+
api_meta = {
202+
'parameters': [
203+
{'name': 'foo', 'in': 'query', 'schema': 'string'}, # 字符串
204+
{'name': 'bar', 'in': 'query', 'schema': 123}, # 数字
205+
{'name': 'baz', 'in': 'query', 'schema': []}, # 列表
206+
{'name': 'qux', 'in': 'query', 'schema': None}, # None
207+
]
208+
}
209+
mock_get_meta.return_value = (api_meta, '2014-05-26')
210+
params = api_meta_client.ApiMetaClient.get_api_parameters('ecs', 'DescribeInstances')
211+
assert 'foo' in params
212+
assert 'bar' in params
213+
assert 'baz' in params
214+
assert 'qux' in params
215+
216+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.requests.get')
217+
def test_get_apis_in_service_normal(mock_get):
218+
"""测试get_apis_in_service方法正常返回API列表"""
219+
mock_get.return_value.json.return_value = {"apis": {"DescribeInstances": {}, "StartInstance": {}}}
220+
apis = api_meta_client.ApiMetaClient.get_apis_in_service('ecs', '2014-05-26')
221+
assert set(apis) == {"DescribeInstances", "StartInstance"}
222+
assert len(apis) == 2
223+
224+
225+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=('ecs', 'api'))
226+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
227+
def test_get_ref_api_meta_invalid_path(mock_pop_api, mock_std):
228+
# 模拟 ref_path 指向不存在的 key
229+
mock_pop_api.return_value = {'apis': {'DescribeInstances': {}}}
230+
with pytest.raises(KeyError):
231+
api_meta_client.ApiMetaClient.get_ref_api_meta({'$ref': '#/notfound'}, 'ecs', '2014-05-26')
232+
233+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=('ecs', 'api'))
234+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
235+
def test_get_ref_api_meta_invalid_path(mock_pop_api, mock_std):
236+
# 模拟 ref_path 指向不存在的 key
237+
mock_pop_api.return_value = {'apis': {'DescribeInstances': {}}}
238+
with pytest.raises(KeyError):
239+
api_meta_client.ApiMetaClient.get_ref_api_meta({'$ref': '#/notfound'}, 'ecs', '2014-05-26')
240+
241+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_api_meta')
242+
def test_get_api_field_default_value(mock_get_meta):
243+
# 模拟 get_api_meta 返回无 field_type 的数据
244+
mock_get_meta.return_value = ({}, '2014-05-26')
245+
val = api_meta_client.ApiMetaClient.get_api_field('parameters', 'ecs', 'DescribeInstances', default='default_val')
246+
assert val == 'default_val'
247+
248+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_api_meta')
249+
def test_get_api_parameters_nested_ref(mock_get_meta):
250+
# 模拟嵌套 $ref
251+
api_meta = {
252+
'parameters': [
253+
{'name': 'foo', 'in': 'query', 'schema': {'$ref': '#/defs/A'}}
254+
]
255+
}
256+
def fake_get_ref(data, service, version):
257+
if '#/defs/A' in data.get('$ref', ''):
258+
return {'properties': {'a': {'$ref': '#/defs/B'}}}
259+
elif '#/defs/B' in data.get('$ref', ''):
260+
return {'properties': {'b': {}}}
261+
return {}
262+
with patch.object(api_meta_client.ApiMetaClient, 'get_ref_api_meta', side_effect=fake_get_ref):
263+
mock_get_meta.return_value = (api_meta, '2014-05-26')
264+
params = api_meta_client.ApiMetaClient.get_api_parameters('ecs', 'DescribeInstances')
265+
assert 'a' in params and 'b' in params # 深层嵌套属性应被提取
266+
267+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_standard_service_and_api', return_value=('ecs', 'api'))
268+
@patch('alibaba_cloud_ops_mcp_server.alibabacloud.api_meta_client.ApiMetaClient.get_response_from_pop_api')
269+
def test_get_ref_api_meta_valid_path(mock_pop_api, mock_std):
270+
# 模拟 get_response_from_pop_api 返回包含 defs/A 的结构
271+
mock_pop_api.return_value = {
272+
'defs': {
273+
'A': {
274+
'properties': {
275+
'prop1': {'type': 'string'},
276+
'prop2': {'type': 'integer'}
277+
}
278+
}
279+
}
280+
}
281+
282+
# 调用 get_ref_api_meta,传入 $ref 指向 #/defs/A
283+
result = api_meta_client.ApiMetaClient.get_ref_api_meta({'$ref': '#/defs/A'}, 'ecs', '2014-05-26')
284+
285+
# 验证返回结果是否与 defs/A 的结构一致
286+
expected = {
287+
'properties': {
288+
'prop1': {'type': 'string'},
289+
'prop2': {'type': 'integer'}
290+
}
291+
}
292+
assert result == expected

tests/tools/test_api_tools.py

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,3 +229,96 @@ def test_tools_api_call_ecs_list_parameters_non_list():
229229
query_args = mock_OpenApiUtilClient.query.call_args[0][0]
230230
assert query_args['InstanceIds'] == 'i-123'
231231
assert query_args['SecurityGroupIds'] is None
232+
233+
def test_create_tool_function_with_signature_bind_and_apply_defaults():
234+
"""测试func_code函数中的signature.bind和apply_defaults调用"""
235+
api_meta = {
236+
'parameters': [
237+
{'name': 'param1', 'schema': {'type': 'string', 'required': False}},
238+
{'name': 'param2', 'schema': {'type': 'integer', 'required': True}}
239+
],
240+
'summary': 'Test function'
241+
}
242+
243+
schemas = api_tools._create_function_schemas('test', 'TestApi', api_meta)
244+
fields = schemas['TestApi']
245+
246+
# 创建函数
247+
func = api_tools._create_tool_function_with_signature('test', 'TestApi', fields, 'Test function')
248+
249+
# 测试函数调用,确保执行到signature.bind和apply_defaults
250+
with patch('alibaba_cloud_ops_mcp_server.tools.api_tools._tools_api_call') as mock_call:
251+
mock_call.return_value = {'result': 'success'}
252+
253+
# 调用函数,传入部分参数,让apply_defaults生效
254+
result = func(param2=123) # 只传入required参数,让param1使用默认值
255+
256+
# 验证_tools_api_call被调用
257+
mock_call.assert_called_once()
258+
call_args = mock_call.call_args[1]['parameters']
259+
260+
# 验证参数绑定和默认值应用
261+
assert 'param1' in call_args # 默认值被应用
262+
assert call_args['param2'] == 123 # 传入的参数
263+
264+
def test_create_tool_function_with_signature_bind_with_all_args():
265+
"""测试func_code函数中传入所有参数的情况"""
266+
api_meta = {
267+
'parameters': [
268+
{'name': 'param1', 'schema': {'type': 'string', 'required': False}},
269+
{'name': 'param2', 'schema': {'type': 'integer', 'required': False}}
270+
],
271+
'summary': 'Test function'
272+
}
273+
274+
schemas = api_tools._create_function_schemas('test', 'TestApi', api_meta)
275+
fields = schemas['TestApi']
276+
277+
# 创建函数
278+
func = api_tools._create_tool_function_with_signature('test', 'TestApi', fields, 'Test function')
279+
280+
# 测试传入所有参数
281+
with patch('alibaba_cloud_ops_mcp_server.tools.api_tools._tools_api_call') as mock_call:
282+
mock_call.return_value = {'result': 'success'}
283+
284+
# 传入所有参数
285+
result = func(param1='value1', param2=456)
286+
287+
# 验证_tools_api_call被调用
288+
mock_call.assert_called_once()
289+
call_args = mock_call.call_args[1]['parameters']
290+
291+
# 验证所有参数都被正确传递
292+
assert call_args['param1'] == 'value1'
293+
assert call_args['param2'] == 456
294+
295+
def test_create_tool_function_with_signature_bind_with_positional_args():
296+
"""测试func_code函数中使用位置参数的情况"""
297+
api_meta = {
298+
'parameters': [
299+
{'name': 'param1', 'schema': {'type': 'string', 'required': False}},
300+
{'name': 'param2', 'schema': {'type': 'integer', 'required': False}}
301+
],
302+
'summary': 'Test function'
303+
}
304+
305+
schemas = api_tools._create_function_schemas('test', 'TestApi', api_meta)
306+
fields = schemas['TestApi']
307+
308+
# 创建函数
309+
func = api_tools._create_tool_function_with_signature('test', 'TestApi', fields, 'Test function')
310+
311+
# 测试使用位置参数
312+
with patch('alibaba_cloud_ops_mcp_server.tools.api_tools._tools_api_call') as mock_call:
313+
mock_call.return_value = {'result': 'success'}
314+
315+
# 使用位置参数调用
316+
result = func('value1', 789)
317+
318+
# 验证_tools_api_call被调用
319+
mock_call.assert_called_once()
320+
call_args = mock_call.call_args[1]['parameters']
321+
322+
# 验证位置参数被正确绑定
323+
assert call_args['param1'] == 'value1'
324+
assert call_args['param2'] == 789

tests/tools/test_cms_tools.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,62 @@ def describe_metric_last(self, req):
118118
with patch('alibaba_cloud_ops_mcp_server.tools.cms_tools.create_client', return_value=FakeClient()):
119119
result = cms_tools._get_cms_metric_data('cn-test', ['i-1', 'i-2'], 'cpu_total')
120120
assert result == [{'value': 1}, {'value': 2}]
121+
122+
def test_create_client():
123+
"""测试create_client函数的基本功能"""
124+
with patch('alibaba_cloud_ops_mcp_server.tools.cms_tools.create_config') as mock_create_config, \
125+
patch('alibaba_cloud_ops_mcp_server.tools.cms_tools.cms20190101Client') as mock_client:
126+
127+
# 模拟配置对象
128+
mock_config = MagicMock()
129+
mock_create_config.return_value = mock_config
130+
131+
# 模拟客户端对象
132+
mock_client_instance = MagicMock()
133+
mock_client.return_value = mock_client_instance
134+
135+
# 调用函数
136+
region_id = 'cn-hangzhou'
137+
result = cms_tools.create_client(region_id)
138+
139+
# 验证create_config被调用
140+
mock_create_config.assert_called_once()
141+
142+
# 验证endpoint被正确设置
143+
assert mock_config.endpoint == f'metrics.{region_id}.aliyuncs.com'
144+
145+
# 验证cms20190101Client被正确调用
146+
mock_client.assert_called_once_with(mock_config)
147+
148+
# 验证返回的是客户端实例
149+
assert result == mock_client_instance
150+
151+
152+
def test_create_client_different_regions():
153+
"""测试create_client函数在不同region下的行为"""
154+
with patch('alibaba_cloud_ops_mcp_server.tools.cms_tools.create_config') as mock_create_config, \
155+
patch('alibaba_cloud_ops_mcp_server.tools.cms_tools.cms20190101Client') as mock_client:
156+
157+
mock_config = MagicMock()
158+
mock_create_config.return_value = mock_config
159+
mock_client_instance = MagicMock()
160+
mock_client.return_value = mock_client_instance
161+
162+
# 测试不同的region
163+
test_regions = ['cn-hangzhou', 'cn-beijing', 'us-west-1']
164+
165+
for region_id in test_regions:
166+
# 重置mock
167+
mock_config.reset_mock()
168+
mock_client.reset_mock()
169+
170+
# 调用函数
171+
result = cms_tools.create_client(region_id)
172+
173+
# 验证endpoint格式正确
174+
expected_endpoint = f'metrics.{region_id}.aliyuncs.com'
175+
assert mock_config.endpoint == expected_endpoint
176+
177+
# 验证客户端被创建
178+
mock_client.assert_called_once_with(mock_config)
179+
assert result == mock_client_instance

tests/tools/test_oos_tools.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,62 @@ class DoneListResp:
160160
result = oos_tools._start_execution_sync('cn-test', 'tpl', {})
161161
assert hasattr(result, 'executions')
162162
assert mock_sleep.call_count >= 1
163+
164+
def test_create_client():
165+
"""测试create_client函数的基本功能"""
166+
with patch('alibaba_cloud_ops_mcp_server.tools.oos_tools.create_config') as mock_create_config, \
167+
patch('alibaba_cloud_ops_mcp_server.tools.oos_tools.oos20190601Client') as mock_client:
168+
169+
# 模拟配置对象
170+
mock_config = MagicMock()
171+
mock_create_config.return_value = mock_config
172+
173+
# 模拟客户端对象
174+
mock_client_instance = MagicMock()
175+
mock_client.return_value = mock_client_instance
176+
177+
# 调用函数
178+
region_id = 'cn-hangzhou'
179+
result = oos_tools.create_client(region_id)
180+
181+
# 验证create_config被调用
182+
mock_create_config.assert_called_once()
183+
184+
# 验证endpoint被正确设置
185+
assert mock_config.endpoint == f'oos.{region_id}.aliyuncs.com'
186+
187+
# 验证oos20190601Client被正确调用
188+
mock_client.assert_called_once_with(mock_config)
189+
190+
# 验证返回的是客户端实例
191+
assert result == mock_client_instance
192+
193+
194+
def test_create_client_different_regions():
195+
"""测试create_client函数在不同region下的行为"""
196+
with patch('alibaba_cloud_ops_mcp_server.tools.oos_tools.create_config') as mock_create_config, \
197+
patch('alibaba_cloud_ops_mcp_server.tools.oos_tools.oos20190601Client') as mock_client:
198+
199+
mock_config = MagicMock()
200+
mock_create_config.return_value = mock_config
201+
mock_client_instance = MagicMock()
202+
mock_client.return_value = mock_client_instance
203+
204+
# 测试不同的region
205+
test_regions = ['cn-hangzhou', 'cn-beijing', 'us-west-1']
206+
207+
for region_id in test_regions:
208+
# 重置mock
209+
mock_config.reset_mock()
210+
mock_client.reset_mock()
211+
212+
# 调用函数
213+
result = oos_tools.create_client(region_id)
214+
215+
# 验证endpoint格式正确
216+
expected_endpoint = f'oos.{region_id}.aliyuncs.com'
217+
assert mock_config.endpoint == expected_endpoint
218+
219+
# 验证客户端被创建
220+
mock_client.assert_called_once_with(mock_config)
221+
assert result == mock_client_instance

0 commit comments

Comments
 (0)