swaggerforge 0.2.0__tar.gz → 0.3.1__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 (38) hide show
  1. {swaggerforge-0.2.0/src/swaggerforge.egg-info → swaggerforge-0.3.1}/PKG-INFO +88 -2
  2. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/README.md +85 -1
  3. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/pyproject.toml +2 -1
  4. swaggerforge-0.3.1/src/swaggerforge/auth.py +274 -0
  5. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/cli.py +15 -4
  6. swaggerforge-0.3.1/src/swaggerforge/config.py +219 -0
  7. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/models.py +1 -0
  8. swaggerforge-0.3.1/src/swaggerforge/output.py +123 -0
  9. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/parser.py +35 -3
  10. swaggerforge-0.3.1/src/swaggerforge/template.py +215 -0
  11. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/templates/test_file.py.j2 +14 -3
  12. {swaggerforge-0.2.0 → swaggerforge-0.3.1/src/swaggerforge.egg-info}/PKG-INFO +88 -2
  13. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge.egg-info/SOURCES.txt +3 -0
  14. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge.egg-info/requires.txt +3 -0
  15. swaggerforge-0.3.1/tests/test_auth.py +523 -0
  16. swaggerforge-0.3.1/tests/test_config.py +408 -0
  17. swaggerforge-0.3.1/tests/test_e2e_auth.py +253 -0
  18. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/tests/test_generator.py +4 -4
  19. swaggerforge-0.3.1/tests/test_output.py +195 -0
  20. swaggerforge-0.3.1/tests/test_parser.py +240 -0
  21. swaggerforge-0.3.1/tests/test_template.py +290 -0
  22. swaggerforge-0.2.0/src/swaggerforge/config.py +0 -102
  23. swaggerforge-0.2.0/src/swaggerforge/output.py +0 -53
  24. swaggerforge-0.2.0/src/swaggerforge/template.py +0 -121
  25. swaggerforge-0.2.0/tests/test_config.py +0 -120
  26. swaggerforge-0.2.0/tests/test_output.py +0 -46
  27. swaggerforge-0.2.0/tests/test_parser.py +0 -79
  28. swaggerforge-0.2.0/tests/test_template.py +0 -97
  29. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/LICENSE +0 -0
  30. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/setup.cfg +0 -0
  31. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/__init__.py +0 -0
  32. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/__main__.py +0 -0
  33. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/generator.py +0 -0
  34. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/validator.py +0 -0
  35. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge.egg-info/dependency_links.txt +0 -0
  36. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge.egg-info/entry_points.txt +0 -0
  37. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge.egg-info/top_level.txt +0 -0
  38. {swaggerforge-0.2.0 → swaggerforge-0.3.1}/tests/test_validator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: swaggerforge
3
- Version: 0.2.0
3
+ Version: 0.3.1
4
4
  Summary: Automatic pytest test generation from OpenAPI (Swagger) specifications
5
5
  Author: Viktor Pylypenko
6
6
  License-Expression: MIT
@@ -20,6 +20,8 @@ Requires-Dist: pytest>=9.0; extra == "dev"
20
20
  Requires-Dist: flake8>=7.0; extra == "dev"
21
21
  Requires-Dist: pytest-cov>=7.0; extra == "dev"
22
22
  Requires-Dist: tox>=4.0; extra == "dev"
23
+ Provides-Extra: oauth2
24
+ Requires-Dist: requests-oauth2client>=1.6; extra == "oauth2"
23
25
  Dynamic: license-file
24
26
 
25
27
  # SwaggerForge
@@ -48,6 +50,9 @@ established test-design techniques.
48
50
  - Deterministic output: the same specification always produces identical tests
49
51
  - Generated files use session-scoped pytest fixtures and run with no manual edits
50
52
  - Optional `swaggerforge.toml` config file for project-level defaults
53
+ - Comprehensive authentication: bearer, basic, API key (header/query/cookie),
54
+ multiple-scheme AND/OR requirements, and OAuth2 / OpenID Connect (optional
55
+ extra), with environment-variable substitution for secrets
51
56
 
52
57
  ## Requirements
53
58
 
@@ -109,6 +114,84 @@ Values given on the command line always take precedence over the config file.
109
114
  Without a `timeout`, generated tests place no time limit on requests -
110
115
  setting one makes test runs fail fast when the API is unreachable.
111
116
 
