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.
- {swaggerforge-0.2.0/src/swaggerforge.egg-info → swaggerforge-0.3.1}/PKG-INFO +88 -2
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/README.md +85 -1
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/pyproject.toml +2 -1
- swaggerforge-0.3.1/src/swaggerforge/auth.py +274 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/cli.py +15 -4
- swaggerforge-0.3.1/src/swaggerforge/config.py +219 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/models.py +1 -0
- swaggerforge-0.3.1/src/swaggerforge/output.py +123 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/parser.py +35 -3
- swaggerforge-0.3.1/src/swaggerforge/template.py +215 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/templates/test_file.py.j2 +14 -3
- {swaggerforge-0.2.0 → swaggerforge-0.3.1/src/swaggerforge.egg-info}/PKG-INFO +88 -2
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge.egg-info/SOURCES.txt +3 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge.egg-info/requires.txt +3 -0
- swaggerforge-0.3.1/tests/test_auth.py +523 -0
- swaggerforge-0.3.1/tests/test_config.py +408 -0
- swaggerforge-0.3.1/tests/test_e2e_auth.py +253 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/tests/test_generator.py +4 -4
- swaggerforge-0.3.1/tests/test_output.py +195 -0
- swaggerforge-0.3.1/tests/test_parser.py +240 -0
- swaggerforge-0.3.1/tests/test_template.py +290 -0
- swaggerforge-0.2.0/src/swaggerforge/config.py +0 -102
- swaggerforge-0.2.0/src/swaggerforge/output.py +0 -53
- swaggerforge-0.2.0/src/swaggerforge/template.py +0 -121
- swaggerforge-0.2.0/tests/test_config.py +0 -120
- swaggerforge-0.2.0/tests/test_output.py +0 -46
- swaggerforge-0.2.0/tests/test_parser.py +0 -79
- swaggerforge-0.2.0/tests/test_template.py +0 -97
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/LICENSE +0 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/setup.cfg +0 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/__init__.py +0 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/__main__.py +0 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/generator.py +0 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge/validator.py +0 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge.egg-info/dependency_links.txt +0 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge.egg-info/entry_points.txt +0 -0
- {swaggerforge-0.2.0 → swaggerforge-0.3.1}/src/swaggerforge.egg-info/top_level.txt +0 -0
- {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.
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
-
|
|
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}")
|