swaggerforge 0.3.0__tar.gz → 0.4.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.
- swaggerforge-0.4.0/PKG-INFO +299 -0
- swaggerforge-0.4.0/README.md +273 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/pyproject.toml +2 -1
- swaggerforge-0.4.0/src/swaggerforge/auth.py +274 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/config.py +13 -0
- swaggerforge-0.4.0/src/swaggerforge/dependencies.py +129 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/generator.py +255 -24
- swaggerforge-0.4.0/src/swaggerforge/models.py +85 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/output.py +37 -5
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/parser.py +86 -13
- swaggerforge-0.4.0/src/swaggerforge/template.py +382 -0
- swaggerforge-0.4.0/src/swaggerforge/templates/test_file.py.j2 +96 -0
- swaggerforge-0.4.0/src/swaggerforge.egg-info/PKG-INFO +299 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/SOURCES.txt +3 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/requires.txt +3 -0
- swaggerforge-0.4.0/tests/test_auth.py +523 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/tests/test_config.py +76 -0
- swaggerforge-0.4.0/tests/test_dependencies.py +218 -0
- swaggerforge-0.4.0/tests/test_e2e_auth.py +253 -0
- swaggerforge-0.4.0/tests/test_e2e_stateful.py +207 -0
- swaggerforge-0.4.0/tests/test_generator.py +816 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/tests/test_output.py +86 -3
- swaggerforge-0.4.0/tests/test_parser.py +352 -0
- swaggerforge-0.4.0/tests/test_template.py +548 -0
- swaggerforge-0.3.0/PKG-INFO +0 -170
- swaggerforge-0.3.0/README.md +0 -146
- swaggerforge-0.3.0/src/swaggerforge/auth.py +0 -143
- swaggerforge-0.3.0/src/swaggerforge/models.py +0 -47
- swaggerforge-0.3.0/src/swaggerforge/template.py +0 -132
- swaggerforge-0.3.0/src/swaggerforge/templates/test_file.py.j2 +0 -29
- swaggerforge-0.3.0/src/swaggerforge.egg-info/PKG-INFO +0 -170
- swaggerforge-0.3.0/tests/test_auth.py +0 -142
- swaggerforge-0.3.0/tests/test_e2e_auth.py +0 -109
- swaggerforge-0.3.0/tests/test_generator.py +0 -332
- swaggerforge-0.3.0/tests/test_parser.py +0 -200
- swaggerforge-0.3.0/tests/test_template.py +0 -138
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/LICENSE +0 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/setup.cfg +0 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/__init__.py +0 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/__main__.py +0 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/cli.py +0 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/validator.py +0 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/dependency_links.txt +0 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/entry_points.txt +0 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/top_level.txt +0 -0
- {swaggerforge-0.3.0 → swaggerforge-0.4.0}/tests/test_validator.py +0 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: swaggerforge
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Automatic pytest test generation from OpenAPI (Swagger) specifications
|
|
5
|
+
Author: Viktor Pylypenko
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: openapi,swagger,pytest,test generation,api testing
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Requires-Dist: click>=8.0
|
|
12
|
+
Requires-Dist: prance>=23.6.21.0
|
|
13
|
+
Requires-Dist: openapi-spec-validator>=0.7
|
|
14
|
+
Requires-Dist: jinja2>=3.0
|
|
15
|
+
Requires-Dist: requests>=2.28
|
|
16
|
+
Requires-Dist: jsonschema>=4.0
|
|
17
|
+
Requires-Dist: tomli>=2.0; python_version < "3.11"
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=9.0; extra == "dev"
|
|
20
|
+
Requires-Dist: flake8>=7.0; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest-cov>=7.0; extra == "dev"
|
|
22
|
+
Requires-Dist: tox>=4.0; extra == "dev"
|
|
23
|
+
Provides-Extra: oauth2
|
|
24
|
+
Requires-Dist: requests-oauth2client>=1.6; extra == "oauth2"
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# SwaggerForge
|
|
28
|
+
|
|
29
|
+
Automatic pytest test generation from OpenAPI (Swagger) specifications.
|
|
30
|
+
|
|
31
|
+
SwaggerForge is a Python library and command-line tool that reads an OpenAPI
|
|
32
|
+
specification and generates ready-to-run pytest test files - one per resource
|
|
33
|
+
covering positive, negative, boundary, and boolean scenarios grounded in
|
|
34
|
+
established test-design techniques.
|
|
35
|
+
|
|
36
|
+
## Features
|
|
37
|
+
|
|
38
|
+
- Reads OpenAPI 3.x specifications in JSON or YAML
|
|
39
|
+
- Resolves `$ref` references automatically
|
|
40
|
+
- Generates one pytest file per resource tag
|
|
41
|
+
- Produces six scenario types per endpoint where applicable:
|
|
42
|
+
- **Positive** >>> valid request, expects a 2xx response and validates the
|
|
43
|
+
response schema
|
|
44
|
+
- **Missing required field** >>> omits a required field, expects 400
|
|
45
|
+
- **Wrong data type** >>> sends a mistyped field, expects 400/422
|
|
46
|
+
- **Nonexistent resource** >>> requests a resource that was created and then
|
|
47
|
+
deleted, so its absence is guaranteed (or, when the API offers no delete,
|
|
48
|
+
an unlikely identifier); expects 404
|
|
49
|
+
- **Boundary values** >>> tests values at and just beyond declared
|
|
50
|
+
numeric/length limits (Boundary Value Analysis)
|
|
51
|
+
- **Boolean coverage** >>> exercises both `true` and `false` for boolean fields
|
|
52
|
+
- Stateful tests: operations on an existing resource run against one the test
|
|
53
|
+
creates itself and removes afterwards, instead of a guessed identifier
|
|
54
|
+
- Valid request data built from the specification's own examples, defaults,
|
|
55
|
+
enums and constraints
|
|
56
|
+
- Deterministic output: the same specification always produces identical tests
|
|
57
|
+
- Generated files use pytest fixtures and run with no manual edits
|
|
58
|
+
- Optional `swaggerforge.toml` config file for project-level defaults
|
|
59
|
+
- Comprehensive authentication: bearer, basic, API key (header/query/cookie),
|
|
60
|
+
multiple-scheme AND/OR requirements, and OAuth2 / OpenID Connect (optional
|
|
61
|
+
extra), with environment-variable substitution for secrets
|
|
62
|
+
|
|
63
|
+
## Requirements
|
|
64
|
+
|
|
65
|
+
- Python 3.10 or newer
|
|
66
|
+
|
|
67
|
+
## Installation
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install swaggerforge
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Usage
|
|
74
|
+
|
|
75
|
+
Generate tests from a specification, pointing at the base URL of the API
|
|
76
|
+
under test:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
swaggerforge generate --spec swagger.json --url http://localhost:8080
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
This reads `swagger.json`, writes one `test_<resource>.py` file per resource
|
|
83
|
+
tag into the output directory (default: `tests_generated/`), and the files can
|
|
84
|
+
be run immediately:
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
pytest tests_generated
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Options
|
|
91
|
+
|
|
92
|
+
| Option | Description | Default |
|
|
93
|
+
|------------|----------------------------------------------------|-----------------------------------|
|
|
94
|
+
| `--spec` | Path to the OpenAPI specification (JSON or YAML) | *(required)* |
|
|
95
|
+
| `--url` | Base URL of the API under test | *(required unless in config)* |
|
|
96
|
+
| `--output` | Directory for the generated test files | `tests_generated` |
|
|
97
|
+
| `--config` | Path to a configuration file | `./swaggerforge.toml` if present |
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
Options that stay the same across runs can be kept in a `swaggerforge.toml`
|
|
102
|
+
file instead of being passed on the command line. The file is picked up
|
|
103
|
+
automatically from the directory where the tool is run, or an explicit path
|
|
104
|
+
can be given with `--config`.
|
|
105
|
+
|
|
106
|
+
```toml
|
|
107
|
+
# swaggerforge.toml
|
|
108
|
+
base_url = "http://localhost:8080"
|
|
109
|
+
output_dir = "tests_generated"
|
|
110
|
+
timeout = 30
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Key | Type | Effect |
|
|
114
|
+
|--------------|---------|---------------------------------------------------------------|
|
|
115
|
+
| `base_url` | string | Base URL of the API; makes `--url` optional |
|
|
116
|
+
| `output_dir` | string | Directory for generated files |
|
|
117
|
+
| `timeout` | integer | Embeds `timeout=<n>` into every generated HTTP request call |
|
|
118
|
+
|
|
119
|
+
Values given on the command line always take precedence over the config file.
|
|
120
|
+
Without a `timeout`, generated tests place no time limit on requests -
|
|
121
|
+
setting one makes test runs fail fast when the API is unreachable.
|
|
122
|
+
|
|
123
|
+
## Authentication
|
|
124
|
+
|
|
125
|
+
When a specification declares security requirements, SwaggerForge configures
|
|
126
|
+
the generated tests to authenticate. Credentials are supplied through the
|
|
127
|
+
config file; the specification decides *which* scheme and *where* each value
|
|
128
|
+
goes, and the config supplies the secret values.
|
|
129
|
+
|
|
130
|
+
```toml
|
|
131
|
+
# swaggerforge.toml
|
|
132
|
+
[auth.bearer]
|
|
133
|
+
token = "${API_TOKEN}"
|
|
134
|
+
|
|
135
|
+
[auth.api_key]
|
|
136
|
+
value = "${API_KEY}"
|
|
137
|
+
|
|
138
|
+
[auth.basic]
|
|
139
|
+
username = "${API_USER}"
|
|
140
|
+
password = "${API_PASSWORD}"
|
|
141
|
+
|
|
142
|
+
[auth.oauth2]
|
|
143
|
+
client_id = "${OAUTH_CLIENT_ID}"
|
|
144
|
+
client_secret = "${OAUTH_CLIENT_SECRET}"
|
|
145
|
+
scope = "read write"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Generated tests build a session-scoped `requests.Session` that carries the
|
|
149
|
+
resolved authentication, so every request is authenticated without repeating
|
|
150
|
+
credentials per call.
|
|
151
|
+
|
|
152
|
+
### Supported schemes
|
|
153
|
+
|
|
154
|
+
| Config section | OpenAPI scheme | How it is applied |
|
|
155
|
+
|-----------------|---------------------------------------------|-----------------------------------------------------|
|
|
156
|
+
| `[auth.bearer]` | `http` bearer | `Authorization: Bearer <token>` header |
|
|
157
|
+
| `[auth.api_key]`| `apiKey` in header, query, or cookie | the name and location declared by the specification |
|
|
158
|
+
| `[auth.basic]` | `http` basic | `Authorization: Basic <base64>` header |
|
|
159
|
+
| `[auth.oauth2]` | `oauth2` client credentials, `openIdConnect`| a token fetched at runtime (see below) |
|
|
160
|
+
|
|
161
|
+
For an API key, both the **name** and the **location** (header, query, or
|
|
162
|
+
cookie) come from the specification; the config supplies only the secret
|
|
163
|
+
`value`.
|
|
164
|
+
|
|
165
|
+
### Multiple schemes
|
|
166
|
+
|
|
167
|
+
OpenAPI can require several schemes at once (AND) or offer alternatives (OR).
|
|
168
|
+
SwaggerForge honours both: it uses the first alternative whose credentials are
|
|
169
|
+
fully configured, combining every scheme in an AND group. When endpoints in one
|
|
170
|
+
file need different authentication, the differing ones override the session per
|
|
171
|
+
request.
|
|
172
|
+
|
|
173
|
+
### OAuth2 and OpenID Connect
|
|
174
|
+
|
|
175
|
+
OAuth2 and OIDC support is an optional extra, installed with:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
pip install swaggerforge[oauth2]
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
For the **client credentials** flow, generated tests fetch a fresh access
|
|
182
|
+
token from the specification's `tokenUrl` at runtime (and refresh it on
|
|
183
|
+
expiry), so no token is ever embedded at generation time. **OpenID Connect**
|
|
184
|
+
schemes work the same way, discovering the token endpoint at runtime from the
|
|
185
|
+
specification's `openIdConnectUrl`. Both need `client_id` and `client_secret`
|
|
186
|
+
in `[auth.oauth2]`.
|
|
187
|
+
|
|
188
|
+
Interactive flows (OAuth2 **authorization code**) require a human to log in
|
|
189
|
+
through a browser and so cannot be fully automated. For these, SwaggerForge
|
|
190
|
+
generates a clearly marked placeholder in the session fixture where you paste a
|
|
191
|
+
token obtained manually.
|
|
192
|
+
|
|
193
|
+
### Environment variables and missing credentials
|
|
194
|
+
|
|
195
|
+
Any auth value may reference an environment variable with `${VAR}` syntax, so
|
|
196
|
+
secrets stay out of the config file; a referenced variable that is not set is
|
|
197
|
+
an error. If a specification requires a scheme whose credentials are not
|
|
198
|
+
configured, SwaggerForge prints a warning naming the scheme and generates the
|
|
199
|
+
tests without that authentication rather than failing.
|
|
200
|
+
|
|
201
|
+
## Stateful tests
|
|
202
|
+
|
|
203
|
+
Endpoints that act on an existing resource - `GET`, `PUT`, `PATCH` or
|
|
204
|
+
`DELETE` on `/pets/{petId}` - need a resource that really exists. SwaggerForge
|
|
205
|
+
detects the operation that creates it and generates fixtures that create the
|
|
206
|
+
resource before a test and remove it afterwards.
|
|
207
|
+
|
|
208
|
+
A create operation is recognised by REST convention: a `POST` on a collection
|
|
209
|
+
path (`/pets`) produces resources addressed by that path plus one parameter
|
|
210
|
+
(`/pets/{petId}`). The new resource's identifier is taken from the response
|
|
211
|
+
field named like the path parameter, or else from `id`. When the
|
|
212
|
+
specification declares explicit OpenAPI `links`, they take priority.
|
|
213
|
+
|
|
214
|
+
For each such resource, generated files contain two fixtures:
|
|
215
|
+
|
|
216
|
+
- `created_<resource>` creates the resource, yields its identifier (read from
|
|
217
|
+
the response body, or from the `Location` header) and deletes it after the
|
|
218
|
+
test
|
|
219
|
+
- `deleted_<resource>` creates the resource and deletes it immediately, giving
|
|
220
|
+
the nonexistent-resource test an identifier that is guaranteed not to exist
|
|
221
|
+
|
|
222
|
+
```python
|
|
223
|
+
@pytest.fixture
|
|
224
|
+
def created_pet(base_url, api_session):
|
|
225
|
+
response = api_session.post(f"{base_url}/pet", json={'id': uuid.uuid4().int >> 80, 'name': 'doggie', 'photoUrls': ['string']})
|
|
226
|
+
assert response.status_code in [200], (
|
|
227
|
+
f"Creating pet failed: "
|
|
228
|
+
f"{response.status_code} {response.text}"
|
|
229
|
+
)
|
|
230
|
+
resource_id = _extract_id(response, 'id')
|
|
231
|
+
yield resource_id
|
|
232
|
+
try:
|
|
233
|
+
api_session.delete(f"{base_url}/pet/{resource_id}")
|
|
234
|
+
except requests.RequestException:
|
|
235
|
+
pass
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def test_get_pet_by_id_success(base_url, api_session, created_pet):
|
|
239
|
+
"""GET /pet/{petId} - positive scenario"""
|
|
240
|
+
response = api_session.get(f"{base_url}/pet/{created_pet}")
|
|
241
|
+
assert response.status_code in [200]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
*(Abridged from a file generated for the Swagger Petstore specification.)*
|
|
245
|
+
|
|
246
|
+
Every scenario on such an endpoint uses the created resource - negative
|
|
247
|
+
scenarios included, so a 404 for a missing resource cannot hide the 400 a
|
|
248
|
+
validation test expects. A top-level `id` in the create request is sent as a
|
|
249
|
+
fresh unique value on every run, so tests never share or collide on one
|
|
250
|
+
resource; whether the server keeps that value or assigns its own, the fixture
|
|
251
|
+
reads the actual identifier from the response. The generated files themselves
|
|
252
|
+
stay identical for the same specification - only the runtime data varies.
|
|
253
|
+
|
|
254
|
+
Endpoints without a recognisable create operation are generated exactly as
|
|
255
|
+
before, with example identifiers.
|
|
256
|
+
|
|
257
|
+
## Request data
|
|
258
|
+
|
|
259
|
+
Request bodies and parameters are built deterministically from the
|
|
260
|
+
specification, without random data. For each value, the first available
|
|
261
|
+
source is used: `const`, then `example` (or the first of `examples`), then
|
|
262
|
+
`default`, then the first `enum` member, and finally a placeholder matching
|
|
263
|
+
the declared type and format (for example `user@example.com` for an `email`).
|
|
264
|
+
Placeholder strings are fitted to `minLength` and `maxLength`. Properties
|
|
265
|
+
marked `readOnly` are left out of requests, as OpenAPI prescribes.
|
|
266
|
+
|
|
267
|
+
## How it works
|
|
268
|
+
|
|
269
|
+
SwaggerForge runs a six-stage pipeline: the specification is validated, parsed
|
|
270
|
+
into an internal model (with `$ref`s resolved), turned into test scenarios
|
|
271
|
+
based on test-design techniques - with dependencies between operations
|
|
272
|
+
inferred, so stateful scenarios can share created resources - rendered into
|
|
273
|
+
pytest code via templates, and written to per-resource files.
|
|
274
|
+
|
|
275
|
+
## Limitations
|
|
276
|
+
|
|
277
|
+
- Targets OpenAPI 3.x with JSON request bodies
|
|
278
|
+
- Authentication covers bearer, basic, API key (header/query/cookie), OAuth2
|
|
279
|
+
client credentials, and OpenID Connect; interactive OAuth2 flows
|
|
280
|
+
(authorization code) generate a placeholder for a manually obtained token,
|
|
281
|
+
as they require a browser-based login that cannot be automated
|
|
282
|
+
- Boundary tests require the specification to declare numeric/length constraints
|
|
283
|
+
- Stateful tests cover single-level resources (a collection and its items);
|
|
284
|
+
nested chains such as `/owners/{ownerId}/pets/{petId}` keep example
|
|
285
|
+
identifiers, and only `links` passing a top-level response field by
|
|
286
|
+
`operationId` are used
|
|
287
|
+
- APIs that soft-delete (a deleted resource still answers 200) fail the
|
|
288
|
+
nonexistent-resource test - a reflection of the API's semantics
|
|
289
|
+
- Resource fixtures use the file's session authentication; a create operation
|
|
290
|
+
that needs different credentials than the file's default is not overridden
|
|
291
|
+
- Only a top-level field named `id` is treated as a server-side surrogate key;
|
|
292
|
+
a differently named one is sent with its example value
|
|
293
|
+
- An identifier read from a `Location` header arrives as a string
|
|
294
|
+
- The create operation's own positive test does not remove the resource it
|
|
295
|
+
creates
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file.
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# SwaggerForge
|
|
2
|
+
|
|
3
|
+
Automatic pytest test generation from OpenAPI (Swagger) specifications.
|
|
4
|
+
|
|
5
|
+
SwaggerForge is a Python library and command-line tool that reads an OpenAPI
|
|
6
|
+
specification and generates ready-to-run pytest test files - one per resource
|
|
7
|
+
covering positive, negative, boundary, and boolean scenarios grounded in
|
|
8
|
+
established test-design techniques.
|
|
9
|
+
|
|
10
|
+
## Features
|
|
11
|
+
|
|
12
|
+
- Reads OpenAPI 3.x specifications in JSON or YAML
|
|
13
|
+
- Resolves `$ref` references automatically
|
|
14
|
+
- Generates one pytest file per resource tag
|
|
15
|
+
- Produces six scenario types per endpoint where applicable:
|
|
16
|
+
- **Positive** >>> valid request, expects a 2xx response and validates the
|
|
17
|
+
response schema
|
|
18
|
+
- **Missing required field** >>> omits a required field, expects 400
|
|
19
|
+
- **Wrong data type** >>> sends a mistyped field, expects 400/422
|
|
20
|
+
- **Nonexistent resource** >>> requests a resource that was created and then
|
|
21
|
+
deleted, so its absence is guaranteed (or, when the API offers no delete,
|
|
22
|
+
an unlikely identifier); expects 404
|
|
23
|
+
- **Boundary values** >>> tests values at and just beyond declared
|
|
24
|
+
numeric/length limits (Boundary Value Analysis)
|
|
25
|
+
- **Boolean coverage** >>> exercises both `true` and `false` for boolean fields
|
|
26
|
+
- Stateful tests: operations on an existing resource run against one the test
|
|
27
|
+
creates itself and removes afterwards, instead of a guessed identifier
|
|
28
|
+
- Valid request data built from the specification's own examples, defaults,
|
|
29
|
+
enums and constraints
|
|
30
|
+
- Deterministic output: the same specification always produces identical tests
|
|
31
|
+
- Generated files use pytest fixtures and run with no manual edits
|
|
32
|
+
- Optional `swaggerforge.toml` config file for project-level defaults
|
|
33
|
+
- Comprehensive authentication: bearer, basic, API key (header/query/cookie),
|
|
34
|
+
multiple-scheme AND/OR requirements, and OAuth2 / OpenID Connect (optional
|
|
35
|
+
extra), with environment-variable substitution for secrets
|
|
36
|
+
|
|
37
|
+
## Requirements
|
|
38
|
+
|
|
39
|
+
- Python 3.10 or newer
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install swaggerforge
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
|
|
49
|
+
Generate tests from a specification, pointing at the base URL of the API
|
|
50
|
+
under test:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
swaggerforge generate --spec swagger.json --url http://localhost:8080
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This reads `swagger.json`, writes one `test_<resource>.py` file per resource
|
|
57
|
+
tag into the output directory (default: `tests_generated/`), and the files can
|
|
58
|
+
be run immediately:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pytest tests_generated
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### Options
|
|
65
|
+
|
|
66
|
+
| Option | Description | Default |
|
|
67
|
+
|------------|----------------------------------------------------|-----------------------------------|
|
|
68
|
+
| `--spec` | Path to the OpenAPI specification (JSON or YAML) | *(required)* |
|
|
69
|
+
| `--url` | Base URL of the API under test | *(required unless in config)* |
|
|
70
|
+
| `--output` | Directory for the generated test files | `tests_generated` |
|
|
71
|
+
| `--config` | Path to a configuration file | `./swaggerforge.toml` if present |
|
|
72
|
+
|
|
73
|
+
## Configuration
|
|
74
|
+
|
|
75
|
+
Options that stay the same across runs can be kept in a `swaggerforge.toml`
|
|
76
|
+
file instead of being passed on the command line. The file is picked up
|
|
77
|
+
automatically from the directory where the tool is run, or an explicit path
|
|
78
|
+
can be given with `--config`.
|
|
79
|
+
|
|
80
|
+
```toml
|
|
81
|
+
# swaggerforge.toml
|
|
82
|
+
base_url = "http://localhost:8080"
|
|
83
|
+
output_dir = "tests_generated"
|
|
84
|
+
timeout = 30
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
| Key | Type | Effect |
|
|
88
|
+
|--------------|---------|---------------------------------------------------------------|
|
|
89
|
+
| `base_url` | string | Base URL of the API; makes `--url` optional |
|
|
90
|
+
| `output_dir` | string | Directory for generated files |
|
|
91
|
+
| `timeout` | integer | Embeds `timeout=<n>` into every generated HTTP request call |
|
|
92
|
+
|
|
93
|
+
Values given on the command line always take precedence over the config file.
|
|
94
|
+
Without a `timeout`, generated tests place no time limit on requests -
|
|
95
|
+
setting one makes test runs fail fast when the API is unreachable.
|
|
96
|
+
|
|
97
|
+
## Authentication
|
|
98
|
+
|
|
99
|
+
When a specification declares security requirements, SwaggerForge configures
|
|
100
|
+
the generated tests to authenticate. Credentials are supplied through the
|
|
101
|
+
config file; the specification decides *which* scheme and *where* each value
|
|
102
|
+
goes, and the config supplies the secret values.
|
|
103
|
+
|
|
104
|
+
```toml
|
|
105
|
+
# swaggerforge.toml
|
|
106
|
+
[auth.bearer]
|
|
107
|
+
token = "${API_TOKEN}"
|
|
108
|
+
|
|
109
|
+
[auth.api_key]
|
|
110
|
+
value = "${API_KEY}"
|
|
111
|
+
|
|
112
|
+
[auth.basic]
|
|
113
|
+
username = "${API_USER}"
|
|
114
|
+
password = "${API_PASSWORD}"
|
|
115
|
+
|
|
116
|
+
[auth.oauth2]
|
|
117
|
+
client_id = "${OAUTH_CLIENT_ID}"
|
|
118
|
+
client_secret = "${OAUTH_CLIENT_SECRET}"
|
|
119
|
+
scope = "read write"
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Generated tests build a session-scoped `requests.Session` that carries the
|
|
123
|
+
resolved authentication, so every request is authenticated without repeating
|
|
124
|
+
credentials per call.
|
|
125
|
+
|
|
126
|
+
### Supported schemes
|
|
127
|
+
|
|
128
|
+
| Config section | OpenAPI scheme | How it is applied |
|
|
129
|
+
|-----------------|---------------------------------------------|-----------------------------------------------------|
|
|
130
|
+
| `[auth.bearer]` | `http` bearer | `Authorization: Bearer <token>` header |
|
|
131
|
+
| `[auth.api_key]`| `apiKey` in header, query, or cookie | the name and location declared by the specification |
|
|
132
|
+
| `[auth.basic]` | `http` basic | `Authorization: Basic <base64>` header |
|
|
133
|
+
| `[auth.oauth2]` | `oauth2` client credentials, `openIdConnect`| a token fetched at runtime (see below) |
|
|
134
|
+
|
|
135
|
+
For an API key, both the **name** and the **location** (header, query, or
|
|
136
|
+
cookie) come from the specification; the config supplies only the secret
|
|
137
|
+
`value`.
|
|
138
|
+
|
|
139
|
+
### Multiple schemes
|
|
140
|
+
|
|
141
|
+
OpenAPI can require several schemes at once (AND) or offer alternatives (OR).
|
|
142
|
+
SwaggerForge honours both: it uses the first alternative whose credentials are
|
|
143
|
+
fully configured, combining every scheme in an AND group. When endpoints in one
|
|
144
|
+
file need different authentication, the differing ones override the session per
|
|
145
|
+
request.
|
|
146
|
+
|
|
147
|
+
### OAuth2 and OpenID Connect
|
|
148
|
+
|
|
149
|
+
OAuth2 and OIDC support is an optional extra, installed with:
|
|
150
|
+
|
|
151
|
+
```bash
|
|
152
|
+
pip install swaggerforge[oauth2]
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
For the **client credentials** flow, generated tests fetch a fresh access
|
|
156
|
+
token from the specification's `tokenUrl` at runtime (and refresh it on
|
|
157
|
+
expiry), so no token is ever embedded at generation time. **OpenID Connect**
|
|
158
|
+
schemes work the same way, discovering the token endpoint at runtime from the
|
|
159
|
+
specification's `openIdConnectUrl`. Both need `client_id` and `client_secret`
|
|
160
|
+
in `[auth.oauth2]`.
|
|
161
|
+
|
|
162
|
+
Interactive flows (OAuth2 **authorization code**) require a human to log in
|
|
163
|
+
through a browser and so cannot be fully automated. For these, SwaggerForge
|
|
164
|
+
generates a clearly marked placeholder in the session fixture where you paste a
|
|
165
|
+
token obtained manually.
|
|
166
|
+
|
|
167
|
+
### Environment variables and missing credentials
|
|
168
|
+
|
|
169
|
+
Any auth value may reference an environment variable with `${VAR}` syntax, so
|
|
170
|
+
secrets stay out of the config file; a referenced variable that is not set is
|
|
171
|
+
an error. If a specification requires a scheme whose credentials are not
|
|
172
|
+
configured, SwaggerForge prints a warning naming the scheme and generates the
|
|
173
|
+
tests without that authentication rather than failing.
|
|
174
|
+
|
|
175
|
+
## Stateful tests
|
|
176
|
+
|
|
177
|
+
Endpoints that act on an existing resource - `GET`, `PUT`, `PATCH` or
|
|
178
|
+
`DELETE` on `/pets/{petId}` - need a resource that really exists. SwaggerForge
|
|
179
|
+
detects the operation that creates it and generates fixtures that create the
|
|
180
|
+
resource before a test and remove it afterwards.
|
|
181
|
+
|
|
182
|
+
A create operation is recognised by REST convention: a `POST` on a collection
|
|
183
|
+
path (`/pets`) produces resources addressed by that path plus one parameter
|
|
184
|
+
(`/pets/{petId}`). The new resource's identifier is taken from the response
|
|
185
|
+
field named like the path parameter, or else from `id`. When the
|
|
186
|
+
specification declares explicit OpenAPI `links`, they take priority.
|
|
187
|
+
|
|
188
|
+
For each such resource, generated files contain two fixtures:
|
|
189
|
+
|
|
190
|
+
- `created_<resource>` creates the resource, yields its identifier (read from
|
|
191
|
+
the response body, or from the `Location` header) and deletes it after the
|
|
192
|
+
test
|
|
193
|
+
- `deleted_<resource>` creates the resource and deletes it immediately, giving
|
|
194
|
+
the nonexistent-resource test an identifier that is guaranteed not to exist
|
|
195
|
+
|
|
196
|
+
```python
|
|
197
|
+
@pytest.fixture
|
|
198
|
+
def created_pet(base_url, api_session):
|
|
199
|
+
response = api_session.post(f"{base_url}/pet", json={'id': uuid.uuid4().int >> 80, 'name': 'doggie', 'photoUrls': ['string']})
|
|
200
|
+
assert response.status_code in [200], (
|
|
201
|
+
f"Creating pet failed: "
|
|
202
|
+
f"{response.status_code} {response.text}"
|
|
203
|
+
)
|
|
204
|
+
resource_id = _extract_id(response, 'id')
|
|
205
|
+
yield resource_id
|
|
206
|
+
try:
|
|
207
|
+
api_session.delete(f"{base_url}/pet/{resource_id}")
|
|
208
|
+
except requests.RequestException:
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
def test_get_pet_by_id_success(base_url, api_session, created_pet):
|
|
213
|
+
"""GET /pet/{petId} - positive scenario"""
|
|
214
|
+
response = api_session.get(f"{base_url}/pet/{created_pet}")
|
|
215
|
+
assert response.status_code in [200]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
*(Abridged from a file generated for the Swagger Petstore specification.)*
|
|
219
|
+
|
|
220
|
+
Every scenario on such an endpoint uses the created resource - negative
|
|
221
|
+
scenarios included, so a 404 for a missing resource cannot hide the 400 a
|
|
222
|
+
validation test expects. A top-level `id` in the create request is sent as a
|
|
223
|
+
fresh unique value on every run, so tests never share or collide on one
|
|
224
|
+
resource; whether the server keeps that value or assigns its own, the fixture
|
|
225
|
+
reads the actual identifier from the response. The generated files themselves
|
|
226
|
+
stay identical for the same specification - only the runtime data varies.
|
|
227
|
+
|
|
228
|
+
Endpoints without a recognisable create operation are generated exactly as
|
|
229
|
+
before, with example identifiers.
|
|
230
|
+
|
|
231
|
+
## Request data
|
|
232
|
+
|
|
233
|
+
Request bodies and parameters are built deterministically from the
|
|
234
|
+
specification, without random data. For each value, the first available
|
|
235
|
+
source is used: `const`, then `example` (or the first of `examples`), then
|
|
236
|
+
`default`, then the first `enum` member, and finally a placeholder matching
|
|
237
|
+
the declared type and format (for example `user@example.com` for an `email`).
|
|
238
|
+
Placeholder strings are fitted to `minLength` and `maxLength`. Properties
|
|
239
|
+
marked `readOnly` are left out of requests, as OpenAPI prescribes.
|
|
240
|
+
|
|
241
|
+
## How it works
|
|
242
|
+
|
|
243
|
+
SwaggerForge runs a six-stage pipeline: the specification is validated, parsed
|
|
244
|
+
into an internal model (with `$ref`s resolved), turned into test scenarios
|
|
245
|
+
based on test-design techniques - with dependencies between operations
|
|
246
|
+
inferred, so stateful scenarios can share created resources - rendered into
|
|
247
|
+
pytest code via templates, and written to per-resource files.
|
|
248
|
+
|
|
249
|
+
## Limitations
|
|
250
|
+
|
|
251
|
+
- Targets OpenAPI 3.x with JSON request bodies
|
|
252
|
+
- Authentication covers bearer, basic, API key (header/query/cookie), OAuth2
|
|
253
|
+
client credentials, and OpenID Connect; interactive OAuth2 flows
|
|
254
|
+
(authorization code) generate a placeholder for a manually obtained token,
|
|
255
|
+
as they require a browser-based login that cannot be automated
|
|
256
|
+
- Boundary tests require the specification to declare numeric/length constraints
|
|
257
|
+
- Stateful tests cover single-level resources (a collection and its items);
|
|
258
|
+
nested chains such as `/owners/{ownerId}/pets/{petId}` keep example
|
|
259
|
+
identifiers, and only `links` passing a top-level response field by
|
|
260
|
+
`operationId` are used
|
|
261
|
+
- APIs that soft-delete (a deleted resource still answers 200) fail the
|
|
262
|
+
nonexistent-resource test - a reflection of the API's semantics
|
|
263
|
+
- Resource fixtures use the file's session authentication; a create operation
|
|
264
|
+
that needs different credentials than the file's default is not overridden
|
|
265
|
+
- Only a top-level field named `id` is treated as a server-side surrogate key;
|
|
266
|
+
a differently named one is sent with its example value
|
|
267
|
+
- An identifier read from a `Location` header arrives as a string
|
|
268
|
+
- The create operation's own positive test does not remove the resource it
|
|
269
|
+
creates
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "swaggerforge"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.4.0"
|
|
8
8
|
description = "Automatic pytest test generation from OpenAPI (Swagger) specifications"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -39,3 +39,4 @@ testpaths = ["tests"]
|
|
|
39
39
|
|
|
40
40
|
[project.optional-dependencies]
|
|
41
41
|
dev = ["pytest>=9.0", "flake8>=7.0", "pytest-cov>=7.0", "tox>=4.0"]
|
|
42
|
+
oauth2 = ["requests-oauth2client>=1.6"]
|