pytest-dsl 0.1.0__py3-none-any.whl → 0.1.1__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.
- pytest_dsl/core/auth_provider.py +50 -10
- pytest_dsl/core/http_client.py +11 -6
- pytest_dsl/core/http_request.py +399 -110
- pytest_dsl/examples/csrf_auth_provider.py +232 -0
- pytest_dsl/examples/quickstart/api_basics.auto +55 -0
- pytest_dsl/examples/quickstart/assertions.auto +31 -0
- pytest_dsl/examples/quickstart/loops.auto +24 -0
- pytest_dsl/examples/test_quickstart.py +14 -0
- pytest_dsl-0.1.1.dist-info/METADATA +504 -0
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.1.1.dist-info}/RECORD +14 -9
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.1.1.dist-info}/WHEEL +1 -1
- pytest_dsl-0.1.0.dist-info/METADATA +0 -537
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.1.1.dist-info}/entry_points.txt +0 -0
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.1.1.dist-info}/licenses/LICENSE +0 -0
- {pytest_dsl-0.1.0.dist-info → pytest_dsl-0.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,232 @@
|
|
1
|
+
"""CSRF认证提供者示例
|
2
|
+
|
3
|
+
此模块提供了一个CSRF认证提供者的示例实现,展示如何使用AuthProvider的响应处理钩子。
|
4
|
+
"""
|
5
|
+
|
6
|
+
import logging
|
7
|
+
import json
|
8
|
+
import re
|
9
|
+
from typing import Dict, Any
|
10
|
+
import requests
|
11
|
+
from pytest_dsl.core.auth_provider import CustomAuthProvider, register_auth_provider
|
12
|
+
|
13
|
+
logger = logging.getLogger(__name__)
|
14
|
+
|
15
|
+
|
16
|
+
class CSRFAuthProvider(CustomAuthProvider):
|
17
|
+
"""CSRF令牌认证提供者
|
18
|
+
|
19
|
+
此提供者从响应中提取CSRF令牌,并将其应用到后续请求中。
|
20
|
+
支持从Cookie、响应头或响应体中提取令牌。
|
21
|
+
"""
|
22
|
+
|
23
|
+
def __init__(self,
|
24
|
+
token_source: str = "header", # header, cookie, body
|
25
|
+
source_name: str = "X-CSRF-Token", # 头名称、Cookie名称或JSON路径
|
26
|
+
header_name: str = "X-CSRF-Token", # 请求头名称
|
27
|
+
regex_pattern: str = None): # 从响应体提取的正则表达式
|
28
|
+
"""初始化CSRF令牌认证提供者
|
29
|
+
|
30
|
+
Args:
|
31
|
+
token_source: 令牌来源,可以是 "header"、"cookie" 或 "body"
|
32
|
+
source_name: 源名称,取决于token_source:
|
33
|
+
- 当token_source为"header"时,表示头名称
|
34
|
+
- 当token_source为"cookie"时,表示Cookie名称
|
35
|
+
- 当token_source为"body"时,表示JSON路径或CSS选择器
|
36
|
+
header_name: 请求头名称,用于发送令牌
|
37
|
+
regex_pattern: 从响应体提取令牌的正则表达式(当token_source为"body"时)
|
38
|
+
"""
|
39
|
+
super().__init__()
|
40
|
+
self.token_source = token_source
|
41
|
+
self.source_name = source_name
|
42
|
+
self.header_name = header_name
|
43
|
+
self.regex_pattern = regex_pattern
|
44
|
+
self.csrf_token = None
|
45
|
+
self._session = requests.Session()
|
46
|
+
|
47
|
+
# 标识此提供者管理会话
|
48
|
+
self.manage_session = True
|
49
|
+
|
50
|
+
logger.info(f"初始化CSRF令牌认证提供者 (源: {token_source}, 名称: {source_name})")
|
51
|
+
|
52
|
+
def apply_auth(self, request_kwargs: Dict[str, Any]) -> Dict[str, Any]:
|
53
|
+
"""应用CSRF令牌认证
|
54
|
+
|
55
|
+
将CSRF令牌添加到请求头中。
|
56
|
+
|
57
|
+
Args:
|
58
|
+
request_kwargs: 请求参数
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
更新后的请求参数
|
62
|
+
"""
|
63
|
+
# 确保headers存在
|
64
|
+
if "headers" not in request_kwargs:
|
65
|
+
request_kwargs["headers"] = {}
|
66
|
+
|
67
|
+
# 如果已有令牌,添加到请求头
|
68
|
+
if self.csrf_token:
|
69
|
+
request_kwargs["headers"][self.header_name] = self.csrf_token
|
70
|
+
logger.debug(f"添加CSRF令牌到请求头: {self.header_name}={self.csrf_token}")
|
71
|
+
else:
|
72
|
+
logger.warning("尚未获取到CSRF令牌,无法添加到请求头")
|
73
|
+
|
74
|
+
return request_kwargs
|
75
|
+
|
76
|
+
def process_response(self, response: requests.Response) -> None:
|
77
|
+
"""处理响应以提取CSRF令牌
|
78
|
+
|
79
|
+
从响应的头、Cookie或正文中提取CSRF令牌。
|
80
|
+
|
81
|
+
Args:
|
82
|
+
response: 响应对象
|
83
|
+
"""
|
84
|
+
token = None
|
85
|
+
|
86
|
+
# 从头中提取
|
87
|
+
if self.token_source == "header":
|
88
|
+
token = response.headers.get(self.source_name)
|
89
|
+
if token:
|
90
|
+
logger.debug(f"从响应头提取CSRF令牌: {self.source_name}={token}")
|
91
|
+
else:
|
92
|
+
logger.debug(f"响应头中未找到CSRF令牌: {self.source_name}")
|
93
|
+
|
94
|
+
# 从Cookie中提取
|
95
|
+
elif self.token_source == "cookie":
|
96
|
+
token = response.cookies.get(self.source_name)
|
97
|
+
if token:
|
98
|
+
logger.debug(f"从Cookie提取CSRF令牌: {self.source_name}={token}")
|
99
|
+
else:
|
100
|
+
logger.debug(f"Cookie中未找到CSRF令牌: {self.source_name}")
|
101
|
+
|
102
|
+
# 从响应体中提取
|
103
|
+
elif self.token_source == "body":
|
104
|
+
# 如果有正则表达式模式
|
105
|
+
if self.regex_pattern:
|
106
|
+
try:
|
107
|
+
match = re.search(self.regex_pattern, response.text)
|
108
|
+
if match and match.group(1):
|
109
|
+
token = match.group(1)
|
110
|
+
logger.debug(f"使用正则表达式从响应体提取CSRF令牌: {token}")
|
111
|
+
else:
|
112
|
+
logger.debug(f"正则表达式未匹配到CSRF令牌: {self.regex_pattern}")
|
113
|
+
except Exception as e:
|
114
|
+
logger.error(f"使用正则表达式提取CSRF令牌失败: {str(e)}")
|
115
|
+
# 如果是JSON响应,尝试使用JSON路径
|
116
|
+
elif 'application/json' in response.headers.get('Content-Type', ''):
|
117
|
+
try:
|
118
|
+
json_data = response.json()
|
119
|
+
|
120
|
+
# 简单的点路径解析
|
121
|
+
parts = self.source_name.strip('$').strip('.').split('.')
|
122
|
+
data = json_data
|
123
|
+
for part in parts:
|
124
|
+
if part in data:
|
125
|
+
data = data[part]
|
126
|
+
else:
|
127
|
+
data = None
|
128
|
+
break
|
129
|
+
|
130
|
+
if data and isinstance(data, str):
|
131
|
+
token = data
|
132
|
+
logger.debug(f"从JSON响应提取CSRF令牌: {self.source_name}={token}")
|
133
|
+
else:
|
134
|
+
logger.debug(f"JSON路径未找到CSRF令牌或值不是字符串: {self.source_name}")
|
135
|
+
|
136
|
+
except Exception as e:
|
137
|
+
logger.error(f"从JSON响应提取CSRF令牌失败: {str(e)}")
|
138
|
+
# 如果是HTML响应,可以尝试使用CSS选择器或XPath
|
139
|
+
else:
|
140
|
+
logger.debug("响应不是JSON格式,无法使用JSON路径提取CSRF令牌")
|
141
|
+
# 这里可以添加HTML解析逻辑,例如使用Beautiful Soup或lxml
|
142
|
+
|
143
|
+
# 更新令牌
|
144
|
+
if token:
|
145
|
+
logger.info(f"成功提取CSRF令牌: {token}")
|
146
|
+
self.csrf_token = token
|
147
|
+
|
148
|
+
def clean_auth_state(self, request_kwargs: Dict[str, Any] = None) -> Dict[str, Any]:
|
149
|
+
"""清理认证状态
|
150
|
+
|
151
|
+
清理CSRF认证状态,包括令牌和会话。
|
152
|
+
|
153
|
+
Args:
|
154
|
+
request_kwargs: 请求参数
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
更新后的请求参数
|
158
|
+
"""
|
159
|
+
logger.info("清理CSRF认证状态")
|
160
|
+
|
161
|
+
# 重置CSRF令牌
|
162
|
+
self.csrf_token = None
|
163
|
+
|
164
|
+
# 清理会话Cookie
|
165
|
+
self._session.cookies.clear()
|
166
|
+
|
167
|
+
# 处理请求参数
|
168
|
+
if request_kwargs:
|
169
|
+
if "headers" in request_kwargs:
|
170
|
+
# 移除CSRF相关头
|
171
|
+
csrf_headers = [self.header_name, 'X-CSRF-Token', 'csrf-token', 'CSRF-Token']
|
172
|
+
for header in csrf_headers:
|
173
|
+
if header in request_kwargs["headers"]:
|
174
|
+
request_kwargs["headers"].pop(header)
|
175
|
+
logger.debug(f"已移除请求头: {header}")
|
176
|
+
|
177
|
+
return request_kwargs if request_kwargs else {}
|
178
|
+
|
179
|
+
|
180
|
+
# 使用示例
|
181
|
+
def register_csrf_auth_providers():
|
182
|
+
"""注册CSRF认证提供者实例"""
|
183
|
+
|
184
|
+
# 从头中提取CSRF令牌
|
185
|
+
register_auth_provider(
|
186
|
+
"csrf_header_auth",
|
187
|
+
CSRFAuthProvider,
|
188
|
+
token_source="header",
|
189
|
+
source_name="X-CSRF-Token",
|
190
|
+
header_name="X-CSRF-Token"
|
191
|
+
)
|
192
|
+
|
193
|
+
# 从Cookie中提取CSRF令牌
|
194
|
+
register_auth_provider(
|
195
|
+
"csrf_cookie_auth",
|
196
|
+
CSRFAuthProvider,
|
197
|
+
token_source="cookie",
|
198
|
+
source_name="csrf_token",
|
199
|
+
header_name="X-CSRF-Token"
|
200
|
+
)
|
201
|
+
|
202
|
+
# 从JSON响应体中提取CSRF令牌
|
203
|
+
register_auth_provider(
|
204
|
+
"csrf_json_auth",
|
205
|
+
CSRFAuthProvider,
|
206
|
+
token_source="body",
|
207
|
+
source_name="data.security.csrf_token",
|
208
|
+
header_name="X-CSRF-Token"
|
209
|
+
)
|
210
|
+
|
211
|
+
# 使用正则表达式从HTML响应体中提取CSRF令牌
|
212
|
+
register_auth_provider(
|
213
|
+
"csrf_html_auth",
|
214
|
+
CSRFAuthProvider,
|
215
|
+
token_source="body",
|
216
|
+
header_name="X-CSRF-Token",
|
217
|
+
regex_pattern=r'<meta name="csrf-token" content="([^"]+)">'
|
218
|
+
)
|
219
|
+
|
220
|
+
logger.info("已注册CSRF认证提供者")
|
221
|
+
|
222
|
+
|
223
|
+
# 如果此模块被直接运行,注册CSRF认证提供者
|
224
|
+
if __name__ == "__main__":
|
225
|
+
# 配置日志
|
226
|
+
logging.basicConfig(
|
227
|
+
level=logging.DEBUG,
|
228
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
229
|
+
)
|
230
|
+
|
231
|
+
# 注册提供者
|
232
|
+
register_csrf_auth_providers()
|
@@ -0,0 +1,55 @@
|
|
1
|
+
@name: API测试入门示例
|
2
|
+
@description: 演示基本的API接口测试用法
|
3
|
+
@tags: [API, HTTP, 入门]
|
4
|
+
@author: Felix
|
5
|
+
@date: 2024-01-01
|
6
|
+
|
7
|
+
# 基本GET请求
|
8
|
+
[HTTP请求],客户端:'default',配置:'''
|
9
|
+
method: GET
|
10
|
+
url: https://jsonplaceholder.typicode.com/posts/1
|
11
|
+
asserts:
|
12
|
+
- ["status", "eq", 200]
|
13
|
+
- ["jsonpath", "$.id", "eq", 1]
|
14
|
+
- ["jsonpath", "$.title", "exists"]
|
15
|
+
''',步骤名称:'获取文章详情'
|
16
|
+
|
17
|
+
# 响应数据捕获与使用
|
18
|
+
[HTTP请求],客户端:'default',配置:'''
|
19
|
+
method: GET
|
20
|
+
url: https://jsonplaceholder.typicode.com/posts
|
21
|
+
request:
|
22
|
+
params:
|
23
|
+
userId: 1
|
24
|
+
captures:
|
25
|
+
first_post_id: ["jsonpath", "$[0].id"]
|
26
|
+
post_count: ["jsonpath", "$", "length"]
|
27
|
+
asserts:
|
28
|
+
- ["status", "eq", 200]
|
29
|
+
- ["jsonpath", "$", "type", "array"]
|
30
|
+
''',步骤名称:'获取用户文章列表'
|
31
|
+
|
32
|
+
# 打印捕获的变量
|
33
|
+
[打印],内容:'第一篇文章ID: ${first_post_id}, 文章总数: ${post_count}'
|
34
|
+
|
35
|
+
# POST请求创建资源
|
36
|
+
[HTTP请求],客户端:'default',配置:'''
|
37
|
+
method: POST
|
38
|
+
url: https://jsonplaceholder.typicode.com/posts
|
39
|
+
request:
|
40
|
+
headers:
|
41
|
+
Content-Type: application/json
|
42
|
+
json:
|
43
|
+
title: 测试标题
|
44
|
+
body: 测试内容
|
45
|
+
userId: 1
|
46
|
+
captures:
|
47
|
+
new_post_id: ["jsonpath", "$.id"]
|
48
|
+
asserts:
|
49
|
+
- ["status", "eq", 201]
|
50
|
+
- ["jsonpath", "$.title", "eq", "测试标题"]
|
51
|
+
''',步骤名称:'创建新文章'
|
52
|
+
|
53
|
+
@teardown do
|
54
|
+
[打印],内容:'API测试完成!'
|
55
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
@name: 断言功能示例
|
2
|
+
@description: 演示断言关键字的基本用法
|
3
|
+
@tags: [断言, 入门]
|
4
|
+
@author: Felix
|
5
|
+
@date: 2024-01-01
|
6
|
+
|
7
|
+
# 基本断言
|
8
|
+
[断言],条件:'1 + 1 == 2',消息:'基本算术断言失败'
|
9
|
+
|
10
|
+
# 数字比较
|
11
|
+
num1 = 10
|
12
|
+
num2 = 5
|
13
|
+
[断言],条件:'${num1} > ${num2}',消息:'数字比较断言失败'
|
14
|
+
|
15
|
+
# JSON数据处理
|
16
|
+
json_data = '{"user": {"name": "张三", "age": 30, "roles": ["admin", "user"]}}'
|
17
|
+
|
18
|
+
# JSON断言示例
|
19
|
+
[JSON断言],JSON数据:${json_data},JSONPath:'$.user.age',预期值:30,操作符:'==',消息:'JSON断言失败:年龄不匹配'
|
20
|
+
|
21
|
+
# 类型断言
|
22
|
+
[类型断言],值:${json_data},类型:'string',消息:'类型断言失败'
|
23
|
+
|
24
|
+
# 简单变量赋值和断言
|
25
|
+
result = 53
|
26
|
+
[打印],内容:'结果: ${result}'
|
27
|
+
[断言],条件:'${result} == 53',消息:'结果不正确'
|
28
|
+
|
29
|
+
@teardown do
|
30
|
+
[打印],内容:'断言测试完成!'
|
31
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
@name: 变量和循环示例
|
2
|
+
@description: 演示变量使用和循环结构
|
3
|
+
@tags: [变量, 循环, 入门]
|
4
|
+
@author: Felix
|
5
|
+
@date: 2024-01-01
|
6
|
+
|
7
|
+
# 基本变量定义和使用
|
8
|
+
name = "pytest-dsl"
|
9
|
+
version = "1.0.0"
|
10
|
+
[打印],内容:'测试框架: ${name}, 版本: ${version}'
|
11
|
+
|
12
|
+
# 循环结构示例
|
13
|
+
[打印],内容:'开始循环测试'
|
14
|
+
count = 3
|
15
|
+
|
16
|
+
for i in range(1, ${count}) do
|
17
|
+
[打印],内容:'循环次数: ${i}'
|
18
|
+
end
|
19
|
+
|
20
|
+
[打印],内容:'循环结束'
|
21
|
+
|
22
|
+
@teardown do
|
23
|
+
[打印],内容:'变量和循环测试完成!'
|
24
|
+
end
|