117
+ ## Authentication
118
+
119
+ When a specification declares security requirements, SwaggerForge configures
120
+ the generated tests to authenticate. Credentials are supplied through the
121
+ config file; the specification decides *which* scheme and *where* each value
122
+ goes, and the config supplies the secret values.
123
+
124
+ ```toml
125
+ # swaggerforge.toml
126
+ [auth.bearer]
127
+ token = "${API_TOKEN}"
128
+
129
+ [auth.api_key]
130
+ value = "${API_KEY}"
131
+
132
+ [auth.basic]
133
+ username = "${API_USER}"
134
+ password = "${API_PASSWORD}"
135
+
136
+ [auth.oauth2]
137
+ client_id = "${OAUTH_CLIENT_ID}"
138
+ client_secret = "${OAUTH_CLIENT_SECRET}"
139
+ scope = "read write"
140
+ ```
141
+
142
+ Generated tests build a session-scoped `requests.Session` that carries the
143
+ resolved authentication, so every request is authenticated without repeating
144
+ credentials per call.
145
+
146
+ ### Supported schemes
147
+
148
+ | Config section | OpenAPI scheme | How it is applied |
149
+ |-----------------|---------------------------------------------|-----------------------------------------------------|
150
+ | `[auth.bearer]` | `http` bearer | `Authorization: Bearer <token>` header |
151
+ | `[auth.api_key]`| `apiKey` in header, query, or cookie | the name and location declared by the specification |
152
+ | `[auth.basic]` | `http` basic | `Authorization: Basic <base64>` header |
153
+ | `[auth.oauth2]` | `oauth2` client credentials, `openIdConnect`| a token fetched at runtime (see below) |
154
+
155
+ For an API key, both the **name** and the **location** (header, query, or
156
+ cookie) come from the specification; the config supplies only the secret
157
+ `value`.
158
+
159
+ ### Multiple schemes
160
+
161
+ OpenAPI can require several schemes at once (AND) or offer alternatives (OR).
162
+ SwaggerForge honours both: it uses the first alternative whose credentials are
163
+ fully configured, combining every scheme in an AND group. When endpoints in one
164
+ file need different authentication, the differing ones override the session per
165
+ request.
166
+
167
+ ### OAuth2 and OpenID Connect
168
+
169
+ OAuth2 and OIDC support is an optional extra, installed with:
170
+
171
+ ```bash
172
+ pip install swaggerforge[oauth2]
173
+ ```
174
+
175
+ For the **client credentials** flow, generated tests fetch a fresh access
176
+ token from the specification's `tokenUrl` at runtime (and refresh it on
177
+ expiry), so no token is ever embedded at generation time. **OpenID Connect**
178
+ schemes work the same way, discovering the token endpoint at runtime from the
179
+ specification's `openIdConnectUrl`. Both need `client_id` and `client_secret`
180
+ in `[auth.oauth2]`.
181
+
182
+ Interactive flows (OAuth2 **authorization code**) require a human to log in
183
+ through a browser and so cannot be fully automated. For these, SwaggerForge
184
+ generates a clearly marked placeholder in the session fixture where you paste a
185
+ token obtained manually.
186
+
187
+ ### Environment variables and missing credentials
188
+
189
+ Any auth value may reference an environment variable with `${VAR}` syntax, so
190
+ secrets stay out of the config file; a referenced variable that is not set is
191
+ an error. If a specification requires a scheme whose credentials are not
192
+ configured, SwaggerForge prints a warning naming the scheme and generates the
193
+ tests without that authentication rather than failing.
194
+
112
195
  ## How it works
113
196
 
114
197
  SwaggerForge runs a six-stage pipeline: the specification is validated, parsed
@@ -119,7 +202,10 @@ written to per-resource files.
119
202
  ## Limitations
120
203
 
121
204
  - Targets OpenAPI 3.x with JSON request bodies
122
- - Authentication is not yet handled (planned)
205
+ - Authentication covers bearer, basic, API key (header/query/cookie), OAuth2
206
+ client credentials, and OpenID Connect; interactive OAuth2 flows
207
+ (authorization code) generate a placeholder for a manually obtained token,
208
+ as they require a browser-based login that cannot be automated
123
209
  - Boundary tests require the specification to declare numeric/length constraints
124
210
 
125
211
  ## License
@@ -24,6 +24,9 @@ established test-design techniques.
24
24
  - Deterministic output: the same specification always produces identical tests
