CloudApiRequest 1.0.0__py3-none-any.whl → 1.1.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.

Potentially problematic release.


This version of CloudApiRequest might be problematic. Click here for more details.

cloud/CloudAPIRequest.py CHANGED
@@ -1,426 +1,510 @@
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 testcase in yamlTestcase:
67
- with allure.step(testcase.get('name')):
68
- if testcase.get('skip'):
69
- log.info(f"用例: {testcase.get('name')}跳过")
70
- continue
71
-
72
- sleeps = testcase.get('sleep')
73
- if sleeps:
74
- time.sleep(int(sleeps))
75
- log.info(f"当前用例: {testcase.get('name')}执行前等待{sleeps}秒")
76
-
77
- # 处理公共用例
78
- config_instance = self._get_config()
79
- if testcase.get('kind') and testcase.get('kind').lower() == 'common' and hasattr(config_instance, '_commonTestCasePath') and config_instance._commonTestCasePath is not None:
80
- testcase = self.getCommonTestCase(testcase, config_instance._commonTestCasePath, testcase.get('id'))
81
- elif testcase.get('kind') and testcase.get('kind').lower() == 'common' and (not hasattr(config_instance, '_commonTestCasePath') or config_instance._commonTestCasePath is None):
82
- log.error(f"commonPath路径未配置,请检查配置文件")
83
- raise Exception(f"commonPath路径未配置,请检查配置文件")
84
-
85
- # 参数替换
86
- if testcase.get('requestType') is None:
87
- requestType = 'json'
88
- else:
89
- requestType = testcase.get('requestType')
90
- repParameter = self.replaceParameterAttr(dataSaveBean, testcase.get('parameter'), requestType)
91
- repApi = self.replaceParameterAttr(dataSaveBean, testcase.get('api'))
92
- headers = self.replaceParameterAttr(dataSaveBean, testcase.get('headers'))
93
-
94
- # 鉴权处理
95
- requestParameter, requestUrl, authHeaders = self.authType(testcase.get('authType'), repApi, testcase.get('method'), repParameter)
96
-
97
- # 请求类型处理
98
- dataRequestParameter, jsonRequestParameter, paramsData, ModelData, requestType = self.requestType(requestType, requestParameter)
99
-
100
- # 合并请求头
101
- if headers:
102
- headers.update(authHeaders)
103
- else:
104
- headers = authHeaders
105
-
106
- # 执行请求
107
- log.info(f"开始请求地址: {requestUrl}")
108
- log.info(f"开始请求方式: {testcase.get('method')}")
109
-
110
- if dataRequestParameter is not None and requestType.lower() in ['form-data', 'form-file']:
111
- headers['Content-Type'] = dataRequestParameter.content_type
112
-
113
- if testcase.get('stream_check'):
114
- response = self.handle_stream_response(clientSession, testcase.get('method'), requestUrl, dataRequestParameter, jsonRequestParameter, paramsData, ModelData, headers)
115
- else:
116
- response = clientSession.request(method=testcase.get('method'), url=requestUrl, data=dataRequestParameter, json=jsonRequestParameter, params=paramsData, files=ModelData, headers=headers)
117
-
118
- # 记录响应
119
- try:
120
- log.info(f"当前用例response: {json.dumps(response.json(), indent=4, ensure_ascii=False)}")
121
- except:
122
- log.info(f"当前用例response: {response.text}")
123
-
124
- # 处理断言
125
- if testcase.get('assertFail'):
126
- failtype = testcase.get('assertFail')
127
- else:
128
- failtype = self.assertFail
129
- self.assertType(testcase.get('assert'), response, dataSaveBean, failtype)
130
-
131
- # 保存数据
132
- try:
133
- self.addAttrSaveBean(dataSaveBean, self.globalBean, testcase.get('saveData'), response.json())
134
- except:
135
- self.addAttrSaveBean(dataSaveBean, self.globalBean, testcase.get('saveData'), response.text)
136
-
137
- return clientSession
138
-
139
- def assertType(self, assertType, response, bean, failType):
140
- """
141
- 处理断言
142
- """
143
- if assertType is None:
144
- log.info(f"断言为空,跳过断言")
145
- return None
146
-
147
- for ass in assertType:
148
- key = list(ass.keys())[0]
149
- log.info(f"开始判断{key}断言: {ass.get(key)}")
150
-
151
- if 'status_code' in ass:
152
- if ass.get('status_code'):
153
- self.assertChoose(str(response.status_code) == str(ass.get('status_code')),
154
- f"status_code断言失败: {ass.get('status_code')} ,response结果: {response.status_code}", failType)
155
- continue
156
-
157
- jsonpathResults = jsonpath.jsonpath(response.json(), ass.get(key)[0])
158
- if jsonpathResults is False and 'not_found' not in ass:
159
- self.assertChoose(1 > 2, f"提取{ass.get(key)[0]}失败,断言失败", failType)
160
- continue
161
-
162
-
163
-
164
- if 'eq' in ass:
165
- expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('eq')[1]).replace().replaced_str
166
- assResults = str(expectedResults) in [str(item) for item in jsonpathResults]
167
- self.assertChoose(assResults is True, f"eq断言失败: {jsonpathResults} 不等于 {expectedResults}", failType)
168
- elif 'sge' in ass:
169
- expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('sge')[1]).replace().replaced_str
170
- self.assertChoose(len(jsonpathResults) >= int(expectedResults), f"sge断言失败: {jsonpathResults} 小于 {expectedResults}", failType)
171
- elif 'nn' in ass:
172
- self.assertChoose(jsonpathResults is not None, f"not none断言失败: {ass.get('nn')[0]}", failType)
173
- elif 'none' in ass:
174
- self.assertChoose(jsonpathResults is True, f"none断言失败: {ass.get('none')[0]}", failType)
175
- elif 'not_found' in ass:
176
- self.assertChoose(jsonpathResults is False, f"not_found断言失败,字段存在", failType)
177
- elif 'in' in ass:
178
- expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('in')[1]).replace().replaced_str
179
- self.assertChoose(str(expectedResults) in str(jsonpathResults), f"断言in失败: {expectedResults} 不在 {jsonpathResults} 内", failType)
180
- elif 'len' in ass:
181
- expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('in')[1]).replace().replaced_str
182
- jsonpathResults_len = len(jsonpathResults[0])
183
- self.assertChoose(jsonpathResults_len == int(expectedResults), f"断言len失败: {jsonpathResults_len} 长度不等于 {expectedResults}", failType)
184
-
185
- def addAttrSaveBean(self, bean, globalBean, data: list, response):
186
- """
187
- 保存响应数据到Bean
188
- """
189
- if data is None:
190
- return
191
- for d in data:
192
- if 'json' in d:
193
- jsonPath = d.get('json')[1]
194
- value = jsonpath.jsonpath(response, jsonPath)
195
- if value is False:
196
- value = None
197
-
198
- saveBean = bean
199
- if d.get('json').__len__() == 3 and d.get('json')[2].lower() == 'global':
200
- saveBean = globalBean
201
-
202
- key_parts = d.get('json')[0].split(':')
203
- d.get('json')[0] = key_parts[0]
204
-
205
- if value is not None and len(value) > 1:
206
- setattr(saveBean, d.get('json')[0], list(value))
207
- elif value is not None and len(value) == 1:
208
- if len(key_parts) > 1 and key_parts[1].lower() == 'str':
209
- value[0] = str(value[0])
210
- setattr(saveBean, d.get('json')[0], value[0])
211
-
212
- def replaceParameterAttr(self, bean, parameter, requestType='json'):
213
- """
214
- 替换参数中的占位符
215
- """
216
- if parameter is None:
217
- return None
218
-
219
- # 获取配置实例
220
- config_instance = self._get_config()
221
-
222
- if requestType.lower() == 'json-text':
223
- repParameter = CloudPlaceholderYaml(yaml_str=parameter, attrObj=bean, methObj=config_instance.methObj).replace().textLoad()
224
- else:
225
- repParameter = CloudPlaceholderYaml(yaml_str=parameter, attrObj=bean, methObj=config_instance.methObj).replace().jsonLoad()
226
-
227
- return repParameter
228
-
229
- def requestType(self, requestType, data):
230
- """
231
- 处理请求类型
232
- """
233
- jsonRequestParameter = None
234
- dataRequestParameter = None
235
- paramsData = None
236
- ModelData = None
237
-
238
- if isinstance(data, dict) and data.get('MIME'):
239
- MIME = data.get('MIME')
240
- else:
241
- MIME = 'application/octet-stream'
242
-
243
- if requestType is None:
244
- jsonRequestParameter = data
245
- elif requestType.lower() in ["json", "json-text"]:
246
- jsonRequestParameter = data
247
- elif requestType.lower() == "form-data":
248
- dataRequestParameter = MultipartEncoder(fields=data)
249
- elif requestType.lower() == "form-model":
250
- filename = data['filename']
251
- file_name = data[filename].split('\\')
252
- data[filename] = (file_name[-1], open(data[filename], 'rb'), MIME)
253
- for k, v in data.items():
254
- if type(v) == dict:
255
- data[k] = (None, json.dumps(data[k]))
256
- ModelData = data
257
- elif requestType.lower() == "form-file":
258
- filename = data['filename']
259
- data[filename] = (os.path.basename(data[filename]), open(data[filename], 'rb'), MIME)
260
- dataRequestParameter = MultipartEncoder(fields=data)
261
- elif requestType == "PARAMS":
262
- paramsData = data
263
- elif requestType == "DATA":
264
- dataRequestParameter = data
265
- else:
266
- log.error("请求方式不支持")
267
-
268
- return dataRequestParameter, jsonRequestParameter, paramsData, ModelData, requestType
269
-
270
- def authType(self, authType, url, method, parameter):
271
- """
272
- 处理鉴权方式
273
- """
274
- if self.baseUrl is None or self.isValidUrl(url):
275
- requestUrl = url
276
- else:
277
- requestUrl = self.baseUrl + url
278
- requestParameter = None
279
- authHeaders = {}
280
-
281
- if authType == "SIGN":
282
- # 云服务MD5签名鉴权
283
- config_instance = self._get_config()
284
- # 从config中获取企业ID
285
- enterprise_id = config_instance._enterpriseId if hasattr(config_instance, '_enterpriseId') else None
286
- if not enterprise_id:
287
- log.error("企业ID未配置,请在环境配置中设置enterpriseId")
288
- raise ValueError("企业ID未配置,请在环境配置中设置enterpriseId")
289
-
290
- if method.upper() == "GET":
291
- # GET请求:将签名参数添加到URL参数中
292
- # 添加默认的validateType参数
293
- if parameter is None:
294
- parameter = {}
295
- if 'validateType' not in parameter:
296
- parameter['validateType'] = config_instance._validateType if hasattr(config_instance, '_validateType') else '2' # 从配置中获取验证类型
297
-
298
- signed_params = CloudSignUtil.generate_params_with_signature(
299
- enterprise_id=enterprise_id,
300
- token=config_instance._token if hasattr(config_instance, '_token') else None,
301
- additional_params=parameter
302
- )
303
- # 构建带签名的URL
304
- from urllib.parse import urlencode
305
- query_string = urlencode(signed_params)
306
- if '?' in requestUrl:
307
- requestUrl += '&' + query_string
308
- else:
309
- requestUrl += '?' + query_string
310
- requestParameter = None
311
- log.info(f"GET请求URL: {requestUrl}")
312
- else:
313
- # POST/PUT/PATCH请求:签名信息放在请求头中
314
- authHeaders = CloudSignUtil.generate_headers(
315
- method=method,
316
- url=requestUrl,
317
- params=None,
318
- enterprise_id=enterprise_id,
319
- token=config_instance._token if hasattr(config_instance, '_token') else None,
320
- body=parameter
321
- )
322
- requestParameter = parameter
323
-
324
- return requestParameter, requestUrl, authHeaders
325
- elif authType == "COOKIE" or authType is None:
326
- requestParameter = parameter
327
- return requestParameter, requestUrl, authHeaders
328
- else:
329
- log.error("鉴权方式不支持")
330
- return requestParameter, requestUrl, authHeaders
331
-
332
- def getCommonTestCase(self, testcase, commonFile, caseId):
333
- """
334
- 获取公共测试用例
335
- """
336
- if self.commonCase is None:
337
- commonFile = commonFile
338
- else:
339
- commonFile = os.path.join(commonFile.split('common')[0], f'common/{self.commonCase}')
340
-
341
- yaml = CloudReadYaml(commonFile).load_yaml()
342
- commonCase = yaml.get('testcases')
343
-
344
- for case in commonCase:
345
- if case.get('id') == caseId:
346
- case['assert'] = [item for item in (case.get('assert') or []) + (testcase.get('assert') or []) if item is not None]
347
- case['saveData'] = [item for item in (case.get('saveData') or []) + (testcase.get('saveData') or []) if item is not None]
348
- return case
349
-
350
- raise ValueError("Case with id {} not found".format(caseId))
351
-
352
- def assertChoose(self, ass, tips, type):
353
- """
354
- 断言选择器
355
- """
356
- if type == 'stop':
357
- assert ass, tips
358
- elif type == 'continue':
359
- pytest.assume(ass, tips)
360
-
361
- def isValidUrl(self, url):
362
- """
363
- 验证URL是否有效
364
- """
365
- try:
366
- result = urlparse(url)
367
- return all([result.scheme, result.netloc])
368
- except ValueError:
369
- return False
370
-
371
- def handle_stream_response(self, clientSession, method, url, data, json_data, params, files, headers):
372
- """
373
- 处理流式响应
374
- """
375
- try:
376
- response_dict = {}
377
- answer_count = 0
378
- complete_message = ""
379
-
380
- with clientSession.request(
381
- method=method,
382
- url=url,
383
- data=data,
384
- json=json_data,
385
- params=params,
386
- files=files,
387
- headers=headers,
388
- stream=True
389
- ) as response:
390
- for chunk in response.iter_lines():
391
- if chunk:
392
- data = chunk.decode('utf-8')
393
- if data.startswith('data: '):
394
- try:
395
- json_str = data[6:]
396
- if json_str.strip() == '[DONE]':
397
- if complete_message:
398
- response_dict["complete_message"] = complete_message
399
- continue
400
-
401
- json_data = json.loads(json_str)
402
-
403
- if 'answer' in json_data:
404
- answer_count += 1
405
- key = f"answer{answer_count}"
406
- response_dict[key] = json_data
407
-
408
- answer_content = json_data.get('answer', '')
409
- if isinstance(answer_content, list):
410
- answer_content = ''.join(str(item) for item in answer_content)
411
- elif not isinstance(answer_content, str):
412
- answer_content = str(answer_content)
413
-
414
- complete_message += answer_content
415
-
416
- except json.JSONDecodeError as e:
417
- log.error(f"JSON解析错误: {e}")
418
- continue
419
-
420
- response.json = lambda: response_dict
421
- response._content = json.dumps(response_dict).encode()
422
- return response
423
-
424
- except Exception as e:
425
- log.error(f"流式处理错误: {e}")
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 'sge' in ass:
224
+ expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('sge')[1]).replace().replaced_str
225
+ self.assertChoose(len(jsonpathResults) >= int(expectedResults), f"sge断言失败: {jsonpathResults} 小于 {expectedResults}", failType)
226
+ elif 'nn' in ass:
227
+ self.assertChoose(jsonpathResults is not None, f"not none断言失败: {ass.get('nn')[0]}", failType)
228
+ elif 'none' in ass:
229
+ self.assertChoose(jsonpathResults is True, f"none断言失败: {ass.get('none')[0]}", failType)
230
+ elif 'not_found' in ass:
231
+ self.assertChoose(jsonpathResults is False, f"not_found断言失败,字段存在", failType)
232
+ elif 'in' in ass:
233
+ expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('in')[1]).replace().replaced_str
234
+ self.assertChoose(str(expectedResults) in str(jsonpathResults), f"断言in失败: {expectedResults} 不在 {jsonpathResults} 内", failType)
235
+ elif 'len' in ass:
236
+ expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('len')[1]).replace().replaced_str
237
+ jsonpathResults_len = len(jsonpathResults[0])
238
+ self.assertChoose(jsonpathResults_len == int(expectedResults), f"断言len失败: {jsonpathResults_len} 长度不等于 {expectedResults}", failType)
239
+ elif 'contains' in ass:
240
+ expectedResults = CloudPlaceholderYaml(attrObj=bean, reString=ass.get('contains')[1]).replace().replaced_str
241
+ self.assertChoose(str(expectedResults) in str(jsonpathResults), f"断言contains失败: {expectedResults} 不在 {jsonpathResults} 内", failType)
242
+
243
+ def addAttrSaveBean(self, bean, globalBean, data: list, response):
244
+ """
245
+ 保存响应数据到Bean
246
+ """
247
+ if data is None:
248
+ return
249
+ for d in data:
250
+ if 'json' in d:
251
+ jsonPath = d.get('json')[1]
252
+ value = jsonpath.jsonpath(response, jsonPath)
253
+ if value is False:
254
+ value = None
255
+
256
+ saveBean = bean
257
+ if d.get('json').__len__() == 3 and d.get('json')[2].lower() == 'global':
258
+ saveBean = globalBean
259
+
260
+ key_parts = d.get('json')[0].split(':')
261
+ d.get('json')[0] = key_parts[0]
262
+
263
+ if value is not None and len(value) > 1:
264
+ setattr(saveBean, d.get('json')[0], list(value))
265
+ elif value is not None and len(value) == 1:
266
+ if len(key_parts) > 1 and key_parts[1].lower() == 'str':
267
+ value[0] = str(value[0])
268
+ setattr(saveBean, d.get('json')[0], value[0])
269
+
270
+ def replaceParameterAttr(self, bean, parameter, requestType='json'):
271
+ """
272
+ 替换参数中的占位符
273
+ """
274
+ if parameter is None:
275
+ return None
276
+
277
+ # 获取配置实例
278
+ config_instance = self._get_config()
279
+
280
+ if requestType.lower() == 'json-text':
281
+ repParameter = CloudPlaceholderYaml(yaml_str=parameter, attrObj=bean, methObj=config_instance.methObj, gloObj=config_instance).replace().textLoad()
282
+ else:
283
+ repParameter = CloudPlaceholderYaml(yaml_str=parameter, attrObj=bean, methObj=config_instance.methObj, gloObj=config_instance).replace().jsonLoad()
284
+
285
+ return repParameter
286
+
287
+ def requestType(self, requestType, data):
288
+ """
289
+ 处理请求类型
290
+ """
291
+ jsonRequestParameter = None
292
+ dataRequestParameter = None
293
+ paramsData = None
294
+ ModelData = None
295
+
296
+ if isinstance(data, dict) and data.get('MIME'):
297
+ MIME = data.get('MIME')
298
+ else:
299
+ MIME = 'application/octet-stream'
300
+
301
+ if requestType is None:
302
+ jsonRequestParameter = data
303
+ elif requestType.lower() in ["json", "json-text"]:
304
+ jsonRequestParameter = data
305
+ elif requestType.lower() == "form-data":
306
+ dataRequestParameter = MultipartEncoder(fields=data)
307
+ elif requestType.lower() == "form-model":
308
+ filename = data['filename']
309
+ file_name = data[filename].split('\\')
310
+ data[filename] = (file_name[-1], open(data[filename], 'rb'), MIME)
311
+ for k, v in data.items():
312
+ if type(v) == dict:
313
+ data[k] = (None, json.dumps(data[k]))
314
+ ModelData = data
315
+ elif requestType.lower() == "form-file":
316
+ filename = data['filename']
317
+ data[filename] = (os.path.basename(data[filename]), open(data[filename], 'rb'), MIME)
318
+ dataRequestParameter = MultipartEncoder(fields=data)
319
+ elif requestType == "PARAMS":
320
+ paramsData = data
321
+ elif requestType == "DATA":
322
+ dataRequestParameter = data
323
+ else:
324
+ log.error("请求方式不支持")
325
+
326
+ return dataRequestParameter, jsonRequestParameter, paramsData, ModelData, requestType
327
+
328
+ def authType(self, authType, url, method, parameter):
329
+ """
330
+ 处理鉴权方式
331
+ """
332
+ if self.baseUrl is None or self.isValidUrl(url):
333
+ requestUrl = url
334
+ else:
335
+ requestUrl = self.baseUrl + url
336
+ requestParameter = None
337
+ authHeaders = {}
338
+
339
+ if authType == "SIGN":
340
+ # 云服务MD5签名鉴权
341
+ config_instance = self._get_config()
342
+ # 从config中获取企业ID
343
+ enterprise_id = config_instance._enterpriseId if hasattr(config_instance, '_enterpriseId') else None
344
+ if not enterprise_id:
345
+ log.error("企业ID未配置,请在环境配置中设置enterpriseId")
346
+ raise ValueError("企业ID未配置,请在环境配置中设置enterpriseId")
347
+
348
+ if method.upper() == "GET":
349
+ # GET请求:将签名参数添加到URL参数中
350
+ # 添加默认的validateType参数
351
+ if parameter is None:
352
+ parameter = {}
353
+ if 'validateType' not in parameter:
354
+ parameter['validateType'] = config_instance._validateType if hasattr(config_instance, '_validateType') else '2' # 从配置中获取验证类型
355
+
356
+ signed_params = CloudSignUtil.generate_params_with_signature(
357
+ enterprise_id=enterprise_id,
358
+ token=config_instance._token if hasattr(config_instance, '_token') else None,
359
+ additional_params=parameter
360
+ )
361
+ # 构建带签名的URL
362
+ from urllib.parse import urlencode
363
+ query_string = urlencode(signed_params)
364
+ if '?' in requestUrl:
365
+ requestUrl += '&' + query_string
366
+ else:
367
+ requestUrl += '?' + query_string
368
+ requestParameter = None
369
+ log.info(f"GET请求URL: {requestUrl}")
370
+ else:
371
+ # POST/PUT/PATCH请求:签名信息放在请求头中,但URL中也需要包含签名参数
372
+ # 添加签名参数到URL查询参数中
373
+ url_params = {
374
+ 'validateType': config_instance._validateType if hasattr(config_instance, '_validateType') else '2',
375
+ 'enterpriseId': enterprise_id,
376
+ 'timestamp': int(time.time()),
377
+ 'sign': CloudSignUtil.generate_md5_signature(
378
+ enterprise_id,
379
+ int(time.time()),
380
+ config_instance._token if hasattr(config_instance, '_token') else None
381
+ )
382
+ }
383
+
384
+ # 如果parameter中包含validateType,使用parameter中的值
385
+ if parameter and 'validateType' in parameter:
386
+ url_params['validateType'] = parameter.pop('validateType')
387
+
388
+ # 构建URL(包含所有签名参数)
389
+ from urllib.parse import urlencode
390
+ query_string = urlencode(url_params)
391
+ if '?' in requestUrl:
392
+ requestUrl += '&' + query_string
393
+ else:
394
+ requestUrl += '?' + query_string
395
+ log.info(f"POST请求URL: {requestUrl}")
396
+
397
+ # 生成请求头签名(包含请求体内容)
398
+ authHeaders = CloudSignUtil.generate_headers(
399
+ method=method,
400
+ url=requestUrl,
401
+ params=None, # POST请求不需要URL参数签名
402
+ enterprise_id=enterprise_id,
403
+ token=config_instance._token if hasattr(config_instance, '_token') else None,
404
+ body=parameter
405
+ )
406
+ requestParameter = parameter
407
+
408
+ return requestParameter, requestUrl, authHeaders
409
+ elif authType == "COOKIE" or authType is None:
410
+ requestParameter = parameter
411
+ return requestParameter, requestUrl, authHeaders
412
+ else:
413
+ log.error("鉴权方式不支持")
414
+ return requestParameter, requestUrl, authHeaders
415
+
416
+ def getCommonTestCase(self, testcase, commonFile, caseId):
417
+ """
418
+ 获取公共测试用例
419
+ """
420
+ if self.commonCase is None:
421
+ commonFile = commonFile
422
+ else:
423
+ commonFile = os.path.join(commonFile.split('common')[0], f'common/{self.commonCase}')
424
+
425
+ yaml = CloudReadYaml(commonFile).load_yaml()
426
+ commonCase = yaml.get('testcases')
427
+
428
+ for case in commonCase:
429
+ if case.get('id') == caseId:
430
+ case['assert'] = [item for item in (case.get('assert') or []) + (testcase.get('assert') or []) if item is not None]
431
+ case['saveData'] = [item for item in (case.get('saveData') or []) + (testcase.get('saveData') or []) if item is not None]
432
+ return case
433
+
434
+ raise ValueError("Case with id {} not found".format(caseId))
435
+
436
+ def assertChoose(self, ass, tips, type):
437
+ """
438
+ 断言选择器
439
+ """
440
+ if type == 'stop':
441
+ assert ass, tips
442
+ elif type == 'continue':
443
+ pytest.assume(ass, tips)
444
+
445
+ def isValidUrl(self, url):
446
+ """
447
+ 验证URL是否有效
448
+ """
449
+ try:
450
+ result = urlparse(url)
451
+ return all([result.scheme, result.netloc])
452
+ except ValueError:
453
+ return False
454
+
455
+ def handle_stream_response(self, clientSession, method, url, data, json_data, params, files, headers):
456
+ """
457
+ 处理流式响应
458
+ """
459
+ try:
460
+ response_dict = {}
461
+ answer_count = 0
462
+ complete_message = ""
463
+
464
+ with clientSession.request(
465
+ method=method,
466
+ url=url,
467
+ data=data,
468
+ json=json_data,
469
+ params=params,
470
+ files=files,
471
+ headers=headers,
472
+ stream=True
473
+ ) as response:
474
+ for chunk in response.iter_lines():
475
+ if chunk:
476
+ data = chunk.decode('utf-8')
477
+ if data.startswith('data: '):
478
+ try:
479
+ json_str = data[6:]
480
+ if json_str.strip() == '[DONE]':
481
+ if complete_message:
482
+ response_dict["complete_message"] = complete_message
483
+ continue
484
+
485
+ json_data = json.loads(json_str)
486
+
487
+ if 'answer' in json_data:
488
+ answer_count += 1
489
+ key = f"answer{answer_count}"
490
+ response_dict[key] = json_data
491
+
492
+ answer_content = json_data.get('answer', '')
493
+ if isinstance(answer_content, list):
494
+ answer_content = ''.join(str(item) for item in answer_content)
495
+ elif not isinstance(answer_content, str):
496
+ answer_content = str(answer_content)
497
+
498
+ complete_message += answer_content
499
+
500
+ except json.JSONDecodeError as e:
501
+ log.error(f"JSON解析错误: {e}")
502
+ continue
503
+
504
+ response.json = lambda: response_dict
505
+ response._content = json.dumps(response_dict).encode()
506
+ return response
507
+
508
+ except Exception as e:
509
+ log.error(f"流式处理错误: {e}")
426
510
  raise