schemathesis 3.15.4__py3-none-any.whl → 4.4.2__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/__init__.py +53 -25
- schemathesis/auths.py +507 -0
- schemathesis/checks.py +190 -25
- schemathesis/cli/__init__.py +27 -1219
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +133 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +602 -0
- schemathesis/cli/commands/run/context.py +228 -0
- schemathesis/cli/commands/run/events.py +60 -0
- schemathesis/cli/commands/run/executor.py +157 -0
- schemathesis/cli/commands/run/filters.py +53 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +45 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
- schemathesis/cli/commands/run/handlers/output.py +1750 -0
- schemathesis/cli/commands/run/loaders.py +118 -0
- schemathesis/cli/commands/run/validation.py +256 -0
- schemathesis/cli/constants.py +5 -0
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +203 -0
- schemathesis/cli/ext/options.py +81 -0
- schemathesis/config/__init__.py +202 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +101 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +163 -0
- schemathesis/config/_generation.py +157 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +335 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +253 -0
- schemathesis/config/_projects.py +543 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +89 -0
- schemathesis/config/schema.json +975 -0
- schemathesis/core/__init__.py +72 -0
- schemathesis/core/adapter.py +34 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +100 -0
- schemathesis/core/deserialization.py +210 -0
- schemathesis/core/errors.py +588 -0
- schemathesis/core/failures.py +316 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/jsonschema/__init__.py +13 -0
- schemathesis/core/jsonschema/bundler.py +183 -0
- schemathesis/core/jsonschema/keywords.py +40 -0
- schemathesis/core/jsonschema/references.py +222 -0
- schemathesis/core/jsonschema/types.py +41 -0
- schemathesis/core/lazy_import.py +15 -0
- schemathesis/core/loaders.py +107 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/core/media_types.py +79 -0
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/core/parameters.py +45 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +34 -0
- schemathesis/core/result.py +27 -0
- schemathesis/core/schema_analysis.py +17 -0
- schemathesis/core/shell.py +203 -0
- schemathesis/core/transforms.py +144 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +73 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +152 -0
- schemathesis/engine/control.py +44 -0
- schemathesis/engine/core.py +201 -0
- schemathesis/engine/errors.py +446 -0
- schemathesis/engine/events.py +284 -0
- schemathesis/engine/observations.py +42 -0
- schemathesis/engine/phases/__init__.py +108 -0
- schemathesis/engine/phases/analysis.py +28 -0
- schemathesis/engine/phases/probes.py +172 -0
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +364 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +220 -0
- schemathesis/engine/phases/unit/_executor.py +459 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +254 -0
- schemathesis/errors.py +47 -0
- schemathesis/filters.py +395 -0
- schemathesis/generation/__init__.py +25 -0
- schemathesis/generation/case.py +478 -0
- schemathesis/generation/coverage.py +1528 -0
- schemathesis/generation/hypothesis/__init__.py +121 -0
- schemathesis/generation/hypothesis/builder.py +992 -0
- schemathesis/generation/hypothesis/examples.py +56 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +285 -0
- schemathesis/generation/meta.py +227 -0
- schemathesis/generation/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +127 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +294 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +285 -0
- schemathesis/hooks.py +270 -91
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +467 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +315 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +341 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/pytest/plugin.py +357 -0
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +682 -257
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/nodes.py +26 -2
- schemathesis/specs/graphql/scalars.py +77 -12
- schemathesis/specs/graphql/schemas.py +367 -148
- schemathesis/specs/graphql/validation.py +33 -0
- schemathesis/specs/openapi/__init__.py +9 -1
- schemathesis/specs/openapi/_hypothesis.py +555 -318
- schemathesis/specs/openapi/adapter/__init__.py +10 -0
- schemathesis/specs/openapi/adapter/parameters.py +729 -0
- schemathesis/specs/openapi/adapter/protocol.py +59 -0
- schemathesis/specs/openapi/adapter/references.py +19 -0
- schemathesis/specs/openapi/adapter/responses.py +368 -0
- schemathesis/specs/openapi/adapter/security.py +144 -0
- schemathesis/specs/openapi/adapter/v2.py +30 -0
- schemathesis/specs/openapi/adapter/v3_0.py +30 -0
- schemathesis/specs/openapi/adapter/v3_1.py +30 -0
- schemathesis/specs/openapi/analysis.py +96 -0
- schemathesis/specs/openapi/checks.py +748 -82
- schemathesis/specs/openapi/converter.py +176 -37
- schemathesis/specs/openapi/definitions.py +599 -4
- schemathesis/specs/openapi/examples.py +581 -165
- schemathesis/specs/openapi/expressions/__init__.py +52 -5
- schemathesis/specs/openapi/expressions/extractors.py +25 -0
- schemathesis/specs/openapi/expressions/lexer.py +34 -31
- schemathesis/specs/openapi/expressions/nodes.py +97 -46
- schemathesis/specs/openapi/expressions/parser.py +35 -13
- schemathesis/specs/openapi/formats.py +122 -0
- schemathesis/specs/openapi/media_types.py +75 -0
- schemathesis/specs/openapi/negative/__init__.py +93 -73
- schemathesis/specs/openapi/negative/mutations.py +294 -103
- schemathesis/specs/openapi/negative/utils.py +0 -9
- schemathesis/specs/openapi/patterns.py +458 -0
- schemathesis/specs/openapi/references.py +60 -81
- schemathesis/specs/openapi/schemas.py +647 -666
- schemathesis/specs/openapi/serialization.py +53 -30
- schemathesis/specs/openapi/stateful/__init__.py +403 -68
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
- schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
- schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
- schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
- schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
- schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
- schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
- schemathesis/specs/openapi/stateful/inference.py +254 -0
- schemathesis/specs/openapi/stateful/links.py +219 -78
- schemathesis/specs/openapi/types/__init__.py +3 -0
- schemathesis/specs/openapi/types/common.py +23 -0
- schemathesis/specs/openapi/types/v2.py +129 -0
- schemathesis/specs/openapi/types/v3.py +134 -0
- schemathesis/specs/openapi/utils.py +7 -6
- schemathesis/specs/openapi/warnings.py +75 -0
- schemathesis/transport/__init__.py +224 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +126 -0
- schemathesis/transport/requests.py +278 -0
- schemathesis/transport/serialization.py +329 -0
- schemathesis/transport/wsgi.py +175 -0
- schemathesis-4.4.2.dist-info/METADATA +213 -0
- schemathesis-4.4.2.dist-info/RECORD +192 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
- schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
- {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
- schemathesis/_compat.py +0 -57
- schemathesis/_hypothesis.py +0 -123
- schemathesis/auth.py +0 -214
- schemathesis/cli/callbacks.py +0 -240
- schemathesis/cli/cassettes.py +0 -351
- schemathesis/cli/context.py +0 -38
- schemathesis/cli/debug.py +0 -21
- schemathesis/cli/handlers.py +0 -11
- schemathesis/cli/junitxml.py +0 -41
- schemathesis/cli/options.py +0 -70
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -521
- schemathesis/cli/output/short.py +0 -40
- schemathesis/constants.py +0 -88
- schemathesis/exceptions.py +0 -257
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -16
- schemathesis/extra/pytest_plugin.py +0 -251
- schemathesis/failures.py +0 -145
- schemathesis/fixups/__init__.py +0 -29
- schemathesis/fixups/fast_api.py +0 -30
- schemathesis/graphql.py +0 -5
- schemathesis/internal.py +0 -6
- schemathesis/lazy.py +0 -301
- schemathesis/models.py +0 -1113
- schemathesis/parameters.py +0 -91
- schemathesis/runner/__init__.py +0 -470
- schemathesis/runner/events.py +0 -242
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -791
- schemathesis/runner/impl/solo.py +0 -85
- schemathesis/runner/impl/threadpool.py +0 -367
- schemathesis/runner/serialization.py +0 -206
- schemathesis/serializers.py +0 -253
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -10
- schemathesis/service/client.py +0 -62
- schemathesis/service/constants.py +0 -25
- schemathesis/service/events.py +0 -39
- schemathesis/service/handler.py +0 -46
- schemathesis/service/hosts.py +0 -74
- schemathesis/service/metadata.py +0 -42
- schemathesis/service/models.py +0 -21
- schemathesis/service/serialization.py +0 -184
- schemathesis/service/worker.py +0 -39
- schemathesis/specs/graphql/loaders.py +0 -215
- schemathesis/specs/openapi/constants.py +0 -7
- schemathesis/specs/openapi/expressions/context.py +0 -12
- schemathesis/specs/openapi/expressions/pointers.py +0 -29
- schemathesis/specs/openapi/filters.py +0 -44
- schemathesis/specs/openapi/links.py +0 -303
- schemathesis/specs/openapi/loaders.py +0 -453
- schemathesis/specs/openapi/parameters.py +0 -430
- schemathesis/specs/openapi/security.py +0 -129
- schemathesis/specs/openapi/validation.py +0 -24
- schemathesis/stateful.py +0 -358
- schemathesis/targets.py +0 -32
- schemathesis/types.py +0 -38
- schemathesis/utils.py +0 -475
- schemathesis-3.15.4.dist-info/METADATA +0 -202
- schemathesis-3.15.4.dist-info/RECORD +0 -99
- schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
|
@@ -0,0 +1,729 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from itertools import chain
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Iterable, Iterator, Mapping, Sequence, cast
|
|
7
|
+
|
|
8
|
+
from schemathesis.config import GenerationConfig
|
|
9
|
+
from schemathesis.core import NOT_SET, NotSet
|
|
10
|
+
from schemathesis.core.adapter import OperationParameter
|
|
11
|
+
from schemathesis.core.errors import InvalidSchema
|
|
12
|
+
from schemathesis.core.jsonschema import BundleError, Bundler
|
|
13
|
+
from schemathesis.core.jsonschema.bundler import BUNDLE_STORAGE_KEY
|
|
14
|
+
from schemathesis.core.jsonschema.types import JsonSchema, JsonSchemaObject
|
|
15
|
+
from schemathesis.core.parameters import HEADER_LOCATIONS, ParameterLocation
|
|
16
|
+
from schemathesis.core.validation import check_header_name
|
|
17
|
+
from schemathesis.generation.modes import GenerationMode
|
|
18
|
+
from schemathesis.schemas import APIOperation, ParameterSet
|
|
19
|
+
from schemathesis.specs.openapi.adapter.protocol import SpecificationAdapter
|
|
20
|
+
from schemathesis.specs.openapi.adapter.references import maybe_resolve
|
|
21
|
+
from schemathesis.specs.openapi.converter import to_json_schema
|
|
22
|
+
from schemathesis.specs.openapi.formats import HEADER_FORMAT
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from hypothesis import strategies as st
|
|
26
|
+
|
|
27
|
+
from schemathesis.core.compat import RefResolver
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
MISSING_SCHEMA_OR_CONTENT_MESSAGE = (
|
|
31
|
+
"Can not generate data for {location} parameter `{name}`! "
|
|
32
|
+
"It should have either `schema` or `content` keywords defined"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
INVALID_SCHEMA_MESSAGE = (
|
|
36
|
+
"Can not generate data for {location} parameter `{name}`! Its schema should be an object or boolean, got {schema}"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
FORM_MEDIA_TYPES = frozenset(["multipart/form-data", "application/x-www-form-urlencoded"])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass
|
|
43
|
+
class OpenApiComponent(ABC):
|
|
44
|
+
definition: Mapping[str, Any]
|
|
45
|
+
is_required: bool
|
|
46
|
+
name_to_uri: dict[str, str]
|
|
47
|
+
adapter: SpecificationAdapter
|
|
48
|
+
|
|
49
|
+
__slots__ = (
|
|
50
|
+
"definition",
|
|
51
|
+
"is_required",
|
|
52
|
+
"name_to_uri",
|
|
53
|
+
"adapter",
|
|
54
|
+
"_optimized_schema",
|
|
55
|
+
"_unoptimized_schema",
|
|
56
|
+
"_raw_schema",
|
|
57
|
+
"_examples",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def __post_init__(self) -> None:
|
|
61
|
+
self._optimized_schema: JsonSchema | NotSet = NOT_SET
|
|
62
|
+
self._unoptimized_schema: JsonSchema | NotSet = NOT_SET
|
|
63
|
+
self._raw_schema: JsonSchema | NotSet = NOT_SET
|
|
64
|
+
self._examples: list | NotSet = NOT_SET
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def optimized_schema(self) -> JsonSchema:
|
|
68
|
+
"""JSON schema optimized for data generation."""
|
|
69
|
+
if self._optimized_schema is NOT_SET:
|
|
70
|
+
self._optimized_schema = self._build_schema(optimize=True)
|
|
71
|
+
assert not isinstance(self._optimized_schema, NotSet)
|
|
72
|
+
return self._optimized_schema
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def unoptimized_schema(self) -> JsonSchema:
|
|
76
|
+
"""JSON schema preserving original constraint structure."""
|
|
77
|
+
if self._unoptimized_schema is NOT_SET:
|
|
78
|
+
self._unoptimized_schema = self._build_schema(optimize=False)
|
|
79
|
+
assert not isinstance(self._unoptimized_schema, NotSet)
|
|
80
|
+
return self._unoptimized_schema
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def raw_schema(self) -> JsonSchema:
|
|
84
|
+
"""Raw schema extracted from definition before JSON Schema conversion."""
|
|
85
|
+
if self._raw_schema is NOT_SET:
|
|
86
|
+
self._raw_schema = self._get_raw_schema()
|
|
87
|
+
assert not isinstance(self._raw_schema, NotSet)
|
|
88
|
+
return self._raw_schema
|
|
89
|
+
|
|
90
|
+
@abstractmethod
|
|
91
|
+
def _get_raw_schema(self) -> JsonSchema:
|
|
92
|
+
"""Get the raw schema for this component."""
|
|
93
|
+
raise NotImplementedError
|
|
94
|
+
|
|
95
|
+
@abstractmethod
|
|
96
|
+
def _get_default_type(self) -> str | None:
|
|
97
|
+
"""Get default type for this parameter."""
|
|
98
|
+
raise NotImplementedError
|
|
99
|
+
|
|
100
|
+
def _build_schema(self, *, optimize: bool) -> JsonSchema:
|
|
101
|
+
"""Build JSON schema with optional optimizations for data generation."""
|
|
102
|
+
schema = to_json_schema(
|
|
103
|
+
self.raw_schema,
|
|
104
|
+
nullable_keyword=self.adapter.nullable_keyword,
|
|
105
|
+
update_quantifiers=optimize,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Missing the `type` keyword may significantly slowdown data generation, ensure it is set
|
|
109
|
+
default_type = self._get_default_type()
|
|
110
|
+
if isinstance(schema, dict):
|
|
111
|
+
if default_type is not None:
|
|
112
|
+
schema.setdefault("type", default_type)
|
|
113
|
+
elif schema is True and default_type is not None:
|
|
114
|
+
# Restrict such cases too
|
|
115
|
+
schema = {"type": default_type}
|
|
116
|
+
|
|
117
|
+
return schema
|
|
118
|
+
|
|
119
|
+
@property
|
|
120
|
+
def examples(self) -> list:
|
|
121
|
+
"""All examples extracted from definition.
|
|
122
|
+
|
|
123
|
+
Combines both single 'example' and 'examples' container values.
|
|
124
|
+
"""
|
|
125
|
+
if self._examples is NOT_SET:
|
|
126
|
+
self._examples = self._extract_examples()
|
|
127
|
+
assert not isinstance(self._examples, NotSet)
|
|
128
|
+
return self._examples
|
|
129
|
+
|
|
130
|
+
def _extract_examples(self) -> list[object]:
|
|
131
|
+
"""Extract examples from both single example and examples container."""
|
|
132
|
+
examples: list[object] = []
|
|
133
|
+
|
|
134
|
+
container = self.definition.get(self.adapter.examples_container_keyword)
|
|
135
|
+
if isinstance(container, dict):
|
|
136
|
+
examples.extend(ex["value"] for ex in container.values() if isinstance(ex, dict) and "value" in ex)
|
|
137
|
+
elif isinstance(container, list):
|
|
138
|
+
examples.extend(container)
|
|
139
|
+
|
|
140
|
+
example = self.definition.get(self.adapter.example_keyword, NOT_SET)
|
|
141
|
+
if example is not NOT_SET:
|
|
142
|
+
examples.append(example)
|
|
143
|
+
|
|
144
|
+
return examples
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class OpenApiParameter(OpenApiComponent):
|
|
149
|
+
"""OpenAPI operation parameter."""
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def from_definition(
|
|
153
|
+
cls, *, definition: Mapping[str, Any], name_to_uri: dict[str, str], adapter: SpecificationAdapter
|
|
154
|
+
) -> OpenApiParameter:
|
|
155
|
+
is_required = definition.get("required", False)
|
|
156
|
+
return cls(definition=definition, is_required=is_required, name_to_uri=name_to_uri, adapter=adapter)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def name(self) -> str:
|
|
160
|
+
"""Parameter name."""
|
|
161
|
+
return self.definition["name"]
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def location(self) -> ParameterLocation:
|
|
165
|
+
"""Where this parameter is located."""
|
|
166
|
+
try:
|
|
167
|
+
return ParameterLocation(self.definition["in"])
|
|
168
|
+
except ValueError:
|
|
169
|
+
return ParameterLocation.UNKNOWN
|
|
170
|
+
|
|
171
|
+
def _get_raw_schema(self) -> JsonSchema:
|
|
172
|
+
"""Get raw parameter schema."""
|
|
173
|
+
return self.adapter.extract_parameter_schema(self.definition)
|
|
174
|
+
|
|
175
|
+
def _get_default_type(self) -> str | None:
|
|
176
|
+
"""Return default type if parameter is in string-type location."""
|
|
177
|
+
return "string" if self.location.is_in_header else None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@dataclass
|
|
181
|
+
class OpenApiBody(OpenApiComponent):
|
|
182
|
+
"""OpenAPI request body."""
|
|
183
|
+
|
|
184
|
+
media_type: str
|
|
185
|
+
resource_name: str | None
|
|
186
|
+
name_to_uri: dict[str, str]
|
|
187
|
+
|
|
188
|
+
__slots__ = (
|
|
189
|
+
"definition",
|
|
190
|
+
"is_required",
|
|
191
|
+
"media_type",
|
|
192
|
+
"resource_name",
|
|
193
|
+
"name_to_uri",
|
|
194
|
+
"adapter",
|
|
195
|
+
"_optimized_schema",
|
|
196
|
+
"_unoptimized_schema",
|
|
197
|
+
"_raw_schema",
|
|
198
|
+
"_examples",
|
|
199
|
+
"_positive_strategy_cache",
|
|
200
|
+
"_negative_strategy_cache",
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def from_definition(
|
|
205
|
+
cls,
|
|
206
|
+
*,
|
|
207
|
+
definition: Mapping[str, Any],
|
|
208
|
+
is_required: bool,
|
|
209
|
+
media_type: str,
|
|
210
|
+
resource_name: str | None,
|
|
211
|
+
name_to_uri: dict[str, str],
|
|
212
|
+
adapter: SpecificationAdapter,
|
|
213
|
+
) -> OpenApiBody:
|
|
214
|
+
return cls(
|
|
215
|
+
definition=definition,
|
|
216
|
+
is_required=is_required,
|
|
217
|
+
media_type=media_type,
|
|
218
|
+
resource_name=resource_name,
|
|
219
|
+
name_to_uri=name_to_uri,
|
|
220
|
+
adapter=adapter,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def from_form_parameters(
|
|
225
|
+
cls,
|
|
226
|
+
*,
|
|
227
|
+
definition: Mapping[str, Any],
|
|
228
|
+
media_type: str,
|
|
229
|
+
name_to_uri: dict[str, str],
|
|
230
|
+
adapter: SpecificationAdapter,
|
|
231
|
+
) -> OpenApiBody:
|
|
232
|
+
return cls(
|
|
233
|
+
definition=definition,
|
|
234
|
+
is_required=True,
|
|
235
|
+
media_type=media_type,
|
|
236
|
+
resource_name=None,
|
|
237
|
+
name_to_uri=name_to_uri,
|
|
238
|
+
adapter=adapter,
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
def __post_init__(self) -> None:
|
|
242
|
+
super().__post_init__()
|
|
243
|
+
self._positive_strategy_cache: st.SearchStrategy | NotSet = NOT_SET
|
|
244
|
+
self._negative_strategy_cache: st.SearchStrategy | NotSet = NOT_SET
|
|
245
|
+
|
|
246
|
+
@property
|
|
247
|
+
def location(self) -> ParameterLocation:
|
|
248
|
+
return ParameterLocation.BODY
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def name(self) -> str:
|
|
252
|
+
# The name doesn't matter but is here for the interface completeness.
|
|
253
|
+
return "body"
|
|
254
|
+
|
|
255
|
+
def _get_raw_schema(self) -> JsonSchema:
|
|
256
|
+
"""Get raw body schema."""
|
|
257
|
+
return self.definition.get("schema", {})
|
|
258
|
+
|
|
259
|
+
def _get_default_type(self) -> str | None:
|
|
260
|
+
"""Return default type if body is a form type."""
|
|
261
|
+
return "object" if self.media_type in FORM_MEDIA_TYPES else None
|
|
262
|
+
|
|
263
|
+
def get_property_content_type(self, property_name: str) -> str | None:
|
|
264
|
+
"""Get custom contentType for a form property from `encoding` definition."""
|
|
265
|
+
encoding = self.definition.get("encoding", {})
|
|
266
|
+
property_encoding = encoding.get(property_name, {})
|
|
267
|
+
return property_encoding.get("contentType")
|
|
268
|
+
|
|
269
|
+
def get_strategy(
|
|
270
|
+
self,
|
|
271
|
+
operation: APIOperation,
|
|
272
|
+
generation_config: GenerationConfig,
|
|
273
|
+
generation_mode: GenerationMode,
|
|
274
|
+
) -> st.SearchStrategy:
|
|
275
|
+
"""Get a Hypothesis strategy for this body parameter."""
|
|
276
|
+
# Check cache based on generation mode
|
|
277
|
+
if generation_mode == GenerationMode.POSITIVE:
|
|
278
|
+
if self._positive_strategy_cache is not NOT_SET:
|
|
279
|
+
assert not isinstance(self._positive_strategy_cache, NotSet)
|
|
280
|
+
return self._positive_strategy_cache
|
|
281
|
+
elif self._negative_strategy_cache is not NOT_SET:
|
|
282
|
+
assert not isinstance(self._negative_strategy_cache, NotSet)
|
|
283
|
+
return self._negative_strategy_cache
|
|
284
|
+
|
|
285
|
+
# Import here to avoid circular dependency
|
|
286
|
+
from schemathesis.specs.openapi._hypothesis import GENERATOR_MODE_TO_STRATEGY_FACTORY
|
|
287
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
|
288
|
+
|
|
289
|
+
# Build the strategy
|
|
290
|
+
strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
|
|
291
|
+
schema = self.optimized_schema
|
|
292
|
+
assert isinstance(operation.schema, BaseOpenAPISchema)
|
|
293
|
+
strategy = strategy_factory(
|
|
294
|
+
schema,
|
|
295
|
+
operation.label,
|
|
296
|
+
ParameterLocation.BODY,
|
|
297
|
+
self.media_type,
|
|
298
|
+
generation_config,
|
|
299
|
+
operation.schema.adapter.jsonschema_validator_cls,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Cache the strategy
|
|
303
|
+
if generation_mode == GenerationMode.POSITIVE:
|
|
304
|
+
self._positive_strategy_cache = strategy
|
|
305
|
+
else:
|
|
306
|
+
self._negative_strategy_cache = strategy
|
|
307
|
+
|
|
308
|
+
return strategy
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
OPENAPI_20_EXCLUDE_KEYS = frozenset(["required", "name", "in", "title", "description"])
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def extract_parameter_schema_v2(parameter: Mapping[str, Any]) -> JsonSchemaObject:
|
|
315
|
+
# In Open API 2.0, schema for non-body parameters lives directly in the parameter definition
|
|
316
|
+
return {key: value for key, value in parameter.items() if key not in OPENAPI_20_EXCLUDE_KEYS}
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def extract_parameter_schema_v3(parameter: Mapping[str, Any]) -> JsonSchema:
|
|
320
|
+
if "schema" in parameter:
|
|
321
|
+
if not isinstance(parameter["schema"], (dict, bool)):
|
|
322
|
+
raise InvalidSchema(
|
|
323
|
+
INVALID_SCHEMA_MESSAGE.format(
|
|
324
|
+
location=parameter.get("in", ""),
|
|
325
|
+
name=parameter.get("name", "<UNKNOWN>"),
|
|
326
|
+
schema=parameter["schema"],
|
|
327
|
+
),
|
|
328
|
+
)
|
|
329
|
+
return parameter["schema"]
|
|
330
|
+
# https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#fixed-fields-10
|
|
331
|
+
# > The map MUST only contain one entry.
|
|
332
|
+
try:
|
|
333
|
+
content = parameter["content"]
|
|
334
|
+
except KeyError as exc:
|
|
335
|
+
raise InvalidSchema(
|
|
336
|
+
MISSING_SCHEMA_OR_CONTENT_MESSAGE.format(
|
|
337
|
+
location=parameter.get("in", ""), name=parameter.get("name", "<UNKNOWN>")
|
|
338
|
+
),
|
|
339
|
+
) from exc
|
|
340
|
+
options = iter(content.values())
|
|
341
|
+
media_type_object = next(options)
|
|
342
|
+
return media_type_object.get("schema", {})
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def _bundle_parameter(
|
|
346
|
+
parameter: Mapping, resolver: RefResolver, bundler: Bundler
|
|
347
|
+
) -> tuple[dict[str, Any], dict[str, str]]:
|
|
348
|
+
"""Bundle a parameter definition to make it self-contained."""
|
|
349
|
+
_, definition = maybe_resolve(parameter, resolver, "")
|
|
350
|
+
schema = definition.get("schema")
|
|
351
|
+
name_to_uri = {}
|
|
352
|
+
if schema is not None:
|
|
353
|
+
definition = {k: v for k, v in definition.items() if k != "schema"}
|
|
354
|
+
try:
|
|
355
|
+
bundled = bundler.bundle(schema, resolver, inline_recursive=True)
|
|
356
|
+
definition["schema"] = bundled.schema
|
|
357
|
+
name_to_uri = bundled.name_to_uri
|
|
358
|
+
except BundleError as exc:
|
|
359
|
+
location = parameter.get("in", "")
|
|
360
|
+
name = parameter.get("name", "<UNKNOWN>")
|
|
361
|
+
raise InvalidSchema.from_bundle_error(exc, location, name) from exc
|
|
362
|
+
return cast(dict, definition), name_to_uri
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
OPENAPI_20_DEFAULT_BODY_MEDIA_TYPE = "application/json"
|
|
366
|
+
OPENAPI_20_DEFAULT_FORM_MEDIA_TYPE = "multipart/form-data"
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def iter_parameters_v2(
|
|
370
|
+
definition: Mapping[str, Any],
|
|
371
|
+
shared_parameters: Sequence[Mapping[str, Any]],
|
|
372
|
+
default_media_types: list[str],
|
|
373
|
+
resolver: RefResolver,
|
|
374
|
+
adapter: SpecificationAdapter,
|
|
375
|
+
) -> Iterator[OperationParameter]:
|
|
376
|
+
media_types = definition.get("consumes", default_media_types)
|
|
377
|
+
# For `in=body` parameters, we imply `application/json` as the default media type because it is the most common.
|
|
378
|
+
body_media_types = media_types or (OPENAPI_20_DEFAULT_BODY_MEDIA_TYPE,)
|
|
379
|
+
# If an API operation has parameters with `in=formData`, Schemathesis should know how to serialize it.
|
|
380
|
+
# We can't be 100% sure what media type is expected by the server and chose `multipart/form-data` as
|
|
381
|
+
# the default because it is broader since it allows us to upload files.
|
|
382
|
+
form_data_media_types = media_types or (OPENAPI_20_DEFAULT_FORM_MEDIA_TYPE,)
|
|
383
|
+
|
|
384
|
+
form_parameters = []
|
|
385
|
+
form_name_to_uri = {}
|
|
386
|
+
bundler = Bundler()
|
|
387
|
+
for parameter in chain(definition.get("parameters", []), shared_parameters):
|
|
388
|
+
parameter, name_to_uri = _bundle_parameter(parameter, resolver, bundler)
|
|
389
|
+
if parameter["in"] in HEADER_LOCATIONS:
|
|
390
|
+
check_header_name(parameter["name"])
|
|
391
|
+
|
|
392
|
+
if parameter["in"] == "formData":
|
|
393
|
+
# We need to gather form parameters first before creating a composite parameter for them
|
|
394
|
+
form_parameters.append(parameter)
|
|
395
|
+
form_name_to_uri.update(name_to_uri)
|
|
396
|
+
elif parameter["in"] == ParameterLocation.BODY:
|
|
397
|
+
# Take the original definition & extract the resource_name from there
|
|
398
|
+
resource_name = None
|
|
399
|
+
for param in chain(definition.get("parameters", []), shared_parameters):
|
|
400
|
+
_, param = maybe_resolve(param, resolver, "")
|
|
401
|
+
if param.get("in") == ParameterLocation.BODY:
|
|
402
|
+
if "$ref" in param["schema"]:
|
|
403
|
+
resource_name = resource_name_from_ref(param["schema"]["$ref"])
|
|
404
|
+
for media_type in body_media_types:
|
|
405
|
+
yield OpenApiBody.from_definition(
|
|
406
|
+
definition=parameter,
|
|
407
|
+
is_required=parameter.get("required", False),
|
|
408
|
+
media_type=media_type,
|
|
409
|
+
name_to_uri=name_to_uri,
|
|
410
|
+
resource_name=resource_name,
|
|
411
|
+
adapter=adapter,
|
|
412
|
+
)
|
|
413
|
+
else:
|
|
414
|
+
yield OpenApiParameter.from_definition(definition=parameter, name_to_uri=name_to_uri, adapter=adapter)
|
|
415
|
+
|
|
416
|
+
if form_parameters:
|
|
417
|
+
form_data = form_data_to_json_schema(form_parameters)
|
|
418
|
+
for media_type in form_data_media_types:
|
|
419
|
+
# Individual `formData` parameters are joined into a single "composite" one.
|
|
420
|
+
yield OpenApiBody.from_form_parameters(
|
|
421
|
+
definition=form_data, media_type=media_type, name_to_uri=form_name_to_uri, adapter=adapter
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def iter_parameters_v3(
|
|
426
|
+
definition: Mapping[str, Any],
|
|
427
|
+
shared_parameters: Sequence[Mapping[str, Any]],
|
|
428
|
+
default_media_types: list[str],
|
|
429
|
+
resolver: RefResolver,
|
|
430
|
+
adapter: SpecificationAdapter,
|
|
431
|
+
) -> Iterator[OperationParameter]:
|
|
432
|
+
# Open API 3.0 has the `requestBody` keyword, which may contain multiple different payload variants.
|
|
433
|
+
# TODO: Typing
|
|
434
|
+
operation = definition
|
|
435
|
+
|
|
436
|
+
bundler = Bundler()
|
|
437
|
+
for parameter in chain(definition.get("parameters", []), shared_parameters):
|
|
438
|
+
parameter, name_to_uri = _bundle_parameter(parameter, resolver, bundler)
|
|
439
|
+
if parameter["in"] in HEADER_LOCATIONS:
|
|
440
|
+
check_header_name(parameter["name"])
|
|
441
|
+
|
|
442
|
+
yield OpenApiParameter.from_definition(definition=parameter, name_to_uri=name_to_uri, adapter=adapter)
|
|
443
|
+
|
|
444
|
+
request_body_or_ref = operation.get("requestBody")
|
|
445
|
+
if request_body_or_ref is not None:
|
|
446
|
+
scope, request_body_or_ref = maybe_resolve(request_body_or_ref, resolver, "")
|
|
447
|
+
# It could be an object inside `requestBodies`, which could be a reference itself
|
|
448
|
+
_, request_body = maybe_resolve(request_body_or_ref, resolver, scope)
|
|
449
|
+
|
|
450
|
+
required = request_body.get("required", False)
|
|
451
|
+
for media_type, content in request_body["content"].items():
|
|
452
|
+
resource_name = None
|
|
453
|
+
schema = content.get("schema")
|
|
454
|
+
name_to_uri = {}
|
|
455
|
+
if isinstance(schema, dict):
|
|
456
|
+
content = dict(content)
|
|
457
|
+
if "$ref" in schema:
|
|
458
|
+
resource_name = resource_name_from_ref(schema["$ref"])
|
|
459
|
+
try:
|
|
460
|
+
to_bundle = cast(dict[str, Any], schema)
|
|
461
|
+
bundled = bundler.bundle(to_bundle, resolver, inline_recursive=True)
|
|
462
|
+
content["schema"] = bundled.schema
|
|
463
|
+
name_to_uri = bundled.name_to_uri
|
|
464
|
+
except BundleError as exc:
|
|
465
|
+
raise InvalidSchema.from_bundle_error(exc, "body") from exc
|
|
466
|
+
yield OpenApiBody.from_definition(
|
|
467
|
+
definition=content,
|
|
468
|
+
is_required=required,
|
|
469
|
+
media_type=media_type,
|
|
470
|
+
resource_name=resource_name,
|
|
471
|
+
name_to_uri=name_to_uri,
|
|
472
|
+
adapter=adapter,
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def resource_name_from_ref(reference: str) -> str:
|
|
477
|
+
return reference.rsplit("/", maxsplit=1)[1]
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def build_path_parameter_v2(kwargs: Mapping[str, Any]) -> OpenApiParameter:
|
|
481
|
+
from schemathesis.specs.openapi.adapter import v2
|
|
482
|
+
|
|
483
|
+
return OpenApiParameter.from_definition(
|
|
484
|
+
definition={"in": ParameterLocation.PATH.value, "required": True, "type": "string", "minLength": 1, **kwargs},
|
|
485
|
+
name_to_uri={},
|
|
486
|
+
adapter=v2,
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def build_path_parameter_v3_0(kwargs: Mapping[str, Any]) -> OpenApiParameter:
|
|
491
|
+
from schemathesis.specs.openapi.adapter import v3_0
|
|
492
|
+
|
|
493
|
+
return OpenApiParameter.from_definition(
|
|
494
|
+
definition={
|
|
495
|
+
"in": ParameterLocation.PATH.value,
|
|
496
|
+
"required": True,
|
|
497
|
+
"schema": {"type": "string", "minLength": 1},
|
|
498
|
+
**kwargs,
|
|
499
|
+
},
|
|
500
|
+
name_to_uri={},
|
|
501
|
+
adapter=v3_0,
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def build_path_parameter_v3_1(kwargs: Mapping[str, Any]) -> OpenApiParameter:
|
|
506
|
+
from schemathesis.specs.openapi.adapter import v3_1
|
|
507
|
+
|
|
508
|
+
return OpenApiParameter.from_definition(
|
|
509
|
+
definition={
|
|
510
|
+
"in": ParameterLocation.PATH.value,
|
|
511
|
+
"required": True,
|
|
512
|
+
"schema": {"type": "string", "minLength": 1},
|
|
513
|
+
**kwargs,
|
|
514
|
+
},
|
|
515
|
+
name_to_uri={},
|
|
516
|
+
adapter=v3_1,
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
@dataclass
|
|
521
|
+
class OpenApiParameterSet(ParameterSet):
|
|
522
|
+
items: list[OpenApiParameter]
|
|
523
|
+
location: ParameterLocation
|
|
524
|
+
|
|
525
|
+
__slots__ = ("items", "location", "_schema", "_schema_cache", "_strategy_cache")
|
|
526
|
+
|
|
527
|
+
def __init__(self, location: ParameterLocation, items: list[OpenApiParameter] | None = None) -> None:
|
|
528
|
+
self.location = location
|
|
529
|
+
self.items = items or []
|
|
530
|
+
self._schema: dict | NotSet = NOT_SET
|
|
531
|
+
self._schema_cache: dict[frozenset[str], dict[str, Any]] = {}
|
|
532
|
+
self._strategy_cache: dict[tuple[frozenset[str], GenerationMode], st.SearchStrategy] = {}
|
|
533
|
+
|
|
534
|
+
@property
|
|
535
|
+
def schema(self) -> dict[str, Any]:
|
|
536
|
+
if self._schema is NOT_SET:
|
|
537
|
+
self._schema = parameters_to_json_schema(self.items, self.location)
|
|
538
|
+
assert not isinstance(self._schema, NotSet)
|
|
539
|
+
return self._schema
|
|
540
|
+
|
|
541
|
+
def get_schema_with_exclusions(self, exclude: Iterable[str]) -> dict[str, Any]:
|
|
542
|
+
"""Get cached schema with specified parameters excluded."""
|
|
543
|
+
exclude_key = frozenset(exclude)
|
|
544
|
+
|
|
545
|
+
if exclude_key in self._schema_cache:
|
|
546
|
+
return self._schema_cache[exclude_key]
|
|
547
|
+
|
|
548
|
+
schema = self.schema
|
|
549
|
+
if exclude_key:
|
|
550
|
+
# Need to exclude some parameters - create a shallow copy to avoid mutating cached schema
|
|
551
|
+
schema = dict(schema)
|
|
552
|
+
if self.location == ParameterLocation.HEADER:
|
|
553
|
+
# Remove excluded headers case-insensitively
|
|
554
|
+
exclude_lower = {name.lower() for name in exclude_key}
|
|
555
|
+
schema["properties"] = {
|
|
556
|
+
key: value for key, value in schema["properties"].items() if key.lower() not in exclude_lower
|
|
557
|
+
}
|
|
558
|
+
if "required" in schema:
|
|
559
|
+
schema["required"] = [key for key in schema["required"] if key.lower() not in exclude_lower]
|
|
560
|
+
else:
|
|
561
|
+
# Non-header locations: remove by exact name
|
|
562
|
+
schema["properties"] = {
|
|
563
|
+
key: value for key, value in schema["properties"].items() if key not in exclude_key
|
|
564
|
+
}
|
|
565
|
+
if "required" in schema:
|
|
566
|
+
schema["required"] = [key for key in schema["required"] if key not in exclude_key]
|
|
567
|
+
|
|
568
|
+
self._schema_cache[exclude_key] = schema
|
|
569
|
+
return schema
|
|
570
|
+
|
|
571
|
+
def get_strategy(
|
|
572
|
+
self,
|
|
573
|
+
operation: APIOperation,
|
|
574
|
+
generation_config: GenerationConfig,
|
|
575
|
+
generation_mode: GenerationMode,
|
|
576
|
+
exclude: Iterable[str] = (),
|
|
577
|
+
) -> st.SearchStrategy:
|
|
578
|
+
"""Get a Hypothesis strategy for this parameter set with specified exclusions."""
|
|
579
|
+
exclude_key = frozenset(exclude)
|
|
580
|
+
cache_key = (exclude_key, generation_mode)
|
|
581
|
+
|
|
582
|
+
if cache_key in self._strategy_cache:
|
|
583
|
+
return self._strategy_cache[cache_key]
|
|
584
|
+
|
|
585
|
+
# Import here to avoid circular dependency
|
|
586
|
+
from hypothesis import strategies as st
|
|
587
|
+
|
|
588
|
+
from schemathesis.openapi.generation.filters import is_valid_header, is_valid_path, is_valid_query
|
|
589
|
+
from schemathesis.specs.openapi._hypothesis import (
|
|
590
|
+
GENERATOR_MODE_TO_STRATEGY_FACTORY,
|
|
591
|
+
_can_skip_header_filter,
|
|
592
|
+
jsonify_python_specific_types,
|
|
593
|
+
make_negative_strategy,
|
|
594
|
+
quote_all,
|
|
595
|
+
)
|
|
596
|
+
from schemathesis.specs.openapi.negative import GeneratedValue
|
|
597
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
|
598
|
+
|
|
599
|
+
# Get schema with exclusions
|
|
600
|
+
schema = self.get_schema_with_exclusions(exclude)
|
|
601
|
+
|
|
602
|
+
strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
|
|
603
|
+
|
|
604
|
+
if not schema["properties"] and strategy_factory is make_negative_strategy:
|
|
605
|
+
# Nothing to negate - all properties were excluded
|
|
606
|
+
strategy = st.none()
|
|
607
|
+
else:
|
|
608
|
+
assert isinstance(operation.schema, BaseOpenAPISchema)
|
|
609
|
+
strategy = strategy_factory(
|
|
610
|
+
schema,
|
|
611
|
+
operation.label,
|
|
612
|
+
self.location,
|
|
613
|
+
None,
|
|
614
|
+
generation_config,
|
|
615
|
+
operation.schema.adapter.jsonschema_validator_cls,
|
|
616
|
+
)
|
|
617
|
+
|
|
618
|
+
# For negative strategies, we need to handle GeneratedValue wrappers
|
|
619
|
+
is_negative = strategy_factory is make_negative_strategy
|
|
620
|
+
|
|
621
|
+
serialize = operation.get_parameter_serializer(self.location)
|
|
622
|
+
if serialize is not None:
|
|
623
|
+
if is_negative:
|
|
624
|
+
# Apply serialize only to the value part of GeneratedValue
|
|
625
|
+
strategy = strategy.map(lambda x: GeneratedValue(serialize(x.value), x.meta))
|
|
626
|
+
else:
|
|
627
|
+
strategy = strategy.map(serialize)
|
|
628
|
+
|
|
629
|
+
filter_func = {
|
|
630
|
+
ParameterLocation.PATH: is_valid_path,
|
|
631
|
+
ParameterLocation.HEADER: is_valid_header,
|
|
632
|
+
ParameterLocation.COOKIE: is_valid_header,
|
|
633
|
+
ParameterLocation.QUERY: is_valid_query,
|
|
634
|
+
}[self.location]
|
|
635
|
+
# Headers with special format do not need filtration
|
|
636
|
+
if not (self.location.is_in_header and _can_skip_header_filter(schema)):
|
|
637
|
+
if is_negative:
|
|
638
|
+
# Apply filter only to the value part of GeneratedValue
|
|
639
|
+
strategy = strategy.filter(lambda x: filter_func(x.value))
|
|
640
|
+
else:
|
|
641
|
+
strategy = strategy.filter(filter_func)
|
|
642
|
+
|
|
643
|
+
# Path & query parameters will be cast to string anyway, but having their JSON equivalents for
|
|
644
|
+
# `True` / `False` / `None` improves chances of them passing validation in apps
|
|
645
|
+
# that expect boolean / null types
|
|
646
|
+
# and not aware of Python-specific representation of those types
|
|
647
|
+
if self.location == ParameterLocation.PATH:
|
|
648
|
+
if is_negative:
|
|
649
|
+
strategy = strategy.map(
|
|
650
|
+
lambda x: GeneratedValue(quote_all(jsonify_python_specific_types(x.value)), x.meta)
|
|
651
|
+
)
|
|
652
|
+
else:
|
|
653
|
+
strategy = strategy.map(quote_all).map(jsonify_python_specific_types)
|
|
654
|
+
elif self.location == ParameterLocation.QUERY:
|
|
655
|
+
if is_negative:
|
|
656
|
+
strategy = strategy.map(lambda x: GeneratedValue(jsonify_python_specific_types(x.value), x.meta))
|
|
657
|
+
else:
|
|
658
|
+
strategy = strategy.map(jsonify_python_specific_types)
|
|
659
|
+
|
|
660
|
+
self._strategy_cache[cache_key] = strategy
|
|
661
|
+
return strategy
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
COMBINED_FORM_DATA_MARKER = "x-schemathesis-form-parameter"
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def form_data_to_json_schema(parameters: Sequence[Mapping[str, Any]]) -> dict[str, Any]:
|
|
668
|
+
"""Convert raw form parameter definitions to a JSON Schema."""
|
|
669
|
+
parameter_data = (
|
|
670
|
+
(param["name"], extract_parameter_schema_v2(param), param.get("required", False)) for param in parameters
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
merged = _merge_parameters_to_object_schema(parameter_data, ParameterLocation.BODY)
|
|
674
|
+
|
|
675
|
+
return {"schema": merged, COMBINED_FORM_DATA_MARKER: True}
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def parameters_to_json_schema(parameters: Iterable[OpenApiParameter], location: ParameterLocation) -> dict[str, Any]:
|
|
679
|
+
"""Convert multiple Open API parameters to a JSON Schema."""
|
|
680
|
+
parameter_data = ((param.name, param.optimized_schema, param.is_required) for param in parameters)
|
|
681
|
+
|
|
682
|
+
return _merge_parameters_to_object_schema(parameter_data, location)
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def _merge_parameters_to_object_schema(
|
|
686
|
+
parameters: Iterable[tuple[str, Any, bool]], location: ParameterLocation
|
|
687
|
+
) -> dict[str, Any]:
|
|
688
|
+
"""Merge parameter data into a JSON Schema object."""
|
|
689
|
+
properties = {}
|
|
690
|
+
required = []
|
|
691
|
+
bundled = {}
|
|
692
|
+
|
|
693
|
+
for name, subschema, is_required in parameters:
|
|
694
|
+
# Extract bundled data if present
|
|
695
|
+
if isinstance(subschema, dict) and BUNDLE_STORAGE_KEY in subschema:
|
|
696
|
+
subschema = dict(subschema)
|
|
697
|
+
subschema_bundle = subschema.pop(BUNDLE_STORAGE_KEY)
|
|
698
|
+
# NOTE: Bundled schema names are not overlapping as they were bundled via the same `Bundler` that
|
|
699
|
+
# ensures unique names
|
|
700
|
+
bundled.update(subschema_bundle)
|
|
701
|
+
|
|
702
|
+
# Apply location-specific adjustments to individual parameter schemas
|
|
703
|
+
if isinstance(subschema, dict):
|
|
704
|
+
# Headers: add HEADER_FORMAT for plain string types
|
|
705
|
+
if location.is_in_header and list(subschema) == ["type"] and subschema["type"] == "string":
|
|
706
|
+
subschema = {**subschema, "format": HEADER_FORMAT}
|
|
707
|
+
|
|
708
|
+
# Path parameters: ensure string types have minLength >= 1
|
|
709
|
+
elif location == ParameterLocation.PATH and subschema.get("type") == "string":
|
|
710
|
+
if "minLength" not in subschema:
|
|
711
|
+
subschema = {**subschema, "minLength": 1}
|
|
712
|
+
|
|
713
|
+
properties[name] = subschema
|
|
714
|
+
|
|
715
|
+
# Path parameters are always required
|
|
716
|
+
if (location == ParameterLocation.PATH or is_required) and name not in required:
|
|
717
|
+
required.append(name)
|
|
718
|
+
|
|
719
|
+
merged = {
|
|
720
|
+
"properties": properties,
|
|
721
|
+
"additionalProperties": False,
|
|
722
|
+
"type": "object",
|
|
723
|
+
}
|
|
724
|
+
if required:
|
|
725
|
+
merged["required"] = required
|
|
726
|
+
if bundled:
|
|
727
|
+
merged[BUNDLE_STORAGE_KEY] = bundled
|
|
728
|
+
|
|
729
|
+
return merged
|