api-forge-cli 1.0.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.
- api_forge/__init__.py +3 -0
- api_forge/__main__.py +6 -0
- api_forge/cli.py +277 -0
- api_forge/client.py +197 -0
- api_forge/mock_server.py +190 -0
- api_forge/models.py +296 -0
- api_forge/output.py +190 -0
- api_forge/parser.py +291 -0
- api_forge_cli-1.0.0.dist-info/METADATA +393 -0
- api_forge_cli-1.0.0.dist-info/RECORD +13 -0
- api_forge_cli-1.0.0.dist-info/WHEEL +4 -0
- api_forge_cli-1.0.0.dist-info/entry_points.txt +2 -0
- api_forge_cli-1.0.0.dist-info/licenses/LICENSE +21 -0
api_forge/parser.py
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"""YAML test suite parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from .models import (
|
|
10
|
+
Assertion,
|
|
11
|
+
AssertionType,
|
|
12
|
+
HttpMethod,
|
|
13
|
+
LoadTestConfig,
|
|
14
|
+
MockEndpoint,
|
|
15
|
+
MockServerConfig,
|
|
16
|
+
RequestConfig,
|
|
17
|
+
TestSuite,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_test_suite(file_path: str | Path) -> TestSuite:
|
|
22
|
+
"""Parse a YAML test suite file."""
|
|
23
|
+
path = Path(file_path)
|
|
24
|
+
if not path.exists():
|
|
25
|
+
raise FileNotFoundError(f"Test suite file not found: {path}")
|
|
26
|
+
|
|
27
|
+
with open(path, encoding="utf-8") as f:
|
|
28
|
+
data = yaml.safe_load(f)
|
|
29
|
+
|
|
30
|
+
if not isinstance(data, dict):
|
|
31
|
+
raise ValueError("Invalid test suite format: expected YAML object")
|
|
32
|
+
|
|
33
|
+
suite = TestSuite(
|
|
34
|
+
name=data.get("name", path.stem),
|
|
35
|
+
base_url=data.get("base_url", ""),
|
|
36
|
+
variables=data.get("variables", {}),
|
|
37
|
+
headers=data.get("headers", {}),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
for req_data in data.get("requests", []):
|
|
41
|
+
suite.requests.append(_parse_request(req_data))
|
|
42
|
+
|
|
43
|
+
return suite
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_request(data: dict) -> RequestConfig:
|
|
47
|
+
"""Parse a single request configuration."""
|
|
48
|
+
method_str = data.get("method", "GET").upper()
|
|
49
|
+
try:
|
|
50
|
+
method = HttpMethod(method_str)
|
|
51
|
+
except ValueError:
|
|
52
|
+
method = HttpMethod.GET
|
|
53
|
+
|
|
54
|
+
config = RequestConfig(
|
|
55
|
+
name=data.get("name", "Unnamed Request"),
|
|
56
|
+
url=data.get("url", ""),
|
|
57
|
+
method=method,
|
|
58
|
+
headers=data.get("headers", {}),
|
|
59
|
+
body=data.get("body"),
|
|
60
|
+
timeout=data.get("timeout", 30.0),
|
|
61
|
+
variables=data.get("variables", {}),
|
|
62
|
+
depends_on=data.get("depends_on"),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Parse assertions
|
|
66
|
+
for assertion_data in data.get("assertions", []):
|
|
67
|
+
config.assertions.append(_parse_assertion(assertion_data))
|
|
68
|
+
|
|
69
|
+
return config
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _parse_assertion(data: dict) -> Assertion:
|
|
73
|
+
"""Parse a single assertion."""
|
|
74
|
+
type_str = data.get("type", "status").lower()
|
|
75
|
+
try:
|
|
76
|
+
assertion_type = AssertionType(type_str)
|
|
77
|
+
except ValueError:
|
|
78
|
+
assertion_type = AssertionType.STATUS
|
|
79
|
+
|
|
80
|
+
return Assertion(
|
|
81
|
+
type=assertion_type,
|
|
82
|
+
expected=data.get("expected", data.get("value")),
|
|
83
|
+
path=data.get("path", data.get("header")),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def parse_load_test_config(file_path: str | Path) -> LoadTestConfig:
|
|
88
|
+
"""Parse a YAML load test configuration file."""
|
|
89
|
+
path = Path(file_path)
|
|
90
|
+
if not path.exists():
|
|
91
|
+
raise FileNotFoundError(f"Load test config file not found: {path}")
|
|
92
|
+
|
|
93
|
+
with open(path, encoding="utf-8") as f:
|
|
94
|
+
data = yaml.safe_load(f)
|
|
95
|
+
|
|
96
|
+
method_str = data.get("method", "GET").upper()
|
|
97
|
+
try:
|
|
98
|
+
method = HttpMethod(method_str)
|
|
99
|
+
except ValueError:
|
|
100
|
+
method = HttpMethod.GET
|
|
101
|
+
|
|
102
|
+
return LoadTestConfig(
|
|
103
|
+
url=data.get("url", ""),
|
|
104
|
+
method=method,
|
|
105
|
+
headers=data.get("headers", {}),
|
|
106
|
+
body=data.get("body"),
|
|
107
|
+
concurrency=data.get("concurrency", 10),
|
|
108
|
+
requests_per_second=data.get("requests_per_second", 0),
|
|
109
|
+
duration_seconds=data.get("duration_seconds", 10),
|
|
110
|
+
timeout=data.get("timeout", 30.0),
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def parse_mock_config(file_path: str | Path) -> MockServerConfig:
|
|
115
|
+
"""Parse a YAML mock server configuration file."""
|
|
116
|
+
path = Path(file_path)
|
|
117
|
+
if not path.exists():
|
|
118
|
+
raise FileNotFoundError(f"Mock config file not found: {path}")
|
|
119
|
+
|
|
120
|
+
with open(path, encoding="utf-8") as f:
|
|
121
|
+
data = yaml.safe_load(f)
|
|
122
|
+
|
|
123
|
+
config = MockServerConfig(
|
|
124
|
+
name=data.get("name", "api-forge-mock"),
|
|
125
|
+
host=data.get("host", "127.0.0.1"),
|
|
126
|
+
port=data.get("port", 8000),
|
|
127
|
+
default_status=data.get("default_status", 404),
|
|
128
|
+
default_body=data.get("default_body", '{"error": "Not Found"}'),
|
|
129
|
+
cors=data.get("cors", True),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
for ep_data in data.get("endpoints", []):
|
|
133
|
+
config.endpoints.append(_parse_mock_endpoint(ep_data))
|
|
134
|
+
|
|
135
|
+
return config
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _parse_mock_endpoint(data: dict) -> MockEndpoint:
|
|
139
|
+
"""Parse a single mock endpoint."""
|
|
140
|
+
method_str = data.get("method", "GET").upper()
|
|
141
|
+
try:
|
|
142
|
+
method = HttpMethod(method_str)
|
|
143
|
+
except ValueError:
|
|
144
|
+
method = HttpMethod.GET
|
|
145
|
+
|
|
146
|
+
body = data.get("body", "")
|
|
147
|
+
if isinstance(body, dict):
|
|
148
|
+
import json
|
|
149
|
+
|
|
150
|
+
body = json.dumps(body)
|
|
151
|
+
|
|
152
|
+
return MockEndpoint(
|
|
153
|
+
path=data.get("path", "/"),
|
|
154
|
+
method=method,
|
|
155
|
+
status_code=data.get("status", 200),
|
|
156
|
+
headers=data.get("headers", {}),
|
|
157
|
+
body=body,
|
|
158
|
+
delay_ms=data.get("delay_ms", 0),
|
|
159
|
+
match_headers=data.get("match_headers", {}),
|
|
160
|
+
match_body=data.get("match_body"),
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def generate_example_suite() -> str:
|
|
165
|
+
"""Generate an example test suite YAML."""
|
|
166
|
+
return """# API Test Suite Example
|
|
167
|
+
name: "User API Tests"
|
|
168
|
+
base_url: "https://jsonplaceholder.typicode.com"
|
|
169
|
+
|
|
170
|
+
# Global headers applied to all requests
|
|
171
|
+
headers:
|
|
172
|
+
Content-Type: "application/json"
|
|
173
|
+
Accept: "application/json"
|
|
174
|
+
|
|
175
|
+
# Global variables
|
|
176
|
+
variables:
|
|
177
|
+
api_version: "v1"
|
|
178
|
+
|
|
179
|
+
requests:
|
|
180
|
+
- name: "Get all users"
|
|
181
|
+
url: "/users"
|
|
182
|
+
method: GET
|
|
183
|
+
assertions:
|
|
184
|
+
- type: status
|
|
185
|
+
expected: 200
|
|
186
|
+
- type: response_time
|
|
187
|
+
expected: 2000
|
|
188
|
+
- type: body_contains
|
|
189
|
+
expected: '"email"'
|
|
190
|
+
|
|
191
|
+
- name: "Get single user"
|
|
192
|
+
url: "/users/1"
|
|
193
|
+
method: GET
|
|
194
|
+
assertions:
|
|
195
|
+
- type: status
|
|
196
|
+
expected: 200
|
|
197
|
+
- type: json_path
|
|
198
|
+
path: "id"
|
|
199
|
+
expected: 1
|
|
200
|
+
variables:
|
|
201
|
+
user_name: "name" # Extract name for next request
|
|
202
|
+
|
|
203
|
+
- name: "Create post"
|
|
204
|
+
url: "/posts"
|
|
205
|
+
method: POST
|
|
206
|
+
headers:
|
|
207
|
+
X-Custom-Header: "test"
|
|
208
|
+
body:
|
|
209
|
+
title: "Test Post"
|
|
210
|
+
body: "This is a test"
|
|
211
|
+
userId: 1
|
|
212
|
+
assertions:
|
|
213
|
+
- type: status
|
|
214
|
+
expected: 201
|
|
215
|
+
- type: json_path
|
|
216
|
+
path: "id"
|
|
217
|
+
expected: 101
|
|
218
|
+
"""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def generate_example_load_test() -> str:
|
|
222
|
+
"""Generate an example load test YAML."""
|
|
223
|
+
return """# Load Test Configuration
|
|
224
|
+
url: "https://jsonplaceholder.typicode.com/posts/1"
|
|
225
|
+
method: GET
|
|
226
|
+
|
|
227
|
+
# Concurrency settings
|
|
228
|
+
concurrency: 50
|
|
229
|
+
requests_per_second: 100 # 0 = unlimited
|
|
230
|
+
duration_seconds: 30
|
|
231
|
+
|
|
232
|
+
# Request configuration
|
|
233
|
+
timeout: 10.0
|
|
234
|
+
headers:
|
|
235
|
+
Accept: "application/json"
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def generate_example_mock_config() -> str:
|
|
240
|
+
"""Generate an example mock server YAML."""
|
|
241
|
+
return """# Mock Server Configuration
|
|
242
|
+
name: "Test API Mock"
|
|
243
|
+
host: "127.0.0.1"
|
|
244
|
+
port: 8000
|
|
245
|
+
cors: true
|
|
246
|
+
|
|
247
|
+
# Default response for unmatched routes
|
|
248
|
+
default_status: 404
|
|
249
|
+
default_body: '{"error": "Not Found"}'
|
|
250
|
+
|
|
251
|
+
endpoints:
|
|
252
|
+
- path: "/health"
|
|
253
|
+
method: GET
|
|
254
|
+
status: 200
|
|
255
|
+
body:
|
|
256
|
+
status: "healthy"
|
|
257
|
+
version: "1.0.0"
|
|
258
|
+
|
|
259
|
+
- path: "/users"
|
|
260
|
+
method: GET
|
|
261
|
+
status: 200
|
|
262
|
+
headers:
|
|
263
|
+
X-Total-Count: "100"
|
|
264
|
+
body:
|
|
265
|
+
- id: 1
|
|
266
|
+
name: "Alice"
|
|
267
|
+
email: "alice@example.com"
|
|
268
|
+
- id: 2
|
|
269
|
+
name: "Bob"
|
|
270
|
+
email: "bob@example.com"
|
|
271
|
+
|
|
272
|
+
- path: "/users"
|
|
273
|
+
method: POST
|
|
274
|
+
status: 201
|
|
275
|
+
body:
|
|
276
|
+
id: 3
|
|
277
|
+
message: "User created"
|
|
278
|
+
|
|
279
|
+
- path: "/slow"
|
|
280
|
+
method: GET
|
|
281
|
+
status: 200
|
|
282
|
+
delay_ms: 2000 # Simulate slow response
|
|
283
|
+
body:
|
|
284
|
+
message: "This took a while"
|
|
285
|
+
|
|
286
|
+
- path: "/error"
|
|
287
|
+
method: GET
|
|
288
|
+
status: 500
|
|
289
|
+
body:
|
|
290
|
+
error: "Internal Server Error"
|
|
291
|
+
"""
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: api-forge-cli
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: CLI tool for API testing, load testing, and mock server generation
|
|
5
|
+
Project-URL: Homepage, https://github.com/SanjaySundarMurthy/api-forge
|
|
6
|
+
Project-URL: Repository, https://github.com/SanjaySundarMurthy/api-forge
|
|
7
|
+
Project-URL: Issues, https://github.com/SanjaySundarMurthy/api-forge/issues
|
|
8
|
+
Author-email: Sanjay Sundar Murthy <sanjaysundarmurthy@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: api,cli,http,load-testing,mock,rest,testing
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Topic :: Internet :: WWW/HTTP
|
|
22
|
+
Classifier: Topic :: Software Development :: Testing
|
|
23
|
+
Requires-Python: >=3.9
|
|
24
|
+
Requires-Dist: click>=8.0
|
|
25
|
+
Requires-Dist: httpx>=0.27
|
|
26
|
+
Requires-Dist: pyyaml>=6.0
|
|
27
|
+
Requires-Dist: rich>=13.0
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
32
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
34
|
+
Description-Content-Type: text/markdown
|
|
35
|
+
|
|
36
|
+
# api-forge
|
|
37
|
+
|
|
38
|
+
**CLI tool for API testing, load testing, and mock server generation.**
|
|
39
|
+
|
|
40
|
+
[](https://pypi.org/project/api-forge-cli/)
|
|
41
|
+
[](https://www.python.org/downloads/)
|
|
42
|
+
[](LICENSE)
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Why api-forge?
|
|
47
|
+
|
|
48
|
+
Testing APIs shouldn't require heavy frameworks or complex setup. **api-forge** is a lightweight CLI tool that provides:
|
|
49
|
+
|
|
50
|
+
- **Test Suites** — Define API tests in simple YAML files
|
|
51
|
+
- **Assertions** — Validate status codes, headers, JSON paths, response times
|
|
52
|
+
- **Variable Chaining** — Extract values from responses for use in subsequent requests
|
|
53
|
+
- **Load Testing** — Stress test endpoints with configurable concurrency
|
|
54
|
+
- **Mock Server** — Spin up mock APIs from YAML for development/testing
|
|
55
|
+
- **Quick Requests** — cURL-like commands with pretty output
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install api-forge-cli
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## Quick Start
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Make a quick GET request
|
|
71
|
+
api-forge get https://jsonplaceholder.typicode.com/posts/1
|
|
72
|
+
|
|
73
|
+
# Run a test suite
|
|
74
|
+
api-forge test suite.yaml
|
|
75
|
+
|
|
76
|
+
# Load test an endpoint
|
|
77
|
+
api-forge load https://api.example.com/health -n 50 -d 10
|
|
78
|
+
|
|
79
|
+
# Start a mock server
|
|
80
|
+
api-forge mock mock.yaml
|
|
81
|
+
|
|
82
|
+
# Generate example configs
|
|
83
|
+
api-forge init --type test -o suite.yaml
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Commands
|
|
89
|
+
|
|
90
|
+
### `get` / `post` / `put` / `delete` — Quick Requests
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# GET request
|
|
94
|
+
api-forge get https://api.example.com/users
|
|
95
|
+
|
|
96
|
+
# POST with JSON body
|
|
97
|
+
api-forge post https://api.example.com/users -d '{"name": "Alice"}'
|
|
98
|
+
|
|
99
|
+
# With custom headers
|
|
100
|
+
api-forge get https://api.example.com/users -H "Authorization:Bearer token"
|
|
101
|
+
|
|
102
|
+
# PUT request
|
|
103
|
+
api-forge put https://api.example.com/users/1 -d '{"name": "Bob"}'
|
|
104
|
+
|
|
105
|
+
# DELETE request
|
|
106
|
+
api-forge delete https://api.example.com/users/1
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### `test` — Run API Test Suites
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
api-forge test suite.yaml
|
|
113
|
+
api-forge test tests/api-tests.yaml -v
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Test suites are defined in YAML:
|
|
117
|
+
|
|
118
|
+
```yaml
|
|
119
|
+
name: "User API Tests"
|
|
120
|
+
base_url: "https://api.example.com"
|
|
121
|
+
|
|
122
|
+
headers:
|
|
123
|
+
Authorization: "Bearer {{token}}"
|
|
124
|
+
|
|
125
|
+
variables:
|
|
126
|
+
token: "your-api-token"
|
|
127
|
+
|
|
128
|
+
requests:
|
|
129
|
+
- name: "Get all users"
|
|
130
|
+
url: "/users"
|
|
131
|
+
method: GET
|
|
132
|
+
assertions:
|
|
133
|
+
- type: status
|
|
134
|
+
expected: 200
|
|
135
|
+
- type: response_time
|
|
136
|
+
expected: 2000
|
|
137
|
+
|
|
138
|
+
- name: "Create user"
|
|
139
|
+
url: "/users"
|
|
140
|
+
method: POST
|
|
141
|
+
body:
|
|
142
|
+
name: "Alice"
|
|
143
|
+
email: "alice@example.com"
|
|
144
|
+
assertions:
|
|
145
|
+
- type: status
|
|
146
|
+
expected: 201
|
|
147
|
+
- type: json_path
|
|
148
|
+
path: "id"
|
|
149
|
+
expected: 1
|
|
150
|
+
variables:
|
|
151
|
+
new_user_id: "id" # Extract for next request
|
|
152
|
+
|
|
153
|
+
- name: "Get created user"
|
|
154
|
+
url: "/users/{{new_user_id}}"
|
|
155
|
+
method: GET
|
|
156
|
+
assertions:
|
|
157
|
+
- type: status
|
|
158
|
+
expected: 200
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### `load` — Load Testing
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
# Quick load test
|
|
165
|
+
api-forge load https://api.example.com/health -n 50 -d 30
|
|
166
|
+
|
|
167
|
+
# From config file
|
|
168
|
+
api-forge load -c loadtest.yaml
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
| Option | Description |
|
|
172
|
+
|--------|-------------|
|
|
173
|
+
| `-n, --concurrency` | Number of concurrent connections (default: 10) |
|
|
174
|
+
| `-d, --duration` | Test duration in seconds (default: 10) |
|
|
175
|
+
| `--rps` | Requests per second limit (0 = unlimited) |
|
|
176
|
+
| `-c, --config` | Load test config YAML file |
|
|
177
|
+
|
|
178
|
+
Load test config:
|
|
179
|
+
|
|
180
|
+
```yaml
|
|
181
|
+
url: "https://api.example.com/users"
|
|
182
|
+
method: GET
|
|
183
|
+
concurrency: 100
|
|
184
|
+
duration_seconds: 60
|
|
185
|
+
requests_per_second: 500
|
|
186
|
+
timeout: 10.0
|
|
187
|
+
headers:
|
|
188
|
+
Authorization: "Bearer token"
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**Output includes:**
|
|
192
|
+
- Total requests, success/failure counts
|
|
193
|
+
- Requests per second achieved
|
|
194
|
+
- Latency percentiles (min, avg, p50, p95, p99, max)
|
|
195
|
+
- Status code distribution
|
|
196
|
+
- Error samples
|
|
197
|
+
|
|
198
|
+
### `mock` — Mock Server
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
# Start mock server from config
|
|
202
|
+
api-forge mock mock.yaml
|
|
203
|
+
|
|
204
|
+
# Custom port
|
|
205
|
+
api-forge mock mock.yaml -p 3000
|
|
206
|
+
|
|
207
|
+
# Default server (returns 404 for all)
|
|
208
|
+
api-forge mock
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Mock server config:
|
|
212
|
+
|
|
213
|
+
```yaml
|
|
214
|
+
name: "Test API"
|
|
215
|
+
port: 8000
|
|
216
|
+
cors: true
|
|
217
|
+
|
|
218
|
+
endpoints:
|
|
219
|
+
- path: "/health"
|
|
220
|
+
method: GET
|
|
221
|
+
status: 200
|
|
222
|
+
body:
|
|
223
|
+
status: "healthy"
|
|
224
|
+
|
|
225
|
+
- path: "/users"
|
|
226
|
+
method: GET
|
|
227
|
+
status: 200
|
|
228
|
+
headers:
|
|
229
|
+
X-Total-Count: "100"
|
|
230
|
+
body:
|
|
231
|
+
- id: 1
|
|
232
|
+
name: "Alice"
|
|
233
|
+
- id: 2
|
|
234
|
+
name: "Bob"
|
|
235
|
+
|
|
236
|
+
- path: "/users"
|
|
237
|
+
method: POST
|
|
238
|
+
status: 201
|
|
239
|
+
body:
|
|
240
|
+
id: 3
|
|
241
|
+
message: "Created"
|
|
242
|
+
|
|
243
|
+
- path: "/slow"
|
|
244
|
+
method: GET
|
|
245
|
+
status: 200
|
|
246
|
+
delay_ms: 2000 # Simulate latency
|
|
247
|
+
body:
|
|
248
|
+
message: "Slow response"
|
|
249
|
+
|
|
250
|
+
- path: "/users/*" # Wildcard matching
|
|
251
|
+
method: GET
|
|
252
|
+
status: 200
|
|
253
|
+
body:
|
|
254
|
+
id: 1
|
|
255
|
+
name: "User"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### `init` — Generate Examples
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
api-forge init --type test -o suite.yaml
|
|
262
|
+
api-forge init --type load -o loadtest.yaml
|
|
263
|
+
api-forge init --type mock -o mock.yaml
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
## Assertion Types
|
|
269
|
+
|
|
270
|
+
| Type | Description | Example |
|
|
271
|
+
|------|-------------|---------|
|
|
272
|
+
| `status` | HTTP status code | `expected: 200` or `expected: [200, 201]` |
|
|
273
|
+
| `header` | Header value contains | `header: Content-Type`, `expected: json` |
|
|
274
|
+
| `body_contains` | Body contains string | `expected: "success"` |
|
|
275
|
+
| `json_path` | JSON path equals value | `path: user.id`, `expected: 123` |
|
|
276
|
+
| `body_regex` | Body matches regex | `expected: "\\d{5}"` |
|
|
277
|
+
| `response_time` | Max response time (ms) | `expected: 500` |
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Variable Substitution
|
|
282
|
+
|
|
283
|
+
Use `{{variable_name}}` syntax in URLs, headers, and body:
|
|
284
|
+
|
|
285
|
+
```yaml
|
|
286
|
+
variables:
|
|
287
|
+
api_key: "secret123"
|
|
288
|
+
user_id: "456"
|
|
289
|
+
|
|
290
|
+
requests:
|
|
291
|
+
- name: "Get user"
|
|
292
|
+
url: "/users/{{user_id}}"
|
|
293
|
+
headers:
|
|
294
|
+
X-API-Key: "{{api_key}}"
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Extract variables from responses** for request chaining:
|
|
298
|
+
|
|
299
|
+
```yaml
|
|
300
|
+
requests:
|
|
301
|
+
- name: "Login"
|
|
302
|
+
url: "/auth/login"
|
|
303
|
+
method: POST
|
|
304
|
+
body:
|
|
305
|
+
username: "admin"
|
|
306
|
+
password: "secret"
|
|
307
|
+
variables:
|
|
308
|
+
auth_token: "token" # Extract $.token from response
|
|
309
|
+
|
|
310
|
+
- name: "Protected endpoint"
|
|
311
|
+
url: "/protected"
|
|
312
|
+
headers:
|
|
313
|
+
Authorization: "Bearer {{auth_token}}"
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
## Examples
|
|
319
|
+
|
|
320
|
+
### Full Test Suite with Chaining
|
|
321
|
+
|
|
322
|
+
```yaml
|
|
323
|
+
name: "E-commerce API"
|
|
324
|
+
base_url: "https://api.shop.example.com"
|
|
325
|
+
|
|
326
|
+
requests:
|
|
327
|
+
- name: "Create product"
|
|
328
|
+
url: "/products"
|
|
329
|
+
method: POST
|
|
330
|
+
body:
|
|
331
|
+
name: "Widget"
|
|
332
|
+
price: 29.99
|
|
333
|
+
assertions:
|
|
334
|
+
- type: status
|
|
335
|
+
expected: 201
|
|
336
|
+
variables:
|
|
337
|
+
product_id: "id"
|
|
338
|
+
|
|
339
|
+
- name: "Get product"
|
|
340
|
+
url: "/products/{{product_id}}"
|
|
341
|
+
assertions:
|
|
342
|
+
- type: json_path
|
|
343
|
+
path: "name"
|
|
344
|
+
expected: "Widget"
|
|
345
|
+
|
|
346
|
+
- name: "Delete product"
|
|
347
|
+
url: "/products/{{product_id}}"
|
|
348
|
+
method: DELETE
|
|
349
|
+
assertions:
|
|
350
|
+
- type: status
|
|
351
|
+
expected: 204
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Load Test with Detailed Config
|
|
355
|
+
|
|
356
|
+
```yaml
|
|
357
|
+
url: "https://api.example.com/search"
|
|
358
|
+
method: POST
|
|
359
|
+
concurrency: 200
|
|
360
|
+
duration_seconds: 120
|
|
361
|
+
requests_per_second: 1000
|
|
362
|
+
timeout: 5.0
|
|
363
|
+
headers:
|
|
364
|
+
Content-Type: "application/json"
|
|
365
|
+
body:
|
|
366
|
+
query: "test"
|
|
367
|
+
limit: 10
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
---
|
|
371
|
+
|
|
372
|
+
## Development
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
git clone https://github.com/SanjaySundarMurthy/api-forge.git
|
|
376
|
+
cd api-forge
|
|
377
|
+
pip install -e ".[dev]"
|
|
378
|
+
pytest tests/ -v
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
---
|
|
382
|
+
|
|
383
|
+
## Author
|
|
384
|
+
|
|
385
|
+
**Sanjay Sundar Murthy**
|
|
386
|
+
- GitHub: [@SanjaySundarMurthy](https://github.com/SanjaySundarMurthy)
|
|
387
|
+
- Email: sanjaysundarmurthy@gmail.com
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
## License
|
|
392
|
+
|
|
393
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
api_forge/__init__.py,sha256=Pfo_n30AQ_JA8qP7gNez3yV3N7wqb7ephFe7mFVXMUI,77
|
|
2
|
+
api_forge/__main__.py,sha256=lG27aFqY6oNO-ZaZ5J-Ts3w4ZF3zaYhCngJ8Aa7MqEQ,119
|
|
3
|
+
api_forge/cli.py,sha256=Bipbg-K49klNJR_Q8XbEtKZeqSdrW67023HWywEBhdo,8591
|
|
4
|
+
api_forge/client.py,sha256=9Kxe5xVvRmKoNBrh9TFownQYL8Vl_YiU3eZREOivzIk,6214
|
|
5
|
+
api_forge/mock_server.py,sha256=D_ImoHO0MKAfl3zQ3peev12NZnLTGsKtpIhen_9LmqQ,6274
|
|
6
|
+
api_forge/models.py,sha256=1m0vcF0abwkVvUdvZT3H0WZtz8AGopyXYAwsN1ILYJg,9384
|
|
7
|
+
api_forge/output.py,sha256=ey5VbXFO1IFJNb2xx5ge1rkZdSJcnohz67J7XXuVHoQ,7339
|
|
8
|
+
api_forge/parser.py,sha256=Bi9afyX-RC1ythWxTHC9jNsYTMmdj5fqjGvbz8KoVLM,7399
|
|
9
|
+
api_forge_cli-1.0.0.dist-info/METADATA,sha256=ZVKl60movPZUli6zLNJUKJqbMvO32aWzzNarAyjvpJU,8607
|
|
10
|
+
api_forge_cli-1.0.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
11
|
+
api_forge_cli-1.0.0.dist-info/entry_points.txt,sha256=PsVFVdY6IzEsywDE2K0BvN5G8r7YGlq_Aq4ZGwVihhc,48
|
|
12
|
+
api_forge_cli-1.0.0.dist-info/licenses/LICENSE,sha256=kuYe0v68w5WHNJfYvCLtnsYEEr6NzB-jhnE_qRkxu1g,1098
|
|
13
|
+
api_forge_cli-1.0.0.dist-info/RECORD,,
|