25
25
  - Generated files use session-scoped pytest fixtures and run with no manual edits
26
26
  - Optional `swaggerforge.toml` config file for project-level defaults
27
+ - Comprehensive authentication: bearer, basic, API key (header/query/cookie),
28
+ multiple-scheme AND/OR requirements, and OAuth2 / OpenID Connect (optional
29
+ extra), with environment-variable substitution for secrets
27
30
 
28
31
  ## Requirements
29
32
 
@@ -85,6 +88,84 @@ Values given on the command line always take precedence over the config file.
85
88
  Without a `timeout`, generated tests place no time limit on requests -
86
89
  setting one makes test runs fail fast when the API is unreachable.
87
90
 
91
+ ## Authentication
92
+
93
+ When a specification declares security requirements, SwaggerForge configures
94
+ the generated tests to authenticate. Credentials are supplied through the
95
+ config file; the specification decides *which* scheme and *where* each value
96
+ goes, and the config supplies the secret values.
97
+
98
+ ```toml
99
+ # swaggerforge.toml
100
+ [auth.bearer]
101
+ token = "${API_TOKEN}"
102
+
103
+ [auth.api_key]
104
+ value = "${API_KEY}"
105
+
106
+ [auth.basic]
107
+ username = "${API_USER}"
108
+ password = "${API_PASSWORD}"
109
+
110
+ [auth.oauth2]
111
+ client_id = "${OAUTH_CLIENT_ID}"
112
+ client_secret = "${OAUTH_CLIENT_SECRET}"
113
+ scope = "read write"
114
+ ```
115
+
116
+ Generated tests build a session-scoped `requests.Session` that carries the
117
+ resolved authentication, so every request is authenticated without repeating
118
+ credentials per call.
119
+
120
+ ### Supported schemes
121
+
122
+ | Config section | OpenAPI scheme | How it is applied |
123
+ |-----------------|---------------------------------------------|-----------------------------------------------------|
124
+ | `[auth.bearer]` | `http` bearer | `Authorization: Bearer <token>` header |
125
+ | `[auth.api_key]`| `apiKey` in header, query, or cookie | the name and location declared by the specification |
126
+ | `[auth.basic]` | `http` basic | `Authorization: Basic <base64>` header |
127
+ | `[auth.oauth2]` | `oauth2` client credentials, `openIdConnect`| a token fetched at runtime (see below) |
128
+
129
+ For an API key, both the **name** and the **location** (header, query, or
130
+ cookie) come from the specification; the config supplies only the secret
131
+ `value`.
132
+
133
+ ### Multiple schemes
134
+
135
+ OpenAPI can require several schemes at once (AND) or offer alternatives (OR).
136
+ SwaggerForge honours both: it uses the first alternative whose credentials are
137
+ fully configured, combining every scheme in an AND group. When endpoints in one
138
+ file need different authentication, the differing ones override the session per
139
+ request.
140
+
141
+ ### OAuth2 and OpenID Connect
142
+
143
+ OAuth2 and OIDC support is an optional extra, installed with:
144
+
145
+ ```bash
146
+ pip install swaggerforge[oauth2]
147
+ ```
148
+
149
+ For the **client credentials** flow, generated tests fetch a fresh access
150
+ token from the specification's `tokenUrl` at runtime (and refresh it on
151
+ expiry), so no token is ever embedded at generation time. **OpenID Connect**
152
+ schemes work the same way, discovering the token endpoint at runtime from the
153
+ specification's `openIdConnectUrl`. Both need `client_id` and `client_secret`
154
+ in `[auth.oauth2]`.
155
+
156
+ Interactive flows (OAuth2 **authorization code**) require a human to log in
157
+ through a browser and so cannot be fully automated. For these, SwaggerForge
158
+ generates a clearly marked placeholder in the session fixture where you paste a
159
+ token obtained manually.
160
+
161
+ ### Environment variables and missing credentials
162
+
163
+ Any auth value may reference an environment variable with `${VAR}` syntax, so
164
+ secrets stay out of the config file; a referenced variable that is not set is
165
+ an error. If a specification requires a scheme whose credentials are not
166
+ configured, SwaggerForge prints a warning naming the scheme and generates the
167
+ tests without that authentication rather than failing.
168
+
88
169
  ## How it works
89
170
 
90
171
  SwaggerForge runs a six-stage pipeline: the specification is validated, parsed
