schemathesis 4.1.4__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 +87 -11
- 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.4.dist-info → schemathesis-4.2.0.dist-info}/METADATA +2 -2
- {schemathesis-4.1.4.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.4.dist-info → schemathesis-4.2.0.dist-info}/WHEEL +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.1.4.dist-info → schemathesis-4.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,29 +3,36 @@ from __future__ import annotations
|
|
3
3
|
from contextlib import suppress
|
4
4
|
from dataclasses import dataclass
|
5
5
|
from functools import lru_cache
|
6
|
-
from itertools import
|
7
|
-
from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast
|
6
|
+
from itertools import cycle, islice
|
7
|
+
from typing import TYPE_CHECKING, Any, Generator, Iterator, Union, cast, overload
|
8
8
|
|
9
9
|
import requests
|
10
10
|
from hypothesis_jsonschema import from_schema
|
11
11
|
|
12
12
|
from schemathesis.config import GenerationConfig
|
13
|
+
from schemathesis.core.compat import RefResolutionError, RefResolver
|
14
|
+
from schemathesis.core.errors import InfiniteRecursiveReference, UnresolvableReference
|
15
|
+
from schemathesis.core.jsonschema import references
|
16
|
+
from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
|
13
17
|
from schemathesis.core.transforms import deepclone
|
14
18
|
from schemathesis.core.transport import DEFAULT_RESPONSE_TIMEOUT
|
15
19
|
from schemathesis.generation.case import Case
|
16
20
|
from schemathesis.generation.hypothesis import examples
|
17
21
|
from schemathesis.generation.meta import TestPhase
|
18
22
|
from schemathesis.schemas import APIOperation
|
23
|
+
from schemathesis.specs.openapi.adapter import OpenApiResponses
|
24
|
+
from schemathesis.specs.openapi.adapter.parameters import OpenApiBody, OpenApiParameter
|
25
|
+
from schemathesis.specs.openapi.adapter.security import OpenApiSecurityParameters
|
19
26
|
from schemathesis.specs.openapi.serialization import get_serializers_for_operation
|
20
27
|
|
21
28
|
from ._hypothesis import get_default_format_strategies, openapi_cases
|
22
|
-
from .constants import LOCATION_TO_CONTAINER
|
23
29
|
from .formats import STRING_FORMATS
|
24
|
-
from .parameters import OpenAPIBody, OpenAPIParameter
|
25
30
|
|
26
31
|
if TYPE_CHECKING:
|
27
32
|
from hypothesis.strategies import SearchStrategy
|
28
33
|
|
34
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
35
|
+
|
29
36
|
|
30
37
|
@dataclass
|
31
38
|
class ParameterExample:
|
@@ -66,7 +73,7 @@ def merge_kwargs(left: dict[str, Any], right: dict[str, Any]) -> dict[str, Any]:
|
|
66
73
|
|
67
74
|
|
68
75
|
def get_strategies_from_examples(
|
69
|
-
operation: APIOperation[
|
76
|
+
operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters], **kwargs: Any
|
70
77
|
) -> list[SearchStrategy[Case]]:
|
71
78
|
"""Build a set of strategies that generate test cases based on explicit examples in the schema."""
|
72
79
|
maps = get_serializers_for_operation(operation)
|
@@ -93,67 +100,118 @@ def get_strategies_from_examples(
|
|
93
100
|
]
|
94
101
|
|
95
102
|
|
96
|
-
def extract_top_level(
|
103
|
+
def extract_top_level(
|
104
|
+
operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
|
105
|
+
) -> Generator[Example, None, None]:
|
97
106
|
"""Extract top-level parameter examples from `examples` & `example` fields."""
|
98
|
-
|
107
|
+
from .schemas import BaseOpenAPISchema
|
108
|
+
|
109
|
+
assert isinstance(operation.schema, BaseOpenAPISchema)
|
110
|
+
|
111
|
+
responses = list(operation.responses.iter_examples())
|
112
|
+
seen_references: set[str] = set()
|
99
113
|
for parameter in operation.iter_parameters():
|
100
114
|
if "schema" in parameter.definition:
|
101
|
-
|
115
|
+
schema = parameter.definition["schema"]
|
116
|
+
resolver = RefResolver.from_schema(schema)
|
117
|
+
seen_references.clear()
|
118
|
+
definitions = [
|
119
|
+
parameter.definition,
|
120
|
+
*_expand_subschemas(schema=schema, resolver=resolver, seen_references=seen_references),
|
121
|
+
]
|
102
122
|
else:
|
103
123
|
definitions = [parameter.definition]
|
104
124
|
for definition in definitions:
|
105
125
|
# Open API 2 also supports `example`
|
106
|
-
for
|
107
|
-
if isinstance(definition, dict) and
|
126
|
+
for example_keyword in {"example", parameter.adapter.example_keyword}:
|
127
|
+
if isinstance(definition, dict) and example_keyword in definition:
|
108
128
|
yield ParameterExample(
|
109
|
-
container=
|
129
|
+
container=parameter.location.container_name,
|
110
130
|
name=parameter.name,
|
111
|
-
value=definition[
|
131
|
+
value=definition[example_keyword],
|
112
132
|
)
|
113
|
-
if parameter.
|
114
|
-
|
115
|
-
|
116
|
-
)
|
117
|
-
|
118
|
-
yield ParameterExample(
|
119
|
-
container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
|
120
|
-
)
|
133
|
+
if parameter.adapter.examples_container_keyword in parameter.definition:
|
134
|
+
for value in extract_inner_examples(
|
135
|
+
parameter.definition[parameter.adapter.examples_container_keyword], operation.schema
|
136
|
+
):
|
137
|
+
yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
|
121
138
|
if "schema" in parameter.definition:
|
122
|
-
|
123
|
-
|
124
|
-
|
139
|
+
schema = parameter.definition["schema"]
|
140
|
+
resolver = RefResolver.from_schema(schema)
|
141
|
+
seen_references.clear()
|
142
|
+
for expanded in _expand_subschemas(schema=schema, resolver=resolver, seen_references=seen_references):
|
143
|
+
if isinstance(expanded, dict) and parameter.adapter.examples_container_keyword in expanded:
|
144
|
+
for value in expanded[parameter.adapter.examples_container_keyword]:
|
125
145
|
yield ParameterExample(
|
126
|
-
container=
|
146
|
+
container=parameter.location.container_name, name=parameter.name, value=value
|
127
147
|
)
|
128
148
|
for value in find_matching_in_responses(responses, parameter.name):
|
129
|
-
yield ParameterExample(
|
130
|
-
container=LOCATION_TO_CONTAINER[parameter.location], name=parameter.name, value=value
|
131
|
-
)
|
149
|
+
yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
|
132
150
|
for alternative in operation.body:
|
133
|
-
|
134
|
-
if "schema" in
|
135
|
-
|
151
|
+
body = cast(OpenApiBody, alternative)
|
152
|
+
if "schema" in body.definition:
|
153
|
+
schema = body.definition["schema"]
|
154
|
+
resolver = RefResolver.from_schema(schema)
|
155
|
+
seen_references.clear()
|
156
|
+
definitions = [
|
157
|
+
body.definition,
|
158
|
+
*_expand_subschemas(schema=schema, resolver=resolver, seen_references=seen_references),
|
159
|
+
]
|
136
160
|
else:
|
137
|
-
definitions = [
|
161
|
+
definitions = [body.definition]
|
138
162
|
for definition in definitions:
|
139
163
|
# Open API 2 also supports `example`
|
140
|
-
for
|
141
|
-
if isinstance(definition, dict) and
|
142
|
-
yield BodyExample(value=definition[
|
143
|
-
if
|
144
|
-
unresolved_definition = _find_request_body_examples_definition(operation, alternative)
|
164
|
+
for example_keyword in {"example", body.adapter.example_keyword}:
|
165
|
+
if isinstance(definition, dict) and example_keyword in definition:
|
166
|
+
yield BodyExample(value=definition[example_keyword], media_type=body.media_type)
|
167
|
+
if body.adapter.examples_container_keyword in body.definition:
|
145
168
|
for value in extract_inner_examples(
|
146
|
-
|
169
|
+
body.definition[body.adapter.examples_container_keyword], operation.schema
|
147
170
|
):
|
148
|
-
yield BodyExample(value=value, media_type=
|
149
|
-
if "schema" in
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
171
|
+
yield BodyExample(value=value, media_type=body.media_type)
|
172
|
+
if "schema" in body.definition:
|
173
|
+
schema = body.definition["schema"]
|
174
|
+
resolver = RefResolver.from_schema(schema)
|
175
|
+
seen_references.clear()
|
176
|
+
for expanded in _expand_subschemas(schema=schema, resolver=resolver, seen_references=seen_references):
|
177
|
+
if isinstance(expanded, dict) and body.adapter.examples_container_keyword in expanded:
|
178
|
+
for value in expanded[body.adapter.examples_container_keyword]:
|
179
|
+
yield BodyExample(value=value, media_type=body.media_type)
|
154
180
|
|
155
181
|
|
156
|
-
|
182
|
+
@overload
|
183
|
+
def _resolve_bundled(
|
184
|
+
schema: dict[str, Any], resolver: RefResolver, seen_references: set[str]
|
185
|
+
) -> dict[str, Any]: ... # pragma: no cover
|
186
|
+
|
187
|
+
|
188
|
+
@overload
|
189
|
+
def _resolve_bundled(schema: bool, resolver: RefResolver, seen_references: set[str]) -> bool: ... # pragma: no cover
|
190
|
+
|
191
|
+
|
192
|
+
def _resolve_bundled(
|
193
|
+
schema: dict[str, Any] | bool, resolver: RefResolver, seen_references: set[str]
|
194
|
+
) -> dict[str, Any] | bool:
|
195
|
+
if isinstance(schema, dict):
|
196
|
+
reference = schema.get("$ref")
|
197
|
+
if isinstance(reference, str):
|
198
|
+
if reference in seen_references:
|
199
|
+
# Try to remove recursive references to avoid infinite recursion
|
200
|
+
remaining_references = references.sanitize(schema)
|
201
|
+
if reference in remaining_references:
|
202
|
+
raise InfiniteRecursiveReference(reference)
|
203
|
+
seen_references.add(reference)
|
204
|
+
try:
|
205
|
+
_, schema = resolver.resolve(schema["$ref"])
|
206
|
+
except RefResolutionError as exc:
|
207
|
+
raise UnresolvableReference(reference) from exc
|
208
|
+
return schema
|
209
|
+
|
210
|
+
|
211
|
+
def _expand_subschemas(
|
212
|
+
*, schema: dict[str, Any] | bool, resolver: RefResolver, seen_references: set[str]
|
213
|
+
) -> Generator[dict[str, Any] | bool, None, None]:
|
214
|
+
schema = _resolve_bundled(schema, resolver, seen_references)
|
157
215
|
yield schema
|
158
216
|
if isinstance(schema, dict):
|
159
217
|
for key in ("anyOf", "oneOf"):
|
@@ -162,8 +220,10 @@ def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any
|
|
162
220
|
yield subschema
|
163
221
|
if "allOf" in schema:
|
164
222
|
subschema = deepclone(schema["allOf"][0])
|
223
|
+
subschema = _resolve_bundled(subschema, resolver, seen_references)
|
165
224
|
for sub in schema["allOf"][1:]:
|
166
225
|
if isinstance(sub, dict):
|
226
|
+
sub = _resolve_bundled(sub, resolver, seen_references)
|
167
227
|
for key, value in sub.items():
|
168
228
|
if key == "properties":
|
169
229
|
subschema.setdefault("properties", {}).update(value)
|
@@ -178,63 +238,21 @@ def _expand_subschemas(schema: dict[str, Any] | bool) -> Generator[dict[str, Any
|
|
178
238
|
yield subschema
|
179
239
|
|
180
240
|
|
181
|
-
def
|
182
|
-
operation: APIOperation[OpenAPIParameter], parameter_name: str, field_name: str
|
183
|
-
) -> dict[str, Any]:
|
184
|
-
"""Find the original, unresolved `examples` definition of a parameter."""
|
185
|
-
from .schemas import BaseOpenAPISchema
|
186
|
-
|
187
|
-
schema = cast(BaseOpenAPISchema, operation.schema)
|
188
|
-
raw_schema = schema.raw_schema
|
189
|
-
path_data = raw_schema["paths"][operation.path]
|
190
|
-
parameters = chain(path_data[operation.method].get("parameters", []), path_data.get("parameters", []))
|
191
|
-
for parameter in parameters:
|
192
|
-
if "$ref" in parameter:
|
193
|
-
_, parameter = schema.resolver.resolve(parameter["$ref"])
|
194
|
-
if parameter["name"] == parameter_name:
|
195
|
-
return parameter[field_name]
|
196
|
-
raise RuntimeError("Example definition is not found. It should not happen")
|
197
|
-
|
198
|
-
|
199
|
-
def _find_request_body_examples_definition(
|
200
|
-
operation: APIOperation[OpenAPIParameter], alternative: OpenAPIBody
|
201
|
-
) -> dict[str, Any]:
|
202
|
-
"""Find the original, unresolved `examples` definition of a request body variant."""
|
203
|
-
from .schemas import BaseOpenAPISchema
|
204
|
-
|
205
|
-
schema = cast(BaseOpenAPISchema, operation.schema)
|
206
|
-
if schema.specification.version == "2.0":
|
207
|
-
raw_schema = schema.raw_schema
|
208
|
-
path_data = raw_schema["paths"][operation.path]
|
209
|
-
parameters = chain(path_data[operation.method].get("parameters", []), path_data.get("parameters", []))
|
210
|
-
for parameter in parameters:
|
211
|
-
if "$ref" in parameter:
|
212
|
-
_, parameter = schema.resolver.resolve(parameter["$ref"])
|
213
|
-
if parameter["in"] == "body":
|
214
|
-
return parameter[alternative.examples_field]
|
215
|
-
raise RuntimeError("Example definition is not found. It should not happen")
|
216
|
-
request_body = operation.definition.raw["requestBody"]
|
217
|
-
while "$ref" in request_body:
|
218
|
-
_, request_body = schema.resolver.resolve(request_body["$ref"])
|
219
|
-
return request_body["content"][alternative.media_type][alternative.examples_field]
|
220
|
-
|
221
|
-
|
222
|
-
def extract_inner_examples(
|
223
|
-
examples: dict[str, Any] | list, unresolved_definition: dict[str, Any]
|
224
|
-
) -> Generator[Any, None, None]:
|
241
|
+
def extract_inner_examples(examples: dict[str, Any] | list, schema: BaseOpenAPISchema) -> Generator[Any, None, None]:
|
225
242
|
"""Extract exact examples values from the `examples` dictionary."""
|
226
243
|
if isinstance(examples, dict):
|
227
|
-
for
|
228
|
-
if "$ref" in unresolved_definition[name] and "value" not in example and "externalValue" not in example:
|
229
|
-
# The example here is a resolved example and should be yielded as is
|
230
|
-
yield example
|
244
|
+
for example in examples.values():
|
231
245
|
if isinstance(example, dict):
|
246
|
+
if "$ref" in example:
|
247
|
+
_, example = schema.resolver.resolve(example["$ref"])
|
232
248
|
if "value" in example:
|
233
249
|
yield example["value"]
|
234
250
|
elif "externalValue" in example:
|
235
251
|
with suppress(requests.RequestException):
|
236
252
|
# Report a warning if not available?
|
237
253
|
yield load_external_example(example["externalValue"])
|
254
|
+
elif example:
|
255
|
+
yield example
|
238
256
|
elif isinstance(examples, list):
|
239
257
|
yield from examples
|
240
258
|
|
@@ -247,47 +265,93 @@ def load_external_example(url: str) -> bytes:
|
|
247
265
|
return response.content
|
248
266
|
|
249
267
|
|
250
|
-
def extract_from_schemas(
|
268
|
+
def extract_from_schemas(
|
269
|
+
operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
|
270
|
+
) -> Generator[Example, None, None]:
|
251
271
|
"""Extract examples from parameters' schema definitions."""
|
272
|
+
seen_references: set[str] = set()
|
252
273
|
for parameter in operation.iter_parameters():
|
253
|
-
schema = parameter.
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
274
|
+
schema = parameter.optimized_schema
|
275
|
+
if isinstance(schema, bool):
|
276
|
+
continue
|
277
|
+
resolver = RefResolver.from_schema(schema)
|
278
|
+
seen_references.clear()
|
279
|
+
bundle_storage = schema.get(BUNDLE_STORAGE_KEY)
|
280
|
+
for value in extract_from_schema(
|
281
|
+
operation=operation,
|
282
|
+
schema=schema,
|
283
|
+
example_keyword=parameter.adapter.example_keyword,
|
284
|
+
examples_container_keyword=parameter.adapter.examples_container_keyword,
|
285
|
+
resolver=resolver,
|
286
|
+
seen_references=seen_references,
|
287
|
+
bundle_storage=bundle_storage,
|
288
|
+
):
|
289
|
+
yield ParameterExample(container=parameter.location.container_name, name=parameter.name, value=value)
|
258
290
|
for alternative in operation.body:
|
259
|
-
|
260
|
-
schema =
|
261
|
-
|
262
|
-
|
263
|
-
|
291
|
+
body = cast(OpenApiBody, alternative)
|
292
|
+
schema = body.optimized_schema
|
293
|
+
if isinstance(schema, bool):
|
294
|
+
continue
|
295
|
+
resolver = RefResolver.from_schema(schema)
|
296
|
+
bundle_storage = schema.get(BUNDLE_STORAGE_KEY)
|
297
|
+
for example_keyword, examples_container_keyword in (("example", "examples"), ("x-example", "x-examples")):
|
298
|
+
seen_references.clear()
|
299
|
+
for value in extract_from_schema(
|
300
|
+
operation=operation,
|
301
|
+
schema=schema,
|
302
|
+
example_keyword=example_keyword,
|
303
|
+
examples_container_keyword=examples_container_keyword,
|
304
|
+
resolver=resolver,
|
305
|
+
seen_references=seen_references,
|
306
|
+
bundle_storage=bundle_storage,
|
307
|
+
):
|
308
|
+
yield BodyExample(value=value, media_type=body.media_type)
|
264
309
|
|
265
310
|
|
266
311
|
def extract_from_schema(
|
267
|
-
|
312
|
+
*,
|
313
|
+
operation: APIOperation[OpenApiParameter, OpenApiResponses, OpenApiSecurityParameters],
|
268
314
|
schema: dict[str, Any],
|
269
|
-
|
270
|
-
|
315
|
+
example_keyword: str,
|
316
|
+
examples_container_keyword: str,
|
317
|
+
resolver: RefResolver,
|
318
|
+
seen_references: set[str],
|
319
|
+
bundle_storage: dict[str, Any] | None,
|
271
320
|
) -> Generator[Any, None, None]:
|
272
321
|
"""Extract all examples from a single schema definition."""
|
273
322
|
# This implementation supports only `properties` and `items`
|
323
|
+
schema = _resolve_bundled(schema, resolver, seen_references)
|
274
324
|
if "properties" in schema:
|
275
325
|
variants = {}
|
276
326
|
required = schema.get("required", [])
|
277
327
|
to_generate: dict[str, Any] = {}
|
278
328
|
for name, subschema in schema["properties"].items():
|
279
329
|
values = []
|
280
|
-
for subsubschema in _expand_subschemas(
|
330
|
+
for subsubschema in _expand_subschemas(
|
331
|
+
schema=subschema, resolver=resolver, seen_references=seen_references
|
332
|
+
):
|
281
333
|
if isinstance(subsubschema, bool):
|
282
334
|
to_generate[name] = subsubschema
|
283
335
|
continue
|
284
|
-
if
|
285
|
-
values.append(subsubschema[
|
286
|
-
if
|
336
|
+
if example_keyword in subsubschema:
|
337
|
+
values.append(subsubschema[example_keyword])
|
338
|
+
if examples_container_keyword in subsubschema and isinstance(
|
339
|
+
subsubschema[examples_container_keyword], list
|
340
|
+
):
|
287
341
|
# These are JSON Schema examples, which is an array of values
|
288
|
-
values.extend(subsubschema[
|
342
|
+
values.extend(subsubschema[examples_container_keyword])
|
289
343
|
# Check nested examples as well
|
290
|
-
values.extend(
|
344
|
+
values.extend(
|
345
|
+
extract_from_schema(
|
346
|
+
operation=operation,
|
347
|
+
schema=subsubschema,
|
348
|
+
example_keyword=example_keyword,
|
349
|
+
examples_container_keyword=examples_container_keyword,
|
350
|
+
resolver=resolver,
|
351
|
+
seen_references=seen_references,
|
352
|
+
bundle_storage=bundle_storage,
|
353
|
+
)
|
354
|
+
)
|
291
355
|
if not values:
|
292
356
|
if name in required:
|
293
357
|
# Defer generation to only generate these variants if at least one property has examples
|
@@ -300,7 +364,9 @@ def extract_from_schema(
|
|
300
364
|
if name in variants:
|
301
365
|
# Generated by one of `anyOf` or similar sub-schemas
|
302
366
|
continue
|
303
|
-
|
367
|
+
if bundle_storage is not None:
|
368
|
+
subschema = dict(subschema)
|
369
|
+
subschema[BUNDLE_STORAGE_KEY] = bundle_storage
|
304
370
|
generated = _generate_single_example(subschema, config)
|
305
371
|
variants[name] = [generated]
|
306
372
|
# Calculate the maximum number of examples any property has
|
@@ -313,7 +379,15 @@ def extract_from_schema(
|
|
313
379
|
}
|
314
380
|
elif "items" in schema and isinstance(schema["items"], dict):
|
315
381
|
# Each inner value should be wrapped in an array
|
316
|
-
for value in extract_from_schema(
|
382
|
+
for value in extract_from_schema(
|
383
|
+
operation=operation,
|
384
|
+
schema=schema["items"],
|
385
|
+
example_keyword=example_keyword,
|
386
|
+
examples_container_keyword=examples_container_keyword,
|
387
|
+
resolver=resolver,
|
388
|
+
seen_references=seen_references,
|
389
|
+
bundle_storage=bundle_storage,
|
390
|
+
):
|
317
391
|
yield [value]
|
318
392
|
|
319
393
|
|
@@ -378,69 +452,37 @@ def _produce_parameter_combinations(parameters: dict[str, dict[str, list]]) -> G
|
|
378
452
|
}
|
379
453
|
|
380
454
|
|
381
|
-
def find_in_responses(operation: APIOperation) -> dict[str, list[dict[str, Any]]]:
|
382
|
-
"""Find schema examples in responses."""
|
383
|
-
output: dict[str, list[dict[str, Any]]] = {}
|
384
|
-
for status_code, response in operation.definition.raw.get("responses", {}).items():
|
385
|
-
if not str(status_code).startswith("2"):
|
386
|
-
# Check only 2xx responses
|
387
|
-
continue
|
388
|
-
if isinstance(response, dict) and "$ref" in response:
|
389
|
-
_, response = operation.schema.resolver.resolve_in_scope(response, operation.definition.scope) # type:ignore[attr-defined]
|
390
|
-
for media_type, definition in response.get("content", {}).items():
|
391
|
-
schema_ref = definition.get("schema", {}).get("$ref")
|
392
|
-
if schema_ref:
|
393
|
-
name = schema_ref.split("/")[-1]
|
394
|
-
else:
|
395
|
-
name = f"{status_code}/{media_type}"
|
396
|
-
for examples_field, example_field in (
|
397
|
-
("examples", "example"),
|
398
|
-
("x-examples", "x-example"),
|
399
|
-
):
|
400
|
-
examples = definition.get(examples_field, {})
|
401
|
-
if isinstance(examples, dict):
|
402
|
-
for example in examples.values():
|
403
|
-
if "value" in example:
|
404
|
-
output.setdefault(name, []).append(example["value"])
|
405
|
-
elif isinstance(examples, list):
|
406
|
-
output.setdefault(name, []).extend(examples)
|
407
|
-
if example_field in definition:
|
408
|
-
output.setdefault(name, []).append(definition[example_field])
|
409
|
-
return output
|
410
|
-
|
411
|
-
|
412
455
|
NOT_FOUND = object()
|
413
456
|
|
414
457
|
|
415
|
-
def find_matching_in_responses(examples:
|
458
|
+
def find_matching_in_responses(examples: list[tuple[str, object]], param: str) -> Iterator[Any]:
|
416
459
|
"""Find matching parameter examples."""
|
417
460
|
normalized = param.lower()
|
418
461
|
is_id_param = normalized.endswith("id")
|
419
462
|
# Extract values from response examples that match input parameters.
|
420
463
|
# E.g., for `GET /orders/{id}/`, use "id" or "orderId" from `Order` response
|
421
464
|
# as examples for the "id" path parameter.
|
422
|
-
for schema_name,
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
if
|
428
|
-
|
429
|
-
if inner
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
yield found
|
465
|
+
for schema_name, example in examples:
|
466
|
+
if not isinstance(example, dict):
|
467
|
+
continue
|
468
|
+
# Unwrapping example from `{"item": [{...}]}`
|
469
|
+
if isinstance(example, dict):
|
470
|
+
inner = next((value for key, value in example.items() if key.lower() == schema_name.lower()), None)
|
471
|
+
if inner is not None:
|
472
|
+
if isinstance(inner, list):
|
473
|
+
for sub_example in inner:
|
474
|
+
if isinstance(sub_example, dict):
|
475
|
+
for found in _find_matching_in_responses(
|
476
|
+
sub_example, schema_name, param, normalized, is_id_param
|
477
|
+
):
|
478
|
+
if found is not NOT_FOUND:
|
479
|
+
yield found
|
480
|
+
continue
|
481
|
+
if isinstance(inner, dict):
|
482
|
+
example = inner
|
483
|
+
for found in _find_matching_in_responses(example, schema_name, param, normalized, is_id_param):
|
484
|
+
if found is not NOT_FOUND:
|
485
|
+
yield found
|
444
486
|
|
445
487
|
|
446
488
|
def _find_matching_in_responses(
|
@@ -10,8 +10,10 @@ from hypothesis import strategies as st
|
|
10
10
|
from hypothesis_jsonschema import from_schema
|
11
11
|
|
12
12
|
from schemathesis.config import GenerationConfig
|
13
|
+
from schemathesis.core.jsonschema import ALL_KEYWORDS
|
14
|
+
from schemathesis.core.jsonschema.types import JsonSchema
|
15
|
+
from schemathesis.core.parameters import ParameterLocation
|
13
16
|
|
14
|
-
from ..constants import ALL_KEYWORDS
|
15
17
|
from .mutations import MutationContext
|
16
18
|
|
17
19
|
if TYPE_CHECKING:
|
@@ -27,7 +29,7 @@ class CacheKey:
|
|
27
29
|
|
28
30
|
operation_name: str
|
29
31
|
location: str
|
30
|
-
schema:
|
32
|
+
schema: JsonSchema
|
31
33
|
validator_cls: type[jsonschema.Validator]
|
32
34
|
|
33
35
|
__slots__ = ("operation_name", "location", "schema", "validator_cls")
|
@@ -50,7 +52,8 @@ def split_schema(cache_key: CacheKey) -> tuple[Schema, Schema]:
|
|
50
52
|
The first one contains only validation JSON Schema keywords, the second one everything else.
|
51
53
|
"""
|
52
54
|
keywords, non_keywords = {}, {}
|
53
|
-
|
55
|
+
schema = {} if isinstance(cache_key.schema, bool) else cache_key.schema
|
56
|
+
for keyword, value in schema.items():
|
54
57
|
if keyword in ALL_KEYWORDS:
|
55
58
|
keywords[keyword] = value
|
56
59
|
else:
|
@@ -59,9 +62,9 @@ def split_schema(cache_key: CacheKey) -> tuple[Schema, Schema]:
|
|
59
62
|
|
60
63
|
|
61
64
|
def negative_schema(
|
62
|
-
schema:
|
65
|
+
schema: JsonSchema,
|
63
66
|
operation_name: str,
|
64
|
-
location:
|
67
|
+
location: ParameterLocation,
|
65
68
|
media_type: str | None,
|
66
69
|
generation_config: GenerationConfig,
|
67
70
|
*,
|
@@ -78,7 +81,7 @@ def negative_schema(
|
|
78
81
|
validator = get_validator(cache_key)
|
79
82
|
keywords, non_keywords = split_schema(cache_key)
|
80
83
|
|
81
|
-
if location ==
|
84
|
+
if location == ParameterLocation.QUERY:
|
82
85
|
|
83
86
|
def filter_values(value: dict[str, Any]) -> bool:
|
84
87
|
return is_non_empty_query(value) and not validator.is_valid(value)
|
@@ -113,7 +116,9 @@ def is_non_empty_query(query: dict[str, Any]) -> bool:
|
|
113
116
|
|
114
117
|
|
115
118
|
@st.composite # type: ignore
|
116
|
-
def mutated(
|
119
|
+
def mutated(
|
120
|
+
draw: Draw, keywords: Schema, non_keywords: Schema, location: ParameterLocation, media_type: str | None
|
121
|
+
) -> Any:
|
117
122
|
return MutationContext(
|
118
123
|
keywords=keywords, non_keywords=non_keywords, location=location, media_type=media_type
|
119
124
|
).mutate(draw)
|