CloudApiRequest 1.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cloud/CloudAPIRequest.py +514 -0
- cloud/CloudApiConfig.py +437 -0
- cloud/CloudLogUtil.py +36 -0
- cloud/CloudRequestUtil.py +107 -0
- cloud/CloudSignUtil.py +152 -0
- cloud/CloudYamlUtil.py +224 -0
- cloud/__init__.py +9 -0
- cloud/cli.py +62 -0
- cloudapirequest-1.2.0.dist-info/LICENSE +21 -0
- cloudapirequest-1.2.0.dist-info/METADATA +21 -0
- cloudapirequest-1.2.0.dist-info/RECORD +13 -0
- cloudapirequest-1.2.0.dist-info/WHEEL +5 -0
- cloudapirequest-1.2.0.dist-info/top_level.txt +1 -0
cloud/CloudAPIRequest.py
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""
|
|
2
|
+
cloud接口测试核心类
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from urllib.parse import urlparse
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
import allure
|
|
11
|
+
import jsonpath
|
|
12
|
+
from requests_toolbelt import MultipartEncoder
|
|
13
|
+
from cloud.CloudLogUtil import log
|
|
14
|
+
from cloud.CloudRequestUtil import CloudHttpClient
|
|
15
|
+
from cloud.CloudSignUtil import CloudSignUtil
|
|
16
|
+
from cloud.CloudYamlUtil import CloudReadYaml, CloudPlaceholderYaml
|
|
17
|
+
from cloud.CloudApiConfig import config
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CloudAPIRequest:
|
|
21
|
+
"""
|
|
22
|
+
cloud接口测试核心类
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, commonCase=None):
|
|
26
|
+
"""
|
|
27
|
+
:param commonCase: 公共用例文件
|
|
28
|
+
"""
|
|
29
|
+
self.commonCase = commonCase
|
|
30
|
+
# 延迟初始化配置,避免循环导入
|
|
31
|
+
self._config = None
|
|
32
|
+
self.baseUrl = None
|
|
33
|
+
self.token = None
|
|
34
|
+
self.globalBean = None
|
|
35
|
+
self.assertFail = 'stop'
|
|
36
|
+
self.tenv = 'base'
|
|
37
|
+
|
|
38
|
+
def _get_config(self):
|
|
39
|
+
"""延迟获取配置实例"""
|
|
40
|
+
if self._config is None:
|
|
41
|
+
from cloud import config
|
|
42
|
+
self._config = config
|
|
43
|
+
# 正确获取配置值,而不是property对象
|
|
44
|
+
self.baseUrl = str(config._baseUrl) if hasattr(config, '_baseUrl') and config._baseUrl else None
|
|
45
|
+
self.token = str(config._token) if hasattr(config, '_token') and config._token else None
|
|
46
|
+
self.globalBean = config
|
|
47
|
+
self.assertFail = str(config._assertFail) if hasattr(config, '_assertFail') and config._assertFail else 'stop'
|
|
48
|
+
self.tenv = str(config._tEnv) if hasattr(config, '_tEnv') and config._tEnv else 'base'
|
|
49
|
+
return self._config
|
|
50
|
+
|
|
51
|
+
def doRequest(self, file, bean):
|
|
52
|
+
"""
|
|
53
|
+
执行API请求测试
|
|
54
|
+
"""
|
|
55
|
+
requestParameter = None
|
|
56
|
+
dataSaveBean = bean
|
|
57
|
+
yaml = CloudReadYaml(file).load_yaml()
|
|
58
|
+
yamlId = yaml.get('id')
|
|
59
|
+
yamlName = yaml.get('name')
|
|
60
|
+
yamlTestcase = yaml.get('testcases')
|
|
61
|
+
|
|
62
|
+
log.info(f"开始执行测试用例name: {yamlName}, id: {yamlId}")
|
|
63
|
+
config_instance = self._get_config()
|
|
64
|
+
clientSession = config_instance.Session
|
|
65
|
+
|
|
66
|
+
for index, testcase in enumerate(yamlTestcase, 1):
|
|
67
|
+
testcase_name = testcase.get('name', f'用例{index}')
|
|
68
|
+
testcase_id = testcase.get('id', f'case_{index}')
|
|
69
|
+
log.info(f"{'='*60}")
|
|
70
|
+
log.info(f"正在执行第 {index} 个用例: {testcase_name} (ID: {testcase_id})")
|
|
71
|
+
log.info(f"{'='*60}")
|
|
72
|
+
|
|
73
|
+
with allure.step(testcase.get('name')):
|
|
74
|
+
if testcase.get('skip'):
|
|
75
|
+
log.info(f"用例: {testcase.get('name')}跳过")
|
|
76
|
+
continue
|
|
77
|
+
|
|
78
|
+
sleeps = testcase.get('sleep')
|
|
79
|
+
if sleeps:
|
|
80
|
+
time.sleep(int(sleeps))
|
|
81
|
+
log.info(f"当前用例: {testcase.get('name')}执行前等待{sleeps}秒")
|
|
82
|
+
|
|
83
|
+
# 处理公共用例
|
|
84
|
+
config_instance = self._get_config()
|
|
85
|
+
if testcase.get('kind') and testcase.get('kind').lower() == 'common' and hasattr(config_instance, '_commonTestCasePath') and config_instance._commonTestCasePath is not None:
|
|
86
|
+
testcase = self.getCommonTestCase(testcase, config_instance._commonTestCasePath, testcase.get('id'))
|
|
87
|
+
elif testcase.get('kind') and testcase.get('kind').lower() == 'common' and (not hasattr(config_instance, '_commonTestCasePath') or config_instance._commonTestCasePath is None):
|
|
88
|
+
log.error(f"commonPath路径未配置,请检查配置文件")
|
|
89
|
+
raise Exception(f"commonPath路径未配置,请检查配置文件")
|
|
90
|
+
|
|
91
|
+
# 参数替换
|
|
92
|
+
if testcase.get('requestType') is None:
|
|
93
|
+
requestType = 'json'
|
|
94
|
+
else:
|
|
95
|
+
requestType = testcase.get('requestType')
|
|
96
|
+
repParameter = self.replaceParameterAttr(dataSaveBean, testcase.get('parameter'), requestType)
|
|
97
|
+
repApi = self.replaceParameterAttr(dataSaveBean, testcase.get('api'))
|
|
98
|
+
headers = self.replaceParameterAttr(dataSaveBean, testcase.get('headers'))
|
|
99
|
+
|
|
100
|
+
# 鉴权处理
|
|
101
|
+
requestParameter, requestUrl, authHeaders = self.authType(testcase.get('authType'), repApi, testcase.get('method'), repParameter)
|
|
102
|
+
|
|
103
|
+
# 请求类型处理
|
|
104
|
+
dataRequestParameter, jsonRequestParameter, paramsData, ModelData, requestType = self.requestType(requestType, requestParameter)
|
|
105
|
+
|
|
106
|
+
# 合并请求头
|
|
107
|
+
if headers:
|
|
108
|
+
headers.update(authHeaders)
|
|
109
|
+
else:
|
|
110
|
+
headers = authHeaders
|
|
111
|
+
|
|
112
|
+
# 执行请求
|
|
113
|
+
|
|
114
|
+
if dataRequestParameter is not None and requestType.lower() in ['form-data', 'form-file']:
|
|
115
|
+
headers['Content-Type'] = dataRequestParameter.content_type
|
|
116
|
+
|
|
117
|
+
# 调试日志:打印实际发送的JSON数据
|
|
118
|
+
if jsonRequestParameter is not None:
|
|
119
|
+
log.info(f"发送的JSON数据: {json.dumps(jsonRequestParameter, ensure_ascii=False)}")
|
|
120
|
+
log.info(f"JSON数据类型: {type(jsonRequestParameter)}")
|
|
121
|
+
if isinstance(jsonRequestParameter, dict):
|
|
122
|
+
for key, value in jsonRequestParameter.items():
|
|
123
|
+
log.info(f" {key}: {value} (类型: {type(value)})")
|
|
124
|
+
|
|
125
|
+
if testcase.get('stream_check'):
|
|
126
|
+
response = self.handle_stream_response(clientSession, testcase.get('method'), requestUrl, dataRequestParameter, jsonRequestParameter, paramsData, ModelData, headers)
|
|
127
|
+
else:
|
|
128
|
+
response = clientSession.request(method=testcase.get('method'), url=requestUrl, data=dataRequestParameter, json=jsonRequestParameter, params=paramsData, files=ModelData, headers=headers)
|
|
129
|
+
|
|
130
|
+
# 记录响应
|
|
131
|
+
try:
|
|
132
|
+
response_json = response.json()
|
|
133
|
+
# 打印响应状态码
|
|
134
|
+
log.info(f"[{testcase_name}] 响应状态码: {response.status_code}")
|
|
135
|
+
# 打印完整的响应结果
|
|
136
|
+
log.info(f"[{testcase_name}] 响应结果: {json.dumps(response_json, ensure_ascii=False, indent=2)}")
|
|
137
|
+
except json.JSONDecodeError as e:
|
|
138
|
+
log.error(f"[{testcase_name}] JSON解析失败: {e}")
|
|
139
|
+
log.error(f"[{testcase_name}] 原始响应内容: {response.text}")
|
|
140
|
+
response_json = None
|
|
141
|
+
except Exception as e:
|
|
142
|
+
log.error(f"[{testcase_name}] 响应处理异常: {e}")
|
|
143
|
+
response_json = None
|
|
144
|
+
|
|
145
|
+
# 处理断言
|
|
146
|
+
if testcase.get('assertFail'):
|
|
147
|
+
failtype = testcase.get('assertFail')
|
|
148
|
+
else:
|
|
149
|
+
failtype = self.assertFail
|
|
150
|
+
self.assertType(testcase.get('assert'), response, dataSaveBean, failtype)
|
|
151
|
+
|
|
152
|
+
# 保存数据
|
|
153
|
+
try:
|
|
154
|
+
if response_json is not None:
|
|
155
|
+
self.addAttrSaveBean(dataSaveBean, self.globalBean, testcase.get('saveData'), response_json)
|
|
156
|
+
else:
|
|
157
|
+
log.warning(f"[{testcase_name}] 响应不是JSON格式,跳过数据保存")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
log.error(f"[{testcase_name}] 数据保存异常: {e}")
|
|
160
|
+
self.addAttrSaveBean(dataSaveBean, self.globalBean, testcase.get('saveData'), response.text)
|
|
161
|
+
|
|
162
|
+
return clientSession
|
|
163
|
+
|
|
164
|
+
def assertType(self, assertType, response, bean, failType):
|
|
165
|
+
"""
|
|
166
|
+
处理断言
|
|
167
|
+
"""
|
|
168
|
+
if assertType is None:
|
|
169
|
+
log.info(f"断言为空,跳过断言")
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
for ass in assertType:
|
|
173
|
+
key = list(ass.keys())[0]
|
|
174
|
+
|
|
175
|
+
if 'status_code' in ass:
|
|
176
|
+
if ass.get('status_code'):
|
|
177
|
+
self.assertChoose(str(response.status_code) == str(ass.get('status_code')),
|
|
178
|
+
f"status_code断言失败: {ass.get('status_code')} ,response结果: {response.status_code}", failType)
|
|
179
|
+
continue
|
|
180
|
+
|
|
181
|
+
# 安全地解析JSON响应
|
|
182
|
+
try:
|
|
183
|
+
response_data = response.json()
|
|
184
|
+
jsonpathResults = jsonpath.jsonpath(response_data, ass.get(key)[0])
|
|
185
|
+
except json.JSONDecodeError:
|
|
186
|
+
# 尝试解析JSONP格式
|
|
187
|
+
try:
|
|
188
|
+
response_text = response.text
|
|
189
|
+
# 检查是否是JSONP格式:callback(json_data)
|
|
190
|
+
if '(' in response_text and ')' in response_text:
|
|
191
|
+
# 提取JSON部分
|
|
192
|
+
start = response_text.find('(') + 1
|
|
193
|
+
end = response_text.rfind(')')
|
|
194
|
+
json_str = response_text[start:end]
|
|
195
|
+
|
|
196
|
+
# 处理JSONP中的字符串转义(单引号包围的JSON)
|
|
197
|
+
if json_str.startswith("'") and json_str.endswith("'"):
|
|
198
|
+
json_str = json_str[1:-1] # 移除首尾的单引号
|
|
199
|
+
|
|
200
|
+
response_data = json.loads(json_str)
|
|
201
|
+
log.info(f"成功解析JSONP格式响应: {json_str}")
|
|
202
|
+
jsonpathResults = jsonpath.jsonpath(response_data, ass.get(key)[0])
|
|
203
|
+
else:
|
|
204
|
+
raise json.JSONDecodeError("不是JSONP格式", response_text, 0)
|
|
205
|
+
except (json.JSONDecodeError, ValueError) as e:
|
|
206
|
+
log.error(f"JSON和JSONP解析都失败,无法执行断言: {ass.get(key)[0]}")
|
|
207
|
+
log.error(f"原始响应内容: {response.text}")
|
|
208
|
+
self.assertChoose(False, f"响应不是有效的JSON或JSONP格式,无法执行断言: {ass.get(key)[0]}", failType)
|
|
209
|
+
continue
|
|
210
|
+
except Exception as e:
|
|
211
|
+
log.error(f"断言处理异常: {e}")
|
|
212
|
+
self.assertChoose(False, f"断言处理异常: {e}", failType)
|
|
213
|
+
continue
|
|
214
|
+
|
|
215
|
+
if jsonpathResults is False and 'not_found' not in ass:
|
|
216
|
+
self.assertChoose(1 > 2, f"提取{ass.get(key)[0]}失败,断言失败", failType)
|
|
217
|
+
continue
|
|
218
|
+
|
|
219
|
+
if 'eq' in ass:
|
|
220
|
+
expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('eq')[1]).replace().replaced_str
|
|
221
|
+
assResults = str(expectedResults) in [str(item) for item in jsonpathResults]
|
|
222
|
+
self.assertChoose(assResults is True, f"eq断言失败: {jsonpathResults} 不等于 {expectedResults}", failType)
|
|
223
|
+
elif 'neq' in ass:
|
|
224
|
+
expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('neq')[1]).replace().replaced_str
|
|
225
|
+
assResults = str(expectedResults) not in [str(item) for item in jsonpathResults]
|
|
226
|
+
self.assertChoose(assResults is True, f"neq断言失败: {jsonpathResults} 等于 {expectedResults}", failType)
|
|
227
|
+
elif 'sge' in ass:
|
|
228
|
+
expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('sge')[1]).replace().replaced_str
|
|
229
|
+
self.assertChoose(len(jsonpathResults) >= int(expectedResults), f"sge断言失败: {jsonpathResults} 小于 {expectedResults}", failType)
|
|
230
|
+
elif 'nn' in ass:
|
|
231
|
+
self.assertChoose(jsonpathResults is not None, f"not none断言失败: {ass.get('nn')[0]}", failType)
|
|
232
|
+
elif 'none' in ass:
|
|
233
|
+
self.assertChoose(jsonpathResults is True, f"none断言失败: {ass.get('none')[0]}", failType)
|
|
234
|
+
elif 'not_found' in ass:
|
|
235
|
+
self.assertChoose(jsonpathResults is False, f"not_found断言失败,字段存在", failType)
|
|
236
|
+
elif 'in' in ass:
|
|
237
|
+
expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('in')[1]).replace().replaced_str
|
|
238
|
+
self.assertChoose(str(expectedResults) in str(jsonpathResults), f"断言in失败: {expectedResults} 不在 {jsonpathResults} 内", failType)
|
|
239
|
+
elif 'len' in ass:
|
|
240
|
+
expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('len')[1]).replace().replaced_str
|
|
241
|
+
jsonpathResults_len = len(jsonpathResults[0])
|
|
242
|
+
self.assertChoose(jsonpathResults_len == int(expectedResults), f"断言len失败: {jsonpathResults_len} 长度不等于 {expectedResults}", failType)
|
|
243
|
+
elif 'contains' in ass:
|
|
244
|
+
expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('contains')[1]).replace().replaced_str
|
|
245
|
+
self.assertChoose(str(expectedResults) in str(jsonpathResults), f"断言contains失败: {expectedResults} 不在 {jsonpathResults} 内", failType)
|
|
246
|
+
|
|
247
|
+
def addAttrSaveBean(self, bean, globalBean, data: list, response):
|
|
248
|
+
"""
|
|
249
|
+
保存响应数据到Bean
|
|
250
|
+
"""
|
|
251
|
+
if data is None:
|
|
252
|
+
return
|
|
253
|
+
for d in data:
|
|
254
|
+
if 'json' in d:
|
|
255
|
+
jsonPath = d.get('json')[1]
|
|
256
|
+
value = jsonpath.jsonpath(response, jsonPath)
|
|
257
|
+
if value is False:
|
|
258
|
+
value = None
|
|
259
|
+
|
|
260
|
+
saveBean = bean
|
|
261
|
+
if d.get('json').__len__() == 3 and d.get('json')[2].lower() == 'global':
|
|
262
|
+
saveBean = globalBean
|
|
263
|
+
|
|
264
|
+
key_parts = d.get('json')[0].split(':')
|
|
265
|
+
d.get('json')[0] = key_parts[0]
|
|
266
|
+
|
|
267
|
+
if value is not None and len(value) > 1:
|
|
268
|
+
setattr(saveBean, d.get('json')[0], list(value))
|
|
269
|
+
elif value is not None and len(value) == 1:
|
|
270
|
+
if len(key_parts) > 1 and key_parts[1].lower() == 'str':
|
|
271
|
+
value[0] = str(value[0])
|
|
272
|
+
setattr(saveBean, d.get('json')[0], value[0])
|
|
273
|
+
|
|
274
|
+
def replaceParameterAttr(self, bean, parameter, requestType='json'):
|
|
275
|
+
"""
|
|
276
|
+
替换参数中的占位符
|
|
277
|
+
"""
|
|
278
|
+
if parameter is None:
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
# 获取配置实例
|
|
282
|
+
config_instance = self._get_config()
|
|
283
|
+
|
|
284
|
+
if requestType.lower() == 'json-text':
|
|
285
|
+
repParameter = CloudPlaceholderYaml(yaml_str=parameter, attrObj=bean, methObj=config_instance.methObj, gloObj=config_instance).replace().textLoad()
|
|
286
|
+
else:
|
|
287
|
+
repParameter = CloudPlaceholderYaml(yaml_str=parameter, attrObj=bean, methObj=config_instance.methObj, gloObj=config_instance).replace().jsonLoad()
|
|
288
|
+
|
|
289
|
+
return repParameter
|
|
290
|
+
|
|
291
|
+
def requestType(self, requestType, data):
|
|
292
|
+
"""
|
|
293
|
+
处理请求类型
|
|
294
|
+
"""
|
|
295
|
+
jsonRequestParameter = None
|
|
296
|
+
dataRequestParameter = None
|
|
297
|
+
paramsData = None
|
|
298
|
+
ModelData = None
|
|
299
|
+
|
|
300
|
+
if isinstance(data, dict) and data.get('MIME'):
|
|
301
|
+
MIME = data.get('MIME')
|
|
302
|
+
else:
|
|
303
|
+
MIME = 'application/octet-stream'
|
|
304
|
+
|
|
305
|
+
if requestType is None:
|
|
306
|
+
jsonRequestParameter = data
|
|
307
|
+
elif requestType.lower() in ["json", "json-text"]:
|
|
308
|
+
jsonRequestParameter = data
|
|
309
|
+
elif requestType.lower() == "form-data":
|
|
310
|
+
dataRequestParameter = MultipartEncoder(fields=data)
|
|
311
|
+
elif requestType.lower() == "form-model":
|
|
312
|
+
filename = data['filename']
|
|
313
|
+
file_name = data[filename].split('\\')
|
|
314
|
+
data[filename] = (file_name[-1], open(data[filename], 'rb'), MIME)
|
|
315
|
+
for k, v in data.items():
|
|
316
|
+
if type(v) == dict:
|
|
317
|
+
data[k] = (None, json.dumps(data[k]))
|
|
318
|
+
ModelData = data
|
|
319
|
+
elif requestType.lower() == "form-file":
|
|
320
|
+
filename = data['filename']
|
|
321
|
+
data[filename] = (os.path.basename(data[filename]), open(data[filename], 'rb'), MIME)
|
|
322
|
+
dataRequestParameter = MultipartEncoder(fields=data)
|
|
323
|
+
elif requestType == "PARAMS":
|
|
324
|
+
paramsData = data
|
|
325
|
+
elif requestType == "DATA":
|
|
326
|
+
dataRequestParameter = data
|
|
327
|
+
else:
|
|
328
|
+
log.error("请求方式不支持")
|
|
329
|
+
|
|
330
|
+
return dataRequestParameter, jsonRequestParameter, paramsData, ModelData, requestType
|
|
331
|
+
|
|
332
|
+
def authType(self, authType, url, method, parameter):
|
|
333
|
+
"""
|
|
334
|
+
处理鉴权方式
|
|
335
|
+
"""
|
|
336
|
+
if self.baseUrl is None or self.isValidUrl(url):
|
|
337
|
+
requestUrl = url
|
|
338
|
+
else:
|
|
339
|
+
requestUrl = self.baseUrl + url
|
|
340
|
+
requestParameter = None
|
|
341
|
+
authHeaders = {}
|
|
342
|
+
|
|
343
|
+
if authType == "SIGN":
|
|
344
|
+
# 云服务MD5签名鉴权
|
|
345
|
+
config_instance = self._get_config()
|
|
346
|
+
# 从config中获取企业ID
|
|
347
|
+
enterprise_id = config_instance._enterpriseId if hasattr(config_instance, '_enterpriseId') else None
|
|
348
|
+
if not enterprise_id:
|
|
349
|
+
log.error("企业ID未配置,请在环境配置中设置enterpriseId")
|
|
350
|
+
raise ValueError("企业ID未配置,请在环境配置中设置enterpriseId")
|
|
351
|
+
|
|
352
|
+
if method.upper() == "GET":
|
|
353
|
+
# GET请求:将签名参数添加到URL参数中
|
|
354
|
+
# 添加默认的validateType参数
|
|
355
|
+
if parameter is None:
|
|
356
|
+
parameter = {}
|
|
357
|
+
if 'validateType' not in parameter:
|
|
358
|
+
parameter['validateType'] = config_instance._validateType if hasattr(config_instance, '_validateType') else '2' # 从配置中获取验证类型
|
|
359
|
+
|
|
360
|
+
signed_params = CloudSignUtil.generate_params_with_signature(
|
|
361
|
+
enterprise_id=enterprise_id,
|
|
362
|
+
token=config_instance._token if hasattr(config_instance, '_token') else None,
|
|
363
|
+
additional_params=parameter
|
|
364
|
+
)
|
|
365
|
+
# 构建带签名的URL
|
|
366
|
+
from urllib.parse import urlencode
|
|
367
|
+
query_string = urlencode(signed_params)
|
|
368
|
+
if '?' in requestUrl:
|
|
369
|
+
requestUrl += '&' + query_string
|
|
370
|
+
else:
|
|
371
|
+
requestUrl += '?' + query_string
|
|
372
|
+
requestParameter = None
|
|
373
|
+
log.info(f"GET请求URL: {requestUrl}")
|
|
374
|
+
else:
|
|
375
|
+
# POST/PUT/PATCH请求:签名信息放在请求头中,但URL中也需要包含签名参数
|
|
376
|
+
# 添加签名参数到URL查询参数中
|
|
377
|
+
url_params = {
|
|
378
|
+
'validateType': config_instance._validateType if hasattr(config_instance, '_validateType') else '2',
|
|
379
|
+
'enterpriseId': enterprise_id,
|
|
380
|
+
'timestamp': int(time.time()),
|
|
381
|
+
'sign': CloudSignUtil.generate_md5_signature(
|
|
382
|
+
enterprise_id,
|
|
383
|
+
int(time.time()),
|
|
384
|
+
config_instance._token if hasattr(config_instance, '_token') else None
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
# 如果parameter中包含validateType,使用parameter中的值
|
|
389
|
+
if parameter and 'validateType' in parameter:
|
|
390
|
+
url_params['validateType'] = parameter.pop('validateType')
|
|
391
|
+
|
|
392
|
+
# 构建URL(包含所有签名参数)
|
|
393
|
+
from urllib.parse import urlencode
|
|
394
|
+
query_string = urlencode(url_params)
|
|
395
|
+
if '?' in requestUrl:
|
|
396
|
+
requestUrl += '&' + query_string
|
|
397
|
+
else:
|
|
398
|
+
requestUrl += '?' + query_string
|
|
399
|
+
log.info(f"POST请求URL: {requestUrl}")
|
|
400
|
+
|
|
401
|
+
# 生成请求头签名(包含请求体内容)
|
|
402
|
+
authHeaders = CloudSignUtil.generate_headers(
|
|
403
|
+
method=method,
|
|
404
|
+
url=requestUrl,
|
|
405
|
+
params=None, # POST请求不需要URL参数签名
|
|
406
|
+
enterprise_id=enterprise_id,
|
|
407
|
+
token=config_instance._token if hasattr(config_instance, '_token') else None,
|
|
408
|
+
body=parameter
|
|
409
|
+
)
|
|
410
|
+
requestParameter = parameter
|
|
411
|
+
|
|
412
|
+
return requestParameter, requestUrl, authHeaders
|
|
413
|
+
elif authType == "COOKIE" or authType is None:
|
|
414
|
+
requestParameter = parameter
|
|
415
|
+
return requestParameter, requestUrl, authHeaders
|
|
416
|
+
else:
|
|
417
|
+
log.error("鉴权方式不支持")
|
|
418
|
+
return requestParameter, requestUrl, authHeaders
|
|
419
|
+
|
|
420
|
+
def getCommonTestCase(self, testcase, commonFile, caseId):
|
|
421
|
+
"""
|
|
422
|
+
获取公共测试用例
|
|
423
|
+
"""
|
|
424
|
+
if self.commonCase is None:
|
|
425
|
+
commonFile = commonFile
|
|
426
|
+
else:
|
|
427
|
+
commonFile = os.path.join(commonFile.split('common')[0], f'common/{self.commonCase}')
|
|
428
|
+
|
|
429
|
+
yaml = CloudReadYaml(commonFile).load_yaml()
|
|
430
|
+
commonCase = yaml.get('testcases')
|
|
431
|
+
|
|
432
|
+
for case in commonCase:
|
|
433
|
+
if case.get('id') == caseId:
|
|
434
|
+
case['assert'] = [item for item in (case.get('assert') or []) + (testcase.get('assert') or []) if item is not None]
|
|
435
|
+
case['saveData'] = [item for item in (case.get('saveData') or []) + (testcase.get('saveData') or []) if item is not None]
|
|
436
|
+
return case
|
|
437
|
+
|
|
438
|
+
raise ValueError("Case with id {} not found".format(caseId))
|
|
439
|
+
|
|
440
|
+
def assertChoose(self, ass, tips, type):
|
|
441
|
+
"""
|
|
442
|
+
断言选择器
|
|
443
|
+
"""
|
|
444
|
+
if type == 'stop':
|
|
445
|
+
assert ass, tips
|
|
446
|
+
elif type == 'continue':
|
|
447
|
+
pytest.assume(ass, tips)
|
|
448
|
+
|
|
449
|
+
def isValidUrl(self, url):
|
|
450
|
+
"""
|
|
451
|
+
验证URL是否有效
|
|
452
|
+
"""
|
|
453
|
+
try:
|
|
454
|
+
result = urlparse(url)
|
|
455
|
+
return all([result.scheme, result.netloc])
|
|
456
|
+
except ValueError:
|
|
457
|
+
return False
|
|
458
|
+
|
|
459
|
+
def handle_stream_response(self, clientSession, method, url, data, json_data, params, files, headers):
|
|
460
|
+
"""
|
|
461
|
+
处理流式响应
|
|
462
|
+
"""
|
|
463
|
+
try:
|
|
464
|
+
response_dict = {}
|
|
465
|
+
answer_count = 0
|
|
466
|
+
complete_message = ""
|
|
467
|
+
|
|
468
|
+
with clientSession.request(
|
|
469
|
+
method=method,
|
|
470
|
+
url=url,
|
|
471
|
+
data=data,
|
|
472
|
+
json=json_data,
|
|
473
|
+
params=params,
|
|
474
|
+
files=files,
|
|
475
|
+
headers=headers,
|
|
476
|
+
stream=True
|
|
477
|
+
) as response:
|
|
478
|
+
for chunk in response.iter_lines():
|
|
479
|
+
if chunk:
|
|
480
|
+
data = chunk.decode('utf-8')
|
|
481
|
+
if data.startswith('data: '):
|
|
482
|
+
try:
|
|
483
|
+
json_str = data[6:]
|
|
484
|
+
if json_str.strip() == '[DONE]':
|
|
485
|
+
if complete_message:
|
|
486
|
+
response_dict["complete_message"] = complete_message
|
|
487
|
+
continue
|
|
488
|
+
|
|
489
|
+
json_data = json.loads(json_str)
|
|
490
|
+
|
|
491
|
+
if 'answer' in json_data:
|
|
492
|
+
answer_count += 1
|
|
493
|
+
key = f"answer{answer_count}"
|
|
494
|
+
response_dict[key] = json_data
|
|
495
|
+
|
|
496
|
+
answer_content = json_data.get('answer', '')
|
|
497
|
+
if isinstance(answer_content, list):
|
|
498
|
+
answer_content = ''.join(str(item) for item in answer_content)
|
|
499
|
+
elif not isinstance(answer_content, str):
|
|
500
|
+
answer_content = str(answer_content)
|
|
501
|
+
|
|
502
|
+
complete_message += answer_content
|
|
503
|
+
|
|
504
|
+
except json.JSONDecodeError as e:
|
|
505
|
+
log.error(f"JSON解析错误: {e}")
|
|
506
|
+
continue
|
|
507
|
+
|
|
508
|
+
response.json = lambda: response_dict
|
|
509
|
+
response._content = json.dumps(response_dict).encode()
|
|
510
|
+
return response
|
|
511
|
+
|
|
512
|
+
except Exception as e:
|
|
513
|
+
log.error(f"流式处理错误: {e}")
|
|
514
|
+
raise
|