schemathesis 4.1.3__py3-none-any.whl → 4.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- schemathesis/cli/commands/run/executor.py +1 -1
- schemathesis/cli/commands/run/handlers/base.py +28 -1
- schemathesis/cli/commands/run/handlers/cassettes.py +10 -12
- schemathesis/cli/commands/run/handlers/junitxml.py +5 -6
- schemathesis/cli/commands/run/handlers/output.py +7 -1
- schemathesis/cli/ext/fs.py +1 -1
- schemathesis/config/_diff_base.py +3 -1
- schemathesis/config/_operations.py +2 -0
- schemathesis/config/_phases.py +21 -4
- schemathesis/config/_projects.py +10 -2
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/errors.py +29 -5
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +163 -0
- schemathesis/{specs/openapi/constants.py → core/jsonschema/keywords.py} +0 -8
- schemathesis/core/jsonschema/references.py +122 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/media_types.py +6 -4
- schemathesis/core/parameters.py +37 -0
- schemathesis/core/transforms.py +25 -2
- schemathesis/core/validation.py +19 -0
- schemathesis/engine/context.py +1 -1
- schemathesis/engine/errors.py +11 -18
- schemathesis/engine/phases/stateful/_executor.py +1 -1
- schemathesis/engine/phases/unit/_executor.py +30 -13
- schemathesis/errors.py +4 -0
- schemathesis/filters.py +2 -2
- schemathesis/generation/coverage.py +89 -13
- schemathesis/generation/hypothesis/__init__.py +4 -1
- schemathesis/generation/hypothesis/builder.py +108 -70
- schemathesis/generation/meta.py +5 -14
- schemathesis/generation/overrides.py +17 -17
- schemathesis/pytest/lazy.py +1 -1
- schemathesis/pytest/plugin.py +1 -6
- schemathesis/schemas.py +22 -72
- schemathesis/specs/graphql/schemas.py +27 -16
- schemathesis/specs/openapi/_hypothesis.py +83 -68
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +504 -0
- schemathesis/specs/openapi/adapter/protocol.py +57 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +329 -0
- schemathesis/specs/openapi/adapter/security.py +141 -0
- schemathesis/specs/openapi/adapter/v2.py +28 -0
- schemathesis/specs/openapi/adapter/v3_0.py +28 -0
- schemathesis/specs/openapi/adapter/v3_1.py +28 -0
- schemathesis/specs/openapi/checks.py +99 -90
- schemathesis/specs/openapi/converter.py +114 -27
- schemathesis/specs/openapi/examples.py +210 -168
- schemathesis/specs/openapi/negative/__init__.py +12 -7
- schemathesis/specs/openapi/negative/mutations.py +68 -40
- schemathesis/specs/openapi/references.py +2 -175
- schemathesis/specs/openapi/schemas.py +142 -490
- schemathesis/specs/openapi/serialization.py +15 -7
- schemathesis/specs/openapi/stateful/__init__.py +17 -12
- schemathesis/specs/openapi/stateful/inference.py +13 -11
- schemathesis/specs/openapi/stateful/links.py +5 -20
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/v3.py +68 -0
- schemathesis/specs/openapi/utils.py +1 -13
- schemathesis/transport/requests.py +3 -11
- schemathesis/transport/serialization.py +63 -27
- schemathesis/transport/wsgi.py +1 -8
- {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/METADATA +2 -2
- {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/RECORD +68 -53
- schemathesis/specs/openapi/parameters.py +0 -405
- schemathesis/specs/openapi/security.py +0 -162
- {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/WHEEL +0 -0
- {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.1.3.dist-info → schemathesis-4.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,329 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import TYPE_CHECKING, Any, ItemsView, Iterator, Mapping, cast
|
5
|
+
|
6
|
+
from schemathesis.core import NOT_SET, NotSet
|
7
|
+
from schemathesis.core.compat import RefResolutionError, RefResolver
|
8
|
+
from schemathesis.core.errors import InvalidSchema
|
9
|
+
from schemathesis.core.jsonschema.bundler import bundle
|
10
|
+
from schemathesis.core.jsonschema.types import JsonSchema
|
11
|
+
from schemathesis.specs.openapi import types
|
12
|
+
from schemathesis.specs.openapi.adapter.protocol import SpecificationAdapter
|
13
|
+
from schemathesis.specs.openapi.adapter.references import maybe_resolve
|
14
|
+
from schemathesis.specs.openapi.converter import to_json_schema
|
15
|
+
from schemathesis.specs.openapi.utils import expand_status_code
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from jsonschema.protocols import Validator
|
19
|
+
|
20
|
+
|
21
|
+
@dataclass
|
22
|
+
class OpenApiResponse:
|
23
|
+
"""OpenAPI response definition."""
|
24
|
+
|
25
|
+
status_code: str
|
26
|
+
definition: Mapping[str, Any]
|
27
|
+
resolver: RefResolver
|
28
|
+
scope: str
|
29
|
+
adapter: SpecificationAdapter
|
30
|
+
|
31
|
+
__slots__ = ("status_code", "definition", "resolver", "scope", "adapter", "_schema", "_validator", "_headers")
|
32
|
+
|
33
|
+
def __post_init__(self) -> None:
|
34
|
+
self._schema: JsonSchema | None | NotSet = NOT_SET
|
35
|
+
self._validator: Validator | NotSet = NOT_SET
|
36
|
+
self._headers: OpenApiResponseHeaders | NotSet = NOT_SET
|
37
|
+
|
38
|
+
@property
|
39
|
+
def schema(self) -> JsonSchema | None:
|
40
|
+
"""The response body schema extracted from its definition.
|
41
|
+
|
42
|
+
Returns `None` if the response has no schema.
|
43
|
+
"""
|
44
|
+
if self._schema is NOT_SET:
|
45
|
+
self._schema = self.adapter.extract_response_schema(
|
46
|
+
self.definition, self.resolver, self.scope, self.adapter.nullable_keyword
|
47
|
+
)
|
48
|
+
assert not isinstance(self._schema, NotSet)
|
49
|
+
return self._schema
|
50
|
+
|
51
|
+
@property
|
52
|
+
def validator(self) -> Validator | None:
|
53
|
+
"""JSON Schema validator for this response."""
|
54
|
+
from jsonschema import Draft202012Validator
|
55
|
+
|
56
|
+
schema = self.schema
|
57
|
+
if schema is None:
|
58
|
+
return None
|
59
|
+
if self._validator is NOT_SET:
|
60
|
+
self.adapter.jsonschema_validator_cls.check_schema(schema)
|
61
|
+
self._validator = self.adapter.jsonschema_validator_cls(
|
62
|
+
schema,
|
63
|
+
# Use a recent JSON Schema format checker to get most of formats checked for older drafts as well
|
64
|
+
format_checker=Draft202012Validator.FORMAT_CHECKER,
|
65
|
+
resolver=RefResolver.from_schema(schema),
|
66
|
+
)
|
67
|
+
assert not isinstance(self._validator, NotSet)
|
68
|
+
return self._validator
|
69
|
+
|
70
|
+
@property
|
71
|
+
def headers(self) -> OpenApiResponseHeaders:
|
72
|
+
"""A collection of header definitions for this response."""
|
73
|
+
if self._headers is NOT_SET:
|
74
|
+
headers = self.definition.get("headers", {})
|
75
|
+
self._headers = OpenApiResponseHeaders(
|
76
|
+
dict(_iter_resolved_headers(headers, self.resolver, self.scope, self.adapter))
|
77
|
+
)
|
78
|
+
assert not isinstance(self._headers, NotSet)
|
79
|
+
return self._headers
|
80
|
+
|
81
|
+
def iter_examples(self) -> Iterator[tuple[str, object]]:
|
82
|
+
"""Iterate over examples of this response."""
|
83
|
+
return self.adapter.iter_response_examples(self.definition, self.status_code)
|
84
|
+
|
85
|
+
def iter_links(self) -> Iterator[tuple[str, Mapping[str, Any]]]:
|
86
|
+
links = self.definition.get(self.adapter.links_keyword)
|
87
|
+
if links is None:
|
88
|
+
return
|
89
|
+
for name, link in links.items():
|
90
|
+
_, link = maybe_resolve(link, self.resolver, self.scope)
|
91
|
+
yield name, link
|
92
|
+
|
93
|
+
|
94
|
+
@dataclass
|
95
|
+
class OpenApiResponses:
|
96
|
+
"""Collection of OpenAPI response definitions."""
|
97
|
+
|
98
|
+
_inner: dict[str, OpenApiResponse]
|
99
|
+
resolver: RefResolver
|
100
|
+
scope: str
|
101
|
+
adapter: SpecificationAdapter
|
102
|
+
|
103
|
+
__slots__ = ("_inner", "resolver", "scope", "adapter")
|
104
|
+
|
105
|
+
@classmethod
|
106
|
+
def from_definition(
|
107
|
+
cls, definition: types.v3.Responses, resolver: RefResolver, scope: str, adapter: SpecificationAdapter
|
108
|
+
) -> OpenApiResponses:
|
109
|
+
"""Build new collection of responses from their raw definition."""
|
110
|
+
# TODO: Add also `v2` type
|
111
|
+
return cls(
|
112
|
+
dict(_iter_resolved_responses(definition=definition, resolver=resolver, scope=scope, adapter=adapter)),
|
113
|
+
resolver=resolver,
|
114
|
+
scope=scope,
|
115
|
+
adapter=adapter,
|
116
|
+
)
|
117
|
+
|
118
|
+
def items(self) -> ItemsView[str, OpenApiResponse]:
|
119
|
+
return self._inner.items()
|
120
|
+
|
121
|
+
def add(self, status_code: str, definition: dict[str, Any]) -> OpenApiResponse:
|
122
|
+
instance = OpenApiResponse(
|
123
|
+
status_code=status_code,
|
124
|
+
definition=definition,
|
125
|
+
resolver=self.resolver,
|
126
|
+
scope=self.scope,
|
127
|
+
adapter=self.adapter,
|
128
|
+
)
|
129
|
+
self._inner[status_code] = instance
|
130
|
+
return instance
|
131
|
+
|
132
|
+
@property
|
133
|
+
def status_codes(self) -> tuple[str, ...]:
|
134
|
+
"""All defined status codes."""
|
135
|
+
# Defined as a tuple, so it can be used in a cache key
|
136
|
+
return tuple(self._inner)
|
137
|
+
|
138
|
+
def find_by_status_code(self, status_code: int) -> OpenApiResponse | None:
|
139
|
+
"""Find the most specific response definition matching the given HTTP status code."""
|
140
|
+
responses = self._inner
|
141
|
+
# Full match has the highest priority
|
142
|
+
full_match = responses.get(str(status_code))
|
143
|
+
if full_match is not None:
|
144
|
+
return full_match
|
145
|
+
# Then, ones with wildcards
|
146
|
+
keys = sorted(responses, key=lambda k: k.count("X"))
|
147
|
+
for key in keys:
|
148
|
+
if key == "default":
|
149
|
+
continue
|
150
|
+
status_codes = expand_status_code(key)
|
151
|
+
if status_code in status_codes:
|
152
|
+
return responses[key]
|
153
|
+
# The default response has the lowest priority
|
154
|
+
return responses.get("default")
|
155
|
+
|
156
|
+
def iter_examples(self) -> Iterator[tuple[str, object]]:
|
157
|
+
"""Iterate over all examples for all responses."""
|
158
|
+
for response in self._inner.values():
|
159
|
+
# Check only 2xx responses
|
160
|
+
if response.status_code.startswith("2"):
|
161
|
+
yield from response.iter_examples()
|
162
|
+
|
163
|
+
|
164
|
+
def _iter_resolved_responses(
|
165
|
+
definition: types.v3.Responses, resolver: RefResolver, scope: str, adapter: SpecificationAdapter
|
166
|
+
) -> Iterator[tuple[str, OpenApiResponse]]:
|
167
|
+
for key, response in definition.items():
|
168
|
+
status_code = str(key)
|
169
|
+
scope, resolved = maybe_resolve(response, resolver, scope)
|
170
|
+
yield (
|
171
|
+
status_code,
|
172
|
+
OpenApiResponse(
|
173
|
+
status_code=status_code, definition=resolved, resolver=resolver, scope=scope, adapter=adapter
|
174
|
+
),
|
175
|
+
)
|
176
|
+
|
177
|
+
|
178
|
+
def extract_response_schema_v2(
|
179
|
+
response: Mapping[str, Any], resolver: RefResolver, scope: str, nullable_keyword: str
|
180
|
+
) -> JsonSchema | None:
|
181
|
+
schema = response.get("schema")
|
182
|
+
if schema is not None:
|
183
|
+
return _prepare_schema(schema, resolver, scope, nullable_keyword)
|
184
|
+
return None
|
185
|
+
|
186
|
+
|
187
|
+
def extract_response_schema_v3(
|
188
|
+
response: Mapping[str, Any], resolver: RefResolver, scope: str, nullable_keyword: str
|
189
|
+
) -> JsonSchema | None:
|
190
|
+
options = iter(response.get("content", {}).values())
|
191
|
+
media_type = next(options, None)
|
192
|
+
# "schema" is an optional key in the `MediaType` object
|
193
|
+
if media_type and "schema" in media_type:
|
194
|
+
return _prepare_schema(media_type["schema"], resolver, scope, nullable_keyword)
|
195
|
+
return None
|
196
|
+
|
197
|
+
|
198
|
+
def _prepare_schema(schema: JsonSchema, resolver: RefResolver, scope: str, nullable_keyword: str) -> JsonSchema:
|
199
|
+
schema = _bundle_in_scope(schema, resolver, scope)
|
200
|
+
# Do not clone the schema, as bundling already does it
|
201
|
+
return to_json_schema(schema, nullable_keyword, is_response_schema=True, update_quantifiers=False, clone=False)
|
202
|
+
|
203
|
+
|
204
|
+
def _bundle_in_scope(schema: JsonSchema, resolver: RefResolver, scope: str) -> JsonSchema:
|
205
|
+
resolver.push_scope(scope)
|
206
|
+
try:
|
207
|
+
return bundle(schema, resolver, inline_recursive=False)
|
208
|
+
except RefResolutionError as exc:
|
209
|
+
raise InvalidSchema.from_reference_resolution_error(exc, None, None) from None
|
210
|
+
finally:
|
211
|
+
resolver.pop_scope()
|
212
|
+
|
213
|
+
|
214
|
+
@dataclass
|
215
|
+
class OpenApiResponseHeaders:
|
216
|
+
"""Collection of OpenAPI response header definitions."""
|
217
|
+
|
218
|
+
_inner: dict[str, OpenApiResponseHeader]
|
219
|
+
|
220
|
+
__slots__ = ("_inner",)
|
221
|
+
|
222
|
+
def __bool__(self) -> bool:
|
223
|
+
return bool(self._inner)
|
224
|
+
|
225
|
+
def items(self) -> ItemsView[str, OpenApiResponseHeader]:
|
226
|
+
return self._inner.items()
|
227
|
+
|
228
|
+
|
229
|
+
@dataclass
|
230
|
+
class OpenApiResponseHeader:
|
231
|
+
"""OpenAPI response header definition."""
|
232
|
+
|
233
|
+
name: str
|
234
|
+
definition: Mapping[str, Any]
|
235
|
+
resolver: RefResolver
|
236
|
+
scope: str
|
237
|
+
adapter: SpecificationAdapter
|
238
|
+
|
239
|
+
__slots__ = ("name", "definition", "resolver", "scope", "adapter", "_schema", "_validator")
|
240
|
+
|
241
|
+
def __post_init__(self) -> None:
|
242
|
+
self._schema: JsonSchema | NotSet = NOT_SET
|
243
|
+
self._validator: Validator | NotSet = NOT_SET
|
244
|
+
|
245
|
+
@property
|
246
|
+
def is_required(self) -> bool:
|
247
|
+
return self.definition.get(self.adapter.header_required_keyword, False)
|
248
|
+
|
249
|
+
@property
|
250
|
+
def schema(self) -> JsonSchema:
|
251
|
+
"""The header schema extracted from its definition."""
|
252
|
+
if self._schema is NOT_SET:
|
253
|
+
self._schema = self.adapter.extract_header_schema(
|
254
|
+
self.definition, self.resolver, self.scope, self.adapter.nullable_keyword
|
255
|
+
)
|
256
|
+
assert not isinstance(self._schema, NotSet)
|
257
|
+
return self._schema
|
258
|
+
|
259
|
+
@property
|
260
|
+
def validator(self) -> Validator:
|
261
|
+
"""JSON Schema validator for this header."""
|
262
|
+
from jsonschema import Draft202012Validator
|
263
|
+
|
264
|
+
schema = self.schema
|
265
|
+
if self._validator is NOT_SET:
|
266
|
+
self.adapter.jsonschema_validator_cls.check_schema(schema)
|
267
|
+
self._validator = self.adapter.jsonschema_validator_cls(
|
268
|
+
schema,
|
269
|
+
# Use a recent JSON Schema format checker to get most of formats checked for older drafts as well
|
270
|
+
format_checker=Draft202012Validator.FORMAT_CHECKER,
|
271
|
+
resolver=RefResolver.from_schema(schema),
|
272
|
+
)
|
273
|
+
assert not isinstance(self._validator, NotSet)
|
274
|
+
return self._validator
|
275
|
+
|
276
|
+
|
277
|
+
def _iter_resolved_headers(
|
278
|
+
definition: types.v3.Headers, resolver: RefResolver, scope: str, adapter: SpecificationAdapter
|
279
|
+
) -> Iterator[tuple[str, OpenApiResponseHeader]]:
|
280
|
+
for name, header in definition.items():
|
281
|
+
scope, resolved = maybe_resolve(header, resolver, scope)
|
282
|
+
yield (
|
283
|
+
name,
|
284
|
+
OpenApiResponseHeader(name=name, definition=resolved, resolver=resolver, scope=scope, adapter=adapter),
|
285
|
+
)
|
286
|
+
|
287
|
+
|
288
|
+
def extract_header_schema_v2(
|
289
|
+
header: Mapping[str, Any], resolver: RefResolver, scope: str, nullable_keyword: str
|
290
|
+
) -> JsonSchema:
|
291
|
+
return _prepare_schema(cast(dict, header), resolver, scope, nullable_keyword)
|
292
|
+
|
293
|
+
|
294
|
+
def extract_header_schema_v3(
|
295
|
+
header: Mapping[str, Any], resolver: RefResolver, scope: str, nullable_keyword: str
|
296
|
+
) -> JsonSchema:
|
297
|
+
schema = header.get("schema", {})
|
298
|
+
return _prepare_schema(schema, resolver, scope, nullable_keyword)
|
299
|
+
|
300
|
+
|
301
|
+
def iter_response_examples_v2(response: Mapping[str, Any], status_code: str) -> Iterator[tuple[str, object]]:
|
302
|
+
# In Swagger 2.0, examples are directly in the response under "examples"
|
303
|
+
examples = response.get("examples", {})
|
304
|
+
return iter(examples.items())
|
305
|
+
|
306
|
+
|
307
|
+
def iter_response_examples_v3(response: Mapping[str, Any], status_code: str) -> Iterator[tuple[str, object]]:
|
308
|
+
for media_type, definition in response.get("content", {}).items():
|
309
|
+
# Try to get a more descriptive example name from the `$ref` value
|
310
|
+
schema_ref = definition.get("schema", {}).get("$ref")
|
311
|
+
if schema_ref:
|
312
|
+
name = schema_ref.split("/")[-1]
|
313
|
+
else:
|
314
|
+
name = f"{status_code}/{media_type}"
|
315
|
+
|
316
|
+
for examples_container_keyword, example_keyword in (
|
317
|
+
("examples", "example"),
|
318
|
+
("x-examples", "x-example"),
|
319
|
+
):
|
320
|
+
examples = definition.get(examples_container_keyword, {})
|
321
|
+
if isinstance(examples, dict):
|
322
|
+
for example in examples.values():
|
323
|
+
if "value" in example:
|
324
|
+
yield name, example["value"]
|
325
|
+
elif isinstance(examples, list):
|
326
|
+
for example in examples:
|
327
|
+
yield name, example
|
328
|
+
if example_keyword in definition:
|
329
|
+
yield name, definition[example_keyword]
|
@@ -0,0 +1,141 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
from dataclasses import dataclass
|
4
|
+
from typing import TYPE_CHECKING, Any, Iterator, Mapping
|
5
|
+
|
6
|
+
if TYPE_CHECKING:
|
7
|
+
from schemathesis.core.compat import RefResolver
|
8
|
+
from schemathesis.specs.openapi.adapter.protocol import SpecificationAdapter
|
9
|
+
|
10
|
+
ORIGINAL_SECURITY_TYPE_KEY = "x-original-securuty-type"
|
11
|
+
|
12
|
+
|
13
|
+
@dataclass
|
14
|
+
class OpenApiSecurityParameters:
|
15
|
+
"""Security parameters for an API operation."""
|
16
|
+
|
17
|
+
_parameters: list[Mapping[str, Any]]
|
18
|
+
|
19
|
+
__slots__ = ("_parameters",)
|
20
|
+
|
21
|
+
@classmethod
|
22
|
+
def from_definition(
|
23
|
+
cls,
|
24
|
+
schema: Mapping[str, Any],
|
25
|
+
operation: Mapping[str, Any],
|
26
|
+
resolver: RefResolver,
|
27
|
+
adapter: SpecificationAdapter,
|
28
|
+
) -> OpenApiSecurityParameters:
|
29
|
+
return cls(list(adapter.extract_security_parameters(schema, operation, resolver)))
|
30
|
+
|
31
|
+
def iter_parameters(self) -> Iterator[Mapping[str, Any]]:
|
32
|
+
return iter(self._parameters)
|
33
|
+
|
34
|
+
|
35
|
+
def extract_security_parameters_v2(
|
36
|
+
schema: Mapping[str, Any], operation: Mapping[str, Any], resolver: RefResolver
|
37
|
+
) -> Iterator[Mapping[str, Any]]:
|
38
|
+
"""Extract all required security parameters for this operation."""
|
39
|
+
defined = extract_security_definitions_v2(schema, resolver)
|
40
|
+
required = get_security_requirements(schema, operation)
|
41
|
+
optional = has_optional_auth(schema, operation)
|
42
|
+
|
43
|
+
for key in required:
|
44
|
+
if key not in defined:
|
45
|
+
continue
|
46
|
+
definition = defined[key]
|
47
|
+
ty = definition["type"]
|
48
|
+
|
49
|
+
if ty == "apiKey":
|
50
|
+
param = make_api_key_schema(definition, type="string")
|
51
|
+
elif ty == "basic":
|
52
|
+
parameter_schema = make_auth_header_schema(definition)
|
53
|
+
param = make_auth_header(**parameter_schema)
|
54
|
+
else:
|
55
|
+
continue
|
56
|
+
|
57
|
+
param[ORIGINAL_SECURITY_TYPE_KEY] = ty
|
58
|
+
|
59
|
+
if optional:
|
60
|
+
param = {**param, "required": False}
|
61
|
+
|
62
|
+
yield param
|
63
|
+
|
64
|
+
|
65
|
+
def extract_security_parameters_v3(
|
66
|
+
schema: Mapping[str, Any],
|
67
|
+
operation: Mapping[str, Any],
|
68
|
+
resolver: RefResolver,
|
69
|
+
) -> Iterator[Mapping[str, Any]]:
|
70
|
+
"""Extract all required security parameters for this operation."""
|
71
|
+
defined = extract_security_definitions_v3(schema, resolver)
|
72
|
+
required = get_security_requirements(schema, operation)
|
73
|
+
optional = has_optional_auth(schema, operation)
|
74
|
+
|
75
|
+
for key in required:
|
76
|
+
if key not in defined:
|
77
|
+
continue
|
78
|
+
definition = defined[key]
|
79
|
+
ty = definition["type"]
|
80
|
+
|
81
|
+
if ty == "apiKey":
|
82
|
+
param = make_api_key_schema(definition, schema={"type": "string"})
|
83
|
+
elif ty == "http":
|
84
|
+
parameter_schema = make_auth_header_schema(definition)
|
85
|
+
param = make_auth_header(schema=parameter_schema)
|
86
|
+
else:
|
87
|
+
continue
|
88
|
+
|
89
|
+
param[ORIGINAL_SECURITY_TYPE_KEY] = ty
|
90
|
+
|
91
|
+
if optional:
|
92
|
+
param = {**param, "required": False}
|
93
|
+
|
94
|
+
yield param
|
95
|
+
|
96
|
+
|
97
|
+
def make_auth_header_schema(definition: dict[str, Any]) -> dict[str, str]:
|
98
|
+
schema = definition.get("scheme", "basic").lower()
|
99
|
+
return {"type": "string", "format": f"_{schema}_auth"}
|
100
|
+
|
101
|
+
|
102
|
+
def make_auth_header(**kwargs: Any) -> dict[str, Any]:
|
103
|
+
return {"name": "Authorization", "in": "header", "required": True, **kwargs}
|
104
|
+
|
105
|
+
|
106
|
+
def make_api_key_schema(definition: dict[str, Any], **kwargs: Any) -> dict[str, Any]:
|
107
|
+
return {"name": definition["name"], "required": True, "in": definition["in"], **kwargs}
|
108
|
+
|
109
|
+
|
110
|
+
def get_security_requirements(schema: Mapping[str, Any], operation: Mapping[str, Any]) -> list[str]:
|
111
|
+
requirements = operation.get("security", schema.get("security", []))
|
112
|
+
return [key for requirement in requirements for key in requirement]
|
113
|
+
|
114
|
+
|
115
|
+
def has_optional_auth(schema: Mapping[str, Any], operation: Mapping[str, Any]) -> bool:
|
116
|
+
return {} in operation.get("security", schema.get("security", []))
|
117
|
+
|
118
|
+
|
119
|
+
def extract_security_definitions_v2(schema: Mapping[str, Any], resolver: RefResolver) -> Mapping[str, Any]:
|
120
|
+
return schema.get("securityDefinitions", {})
|
121
|
+
|
122
|
+
|
123
|
+
def extract_security_definitions_v3(schema: Mapping[str, Any], resolver: RefResolver) -> Mapping[str, Any]:
|
124
|
+
"""In Open API 3 security definitions are located in ``components`` and may have references inside."""
|
125
|
+
components = schema.get("components", {})
|
126
|
+
security_schemes = components.get("securitySchemes", {})
|
127
|
+
# At this point, the resolution scope could differ from the root scope, that's why we need to restore it
|
128
|
+
# as now we resolve root-level references
|
129
|
+
if len(resolver._scopes_stack) > 1:
|
130
|
+
scope = resolver.resolution_scope
|
131
|
+
resolver.pop_scope()
|
132
|
+
else:
|
133
|
+
scope = None
|
134
|
+
resolve = resolver.resolve
|
135
|
+
try:
|
136
|
+
if "$ref" in security_schemes:
|
137
|
+
return resolve(security_schemes["$ref"])[1]
|
138
|
+
return {key: resolve(value["$ref"])[1] if "$ref" in value else value for key, value in security_schemes.items()}
|
139
|
+
finally:
|
140
|
+
if scope is not None:
|
141
|
+
resolver._scopes_stack.append(scope)
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from jsonschema import Draft4Validator
|
2
|
+
|
3
|
+
from schemathesis.specs.openapi.adapter import parameters, responses, security
|
4
|
+
from schemathesis.specs.openapi.adapter.protocol import (
|
5
|
+
BuildPathParameter,
|
6
|
+
ExtractHeaderSchema,
|
7
|
+
ExtractParameterSchema,
|
8
|
+
ExtractResponseSchema,
|
9
|
+
ExtractSecurityParameters,
|
10
|
+
IterParameters,
|
11
|
+
IterResponseExamples,
|
12
|
+
)
|
13
|
+
|
14
|
+
nullable_keyword = "x-nullable"
|
15
|
+
header_required_keyword = "x-required"
|
16
|
+
links_keyword = "x-links"
|
17
|
+
example_keyword = "x-example"
|
18
|
+
examples_container_keyword = "x-examples"
|
19
|
+
|
20
|
+
extract_parameter_schema: ExtractParameterSchema = parameters.extract_parameter_schema_v2
|
21
|
+
extract_response_schema: ExtractResponseSchema = responses.extract_response_schema_v2
|
22
|
+
extract_header_schema: ExtractHeaderSchema = responses.extract_header_schema_v2
|
23
|
+
iter_parameters: IterParameters = parameters.iter_parameters_v2
|
24
|
+
build_path_parameter: BuildPathParameter = parameters.build_path_parameter_v2
|
25
|
+
iter_response_examples: IterResponseExamples = responses.iter_response_examples_v2
|
26
|
+
extract_security_parameters: ExtractSecurityParameters = security.extract_security_parameters_v2
|
27
|
+
|
28
|
+
jsonschema_validator_cls = Draft4Validator
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from jsonschema import Draft4Validator
|
2
|
+
|
3
|
+
from schemathesis.specs.openapi.adapter import parameters, responses, security
|
4
|
+
from schemathesis.specs.openapi.adapter.protocol import (
|
5
|
+
BuildPathParameter,
|
6
|
+
ExtractHeaderSchema,
|
7
|
+
ExtractParameterSchema,
|
8
|
+
ExtractResponseSchema,
|
9
|
+
ExtractSecurityParameters,
|
10
|
+
IterParameters,
|
11
|
+
IterResponseExamples,
|
12
|
+
)
|
13
|
+
|
14
|
+
nullable_keyword = "nullable"
|
15
|
+
header_required_keyword = "required"
|
16
|
+
links_keyword = "links"
|
17
|
+
example_keyword = "example"
|
18
|
+
examples_container_keyword = "examples"
|
19
|
+
|
20
|
+
extract_parameter_schema: ExtractParameterSchema = parameters.extract_parameter_schema_v3
|
21
|
+
extract_response_schema: ExtractResponseSchema = responses.extract_response_schema_v3
|
22
|
+
extract_header_schema: ExtractHeaderSchema = responses.extract_header_schema_v3
|
23
|
+
iter_parameters: IterParameters = parameters.iter_parameters_v3
|
24
|
+
build_path_parameter: BuildPathParameter = parameters.build_path_parameter_v3_0
|
25
|
+
iter_response_examples: IterResponseExamples = responses.iter_response_examples_v3
|
26
|
+
extract_security_parameters: ExtractSecurityParameters = security.extract_security_parameters_v3
|
27
|
+
|
28
|
+
jsonschema_validator_cls = Draft4Validator
|
@@ -0,0 +1,28 @@
|
|
1
|
+
from jsonschema import Draft202012Validator
|
2
|
+
|
3
|
+
from schemathesis.specs.openapi.adapter import parameters, responses, security
|
4
|
+
from schemathesis.specs.openapi.adapter.protocol import (
|
5
|
+
BuildPathParameter,
|
6
|
+
ExtractHeaderSchema,
|
7
|
+
ExtractParameterSchema,
|
8
|
+
ExtractResponseSchema,
|
9
|
+
ExtractSecurityParameters,
|
10
|
+
IterParameters,
|
11
|
+
IterResponseExamples,
|
12
|
+
)
|
13
|
+
|
14
|
+
nullable_keyword = "nullable"
|
15
|
+
header_required_keyword = "required"
|
16
|
+
links_keyword = "links"
|
17
|
+
example_keyword = "example"
|
18
|
+
examples_container_keyword = "examples"
|
19
|
+
|
20
|
+
extract_parameter_schema: ExtractParameterSchema = parameters.extract_parameter_schema_v3
|
21
|
+
extract_response_schema: ExtractResponseSchema = responses.extract_response_schema_v3
|
22
|
+
extract_header_schema: ExtractHeaderSchema = responses.extract_header_schema_v3
|
23
|
+
iter_parameters: IterParameters = parameters.iter_parameters_v3
|
24
|
+
build_path_parameter: BuildPathParameter = parameters.build_path_parameter_v3_1
|
25
|
+
iter_response_examples: IterResponseExamples = responses.iter_response_examples_v3
|
26
|
+
extract_security_parameters: ExtractSecurityParameters = security.extract_security_parameters_v3
|
27
|
+
|
28
|
+
jsonschema_validator_cls = Draft202012Validator
|