apitestgenie 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.
- apitestgenie-1.0.0/LICENSE +21 -0
- apitestgenie-1.0.0/PKG-INFO +227 -0
- apitestgenie-1.0.0/README.md +193 -0
- apitestgenie-1.0.0/pyproject.toml +25 -0
- apitestgenie-1.0.0/setup.cfg +4 -0
- apitestgenie-1.0.0/src/apitestgenie/__init__.py +4 -0
- apitestgenie-1.0.0/src/apitestgenie/client.py +73 -0
- apitestgenie-1.0.0/src/apitestgenie/response_wrapper.py +68 -0
- apitestgenie-1.0.0/src/apitestgenie/simple.py +145 -0
- apitestgenie-1.0.0/src/apitestgenie.egg-info/PKG-INFO +227 -0
- apitestgenie-1.0.0/src/apitestgenie.egg-info/SOURCES.txt +13 -0
- apitestgenie-1.0.0/src/apitestgenie.egg-info/dependency_links.txt +1 -0
- apitestgenie-1.0.0/src/apitestgenie.egg-info/requires.txt +4 -0
- apitestgenie-1.0.0/src/apitestgenie.egg-info/top_level.txt +1 -0
- apitestgenie-1.0.0/tests/test_client.py +183 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Akash Gujarathi
|
|
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,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apitestgenie
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight, developer-friendly Python library for API test automation
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Akash Gujarathi
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: httpx>=0.27
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=9.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# APItestGenie
|
|
36
|
+
|
|
37
|
+
APItestGenie is a lightweight, developer-friendly Python library designed to simplify API testing for automation engineers.
|
|
38
|
+
Version 1.0 focuses on clarity, correctness, and essential functionality without unnecessary complexity.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Overview
|
|
43
|
+
|
|
44
|
+
APItestGenie provides:
|
|
45
|
+
|
|
46
|
+
- Two ways to perform API requests: Client mode and Simple mode
|
|
47
|
+
- Built-in JSON assertions
|
|
48
|
+
- JSON path assertions for nested responses
|
|
49
|
+
- Basic retry logic with retries, retry delay, and retry_on_status
|
|
50
|
+
- A ResponseWrapper abstraction for consistent behavior across requests
|
|
51
|
+
- GET, POST, PUT, PATCH, DELETE support
|
|
52
|
+
- Timeout and headers support
|
|
53
|
+
- A complete pytest suite
|
|
54
|
+
- A modern Python packaging layout using the src structure
|
|
55
|
+
|
|
56
|
+
Version 1.0 intentionally avoids heavy features like logging frameworks, async support, or automation framework integrations.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
Clone the repository:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
git clone https://github.com/Akash402/apitestgenie.git
|
|
66
|
+
cd apitestgenie
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Create and activate a virtual environment:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
python3 -m venv venv
|
|
73
|
+
source venv/bin/activate
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Install dependencies:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
pip install httpx pytest
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
(Optional) Install the library locally in editable mode:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
pip install -e .
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Usage Examples
|
|
91
|
+
|
|
92
|
+
### Client Mode
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from apitestgenie.client import ApiClient
|
|
96
|
+
|
|
97
|
+
api = ApiClient("https://jsonplaceholder.typicode.com", timeout=10)
|
|
98
|
+
|
|
99
|
+
response = api.get("/posts/1", retries=2)
|
|
100
|
+
response.assert_status(200)
|
|
101
|
+
response.assert_json_value("id", 1)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
POST example:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
resp = api.post("/posts", json={"title": "foo"})
|
|
108
|
+
resp.assert_status(201)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
PUT example:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
resp = api.put("/posts/1", json={"id": 1, "title": "updated"})
|
|
115
|
+
resp.assert_status(200)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
DELETE example:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
resp = api.delete("/posts/1")
|
|
122
|
+
resp.assert_status(200)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### Simple Mode
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from apitestgenie.simple import get
|
|
131
|
+
|
|
132
|
+
response = get("https://jsonplaceholder.typicode.com/posts/1")
|
|
133
|
+
response.assert_status(200)
|
|
134
|
+
print(response.json())
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
POST example:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from apitestgenie.simple import post
|
|
141
|
+
|
|
142
|
+
resp = post("https://jsonplaceholder.typicode.com/posts", json={"hello": "world"})
|
|
143
|
+
resp.assert_status(201)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Retry and timeout example:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
resp = get(
|
|
150
|
+
"https://jsonplaceholder.typicode.com/posts/1",
|
|
151
|
+
retries=3,
|
|
152
|
+
retry_delay=1,
|
|
153
|
+
retry_on_status=[500],
|
|
154
|
+
timeout=5
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## JSON Assertions
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
resp.assert_status(200)
|
|
164
|
+
resp.assert_json_key("id")
|
|
165
|
+
resp.assert_json_value("id", 1)
|
|
166
|
+
resp.assert_json_path_exists("title")
|
|
167
|
+
resp.assert_json_path_value("id", 1)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Running Tests
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
pytest
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Project Structure
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
apitestgenie/
|
|
184
|
+
│
|
|
185
|
+
├── src/
|
|
186
|
+
│ └── apitestgenie/
|
|
187
|
+
│ ├── client.py
|
|
188
|
+
│ ├── simple.py
|
|
189
|
+
│ ├── response_wrapper.py
|
|
190
|
+
│ └── __init__.py
|
|
191
|
+
│
|
|
192
|
+
├── tests/
|
|
193
|
+
├── playground.py
|
|
194
|
+
├── pytest.ini
|
|
195
|
+
├── README.md
|
|
196
|
+
└── SCOPE.md
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Version 1.0 Scope Summary
|
|
202
|
+
|
|
203
|
+
Included:
|
|
204
|
+
|
|
205
|
+
- CRUD operations
|
|
206
|
+
- Basic retry logic
|
|
207
|
+
- JSON and JSON path assertions
|
|
208
|
+
- ResponseWrapper abstraction
|
|
209
|
+
- Simple and client modes
|
|
210
|
+
- Header and timeout support
|
|
211
|
+
- Test suite
|
|
212
|
+
- Clean structure
|
|
213
|
+
|
|
214
|
+
Excluded:
|
|
215
|
+
|
|
216
|
+
- Advanced retry logic
|
|
217
|
+
- Logging framework
|
|
218
|
+
- Robot/Behave integrations
|
|
219
|
+
- Async support
|
|
220
|
+
- Schema validation
|
|
221
|
+
- Plugin system
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# APItestGenie
|
|
2
|
+
|
|
3
|
+
APItestGenie is a lightweight, developer-friendly Python library designed to simplify API testing for automation engineers.
|
|
4
|
+
Version 1.0 focuses on clarity, correctness, and essential functionality without unnecessary complexity.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Overview
|
|
9
|
+
|
|
10
|
+
APItestGenie provides:
|
|
11
|
+
|
|
12
|
+
- Two ways to perform API requests: Client mode and Simple mode
|
|
13
|
+
- Built-in JSON assertions
|
|
14
|
+
- JSON path assertions for nested responses
|
|
15
|
+
- Basic retry logic with retries, retry delay, and retry_on_status
|
|
16
|
+
- A ResponseWrapper abstraction for consistent behavior across requests
|
|
17
|
+
- GET, POST, PUT, PATCH, DELETE support
|
|
18
|
+
- Timeout and headers support
|
|
19
|
+
- A complete pytest suite
|
|
20
|
+
- A modern Python packaging layout using the src structure
|
|
21
|
+
|
|
22
|
+
Version 1.0 intentionally avoids heavy features like logging frameworks, async support, or automation framework integrations.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
Clone the repository:
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
git clone https://github.com/Akash402/apitestgenie.git
|
|
32
|
+
cd apitestgenie
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Create and activate a virtual environment:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
python3 -m venv venv
|
|
39
|
+
source venv/bin/activate
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Install dependencies:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
pip install httpx pytest
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
(Optional) Install the library locally in editable mode:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
pip install -e .
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Usage Examples
|
|
57
|
+
|
|
58
|
+
### Client Mode
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from apitestgenie.client import ApiClient
|
|
62
|
+
|
|
63
|
+
api = ApiClient("https://jsonplaceholder.typicode.com", timeout=10)
|
|
64
|
+
|
|
65
|
+
response = api.get("/posts/1", retries=2)
|
|
66
|
+
response.assert_status(200)
|
|
67
|
+
response.assert_json_value("id", 1)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
POST example:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
resp = api.post("/posts", json={"title": "foo"})
|
|
74
|
+
resp.assert_status(201)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
PUT example:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
resp = api.put("/posts/1", json={"id": 1, "title": "updated"})
|
|
81
|
+
resp.assert_status(200)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
DELETE example:
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
resp = api.delete("/posts/1")
|
|
88
|
+
resp.assert_status(200)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
### Simple Mode
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from apitestgenie.simple import get
|
|
97
|
+
|
|
98
|
+
response = get("https://jsonplaceholder.typicode.com/posts/1")
|
|
99
|
+
response.assert_status(200)
|
|
100
|
+
print(response.json())
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
POST example:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from apitestgenie.simple import post
|
|
107
|
+
|
|
108
|
+
resp = post("https://jsonplaceholder.typicode.com/posts", json={"hello": "world"})
|
|
109
|
+
resp.assert_status(201)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Retry and timeout example:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
resp = get(
|
|
116
|
+
"https://jsonplaceholder.typicode.com/posts/1",
|
|
117
|
+
retries=3,
|
|
118
|
+
retry_delay=1,
|
|
119
|
+
retry_on_status=[500],
|
|
120
|
+
timeout=5
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## JSON Assertions
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
resp.assert_status(200)
|
|
130
|
+
resp.assert_json_key("id")
|
|
131
|
+
resp.assert_json_value("id", 1)
|
|
132
|
+
resp.assert_json_path_exists("title")
|
|
133
|
+
resp.assert_json_path_value("id", 1)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Running Tests
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
pytest
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Project Structure
|
|
147
|
+
|
|
148
|
+
```
|
|
149
|
+
apitestgenie/
|
|
150
|
+
│
|
|
151
|
+
├── src/
|
|
152
|
+
│ └── apitestgenie/
|
|
153
|
+
│ ├── client.py
|
|
154
|
+
│ ├── simple.py
|
|
155
|
+
│ ├── response_wrapper.py
|
|
156
|
+
│ └── __init__.py
|
|
157
|
+
│
|
|
158
|
+
├── tests/
|
|
159
|
+
├── playground.py
|
|
160
|
+
├── pytest.ini
|
|
161
|
+
├── README.md
|
|
162
|
+
└── SCOPE.md
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Version 1.0 Scope Summary
|
|
168
|
+
|
|
169
|
+
Included:
|
|
170
|
+
|
|
171
|
+
- CRUD operations
|
|
172
|
+
- Basic retry logic
|
|
173
|
+
- JSON and JSON path assertions
|
|
174
|
+
- ResponseWrapper abstraction
|
|
175
|
+
- Simple and client modes
|
|
176
|
+
- Header and timeout support
|
|
177
|
+
- Test suite
|
|
178
|
+
- Clean structure
|
|
179
|
+
|
|
180
|
+
Excluded:
|
|
181
|
+
|
|
182
|
+
- Advanced retry logic
|
|
183
|
+
- Logging framework
|
|
184
|
+
- Robot/Behave integrations
|
|
185
|
+
- Async support
|
|
186
|
+
- Schema validation
|
|
187
|
+
- Plugin system
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "apitestgenie"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "A lightweight, developer-friendly Python library for API test automation"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"httpx>=0.27",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project.optional-dependencies]
|
|
17
|
+
dev = [
|
|
18
|
+
"pytest>=9.0",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["src"]
|
|
23
|
+
|
|
24
|
+
[tool.pytest.ini_options]
|
|
25
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import httpx
|
|
3
|
+
from .response_wrapper import ResponseWrapper
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ApiClient:
|
|
7
|
+
def __init__(self, base_url, headers=None, timeout=30):
|
|
8
|
+
self.base_url = base_url.rstrip("/")
|
|
9
|
+
self.session = httpx.Client(headers=headers, timeout=timeout)
|
|
10
|
+
|
|
11
|
+
def _build_url(self, path):
|
|
12
|
+
return f"{self.base_url}/{path.lstrip('/')}"
|
|
13
|
+
|
|
14
|
+
def _request_with_retry(self, method, url, retries, retry_delay, retry_on_status, **kwargs):
|
|
15
|
+
attempt = 0
|
|
16
|
+
|
|
17
|
+
while True:
|
|
18
|
+
try:
|
|
19
|
+
response = self.session.request(method, url, **kwargs)
|
|
20
|
+
|
|
21
|
+
# If no retry rules → return immediately
|
|
22
|
+
if not retry_on_status:
|
|
23
|
+
return ResponseWrapper(response)
|
|
24
|
+
|
|
25
|
+
# If status code NOT in retry list → return
|
|
26
|
+
if response.status_code not in retry_on_status:
|
|
27
|
+
return ResponseWrapper(response)
|
|
28
|
+
|
|
29
|
+
# If retry limit exceeded → return last response
|
|
30
|
+
attempt += 1
|
|
31
|
+
if attempt > retries:
|
|
32
|
+
return ResponseWrapper(response)
|
|
33
|
+
|
|
34
|
+
time.sleep(retry_delay)
|
|
35
|
+
|
|
36
|
+
except httpx.RequestError:
|
|
37
|
+
attempt += 1
|
|
38
|
+
if attempt > retries:
|
|
39
|
+
raise
|
|
40
|
+
time.sleep(retry_delay)
|
|
41
|
+
|
|
42
|
+
# ------------------------
|
|
43
|
+
# HTTP METHODS
|
|
44
|
+
# ------------------------
|
|
45
|
+
|
|
46
|
+
def get(self, path, retries=0, retry_delay=0, retry_on_status=None, **kwargs):
|
|
47
|
+
url = self._build_url(path)
|
|
48
|
+
return self._request_with_retry("GET", url, retries, retry_delay, retry_on_status, **kwargs)
|
|
49
|
+
|
|
50
|
+
def post(self, path, json=None, retries=0, retry_delay=0, retry_on_status=None, **kwargs):
|
|
51
|
+
url = self._build_url(path)
|
|
52
|
+
return self._request_with_retry("POST", url, retries, retry_delay, retry_on_status, json=json, **kwargs)
|
|
53
|
+
|
|
54
|
+
def put(self, path, json=None, retries=0, retry_delay=0, retry_on_status=None, **kwargs):
|
|
55
|
+
url = self._build_url(path)
|
|
56
|
+
return self._request_with_retry("PUT", url, retries, retry_delay, retry_on_status, json=json, **kwargs)
|
|
57
|
+
|
|
58
|
+
def patch(self, path, json=None, retries=0, retry_delay=0, retry_on_status=None, **kwargs):
|
|
59
|
+
url = self._build_url(path)
|
|
60
|
+
return self._request_with_retry("PATCH", url, retries, retry_delay, retry_on_status, json=json, **kwargs)
|
|
61
|
+
|
|
62
|
+
def delete(self, path, retries=0, retry_delay=0, retry_on_status=None, **kwargs):
|
|
63
|
+
url = self._build_url(path)
|
|
64
|
+
return self._request_with_retry("DELETE", url, retries, retry_delay, retry_on_status, **kwargs)
|
|
65
|
+
|
|
66
|
+
def close(self):
|
|
67
|
+
self.session.close()
|
|
68
|
+
|
|
69
|
+
def __enter__(self):
|
|
70
|
+
return self
|
|
71
|
+
|
|
72
|
+
def __exit__(self, *args):
|
|
73
|
+
self.close()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
class ResponseWrapper:
|
|
2
|
+
def __init__(self, response):
|
|
3
|
+
self.response = response
|
|
4
|
+
|
|
5
|
+
@property
|
|
6
|
+
def status_code(self):
|
|
7
|
+
return self.response.status_code
|
|
8
|
+
|
|
9
|
+
def json(self):
|
|
10
|
+
return self.response.json()
|
|
11
|
+
|
|
12
|
+
def assert_status(self, expected):
|
|
13
|
+
actual = self.status_code
|
|
14
|
+
if actual != expected:
|
|
15
|
+
raise AssertionError(f"Expected {expected}, got {actual}")
|
|
16
|
+
return self
|
|
17
|
+
|
|
18
|
+
def assert_json_key(self, key: str):
|
|
19
|
+
data = self.json()
|
|
20
|
+
if not isinstance(data, dict):
|
|
21
|
+
raise AssertionError(
|
|
22
|
+
f"Expected a JSON object to check key '{key}', but got {type(data).__name__}"
|
|
23
|
+
)
|
|
24
|
+
if key not in data:
|
|
25
|
+
raise AssertionError(f"Expected key '{key}' not found in JSON response")
|
|
26
|
+
return self
|
|
27
|
+
|
|
28
|
+
def assert_json_value(self, key: str, expected_value):
|
|
29
|
+
data = self.json()
|
|
30
|
+
if not isinstance(data, dict):
|
|
31
|
+
raise AssertionError(
|
|
32
|
+
f"Expected a JSON object to check key '{key}', but got {type(data).__name__}"
|
|
33
|
+
)
|
|
34
|
+
if key not in data:
|
|
35
|
+
raise AssertionError(f"Key '{key}' not found in JSON response")
|
|
36
|
+
|
|
37
|
+
actual_value = data[key]
|
|
38
|
+
if actual_value != expected_value:
|
|
39
|
+
raise AssertionError(
|
|
40
|
+
f"Expected '{key}' to be '{expected_value}', got '{actual_value}'"
|
|
41
|
+
)
|
|
42
|
+
return self
|
|
43
|
+
|
|
44
|
+
def _resolve_json_path(self, path):
|
|
45
|
+
"""Resolve a dotted path like 'user.address.city'."""
|
|
46
|
+
parts = path.split(".")
|
|
47
|
+
current = self.json()
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
for p in parts:
|
|
51
|
+
if isinstance(current, list):
|
|
52
|
+
p = int(p) # list index
|
|
53
|
+
current = current[p]
|
|
54
|
+
return current
|
|
55
|
+
except Exception as e:
|
|
56
|
+
raise AssertionError(f"JSON path '{path}' not found: {e}")
|
|
57
|
+
|
|
58
|
+
def assert_json_path_exists(self, path):
|
|
59
|
+
_ = self._resolve_json_path(path)
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def assert_json_path_value(self, path, expected):
|
|
63
|
+
actual = self._resolve_json_path(path)
|
|
64
|
+
if actual != expected:
|
|
65
|
+
raise AssertionError(
|
|
66
|
+
f"JSON path '{path}' expected '{expected}', got '{actual}'"
|
|
67
|
+
)
|
|
68
|
+
return self
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import httpx
|
|
3
|
+
from .response_wrapper import ResponseWrapper
|
|
4
|
+
|
|
5
|
+
def _request_with_retry(
|
|
6
|
+
method,
|
|
7
|
+
url,
|
|
8
|
+
retries=0,
|
|
9
|
+
retry_delay=0,
|
|
10
|
+
retry_on_status=None,
|
|
11
|
+
timeout=None,
|
|
12
|
+
**kwargs
|
|
13
|
+
):
|
|
14
|
+
attempt = 0
|
|
15
|
+
|
|
16
|
+
while True:
|
|
17
|
+
try:
|
|
18
|
+
response = httpx.request(
|
|
19
|
+
method,
|
|
20
|
+
url,
|
|
21
|
+
timeout=timeout,
|
|
22
|
+
**kwargs
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
# If no retry rules → return immediately
|
|
26
|
+
if not retry_on_status:
|
|
27
|
+
return ResponseWrapper(response)
|
|
28
|
+
|
|
29
|
+
# If status code NOT in retry list → return
|
|
30
|
+
if response.status_code not in retry_on_status:
|
|
31
|
+
return ResponseWrapper(response)
|
|
32
|
+
|
|
33
|
+
# If retry limit exceeded → return last response
|
|
34
|
+
attempt += 1
|
|
35
|
+
if attempt > retries:
|
|
36
|
+
return ResponseWrapper(response)
|
|
37
|
+
|
|
38
|
+
# Otherwise retry
|
|
39
|
+
time.sleep(retry_delay)
|
|
40
|
+
|
|
41
|
+
except httpx.RequestError:
|
|
42
|
+
attempt += 1
|
|
43
|
+
if attempt > retries:
|
|
44
|
+
raise
|
|
45
|
+
time.sleep(retry_delay)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# --------------------------------------------------------
|
|
49
|
+
# SIMPLE MODE HTTP METHODS
|
|
50
|
+
# --------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
def get(
|
|
53
|
+
url,
|
|
54
|
+
retries=0,
|
|
55
|
+
retry_delay=0,
|
|
56
|
+
retry_on_status=None,
|
|
57
|
+
timeout=None,
|
|
58
|
+
**kwargs
|
|
59
|
+
):
|
|
60
|
+
return _request_with_retry(
|
|
61
|
+
"GET", url,
|
|
62
|
+
retries=retries,
|
|
63
|
+
retry_delay=retry_delay,
|
|
64
|
+
retry_on_status=retry_on_status,
|
|
65
|
+
timeout=timeout,
|
|
66
|
+
**kwargs
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def post(
|
|
71
|
+
url,
|
|
72
|
+
json=None,
|
|
73
|
+
retries=0,
|
|
74
|
+
retry_delay=0,
|
|
75
|
+
retry_on_status=None,
|
|
76
|
+
timeout=None,
|
|
77
|
+
**kwargs
|
|
78
|
+
):
|
|
79
|
+
return _request_with_retry(
|
|
80
|
+
"POST", url,
|
|
81
|
+
retries=retries,
|
|
82
|
+
retry_delay=retry_delay,
|
|
83
|
+
retry_on_status=retry_on_status,
|
|
84
|
+
timeout=timeout,
|
|
85
|
+
json=json,
|
|
86
|
+
**kwargs
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def put(
|
|
91
|
+
url,
|
|
92
|
+
json=None,
|
|
93
|
+
retries=0,
|
|
94
|
+
retry_delay=0,
|
|
95
|
+
retry_on_status=None,
|
|
96
|
+
timeout=None,
|
|
97
|
+
**kwargs
|
|
98
|
+
):
|
|
99
|
+
return _request_with_retry(
|
|
100
|
+
"PUT", url,
|
|
101
|
+
retries=retries,
|
|
102
|
+
retry_delay=retry_delay,
|
|
103
|
+
retry_on_status=retry_on_status,
|
|
104
|
+
timeout=timeout,
|
|
105
|
+
json=json,
|
|
106
|
+
**kwargs
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def patch(
|
|
111
|
+
url,
|
|
112
|
+
json=None,
|
|
113
|
+
retries=0,
|
|
114
|
+
retry_delay=0,
|
|
115
|
+
retry_on_status=None,
|
|
116
|
+
timeout=None,
|
|
117
|
+
**kwargs
|
|
118
|
+
):
|
|
119
|
+
return _request_with_retry(
|
|
120
|
+
"PATCH", url,
|
|
121
|
+
retries=retries,
|
|
122
|
+
retry_delay=retry_delay,
|
|
123
|
+
retry_on_status=retry_on_status,
|
|
124
|
+
timeout=timeout,
|
|
125
|
+
json=json,
|
|
126
|
+
**kwargs
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def delete(
|
|
131
|
+
url,
|
|
132
|
+
retries=0,
|
|
133
|
+
retry_delay=0,
|
|
134
|
+
retry_on_status=None,
|
|
135
|
+
timeout=None,
|
|
136
|
+
**kwargs
|
|
137
|
+
):
|
|
138
|
+
return _request_with_retry(
|
|
139
|
+
"DELETE", url,
|
|
140
|
+
retries=retries,
|
|
141
|
+
retry_delay=retry_delay,
|
|
142
|
+
retry_on_status=retry_on_status,
|
|
143
|
+
timeout=timeout,
|
|
144
|
+
**kwargs
|
|
145
|
+
)
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apitestgenie
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight, developer-friendly Python library for API test automation
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 Akash Gujarathi
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Requires-Python: >=3.11
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Requires-Dist: httpx>=0.27
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: pytest>=9.0; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# APItestGenie
|
|
36
|
+
|
|
37
|
+
APItestGenie is a lightweight, developer-friendly Python library designed to simplify API testing for automation engineers.
|
|
38
|
+
Version 1.0 focuses on clarity, correctness, and essential functionality without unnecessary complexity.
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Overview
|
|
43
|
+
|
|
44
|
+
APItestGenie provides:
|
|
45
|
+
|
|
46
|
+
- Two ways to perform API requests: Client mode and Simple mode
|
|
47
|
+
- Built-in JSON assertions
|
|
48
|
+
- JSON path assertions for nested responses
|
|
49
|
+
- Basic retry logic with retries, retry delay, and retry_on_status
|
|
50
|
+
- A ResponseWrapper abstraction for consistent behavior across requests
|
|
51
|
+
- GET, POST, PUT, PATCH, DELETE support
|
|
52
|
+
- Timeout and headers support
|
|
53
|
+
- A complete pytest suite
|
|
54
|
+
- A modern Python packaging layout using the src structure
|
|
55
|
+
|
|
56
|
+
Version 1.0 intentionally avoids heavy features like logging frameworks, async support, or automation framework integrations.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
Clone the repository:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
git clone https://github.com/Akash402/apitestgenie.git
|
|
66
|
+
cd apitestgenie
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Create and activate a virtual environment:
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
python3 -m venv venv
|
|
73
|
+
source venv/bin/activate
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
Install dependencies:
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
pip install httpx pytest
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
(Optional) Install the library locally in editable mode:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
pip install -e .
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Usage Examples
|
|
91
|
+
|
|
92
|
+
### Client Mode
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from apitestgenie.client import ApiClient
|
|
96
|
+
|
|
97
|
+
api = ApiClient("https://jsonplaceholder.typicode.com", timeout=10)
|
|
98
|
+
|
|
99
|
+
response = api.get("/posts/1", retries=2)
|
|
100
|
+
response.assert_status(200)
|
|
101
|
+
response.assert_json_value("id", 1)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
POST example:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
resp = api.post("/posts", json={"title": "foo"})
|
|
108
|
+
resp.assert_status(201)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
PUT example:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
resp = api.put("/posts/1", json={"id": 1, "title": "updated"})
|
|
115
|
+
resp.assert_status(200)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
DELETE example:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
resp = api.delete("/posts/1")
|
|
122
|
+
resp.assert_status(200)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### Simple Mode
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from apitestgenie.simple import get
|
|
131
|
+
|
|
132
|
+
response = get("https://jsonplaceholder.typicode.com/posts/1")
|
|
133
|
+
response.assert_status(200)
|
|
134
|
+
print(response.json())
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
POST example:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from apitestgenie.simple import post
|
|
141
|
+
|
|
142
|
+
resp = post("https://jsonplaceholder.typicode.com/posts", json={"hello": "world"})
|
|
143
|
+
resp.assert_status(201)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
Retry and timeout example:
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
resp = get(
|
|
150
|
+
"https://jsonplaceholder.typicode.com/posts/1",
|
|
151
|
+
retries=3,
|
|
152
|
+
retry_delay=1,
|
|
153
|
+
retry_on_status=[500],
|
|
154
|
+
timeout=5
|
|
155
|
+
)
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## JSON Assertions
|
|
161
|
+
|
|
162
|
+
```python
|
|
163
|
+
resp.assert_status(200)
|
|
164
|
+
resp.assert_json_key("id")
|
|
165
|
+
resp.assert_json_value("id", 1)
|
|
166
|
+
resp.assert_json_path_exists("title")
|
|
167
|
+
resp.assert_json_path_value("id", 1)
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Running Tests
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
pytest
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Project Structure
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
apitestgenie/
|
|
184
|
+
│
|
|
185
|
+
├── src/
|
|
186
|
+
│ └── apitestgenie/
|
|
187
|
+
│ ├── client.py
|
|
188
|
+
│ ├── simple.py
|
|
189
|
+
│ ├── response_wrapper.py
|
|
190
|
+
│ └── __init__.py
|
|
191
|
+
│
|
|
192
|
+
├── tests/
|
|
193
|
+
├── playground.py
|
|
194
|
+
├── pytest.ini
|
|
195
|
+
├── README.md
|
|
196
|
+
└── SCOPE.md
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Version 1.0 Scope Summary
|
|
202
|
+
|
|
203
|
+
Included:
|
|
204
|
+
|
|
205
|
+
- CRUD operations
|
|
206
|
+
- Basic retry logic
|
|
207
|
+
- JSON and JSON path assertions
|
|
208
|
+
- ResponseWrapper abstraction
|
|
209
|
+
- Simple and client modes
|
|
210
|
+
- Header and timeout support
|
|
211
|
+
- Test suite
|
|
212
|
+
- Clean structure
|
|
213
|
+
|
|
214
|
+
Excluded:
|
|
215
|
+
|
|
216
|
+
- Advanced retry logic
|
|
217
|
+
- Logging framework
|
|
218
|
+
- Robot/Behave integrations
|
|
219
|
+
- Async support
|
|
220
|
+
- Schema validation
|
|
221
|
+
- Plugin system
|
|
222
|
+
|
|
223
|
+
---
|
|
224
|
+
|
|
225
|
+
## License
|
|
226
|
+
|
|
227
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/apitestgenie/__init__.py
|
|
5
|
+
src/apitestgenie/client.py
|
|
6
|
+
src/apitestgenie/response_wrapper.py
|
|
7
|
+
src/apitestgenie/simple.py
|
|
8
|
+
src/apitestgenie.egg-info/PKG-INFO
|
|
9
|
+
src/apitestgenie.egg-info/SOURCES.txt
|
|
10
|
+
src/apitestgenie.egg-info/dependency_links.txt
|
|
11
|
+
src/apitestgenie.egg-info/requires.txt
|
|
12
|
+
src/apitestgenie.egg-info/top_level.txt
|
|
13
|
+
tests/test_client.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
apitestgenie
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import pytest
|
|
3
|
+
from src.apitestgenie.client import ApiClient
|
|
4
|
+
from src.apitestgenie.simple import get, post, put, patch, delete
|
|
5
|
+
from src.apitestgenie.response_wrapper import ResponseWrapper
|
|
6
|
+
|
|
7
|
+
BASE_URL = "https://jsonplaceholder.typicode.com"
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------
|
|
10
|
+
# GET TESTS
|
|
11
|
+
# ---------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
def test_client_get():
|
|
14
|
+
api = ApiClient(BASE_URL)
|
|
15
|
+
resp = api.get("/posts/1")
|
|
16
|
+
resp.assert_status(200)
|
|
17
|
+
resp.assert_json_key("id")
|
|
18
|
+
resp.assert_json_value("id", 1)
|
|
19
|
+
|
|
20
|
+
def test_simple_get():
|
|
21
|
+
resp = get(f"{BASE_URL}/posts/1")
|
|
22
|
+
resp.assert_status(200)
|
|
23
|
+
resp.assert_json_key("id")
|
|
24
|
+
resp.assert_json_value("id", 1)
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------
|
|
27
|
+
# POST TESTS
|
|
28
|
+
# ---------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
def test_client_post():
|
|
31
|
+
api = ApiClient(BASE_URL)
|
|
32
|
+
resp = api.post("/posts", json={"title": "foo", "body": "bar", "userId": 1})
|
|
33
|
+
resp.assert_status(201)
|
|
34
|
+
resp.assert_json_key("id")
|
|
35
|
+
|
|
36
|
+
def test_simple_post():
|
|
37
|
+
resp = post(f"{BASE_URL}/posts", json={"title": "foo"})
|
|
38
|
+
resp.assert_status(201)
|
|
39
|
+
resp.assert_json_key("id")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------
|
|
43
|
+
# PUT TESTS
|
|
44
|
+
# ---------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
def test_client_put():
|
|
47
|
+
api = ApiClient(BASE_URL)
|
|
48
|
+
resp = api.put("/posts/1", json={"id": 1, "title": "updated"})
|
|
49
|
+
resp.assert_status(200)
|
|
50
|
+
resp.assert_json_key("id")
|
|
51
|
+
resp.assert_json_value("id", 1)
|
|
52
|
+
|
|
53
|
+
def test_simple_put():
|
|
54
|
+
resp = put(f"{BASE_URL}/posts/1", json={"id": 1, "title": "updated"})
|
|
55
|
+
resp.assert_status(200)
|
|
56
|
+
resp.assert_json_key("id")
|
|
57
|
+
resp.assert_json_value("id", 1)
|
|
58
|
+
|
|
59
|
+
# ---------------------------------------------------------
|
|
60
|
+
# PATCH TESTS
|
|
61
|
+
# ---------------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def test_client_patch():
|
|
64
|
+
api = ApiClient(BASE_URL)
|
|
65
|
+
resp = api.patch("/posts/1", json={"title": "patched"})
|
|
66
|
+
resp.assert_status(200)
|
|
67
|
+
resp.assert_json_key("id")
|
|
68
|
+
resp.assert_json_value("id", 1)
|
|
69
|
+
|
|
70
|
+
def test_simple_patch():
|
|
71
|
+
resp = patch(f"{BASE_URL}/posts/1", json={"title": "patched"})
|
|
72
|
+
resp.assert_status(200)
|
|
73
|
+
resp.assert_json_key("id")
|
|
74
|
+
resp.assert_json_value("id", 1)
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------
|
|
77
|
+
# DELETE TESTS
|
|
78
|
+
# ---------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
def test_client_delete():
|
|
81
|
+
api = ApiClient(BASE_URL)
|
|
82
|
+
resp = api.delete("/posts/1")
|
|
83
|
+
resp.assert_status(200)
|
|
84
|
+
|
|
85
|
+
def test_simple_delete():
|
|
86
|
+
resp = delete(f"{BASE_URL}/posts/1")
|
|
87
|
+
resp.assert_status(200)
|
|
88
|
+
|
|
89
|
+
# ---------------------------------------------------------
|
|
90
|
+
# SIMPLE MODE TIMEOUT TESTS
|
|
91
|
+
# ---------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
def test_simple_mode_timeout():
|
|
94
|
+
resp = get(f"{BASE_URL}/posts/1", timeout=5)
|
|
95
|
+
resp.assert_status(200)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------
|
|
99
|
+
# OFFLINE UNIT TESTS (no network required)
|
|
100
|
+
# ---------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def _make_mock_response(status_code, json_body):
|
|
103
|
+
"""Build a ResponseWrapper from a mock httpx response."""
|
|
104
|
+
import json as _json
|
|
105
|
+
body = _json.dumps(json_body).encode()
|
|
106
|
+
mock = httpx.Response(status_code, content=body, headers={"content-type": "application/json"})
|
|
107
|
+
return ResponseWrapper(mock)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def test_assert_status_passes():
|
|
111
|
+
resp = _make_mock_response(200, {"id": 1})
|
|
112
|
+
resp.assert_status(200)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_assert_status_fails():
|
|
116
|
+
resp = _make_mock_response(404, {"error": "not found"})
|
|
117
|
+
with pytest.raises(AssertionError, match="Expected 200, got 404"):
|
|
118
|
+
resp.assert_status(200)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_assert_json_key_passes():
|
|
122
|
+
resp = _make_mock_response(200, {"id": 1, "title": "foo"})
|
|
123
|
+
resp.assert_json_key("id")
|
|
124
|
+
resp.assert_json_key("title")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_assert_json_key_missing():
|
|
128
|
+
resp = _make_mock_response(200, {"id": 1})
|
|
129
|
+
with pytest.raises(AssertionError, match="not found"):
|
|
130
|
+
resp.assert_json_key("missing")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_assert_json_key_on_list_response():
|
|
134
|
+
resp = _make_mock_response(200, [{"id": 1}, {"id": 2}])
|
|
135
|
+
with pytest.raises(AssertionError, match="Expected a JSON object"):
|
|
136
|
+
resp.assert_json_key("id")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_assert_json_value_passes():
|
|
140
|
+
resp = _make_mock_response(200, {"id": 42})
|
|
141
|
+
resp.assert_json_value("id", 42)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_assert_json_value_fails():
|
|
145
|
+
resp = _make_mock_response(200, {"id": 42})
|
|
146
|
+
with pytest.raises(AssertionError, match="Expected 'id' to be '1', got '42'"):
|
|
147
|
+
resp.assert_json_value("id", 1)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_assert_json_path_exists():
|
|
151
|
+
resp = _make_mock_response(200, {"user": {"address": {"city": "London"}}})
|
|
152
|
+
resp.assert_json_path_exists("user.address.city")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_assert_json_path_value():
|
|
156
|
+
resp = _make_mock_response(200, {"user": {"address": {"city": "London"}}})
|
|
157
|
+
resp.assert_json_path_value("user.address.city", "London")
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_assert_json_path_missing():
|
|
161
|
+
resp = _make_mock_response(200, {"user": {}})
|
|
162
|
+
with pytest.raises(AssertionError, match="not found"):
|
|
163
|
+
resp.assert_json_path_exists("user.address.city")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_chained_assertions():
|
|
167
|
+
resp = _make_mock_response(200, {"id": 1, "title": "foo"})
|
|
168
|
+
resp.assert_status(200).assert_json_key("id").assert_json_value("id", 1)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_api_client_context_manager():
|
|
172
|
+
with ApiClient("https://example.com") as api:
|
|
173
|
+
assert api is not None
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def test_response_wrapper_status_code():
|
|
177
|
+
resp = _make_mock_response(201, {})
|
|
178
|
+
assert resp.status_code == 201
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_response_wrapper_json():
|
|
182
|
+
resp = _make_mock_response(200, {"key": "value"})
|
|
183
|
+
assert resp.json() == {"key": "value"}
|