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.
Files changed (46) hide show
  1. swaggerforge-0.4.0/PKG-INFO +299 -0
  2. swaggerforge-0.4.0/README.md +273 -0
  3. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/pyproject.toml +2 -1
  4. swaggerforge-0.4.0/src/swaggerforge/auth.py +274 -0
  5. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/config.py +13 -0
  6. swaggerforge-0.4.0/src/swaggerforge/dependencies.py +129 -0
  7. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/generator.py +255 -24
  8. swaggerforge-0.4.0/src/swaggerforge/models.py +85 -0
  9. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/output.py +37 -5
  10. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/parser.py +86 -13
  11. swaggerforge-0.4.0/src/swaggerforge/template.py +382 -0
  12. swaggerforge-0.4.0/src/swaggerforge/templates/test_file.py.j2 +96 -0
  13. swaggerforge-0.4.0/src/swaggerforge.egg-info/PKG-INFO +299 -0
  14. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/SOURCES.txt +3 -0
  15. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/requires.txt +3 -0
  16. swaggerforge-0.4.0/tests/test_auth.py +523 -0
  17. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/tests/test_config.py +76 -0
  18. swaggerforge-0.4.0/tests/test_dependencies.py +218 -0
  19. swaggerforge-0.4.0/tests/test_e2e_auth.py +253 -0
  20. swaggerforge-0.4.0/tests/test_e2e_stateful.py +207 -0
  21. swaggerforge-0.4.0/tests/test_generator.py +816 -0
  22. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/tests/test_output.py +86 -3
  23. swaggerforge-0.4.0/tests/test_parser.py +352 -0
  24. swaggerforge-0.4.0/tests/test_template.py +548 -0
  25. swaggerforge-0.3.0/PKG-INFO +0 -170
  26. swaggerforge-0.3.0/README.md +0 -146
  27. swaggerforge-0.3.0/src/swaggerforge/auth.py +0 -143
  28. swaggerforge-0.3.0/src/swaggerforge/models.py +0 -47
  29. swaggerforge-0.3.0/src/swaggerforge/template.py +0 -132
  30. swaggerforge-0.3.0/src/swaggerforge/templates/test_file.py.j2 +0 -29
  31. swaggerforge-0.3.0/src/swaggerforge.egg-info/PKG-INFO +0 -170
  32. swaggerforge-0.3.0/tests/test_auth.py +0 -142
  33. swaggerforge-0.3.0/tests/test_e2e_auth.py +0 -109
  34. swaggerforge-0.3.0/tests/test_generator.py +0 -332
  35. swaggerforge-0.3.0/tests/test_parser.py +0 -200
  36. swaggerforge-0.3.0/tests/test_template.py +0 -138
  37. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/LICENSE +0 -0
  38. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/setup.cfg +0 -0
  39. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/__init__.py +0 -0
  40. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/__main__.py +0 -0
  41. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/cli.py +0 -0
  42. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge/validator.py +0 -0
  43. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/dependency_links.txt +0 -0
  44. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/entry_points.txt +0 -0
  45. {swaggerforge-0.3.0 → swaggerforge-0.4.0}/src/swaggerforge.egg-info/top_level.txt +0 -0
  46. {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.3.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"]