@@ -95,7 +176,10 @@ written to per-resource files.
95
176
  ## Limitations
96
177
 
97
178
  - Targets OpenAPI 3.x with JSON request bodies
98
- - Authentication is not yet handled (planned)
179
+ - Authentication covers bearer, basic, API key (header/query/cookie), OAuth2
180
+ client credentials, and OpenID Connect; interactive OAuth2 flows
181
+ (authorization code) generate a placeholder for a manually obtained token,
182
+ as they require a browser-based login that cannot be automated
99
183
  - Boundary tests require the specification to declare numeric/length constraints
100
184
 
101
185
  ## License
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "swaggerforge"
7
- version = "0.2.0"
7
+ version = "0.3.1"
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"]
@@ -0,0 +1,274 @@
1
+ """Resolution of authentication for generated tests.
2
+
3
+ Given an endpoint's declared security requirement, the specification's
4
+ security schemes, and the user's configured credentials, this module
5
+ decides which authentication headers to inject into the generated tests.
6
+
7
+ Supported schemes (v0.3.0): HTTP bearer, HTTP basic, and API key in a
8
+ header. OAuth2 is treated as bearer (the user supplies a pre-obtained
9
+ token). Other schemes - API key in query or cookie, OpenID Connect,
10
+ and any undefined scheme name - are skipped with a warning.
11
+ """
12
+
13
+ import base64
14
+ from typing import NamedTuple
15
+
16
+
17
+ class OAuth2Fixture(NamedTuple):
18
+ """The parameters needed to generate an OAuth2 client-credentials fixture.
19
+
20
+ Exactly one of ``token_url`` (a direct clientCredentials token endpoint
21
+ from the spec) or ``discovery_url`` (an OpenID Connect discovery
22
+ endpoint, resolved at runtime) is set. The client credentials and
23
+ optional scope come from the configuration. The generated test fetches
24
+ a token at runtime rather than embedding one.
25
+ """
26
+
27
+ token_url: str | None = None
28
+ client_id: str = ""
29
+ client_secret: str = ""
30
+ discovery_url: str | None = None
31
+ scope: str | None = None
32
+
33
+
34
+ class ResolvedAuth(NamedTuple):
35
+ """The outcome of resolving auth for one endpoint.
36
+
37
+ ``headers``, ``query_params`` and ``cookies`` hold the static
38
+ authentication values to inject, by location (each empty when it does
39
+ not apply). ``oauth2`` carries the parameters for a runtime
40
+ token-fetching fixture, or None. ``warning`` is a human-readable
41
+ message when a declared scheme was skipped, or None otherwise.
42
+ """
43
+
44
+ headers: dict = {}
45
+ query_params: dict = {}
46
+ cookies: dict = {}
47
+ oauth2: OAuth2Fixture | None = None
48
+ manual_auth: str | None = None
49
+ warning: str | None = None
50
+
51
+
52
+ _NO_AUTH = ResolvedAuth(headers={}, warning=None)
53
+
54
+
55
+ def resolve_auth(endpoint, security_schemes, auth_config):
56
+ """Resolve authentication for a single endpoint.
57
+
58
+ The endpoint's security is a list of alternatives (OR); each
59
+ alternative is a list of scheme names that must all be satisfied
60
+ together (AND). The first alternative whose schemes are all
61
+ resolvable is used, merging every scheme's contribution. If no
62
+ alternative is fully satisfiable, generation proceeds without
63
+ authentication and a warning is returned.
64
+
65
+ Args:
66
+ endpoint: The Endpoint whose security requirement is resolved.
67
+ security_schemes: The specification's securitySchemes definitions.
68
+ auth_config: The AuthConfig carrying user-supplied credentials.
69
+
70
+ Returns:
71
+ A ResolvedAuth with the values to inject and an optional warning.
72
+ """
73
+ if not endpoint.security:
74
+ return _NO_AUTH
75
+
76
+ for alternative in endpoint.security:
77
+ merged = _resolve_alternative(alternative, security_schemes, auth_config)
78
+ if merged is not None:
79
+ if merged.manual_auth is not None and merged.warning is None:
80
+ merged = merged._replace(
81
+ warning=(
82
+ f"Scheme '{merged.manual_auth}' uses an interactive "
83
+ f"OAuth2 flow that cannot be automated; a placeholder "
84
+ f"token is generated for you to complete manually."
85
+ )
86
+ )
87
+ return merged
88
+
89
+ scheme_names = sorted(
90
+ {name for alternative in endpoint.security for name in alternative}
91
+ )
92
+ listed = ", ".join(scheme_names) if scheme_names else "the declared schemes"
93
+ return ResolvedAuth(
94
+ warning=(
95
+ f"Could not satisfy security requirement(s) [{listed}] with the "
96
+ f"configured credentials; affected endpoints are generated "
97
+ f"without authentication."
98
+ ),
99
+ )
100
+
101
+
102
+ def _resolve_alternative(alternative, security_schemes, auth_config):
103
+ """Resolve one AND group, merging all schemes, or None if unsatisfiable.
104
+
105
+ An empty alternative (an empty security requirement in the spec) means
106
+ authentication is optional and resolves to no auth.
107
+ """
108
+ headers = {}
109
+ query_params = {}
110
+ cookies = {}
111
+ oauth2 = None
112
+ manual_auth = None
113
+ for scheme_name in alternative:
114
+ contribution = _resolve_scheme(
115
+ scheme_name, security_schemes, auth_config
116
+ )
117
+ if contribution is None:
118
+ return None
119
+ (
120
+ scheme_headers,
121
+ scheme_query,
122
+ scheme_cookies,
123
+ scheme_oauth2,
124
+ scheme_manual,
125
+ ) = contribution
126
+ headers.update(scheme_headers)
127
+ query_params.update(scheme_query)
128
+ cookies.update(scheme_cookies)
129
+ if scheme_oauth2 is not None:
130
+ oauth2 = scheme_oauth2
131
+ if scheme_manual is not None:
132
+ manual_auth = scheme_manual
133
+
134
+ return ResolvedAuth(
135
+ headers=headers,
136
+ query_params=query_params,
137
+ cookies=cookies,
138
+ oauth2=oauth2,
139
+ manual_auth=manual_auth,
140
+ )
141
+
142
+
143
+ def _resolve_scheme(scheme_name, security_schemes, auth_config):
144
+ """Resolve one scheme to a (headers, query_params, cookies, oauth2) tuple.
145
+
146
+ Returns None when the scheme is undefined, unsupported, or its
147
+ credentials are not configured - signalling that the enclosing
148
+ alternative cannot be satisfied.
149
+ """
150
+ scheme = security_schemes.get(scheme_name)
151
+ if scheme is None:
152
+ return None
153
+
154
+ scheme_type = scheme.get("type")
155
+
156
+ if scheme_type == "oauth2":
157
+ return _resolve_oauth2(scheme_name, scheme, auth_config)
158
+ if scheme_type == "openIdConnect":
159
+ return _resolve_oidc(scheme, auth_config)
160
+ if scheme_type == "http":
161
+ http_scheme = scheme.get("scheme", "").lower()
162
+ if http_scheme == "bearer":
163
+ return _resolve_bearer(auth_config)
164
+ if http_scheme == "basic":
165
+ return _resolve_basic(auth_config)
166
+ if scheme_type == "apiKey" and scheme.get("in") in (
167
+ "header",
168
+ "query",
169
+ "cookie",
170
+ ):
171
+ return _resolve_api_key(scheme, auth_config)
172
+
173
+ return None
174
+
175
+
176
+ def _resolve_bearer(auth_config):
177
+ """Return a bearer Authorization header, or None if unconfigured."""
178
+ bearer = auth_config.bearer
179
+ if bearer is None or not bearer.token:
180
+ return None
181
+ return ({"Authorization": f"Bearer {bearer.token}"}, {}, {}, None, None)
182
+
183
+
184
+ def _resolve_basic(auth_config):
185
+ """Return a basic Authorization header, or None if unconfigured."""
186
+ basic = auth_config.basic
187
+ if basic is None or not basic.username or not basic.password:
188
+ return None
189
+ raw = f"{basic.username}:{basic.password}".encode("utf-8")
190
+ encoded = base64.b64encode(raw).decode("ascii")
191
+ return ({"Authorization": f"Basic {encoded}"}, {}, {}, None, None)
192
+
193
+
194
+ def _resolve_api_key(scheme, auth_config):
195
+ """Return an API-key contribution by location, or None if unconfigured.
196
+
197
+ The key's name and location come from the spec scheme; the config
198
+ supplies only the value.
199
+ """
200
+ api_key = auth_config.api_key
201
+ if api_key is None or not api_key.value:
202
+ return None
203
+ key_name = scheme.get("name")
204
+ if not key_name:
205
+ return None
206
+
207
+ location = scheme.get("in")
208
+ if location == "query":
209
+ return ({}, {key_name: api_key.value}, {}, None, None)
210
+ if location == "cookie":
211
+ return ({}, {}, {key_name: api_key.value}, None, None)
212
+ return ({key_name: api_key.value}, {}, {}, None, None)
213
+
214
+
215
+ def _resolve_oauth2(scheme_name, scheme, auth_config):
216
+ """Resolve an oauth2 scheme.
217
+
218
+ The clientCredentials flow is fully automated (it needs the spec's
219
+ tokenUrl and the configured client id and secret). When only an
220
+ interactive flow such as authorizationCode is offered, it cannot be
221
+ automated, so a manual stub naming the scheme is produced for the user
222
+ to complete. Purely interactive-only flows with no authorizationCode
223
+ (implicit, password) are not supported and make the alternative
224
+ unsatisfiable.
225
+ """
226
+ flows = scheme.get("flows", {})
227
+
228
+ client_credentials = flows.get("clientCredentials")
229
+ if client_credentials is not None:
230
+ token_url = client_credentials.get("tokenUrl")
231
+ oauth2 = auth_config.oauth2
232
+ if (
233
+ token_url
234
+ and oauth2 is not None
235
+ and oauth2.client_id
236
+ and oauth2.client_secret
237
+ ):
238
+ fixture = OAuth2Fixture(
239
+ token_url=token_url,
240
+ client_id=oauth2.client_id,
241
+ client_secret=oauth2.client_secret,
242
+ scope=oauth2.scope,
243
+ )
244
+ return ({}, {}, {}, fixture, None)
245
+
246
+ if "authorizationCode" in flows:
247
+ return ({}, {}, {}, None, scheme_name)
248
+
249
+ return None
250
+
251
+
252
+ def _resolve_oidc(scheme, auth_config):
253
+ """Resolve an openIdConnect scheme to a discovery-based OAuth2 fixture.
254
+
255
+ The generated test performs OpenID Connect discovery at runtime using
256
+ the scheme's openIdConnectUrl, then obtains a token via the client
257
+ credentials grant. It therefore needs configured client credentials;
258
+ without them the scheme is not satisfiable.
259
+ """
260
+ discovery_url = scheme.get("openIdConnectUrl")
261
+ if not discovery_url:
262
+ return None
263
+
264
+ oauth2 = auth_config.oauth2
265
+ if oauth2 is None or not oauth2.client_id or not oauth2.client_secret:
266
+ return None
267
+
268
+ fixture = OAuth2Fixture(
269
+ discovery_url=discovery_url,
270
+ client_id=oauth2.client_id,
271
+ client_secret=oauth2.client_secret,
272
+ scope=oauth2.scope,
273
+ )
274
+ return ({}, {}, {}, fixture, None)
@@ -68,7 +68,8 @@ def generate(spec, url, output, config_path):
68
68
  click.echo(f"API title: {spec_dict['info']['title']}")
