schemathesis 3.25.6__py3-none-any.whl → 4.0.0a1__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 +27 -65
- schemathesis/auths.py +102 -82
- schemathesis/checks.py +126 -46
- schemathesis/cli/__init__.py +11 -1760
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/commands/__init__.py +37 -0
- schemathesis/cli/commands/run/__init__.py +662 -0
- schemathesis/cli/commands/run/checks.py +80 -0
- schemathesis/cli/commands/run/context.py +117 -0
- schemathesis/cli/commands/run/events.py +35 -0
- schemathesis/cli/commands/run/executor.py +138 -0
- schemathesis/cli/commands/run/filters.py +194 -0
- schemathesis/cli/commands/run/handlers/__init__.py +46 -0
- schemathesis/cli/commands/run/handlers/base.py +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
- schemathesis/cli/commands/run/handlers/output.py +746 -0
- schemathesis/cli/commands/run/hypothesis.py +105 -0
- schemathesis/cli/commands/run/loaders.py +129 -0
- schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
- schemathesis/cli/constants.py +5 -52
- schemathesis/cli/core.py +17 -0
- schemathesis/cli/ext/fs.py +14 -0
- schemathesis/cli/ext/groups.py +55 -0
- schemathesis/cli/{options.py → ext/options.py} +39 -10
- schemathesis/cli/hooks.py +36 -0
- schemathesis/contrib/__init__.py +1 -3
- schemathesis/contrib/openapi/__init__.py +1 -3
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
- schemathesis/core/__init__.py +58 -0
- schemathesis/core/compat.py +25 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +370 -0
- schemathesis/core/failures.py +285 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
- schemathesis/core/output/__init__.py +69 -0
- schemathesis/core/output/sanitization.py +197 -0
- schemathesis/core/rate_limit.py +60 -0
- schemathesis/core/registries.py +31 -0
- schemathesis/{internal → core}/result.py +1 -1
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +108 -0
- schemathesis/core/validation.py +38 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +30 -0
- schemathesis/engine/config.py +59 -0
- schemathesis/engine/context.py +119 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +157 -0
- schemathesis/engine/errors.py +394 -0
- schemathesis/engine/events.py +337 -0
- schemathesis/engine/phases/__init__.py +66 -0
- schemathesis/{runner → engine/phases}/probes.py +50 -67
- schemathesis/engine/phases/stateful/__init__.py +65 -0
- schemathesis/engine/phases/stateful/_executor.py +326 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +174 -0
- schemathesis/engine/phases/unit/_executor.py +321 -0
- schemathesis/engine/phases/unit/_pool.py +74 -0
- schemathesis/engine/recorder.py +241 -0
- schemathesis/errors.py +31 -0
- schemathesis/experimental/__init__.py +18 -14
- schemathesis/filters.py +103 -14
- schemathesis/generation/__init__.py +21 -37
- schemathesis/generation/case.py +190 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/generation/hypothesis/__init__.py +30 -0
- schemathesis/generation/hypothesis/builder.py +585 -0
- schemathesis/generation/hypothesis/examples.py +50 -0
- schemathesis/generation/hypothesis/given.py +66 -0
- schemathesis/generation/hypothesis/reporting.py +14 -0
- schemathesis/generation/hypothesis/strategies.py +16 -0
- schemathesis/generation/meta.py +115 -0
- schemathesis/generation/modes.py +28 -0
- schemathesis/generation/overrides.py +96 -0
- schemathesis/generation/stateful/__init__.py +20 -0
- schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
- schemathesis/generation/targets.py +69 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +115 -0
- schemathesis/graphql/loaders.py +131 -0
- schemathesis/hooks.py +99 -67
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +412 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +63 -0
- schemathesis/openapi/loaders.py +178 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +273 -0
- schemathesis/pytest/loaders.py +12 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -261
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +25 -0
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +7 -5
- schemathesis/specs/graphql/schemas.py +215 -187
- schemathesis/specs/graphql/validation.py +11 -18
- schemathesis/specs/openapi/__init__.py +7 -1
- schemathesis/specs/openapi/_cache.py +122 -0
- schemathesis/specs/openapi/_hypothesis.py +146 -165
- schemathesis/specs/openapi/checks.py +565 -67
- schemathesis/specs/openapi/converter.py +33 -6
- schemathesis/specs/openapi/definitions.py +11 -18
- schemathesis/specs/openapi/examples.py +139 -23
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +4 -6
- schemathesis/specs/openapi/expressions/extractors.py +23 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +38 -14
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +45 -0
- schemathesis/specs/openapi/links.py +65 -165
- schemathesis/specs/openapi/media_types.py +32 -0
- schemathesis/specs/openapi/negative/__init__.py +7 -3
- schemathesis/specs/openapi/negative/mutations.py +24 -8
- schemathesis/specs/openapi/parameters.py +46 -30
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +47 -57
- schemathesis/specs/openapi/schemas.py +478 -369
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +11 -6
- schemathesis/specs/openapi/stateful/__init__.py +185 -73
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/transport/__init__.py +104 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +99 -0
- schemathesis/transport/requests.py +221 -0
- schemathesis/{_xml.py → transport/serialization.py} +143 -28
- schemathesis/transport/wsgi.py +165 -0
- schemathesis-4.0.0a1.dist-info/METADATA +297 -0
- schemathesis-4.0.0a1.dist-info/RECORD +152 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -17
- schemathesis/_hypothesis.py +0 -246
- schemathesis/_override.py +0 -49
- schemathesis/cli/cassettes.py +0 -375
- schemathesis/cli/context.py +0 -58
- schemathesis/cli/debug.py +0 -26
- schemathesis/cli/handlers.py +0 -16
- schemathesis/cli/junitxml.py +0 -43
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -790
- schemathesis/cli/output/short.py +0 -44
- schemathesis/cli/sanitization.py +0 -20
- schemathesis/code_samples.py +0 -149
- schemathesis/constants.py +0 -55
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -15
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -560
- schemathesis/extra/_aiohttp.py +0 -27
- schemathesis/extra/_flask.py +0 -10
- schemathesis/extra/_server.py +0 -17
- schemathesis/failures.py +0 -209
- schemathesis/fixups/__init__.py +0 -36
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -29
- schemathesis/graphql.py +0 -4
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/copy.py +0 -13
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -34
- schemathesis/internal/jsonschema.py +0 -35
- schemathesis/internal/transformation.py +0 -15
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -361
- schemathesis/loaders.py +0 -120
- schemathesis/models.py +0 -1234
- schemathesis/parameters.py +0 -86
- schemathesis/runner/__init__.py +0 -570
- schemathesis/runner/events.py +0 -329
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/core.py +0 -1035
- schemathesis/runner/impl/solo.py +0 -90
- schemathesis/runner/impl/threadpool.py +0 -400
- schemathesis/runner/serialization.py +0 -411
- schemathesis/sanitization.py +0 -248
- schemathesis/serializers.py +0 -323
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -201
- schemathesis/service/client.py +0 -100
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -57
- schemathesis/service/hosts.py +0 -107
- schemathesis/service/metadata.py +0 -46
- schemathesis/service/models.py +0 -49
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -199
- schemathesis/service/usage.py +0 -65
- schemathesis/specs/graphql/loaders.py +0 -344
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/loaders.py +0 -667
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis/specs/openapi/validation.py +0 -25
- schemathesis/stateful/__init__.py +0 -133
- schemathesis/targets.py +0 -45
- schemathesis/throttling.py +0 -41
- schemathesis/transports/__init__.py +0 -5
- schemathesis/transports/auth.py +0 -15
- schemathesis/transports/headers.py +0 -35
- schemathesis/transports/responses.py +0 -52
- schemathesis/types.py +0 -35
- schemathesis/utils.py +0 -169
- schemathesis-3.25.6.dist-info/METADATA +0 -356
- schemathesis-3.25.6.dist-info/RECORD +0 -134
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- {schemathesis-3.25.6.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -1,122 +1,65 @@
|
|
1
1
|
from __future__ import annotations
|
2
|
-
|
3
|
-
|
2
|
+
|
3
|
+
import time
|
4
4
|
from contextlib import suppress
|
5
5
|
from dataclasses import dataclass
|
6
|
-
from
|
7
|
-
from typing import Any, Callable, Dict, Iterable, Optional
|
6
|
+
from typing import Any, Callable, Dict, Iterable, Optional, Union, cast
|
8
7
|
from urllib.parse import quote_plus
|
9
8
|
from weakref import WeakKeyDictionary
|
10
9
|
|
11
|
-
from hypothesis import
|
10
|
+
from hypothesis import event, note, reject
|
11
|
+
from hypothesis import strategies as st
|
12
12
|
from hypothesis_jsonschema import from_schema
|
13
|
-
|
14
|
-
from
|
15
|
-
from
|
16
|
-
|
17
|
-
from
|
18
|
-
from
|
19
|
-
from .
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
13
|
+
|
14
|
+
from schemathesis.core import NOT_SET, NotSet, media_types
|
15
|
+
from schemathesis.core.control import SkipTest
|
16
|
+
from schemathesis.core.errors import SERIALIZERS_SUGGESTION_MESSAGE, SerializationNotPossible
|
17
|
+
from schemathesis.core.transforms import deepclone
|
18
|
+
from schemathesis.core.transport import prepare_urlencoded
|
19
|
+
from schemathesis.generation.meta import (
|
20
|
+
CaseMetadata,
|
21
|
+
ComponentInfo,
|
22
|
+
ComponentKind,
|
23
|
+
ExplicitPhaseData,
|
24
|
+
GeneratePhaseData,
|
25
|
+
GenerationInfo,
|
26
|
+
PhaseInfo,
|
27
|
+
TestPhase,
|
28
|
+
)
|
29
|
+
from schemathesis.openapi.generation.filters import is_valid_header, is_valid_path, is_valid_query, is_valid_urlencoded
|
30
|
+
from schemathesis.schemas import APIOperation
|
31
|
+
|
32
|
+
from ... import auths
|
33
|
+
from ...generation import GenerationConfig, GenerationMode
|
24
34
|
from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
|
25
|
-
from ...internal.validation import is_illegal_surrogate
|
26
|
-
from ...models import APIOperation, Case, cant_serialize
|
27
|
-
from ...transports.content_types import parse_content_type
|
28
|
-
from ...transports.headers import has_invalid_characters, is_latin_1_encodable
|
29
|
-
from ...types import NotSet
|
30
|
-
from ...serializers import Binary
|
31
|
-
from ...utils import compose, skip
|
32
35
|
from .constants import LOCATION_TO_CONTAINER
|
36
|
+
from .formats import HEADER_FORMAT, STRING_FORMATS, get_default_format_strategies, header_values
|
37
|
+
from .media_types import MEDIA_TYPES
|
33
38
|
from .negative import negative_schema
|
34
39
|
from .negative.utils import can_negate
|
35
|
-
from .parameters import OpenAPIBody, parameters_to_json_schema
|
40
|
+
from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
|
36
41
|
from .utils import is_header_location
|
37
42
|
|
38
|
-
HEADER_FORMAT = "_header_value"
|
39
43
|
SLASH = "/"
|
40
44
|
StrategyFactory = Callable[[Dict[str, Any], str, str, Optional[str], GenerationConfig], st.SearchStrategy]
|
41
45
|
|
42
46
|
|
43
|
-
def header_values(blacklist_characters: str = "\n\r") -> st.SearchStrategy[str]:
|
44
|
-
return st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255, blacklist_characters="\n\r"))
|
45
|
-
|
46
|
-
|
47
|
-
@lru_cache
|
48
|
-
def get_default_format_strategies() -> dict[str, st.SearchStrategy]:
|
49
|
-
"""Get all default "format" strategies."""
|
50
|
-
|
51
|
-
def make_basic_auth_str(item: tuple[str, str]) -> str:
|
52
|
-
return _basic_auth_str(*item)
|
53
|
-
|
54
|
-
latin1_text = st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255))
|
55
|
-
|
56
|
-
# Define valid characters here to avoid filtering them out in `is_valid_header` later
|
57
|
-
header_value = header_values()
|
58
|
-
|
59
|
-
return {
|
60
|
-
"binary": st.binary().map(Binary),
|
61
|
-
"byte": st.binary().map(lambda x: b64encode(x).decode()),
|
62
|
-
# RFC 7230, Section 3.2.6
|
63
|
-
"_header_name": st.text(
|
64
|
-
min_size=1, alphabet=st.sampled_from("!#$%&'*+-.^_`|~" + string.digits + string.ascii_letters)
|
65
|
-
),
|
66
|
-
# Header values with leading non-visible chars can't be sent with `requests`
|
67
|
-
HEADER_FORMAT: header_value.map(str.lstrip),
|
68
|
-
"_basic_auth": st.tuples(latin1_text, latin1_text).map(make_basic_auth_str),
|
69
|
-
"_bearer_auth": header_value.map("Bearer {}".format),
|
70
|
-
}
|
71
|
-
|
72
|
-
|
73
|
-
def is_valid_header(headers: dict[str, Any]) -> bool:
|
74
|
-
"""Verify if the generated headers are valid."""
|
75
|
-
for name, value in headers.items():
|
76
|
-
if not is_latin_1_encodable(value):
|
77
|
-
return False
|
78
|
-
if has_invalid_characters(name, value):
|
79
|
-
return False
|
80
|
-
return True
|
81
|
-
|
82
|
-
|
83
|
-
def is_valid_query(query: dict[str, Any]) -> bool:
|
84
|
-
"""Surrogates are not allowed in a query string.
|
85
|
-
|
86
|
-
`requests` and `werkzeug` will fail to send it to the application.
|
87
|
-
"""
|
88
|
-
for name, value in query.items():
|
89
|
-
if is_illegal_surrogate(name) or is_illegal_surrogate(value):
|
90
|
-
return False
|
91
|
-
return True
|
92
|
-
|
93
|
-
|
94
|
-
def is_valid_urlencoded(data: Any) -> bool:
|
95
|
-
if data is NOT_SET:
|
96
|
-
return True
|
97
|
-
try:
|
98
|
-
for _, __ in to_key_val_list(data): # type: ignore[no-untyped-call]
|
99
|
-
pass
|
100
|
-
return True
|
101
|
-
except (TypeError, ValueError):
|
102
|
-
return False
|
103
|
-
|
104
|
-
|
105
47
|
@st.composite # type: ignore
|
106
|
-
def
|
48
|
+
def openapi_cases(
|
107
49
|
draw: Callable,
|
50
|
+
*,
|
108
51
|
operation: APIOperation,
|
109
52
|
hooks: HookDispatcher | None = None,
|
110
53
|
auth_storage: auths.AuthStorage | None = None,
|
111
|
-
|
112
|
-
generation_config: GenerationConfig
|
54
|
+
generation_mode: GenerationMode = GenerationMode.default(),
|
55
|
+
generation_config: GenerationConfig,
|
113
56
|
path_parameters: NotSet | dict[str, Any] = NOT_SET,
|
114
57
|
headers: NotSet | dict[str, Any] = NOT_SET,
|
115
58
|
cookies: NotSet | dict[str, Any] = NOT_SET,
|
116
59
|
query: NotSet | dict[str, Any] = NOT_SET,
|
117
60
|
body: Any = NOT_SET,
|
118
61
|
media_type: str | None = None,
|
119
|
-
|
62
|
+
phase: TestPhase = TestPhase.GENERATE,
|
120
63
|
) -> Any:
|
121
64
|
"""A strategy that creates `Case` instances.
|
122
65
|
|
@@ -130,70 +73,110 @@ def get_case_strategy(
|
|
130
73
|
The primary purpose of this behavior is to prevent sending incomplete explicit examples by generating missing parts
|
131
74
|
as it works with `body`.
|
132
75
|
"""
|
133
|
-
|
76
|
+
start = time.monotonic()
|
77
|
+
strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
|
134
78
|
|
135
79
|
context = HookContext(operation)
|
136
80
|
|
137
|
-
generation_config = generation_config or operation.schema.generation_config
|
138
|
-
|
139
81
|
path_parameters_ = generate_parameter(
|
140
|
-
"path", path_parameters, operation, draw, context, hooks,
|
82
|
+
"path", path_parameters, operation, draw, context, hooks, generation_mode, generation_config
|
83
|
+
)
|
84
|
+
headers_ = generate_parameter(
|
85
|
+
"header", headers, operation, draw, context, hooks, generation_mode, generation_config
|
86
|
+
)
|
87
|
+
cookies_ = generate_parameter(
|
88
|
+
"cookie", cookies, operation, draw, context, hooks, generation_mode, generation_config
|
141
89
|
)
|
142
|
-
|
143
|
-
cookies_ = generate_parameter("cookie", cookies, operation, draw, context, hooks, generator, generation_config)
|
144
|
-
query_ = generate_parameter("query", query, operation, draw, context, hooks, generator, generation_config)
|
90
|
+
query_ = generate_parameter("query", query, operation, draw, context, hooks, generation_mode, generation_config)
|
145
91
|
|
146
92
|
if body is NOT_SET:
|
147
93
|
if operation.body:
|
148
|
-
body_generator =
|
149
|
-
if
|
94
|
+
body_generator = generation_mode
|
95
|
+
if generation_mode.is_negative:
|
150
96
|
# Consider only schemas that are possible to negate
|
151
97
|
candidates = [item for item in operation.body.items if can_negate(item.as_json_schema(operation))]
|
152
98
|
# Not possible to negate body, fallback to positive data generation
|
153
99
|
if not candidates:
|
154
100
|
candidates = operation.body.items
|
155
101
|
strategy_factory = make_positive_strategy
|
156
|
-
body_generator =
|
102
|
+
body_generator = GenerationMode.POSITIVE
|
157
103
|
else:
|
158
104
|
candidates = operation.body.items
|
159
105
|
parameter = draw(st.sampled_from(candidates))
|
160
106
|
strategy = _get_body_strategy(parameter, strategy_factory, operation, generation_config)
|
161
107
|
strategy = apply_hooks(operation, context, hooks, strategy, "body")
|
162
108
|
# Parameter may have a wildcard media type. In this case, choose any supported one
|
163
|
-
possible_media_types = sorted(
|
109
|
+
possible_media_types = sorted(
|
110
|
+
operation.schema.transport.get_matching_media_types(parameter.media_type), key=lambda x: x[0]
|
111
|
+
)
|
164
112
|
if not possible_media_types:
|
165
113
|
all_media_types = operation.get_request_payload_content_types()
|
166
|
-
if all(
|
114
|
+
if all(
|
115
|
+
operation.schema.transport.get_first_matching_media_type(media_type) is None
|
116
|
+
for media_type in all_media_types
|
117
|
+
):
|
167
118
|
# None of media types defined for this operation are not supported
|
168
119
|
raise SerializationNotPossible.from_media_types(*all_media_types)
|
169
120
|
# Other media types are possible - avoid choosing this media type in the future
|
170
|
-
|
171
|
-
|
172
|
-
|
121
|
+
event_text = f"Can't serialize data to `{parameter.media_type}`."
|
122
|
+
note(f"{event_text} {SERIALIZERS_SUGGESTION_MESSAGE}")
|
123
|
+
event(event_text)
|
124
|
+
reject() # type: ignore
|
125
|
+
media_type, _ = draw(st.sampled_from(possible_media_types))
|
126
|
+
if media_type is not None and media_types.parse(media_type) == (
|
127
|
+
"application",
|
128
|
+
"x-www-form-urlencoded",
|
129
|
+
):
|
173
130
|
strategy = strategy.map(prepare_urlencoded).filter(is_valid_urlencoded)
|
174
131
|
body_ = ValueContainer(value=draw(strategy), location="body", generator=body_generator)
|
175
132
|
else:
|
176
133
|
body_ = ValueContainer(value=body, location="body", generator=None)
|
177
134
|
else:
|
135
|
+
# This explicit body payload comes for a media type that has a custom strategy registered
|
136
|
+
# Such strategies only support binary payloads, otherwise they can't be serialized
|
137
|
+
if not isinstance(body, bytes) and media_type in MEDIA_TYPES:
|
138
|
+
all_media_types = operation.get_request_payload_content_types()
|
139
|
+
raise SerializationNotPossible.from_media_types(*all_media_types)
|
178
140
|
body_ = ValueContainer(value=body, location="body", generator=None)
|
179
141
|
|
180
|
-
if operation.schema.validate_schema and operation.method.upper() == "GET" and operation.body:
|
181
|
-
raise BodyInGetRequestError("GET requests should not contain body parameters.")
|
182
142
|
# If we need to generate negative cases but no generated values were negated, then skip the whole test
|
183
|
-
if
|
184
|
-
if
|
185
|
-
|
143
|
+
if generation_mode.is_negative and not any_negated_values([query_, cookies_, headers_, path_parameters_, body_]):
|
144
|
+
if generation_config.modes == [GenerationMode.NEGATIVE]:
|
145
|
+
raise SkipTest("Impossible to generate negative test cases")
|
186
146
|
else:
|
187
147
|
reject()
|
188
|
-
|
189
|
-
|
148
|
+
|
149
|
+
_phase_data = {
|
150
|
+
TestPhase.EXPLICIT: ExplicitPhaseData(),
|
151
|
+
TestPhase.GENERATE: GeneratePhaseData(),
|
152
|
+
}[phase]
|
153
|
+
phase_data = cast(Union[ExplicitPhaseData, GeneratePhaseData], _phase_data)
|
154
|
+
|
155
|
+
instance = operation.Case(
|
190
156
|
media_type=media_type,
|
191
157
|
path_parameters=path_parameters_.value,
|
192
|
-
headers=
|
158
|
+
headers=headers_.value,
|
193
159
|
cookies=cookies_.value,
|
194
160
|
query=query_.value,
|
195
161
|
body=body_.value,
|
196
|
-
|
162
|
+
meta=CaseMetadata(
|
163
|
+
generation=GenerationInfo(
|
164
|
+
time=time.monotonic() - start,
|
165
|
+
mode=generation_mode,
|
166
|
+
),
|
167
|
+
phase=PhaseInfo(name=phase, data=phase_data),
|
168
|
+
components={
|
169
|
+
kind: ComponentInfo(mode=value.generator)
|
170
|
+
for kind, value in [
|
171
|
+
(ComponentKind.QUERY, query_),
|
172
|
+
(ComponentKind.PATH_PARAMETERS, path_parameters_),
|
173
|
+
(ComponentKind.HEADERS, headers_),
|
174
|
+
(ComponentKind.COOKIES, cookies_),
|
175
|
+
(ComponentKind.BODY, body_),
|
176
|
+
]
|
177
|
+
if value.generator is not None
|
178
|
+
},
|
179
|
+
),
|
197
180
|
)
|
198
181
|
auth_context = auths.AuthContext(
|
199
182
|
operation=operation,
|
@@ -212,13 +195,15 @@ def _get_body_strategy(
|
|
212
195
|
operation: APIOperation,
|
213
196
|
generation_config: GenerationConfig,
|
214
197
|
) -> st.SearchStrategy:
|
198
|
+
if parameter.media_type in MEDIA_TYPES:
|
199
|
+
return MEDIA_TYPES[parameter.media_type]
|
215
200
|
# The cache key relies on object ids, which means that the parameter should not be mutated
|
216
201
|
# Note, the parent schema is not included as each parameter belong only to one schema
|
217
202
|
if parameter in _BODY_STRATEGIES_CACHE and strategy_factory in _BODY_STRATEGIES_CACHE[parameter]:
|
218
203
|
return _BODY_STRATEGIES_CACHE[parameter][strategy_factory]
|
219
204
|
schema = parameter.as_json_schema(operation)
|
220
205
|
schema = operation.schema.prepare_schema(schema)
|
221
|
-
strategy = strategy_factory(schema, operation.
|
206
|
+
strategy = strategy_factory(schema, operation.label, "body", parameter.media_type, generation_config)
|
222
207
|
if not parameter.is_required:
|
223
208
|
strategy |= st.just(NOT_SET)
|
224
209
|
_BODY_STRATEGIES_CACHE.setdefault(parameter, {})[strategy_factory] = strategy
|
@@ -248,7 +233,7 @@ def get_parameters_value(
|
|
248
233
|
strategy = apply_hooks(operation, context, hooks, strategy, location)
|
249
234
|
new = draw(strategy)
|
250
235
|
if new is not None:
|
251
|
-
copied =
|
236
|
+
copied = deepclone(value)
|
252
237
|
copied.update(new)
|
253
238
|
return copied
|
254
239
|
return value
|
@@ -263,7 +248,9 @@ class ValueContainer:
|
|
263
248
|
|
264
249
|
value: Any
|
265
250
|
location: str
|
266
|
-
generator:
|
251
|
+
generator: GenerationMode | None
|
252
|
+
|
253
|
+
__slots__ = ("value", "location", "generator")
|
267
254
|
|
268
255
|
@property
|
269
256
|
def is_generated(self) -> bool:
|
@@ -273,7 +260,7 @@ class ValueContainer:
|
|
273
260
|
|
274
261
|
def any_negated_values(values: list[ValueContainer]) -> bool:
|
275
262
|
"""Check if any generated values are negated."""
|
276
|
-
return any(value.generator ==
|
263
|
+
return any(value.generator == GenerationMode.NEGATIVE for value in values if value.is_generated)
|
277
264
|
|
278
265
|
|
279
266
|
def generate_parameter(
|
@@ -283,7 +270,7 @@ def generate_parameter(
|
|
283
270
|
draw: Callable,
|
284
271
|
context: HookContext,
|
285
272
|
hooks: HookDispatcher | None,
|
286
|
-
generator:
|
273
|
+
generator: GenerationMode,
|
287
274
|
generation_config: GenerationConfig,
|
288
275
|
) -> ValueContainer:
|
289
276
|
"""Generate a value for a parameter.
|
@@ -297,13 +284,13 @@ def generate_parameter(
|
|
297
284
|
# If we can't negate any parameter, generate positive ones
|
298
285
|
# If nothing else will be negated, then skip the test completely
|
299
286
|
strategy_factory = make_positive_strategy
|
300
|
-
generator =
|
287
|
+
generator = GenerationMode.POSITIVE
|
301
288
|
else:
|
302
|
-
strategy_factory =
|
289
|
+
strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generator]
|
303
290
|
value = get_parameters_value(
|
304
291
|
explicit, location, draw, operation, context, hooks, strategy_factory, generation_config
|
305
292
|
)
|
306
|
-
used_generator:
|
293
|
+
used_generator: GenerationMode | None = generator
|
307
294
|
if value == explicit:
|
308
295
|
# When we pass `explicit`, then its parts are excluded from generation of the final value
|
309
296
|
# If the final value is the same, then other parameters were generated at all
|
@@ -332,6 +319,18 @@ def can_negate_headers(operation: APIOperation, location: str) -> bool:
|
|
332
319
|
return any(header != {"type": "string"} for header in headers.values())
|
333
320
|
|
334
321
|
|
322
|
+
def get_schema_for_location(
|
323
|
+
operation: APIOperation, location: str, parameters: Iterable[OpenAPIParameter]
|
324
|
+
) -> dict[str, Any]:
|
325
|
+
schema = parameters_to_json_schema(operation, parameters)
|
326
|
+
if location == "path":
|
327
|
+
schema["required"] = list(schema["properties"])
|
328
|
+
for prop in schema.get("properties", {}).values():
|
329
|
+
if prop.get("type") == "string":
|
330
|
+
prop.setdefault("minLength", 1)
|
331
|
+
return operation.schema.prepare_schema(schema)
|
332
|
+
|
333
|
+
|
335
334
|
def get_parameters_strategy(
|
336
335
|
operation: APIOperation,
|
337
336
|
strategy_factory: StrategyFactory,
|
@@ -346,13 +345,7 @@ def get_parameters_strategy(
|
|
346
345
|
nested_cache_key = (strategy_factory, location, tuple(sorted(exclude)))
|
347
346
|
if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
|
348
347
|
return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
|
349
|
-
schema =
|
350
|
-
if not operation.schema.validate_schema and location == "path":
|
351
|
-
# If schema validation is disabled, we try to generate data even if the parameter definition
|
352
|
-
# contains errors.
|
353
|
-
# In this case, we know that the `required` keyword should always be `True`.
|
354
|
-
schema["required"] = list(schema["properties"])
|
355
|
-
schema = operation.schema.prepare_schema(schema)
|
348
|
+
schema = get_schema_for_location(operation, location, parameters)
|
356
349
|
for name in exclude:
|
357
350
|
# Values from `exclude` are not necessarily valid for the schema - they come from user-defined examples
|
358
351
|
# that may be invalid
|
@@ -363,7 +356,7 @@ def get_parameters_strategy(
|
|
363
356
|
# Nothing to negate - all properties were excluded
|
364
357
|
strategy = st.none()
|
365
358
|
else:
|
366
|
-
strategy = strategy_factory(schema, operation.
|
359
|
+
strategy = strategy_factory(schema, operation.label, location, None, generation_config)
|
367
360
|
serialize = operation.get_parameter_serializer(location)
|
368
361
|
if serialize is not None:
|
369
362
|
strategy = strategy.map(serialize)
|
@@ -380,12 +373,10 @@ def get_parameters_strategy(
|
|
380
373
|
# `True` / `False` / `None` improves chances of them passing validation in apps
|
381
374
|
# that expect boolean / null types
|
382
375
|
# and not aware of Python-specific representation of those types
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
if map_func:
|
388
|
-
strategy = strategy.map(map_func) # type: ignore
|
376
|
+
if location == "path":
|
377
|
+
strategy = strategy.map(quote_all).map(jsonify_python_specific_types)
|
378
|
+
elif location == "query":
|
379
|
+
strategy = strategy.map(jsonify_python_specific_types)
|
389
380
|
_PARAMETER_STRATEGIES_CACHE.setdefault(operation, {})[nested_cache_key] = strategy
|
390
381
|
return strategy
|
391
382
|
# No parameters defined for this location
|
@@ -412,6 +403,17 @@ def jsonify_python_specific_types(value: dict[str, Any]) -> dict[str, Any]:
|
|
412
403
|
return value
|
413
404
|
|
414
405
|
|
406
|
+
def _build_custom_formats(
|
407
|
+
custom_formats: dict[str, st.SearchStrategy] | None, generation_config: GenerationConfig
|
408
|
+
) -> dict[str, st.SearchStrategy]:
|
409
|
+
custom_formats = {**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})}
|
410
|
+
if generation_config.headers.strategy is not None:
|
411
|
+
custom_formats[HEADER_FORMAT] = generation_config.headers.strategy
|
412
|
+
elif not generation_config.allow_x00:
|
413
|
+
custom_formats[HEADER_FORMAT] = header_values(blacklist_characters="\n\r\x00")
|
414
|
+
return custom_formats
|
415
|
+
|
416
|
+
|
415
417
|
def make_positive_strategy(
|
416
418
|
schema: dict[str, Any],
|
417
419
|
operation_name: str,
|
@@ -428,9 +430,10 @@ def make_positive_strategy(
|
|
428
430
|
for sub_schema in schema.get("properties", {}).values():
|
429
431
|
if list(sub_schema) == ["type"] and sub_schema["type"] == "string":
|
430
432
|
sub_schema.setdefault("format", HEADER_FORMAT)
|
433
|
+
custom_formats = _build_custom_formats(custom_formats, generation_config)
|
431
434
|
return from_schema(
|
432
435
|
schema,
|
433
|
-
custom_formats=
|
436
|
+
custom_formats=custom_formats,
|
434
437
|
allow_x00=generation_config.allow_x00,
|
435
438
|
codec=generation_config.codec,
|
436
439
|
)
|
@@ -449,40 +452,23 @@ def make_negative_strategy(
|
|
449
452
|
generation_config: GenerationConfig,
|
450
453
|
custom_formats: dict[str, st.SearchStrategy] | None = None,
|
451
454
|
) -> st.SearchStrategy:
|
455
|
+
custom_formats = _build_custom_formats(custom_formats, generation_config)
|
452
456
|
return negative_schema(
|
453
457
|
schema,
|
454
458
|
operation_name=operation_name,
|
455
459
|
location=location,
|
456
460
|
media_type=media_type,
|
457
|
-
custom_formats=
|
461
|
+
custom_formats=custom_formats,
|
458
462
|
generation_config=generation_config,
|
459
463
|
)
|
460
464
|
|
461
465
|
|
462
|
-
|
463
|
-
|
464
|
-
|
466
|
+
GENERATOR_MODE_TO_STRATEGY_FACTORY = {
|
467
|
+
GenerationMode.POSITIVE: make_positive_strategy,
|
468
|
+
GenerationMode.NEGATIVE: make_negative_strategy,
|
465
469
|
}
|
466
470
|
|
467
471
|
|
468
|
-
def is_valid_path(parameters: dict[str, Any]) -> bool:
|
469
|
-
"""Empty strings ("") are excluded from path by urllib3.
|
470
|
-
|
471
|
-
A path containing to "/" or "%2F" will lead to ambiguous path resolution in
|
472
|
-
many frameworks and libraries, such behaviour have been observed in both
|
473
|
-
WSGI and ASGI applications.
|
474
|
-
|
475
|
-
In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
|
476
|
-
Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
|
477
|
-
"""
|
478
|
-
disallowed_values = (SLASH, "")
|
479
|
-
|
480
|
-
return not any(
|
481
|
-
(value in disallowed_values or is_illegal_surrogate(value) or isinstance(value, str) and SLASH in value)
|
482
|
-
for value in parameters.values()
|
483
|
-
)
|
484
|
-
|
485
|
-
|
486
472
|
def quote_all(parameters: dict[str, Any]) -> dict[str, Any]:
|
487
473
|
"""Apply URL quotation for all values in a dictionary."""
|
488
474
|
# Even though, "." is an unreserved character, it has a special meaning in "." and ".." strings.
|
@@ -512,8 +498,3 @@ def apply_hooks(
|
|
512
498
|
"""Apply all hooks related to the given location."""
|
513
499
|
container = LOCATION_TO_CONTAINER[location]
|
514
500
|
return apply_to_all_dispatchers(operation, context, hooks, strategy, container)
|
515
|
-
|
516
|
-
|
517
|
-
def clear_cache() -> None:
|
518
|
-
_PARAMETER_STRATEGIES_CACHE.clear()
|
519
|
-
_BODY_STRATEGIES_CACHE.clear()
|