CloudApiRequest 1.0.0__tar.gz
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.
- cloudapirequest-1.0.0/CloudApiRequest.egg-info/PKG-INFO +20 -0
- cloudapirequest-1.0.0/CloudApiRequest.egg-info/SOURCES.txt +16 -0
- cloudapirequest-1.0.0/CloudApiRequest.egg-info/dependency_links.txt +1 -0
- cloudapirequest-1.0.0/CloudApiRequest.egg-info/requires.txt +10 -0
- cloudapirequest-1.0.0/CloudApiRequest.egg-info/top_level.txt +1 -0
- cloudapirequest-1.0.0/LICENSE +21 -0
- cloudapirequest-1.0.0/PKG-INFO +20 -0
- cloudapirequest-1.0.0/README.md +134 -0
- cloudapirequest-1.0.0/cloud/CloudAPIRequest.py +426 -0
- cloudapirequest-1.0.0/cloud/CloudApiConfig.py +311 -0
- cloudapirequest-1.0.0/cloud/CloudLogUtil.py +36 -0
- cloudapirequest-1.0.0/cloud/CloudRequestUtil.py +107 -0
- cloudapirequest-1.0.0/cloud/CloudSignUtil.py +117 -0
- cloudapirequest-1.0.0/cloud/CloudYamlUtil.py +197 -0
- cloudapirequest-1.0.0/cloud/__init__.py +9 -0
- cloudapirequest-1.0.0/cloud/cli.py +62 -0
- cloudapirequest-1.0.0/setup.cfg +4 -0
- cloudapirequest-1.0.0/setup.py +58 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: CloudApiRequest
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: 天润cloud接口测试库
|
|
5
|
+
Home-page: https://www.ti-net.com.cn/
|
|
6
|
+
Author: 天润-测试
|
|
7
|
+
Author-email: wangwd@ti-net.com.cn
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: Faker==18.4.0
|
|
12
|
+
Requires-Dist: jsonpath==0.82
|
|
13
|
+
Requires-Dist: pytz==2023.3
|
|
14
|
+
Requires-Dist: PyYAML==6.0
|
|
15
|
+
Requires-Dist: requests==2.28.2
|
|
16
|
+
Requires-Dist: allure-pytest==2.13.2
|
|
17
|
+
Requires-Dist: allure-python-commons==2.13.2
|
|
18
|
+
Requires-Dist: requests_toolbelt==1.0.0
|
|
19
|
+
Requires-Dist: pycryptodome==3.17
|
|
20
|
+
Requires-Dist: pytest-assume==2.4.3
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
CloudApiRequest.egg-info/PKG-INFO
|
|
5
|
+
CloudApiRequest.egg-info/SOURCES.txt
|
|
6
|
+
CloudApiRequest.egg-info/dependency_links.txt
|
|
7
|
+
CloudApiRequest.egg-info/requires.txt
|
|
8
|
+
CloudApiRequest.egg-info/top_level.txt
|
|
9
|
+
cloud/CloudAPIRequest.py
|
|
10
|
+
cloud/CloudApiConfig.py
|
|
11
|
+
cloud/CloudLogUtil.py
|
|
12
|
+
cloud/CloudRequestUtil.py
|
|
13
|
+
cloud/CloudSignUtil.py
|
|
14
|
+
cloud/CloudYamlUtil.py
|
|
15
|
+
cloud/__init__.py
|
|
16
|
+
cloud/cli.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
cloud
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2025] [天润-测试]
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: CloudApiRequest
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: 天润cloud接口测试库
|
|
5
|
+
Home-page: https://www.ti-net.com.cn/
|
|
6
|
+
Author: 天润-测试
|
|
7
|
+
Author-email: wangwd@ti-net.com.cn
|
|
8
|
+
License: MIT
|
|
9
|
+
Requires-Python: >=3.7
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: Faker==18.4.0
|
|
12
|
+
Requires-Dist: jsonpath==0.82
|
|
13
|
+
Requires-Dist: pytz==2023.3
|
|
14
|
+
Requires-Dist: PyYAML==6.0
|
|
15
|
+
Requires-Dist: requests==2.28.2
|
|
16
|
+
Requires-Dist: allure-pytest==2.13.2
|
|
17
|
+
Requires-Dist: allure-python-commons==2.13.2
|
|
18
|
+
Requires-Dist: requests_toolbelt==1.0.0
|
|
19
|
+
Requires-Dist: pycryptodome==3.17
|
|
20
|
+
Requires-Dist: pytest-assume==2.4.3
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
项目名称
|
|
2
|
+
|
|
3
|
+
Cloud API自动化测试框架,封装requests库.使用Yaml文件管理测试用例,支持数据驱动,支持多环境配置,支持多线程执行,支持测试报告生成.
|
|
4
|
+
|
|
5
|
+
当前版本 https://pypi.org/project/CloudApiRequest/
|
|
6
|
+
|
|
7
|
+
## 项目结构
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
├── dist # 打包目录 版本发布
|
|
11
|
+
├── test # 测试目录 测试类
|
|
12
|
+
├── cloud # 源码目录 框架源码
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 运行条件
|
|
16
|
+
|
|
17
|
+
Python 3.7+
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
pip install -r requirements.txt
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
完成依赖安装
|
|
24
|
+
|
|
25
|
+
## 快速开始
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
进行测试
|
|
29
|
+
pytest -s
|
|
30
|
+
pytest -s ./testcase --env=bj --tenv=gray -n=2 --alluredir ./result
|
|
31
|
+
|
|
32
|
+
--env 环境配置
|
|
33
|
+
-n 多线程执行
|
|
34
|
+
--alluredir allure报告生成目录
|
|
35
|
+
|
|
36
|
+
查看报告
|
|
37
|
+
allure serve ./result
|
|
38
|
+
.result 为allure报告生成目录
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## 开发方式
|
|
42
|
+
|
|
43
|
+
导入cloudrequest库
|
|
44
|
+
```
|
|
45
|
+
pip install CloudApiRequest-v*.whl
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
更新版本
|
|
49
|
+
```
|
|
50
|
+
pip uninstall CloudApiRequest
|
|
51
|
+
pip install CloudApiRequest-v*.whl
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
引用CloudApiRequest
|
|
55
|
+
```
|
|
56
|
+
from cloud import config
|
|
57
|
+
# 平台与运行环境选择
|
|
58
|
+
def pytest_addoption(parser):
|
|
59
|
+
parser.addoption(
|
|
60
|
+
"--env", action="store", default="test", help="test:表示测试环境,默认测试环境"
|
|
61
|
+
)
|
|
62
|
+
parser.addoption(
|
|
63
|
+
"--tenv", action="store", default="gray", help="tenv:表示蓝绿环境,默认gray灰度环境"
|
|
64
|
+
)
|
|
65
|
+
# 配置环境
|
|
66
|
+
config.baseUrl = envConfig.urlPath # 基础请求地址
|
|
67
|
+
config.token = envConfig.token # token
|
|
68
|
+
config.commonTestCasePath = os.path.join(DATA_JSON_PATH, "common.yaml") # common 公共用例路径
|
|
69
|
+
config.methObj=method() # 用于自定义"#{fun}"方法调用的函数 ,不想使用sdk自带的方法时,可以自定义方法,在yaml中使用
|
|
70
|
+
config.session=request.Session() # 用于传递请求session实体类,可以传递cookies
|
|
71
|
+
config.tEnv = request.config.getoption("tenv") # 运行环境选择
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
from cloud import CloudAPIRequest
|
|
77
|
+
# 执行测试 传入yamlFile文件路径 传入Test_pyt测试类 也可以使用自定义commonfile文件,不是用全局配置的commonfile。直接写yaml文件即可
|
|
78
|
+
CloudAPIRequest('cloud_api.yaml').doRequest(yamlFile,Test_pyt)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## 用例示例
|
|
82
|
+
|
|
83
|
+
```yaml
|
|
84
|
+
id: create_client # 用例id
|
|
85
|
+
name: 示例测试用例 # 用例名称
|
|
86
|
+
testcases: # 测试用例步骤 #
|
|
87
|
+
- sleep: 1 # 等待时间
|
|
88
|
+
- skip: 1 # 跳过步骤
|
|
89
|
+
- kind: common #用例类型 common去取公共用例,需要公共用例路径
|
|
90
|
+
id: getQueues
|
|
91
|
+
name: 获取队列信息
|
|
92
|
+
- name: 查找队列信息 # 步骤名称
|
|
93
|
+
id: list_queues # 步骤id
|
|
94
|
+
api: /list_queues # 请求地址 # todo: 支持变量地址 ${args}
|
|
95
|
+
headers: # 请求头 # todo: 支持变量
|
|
96
|
+
Content-Type: application/json
|
|
97
|
+
User-Agent: Chrome$${browser_version}
|
|
98
|
+
authType: SIGN # 鉴权方式 SIGN COOKIE # todo AUTH bear xxx
|
|
99
|
+
method: GET # GET POST
|
|
100
|
+
requestType: JSON # JSON FORM FORM-DATA FORM-FILE FROM-MODEL JSON-TEXT # todo PARAMS
|
|
101
|
+
stream_check: true # 启用流式检查 默认为false或者不写 启用流式检查后,会将response中的answer字段进行拼接,返回完整的answer字段。
|
|
102
|
+
parameter: # 请求参数 '#{}'取FAKER模拟数据方法 '${}' 取缓存变量 # todo '$${}' 取global变量 todo 方法传参数
|
|
103
|
+
offset: 0
|
|
104
|
+
limit: '#{get_sk_password}' # 获取环境变量里 SK 加密密码
|
|
105
|
+
cno: '#{random_number(args)}' # 生成随机数
|
|
106
|
+
name: '[${queues}]'
|
|
107
|
+
names: '$${names}'
|
|
108
|
+
file: '..\..\..\data\files\知识图谱导出文件-物价编码.xlsx' # 该字段填写 上传文件的相对路径 配合form-data使用
|
|
109
|
+
MIME: 'image/gif' # 该字段填写 文件的MIME类型 配合form-data使用
|
|
110
|
+
model: # 该字段 工单模块 使用多。上传文件使用
|
|
111
|
+
"operator": 1223
|
|
112
|
+
"content": "213213"
|
|
113
|
+
"type": 1
|
|
114
|
+
filename: file # 该字段仅限于 FORM-FILE FROM-MODEL 使用 值为 文件入参字段
|
|
115
|
+
assertFail: stop、continue # 断言失败处理模式 stop程序直接终止 continue断言失败仍运行。默认为stop
|
|
116
|
+
assert: # 断言 eq sge # todo 支持多个断言 eq ne gt lt ge le seq sne sgt slt sge sle in
|
|
117
|
+
- status_code: 201
|
|
118
|
+
- eq: [ '$.pageSize', '${size}' ]
|
|
119
|
+
- eq: [ '$.pageSize', '$${size}' ]
|
|
120
|
+
- sge: [ '$.queues[:2].id', 2 ]
|
|
121
|
+
- none : ['$.error'] # 判断 返回值 是否为空
|
|
122
|
+
- nn: ['$.error'] # 判断 返回值 是否不为空
|
|
123
|
+
- in: ['$.pageSize', '$${size}'] # 判断 返回值 是否包含 预期
|
|
124
|
+
- not_found: [ '$.pageSize', ] # 判断 指定数据是否不存在
|
|
125
|
+
- len: [ '$.pageSize', 4 ] # 判断 指定数据的长度
|
|
126
|
+
saveData: # 缓存变量 global变量 #todo cookie变量 header变量 request变量
|
|
127
|
+
- json: [ 'queues:str','$.queues[:2].id'] #:str 指定获取的值 type为str,暂不支持其他类型,有需要可提出优化。
|
|
128
|
+
- json: [ 'names','$.name' ,'global']
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
- name: 创建座席
|
|
132
|
+
id: create_client
|
|
133
|
+
api: ```````
|
|
134
|
+
```
|
|
@@ -0,0 +1,426 @@
|
|
|
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}")
|
|
426
|
+
raise
|