69
69
 
70
70
  try:
71
- endpoints = parse_spec(spec)
71
+ parsed = parse_spec(spec)
72
+ endpoints = parsed.endpoints
72
73
  except SpecParseError as error:
73
74
  raise click.ClickException(str(error))
74
75
 
@@ -79,12 +80,22 @@ def generate(spec, url, output, config_path):
79
80
  click.echo(f"Generated {len(scenarios)} test scenarios.")
80
81
 
81
82
  try:
82
- written = write_test_files(scenarios, url, output, config.timeout)
83
+ result = write_test_files(
84
+ scenarios,
85
+ url,
86
+ output,
87
+ config.timeout,
88
+ security_schemes=parsed.security_schemes,
89
+ auth_config=config.auth,
90
+ )
83
91
  except OSError as error:
84
92
  raise click.ClickException(
85
93
  f"Could not write test files to '{output}': {error}"
86
94
  )
87
95
 
88
- click.echo(f"Wrote {len(written)} test file(s) to '{output}':")
89
- for path in written:
96
+ for warning in result.warnings:
97
+ click.echo(f"Warning: {warning}")
98
+
99
+ click.echo(f"Wrote {len(result.written)} test file(s) to '{output}':")
100
+ for path in result.written:
90
101
  click.echo(f" - {path}")