schemathesis 3.39.15__py3-none-any.whl → 4.0.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/__init__.py +41 -79
- schemathesis/auths.py +111 -122
- schemathesis/checks.py +169 -60
- schemathesis/cli/__init__.py +15 -2117
- schemathesis/cli/commands/__init__.py +85 -0
- schemathesis/cli/commands/data.py +10 -0
- schemathesis/cli/commands/run/__init__.py +590 -0
- schemathesis/cli/commands/run/context.py +204 -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 +18 -0
- schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
- schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
- schemathesis/cli/commands/run/handlers/output.py +1628 -0
- schemathesis/cli/commands/run/loaders.py +114 -0
- schemathesis/cli/commands/run/validation.py +246 -0
- schemathesis/cli/constants.py +5 -58
- schemathesis/cli/core.py +19 -0
- schemathesis/cli/ext/fs.py +16 -0
- schemathesis/cli/ext/groups.py +84 -0
- schemathesis/cli/{options.py → ext/options.py} +36 -34
- schemathesis/config/__init__.py +189 -0
- schemathesis/config/_auth.py +51 -0
- schemathesis/config/_checks.py +268 -0
- schemathesis/config/_diff_base.py +99 -0
- schemathesis/config/_env.py +21 -0
- schemathesis/config/_error.py +156 -0
- schemathesis/config/_generation.py +149 -0
- schemathesis/config/_health_check.py +24 -0
- schemathesis/config/_operations.py +327 -0
- schemathesis/config/_output.py +171 -0
- schemathesis/config/_parameters.py +19 -0
- schemathesis/config/_phases.py +187 -0
- schemathesis/config/_projects.py +527 -0
- schemathesis/config/_rate_limit.py +17 -0
- schemathesis/config/_report.py +120 -0
- schemathesis/config/_validator.py +9 -0
- schemathesis/config/_warnings.py +25 -0
- schemathesis/config/schema.json +885 -0
- schemathesis/core/__init__.py +67 -0
- schemathesis/core/compat.py +32 -0
- schemathesis/core/control.py +2 -0
- schemathesis/core/curl.py +58 -0
- schemathesis/core/deserialization.py +65 -0
- schemathesis/core/errors.py +459 -0
- schemathesis/core/failures.py +315 -0
- schemathesis/core/fs.py +19 -0
- schemathesis/core/hooks.py +20 -0
- schemathesis/core/loaders.py +104 -0
- schemathesis/core/marks.py +66 -0
- schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
- schemathesis/core/output/__init__.py +46 -0
- schemathesis/core/output/sanitization.py +54 -0
- schemathesis/{throttling.py → core/rate_limit.py} +16 -17
- schemathesis/core/registries.py +31 -0
- schemathesis/core/transforms.py +113 -0
- schemathesis/core/transport.py +223 -0
- schemathesis/core/validation.py +54 -0
- schemathesis/core/version.py +7 -0
- schemathesis/engine/__init__.py +28 -0
- schemathesis/engine/context.py +118 -0
- schemathesis/engine/control.py +36 -0
- schemathesis/engine/core.py +169 -0
- schemathesis/engine/errors.py +464 -0
- schemathesis/engine/events.py +258 -0
- schemathesis/engine/phases/__init__.py +88 -0
- schemathesis/{runner → engine/phases}/probes.py +52 -68
- schemathesis/engine/phases/stateful/__init__.py +68 -0
- schemathesis/engine/phases/stateful/_executor.py +356 -0
- schemathesis/engine/phases/stateful/context.py +85 -0
- schemathesis/engine/phases/unit/__init__.py +212 -0
- schemathesis/engine/phases/unit/_executor.py +416 -0
- schemathesis/engine/phases/unit/_pool.py +82 -0
- schemathesis/engine/recorder.py +247 -0
- schemathesis/errors.py +43 -0
- schemathesis/filters.py +17 -98
- schemathesis/generation/__init__.py +5 -33
- schemathesis/generation/case.py +317 -0
- schemathesis/generation/coverage.py +282 -175
- schemathesis/generation/hypothesis/__init__.py +36 -0
- schemathesis/generation/hypothesis/builder.py +800 -0
- schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
- 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/metrics.py +93 -0
- schemathesis/generation/modes.py +20 -0
- schemathesis/generation/overrides.py +116 -0
- schemathesis/generation/stateful/__init__.py +37 -0
- schemathesis/generation/stateful/state_machine.py +278 -0
- schemathesis/graphql/__init__.py +15 -0
- schemathesis/graphql/checks.py +109 -0
- schemathesis/graphql/loaders.py +284 -0
- schemathesis/hooks.py +80 -101
- schemathesis/openapi/__init__.py +13 -0
- schemathesis/openapi/checks.py +455 -0
- schemathesis/openapi/generation/__init__.py +0 -0
- schemathesis/openapi/generation/filters.py +72 -0
- schemathesis/openapi/loaders.py +313 -0
- schemathesis/pytest/__init__.py +5 -0
- schemathesis/pytest/control_flow.py +7 -0
- schemathesis/pytest/lazy.py +281 -0
- schemathesis/pytest/loaders.py +36 -0
- schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
- schemathesis/python/__init__.py +0 -0
- schemathesis/python/asgi.py +12 -0
- schemathesis/python/wsgi.py +12 -0
- schemathesis/schemas.py +537 -273
- schemathesis/specs/graphql/__init__.py +0 -1
- schemathesis/specs/graphql/_cache.py +1 -2
- schemathesis/specs/graphql/scalars.py +42 -6
- schemathesis/specs/graphql/schemas.py +141 -137
- schemathesis/specs/graphql/validation.py +11 -17
- schemathesis/specs/openapi/__init__.py +6 -1
- schemathesis/specs/openapi/_cache.py +1 -2
- schemathesis/specs/openapi/_hypothesis.py +142 -156
- schemathesis/specs/openapi/checks.py +368 -257
- schemathesis/specs/openapi/converter.py +4 -4
- schemathesis/specs/openapi/definitions.py +1 -1
- schemathesis/specs/openapi/examples.py +23 -21
- schemathesis/specs/openapi/expressions/__init__.py +31 -19
- schemathesis/specs/openapi/expressions/extractors.py +1 -4
- schemathesis/specs/openapi/expressions/lexer.py +1 -1
- schemathesis/specs/openapi/expressions/nodes.py +36 -41
- schemathesis/specs/openapi/expressions/parser.py +1 -1
- schemathesis/specs/openapi/formats.py +35 -7
- schemathesis/specs/openapi/media_types.py +53 -12
- schemathesis/specs/openapi/negative/__init__.py +7 -4
- schemathesis/specs/openapi/negative/mutations.py +6 -5
- schemathesis/specs/openapi/parameters.py +7 -10
- schemathesis/specs/openapi/patterns.py +94 -31
- schemathesis/specs/openapi/references.py +12 -53
- schemathesis/specs/openapi/schemas.py +238 -308
- schemathesis/specs/openapi/security.py +1 -1
- schemathesis/specs/openapi/serialization.py +12 -6
- schemathesis/specs/openapi/stateful/__init__.py +268 -133
- schemathesis/specs/openapi/stateful/control.py +87 -0
- schemathesis/specs/openapi/stateful/links.py +209 -0
- schemathesis/transport/__init__.py +142 -0
- schemathesis/transport/asgi.py +26 -0
- schemathesis/transport/prepare.py +124 -0
- schemathesis/transport/requests.py +244 -0
- schemathesis/{_xml.py → transport/serialization.py} +69 -11
- schemathesis/transport/wsgi.py +171 -0
- schemathesis-4.0.0.dist-info/METADATA +204 -0
- schemathesis-4.0.0.dist-info/RECORD +164 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
- schemathesis/_compat.py +0 -74
- schemathesis/_dependency_versions.py +0 -19
- schemathesis/_hypothesis.py +0 -712
- schemathesis/_override.py +0 -50
- schemathesis/_patches.py +0 -21
- schemathesis/_rate_limiter.py +0 -7
- schemathesis/cli/callbacks.py +0 -466
- schemathesis/cli/cassettes.py +0 -561
- schemathesis/cli/context.py +0 -75
- schemathesis/cli/debug.py +0 -27
- schemathesis/cli/handlers.py +0 -19
- schemathesis/cli/junitxml.py +0 -124
- schemathesis/cli/output/__init__.py +0 -1
- schemathesis/cli/output/default.py +0 -920
- schemathesis/cli/output/short.py +0 -59
- schemathesis/cli/reporting.py +0 -79
- schemathesis/cli/sanitization.py +0 -26
- schemathesis/code_samples.py +0 -151
- schemathesis/constants.py +0 -54
- schemathesis/contrib/__init__.py +0 -11
- schemathesis/contrib/openapi/__init__.py +0 -11
- schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
- schemathesis/contrib/openapi/formats/__init__.py +0 -9
- schemathesis/contrib/openapi/formats/uuid.py +0 -16
- schemathesis/contrib/unique_data.py +0 -41
- schemathesis/exceptions.py +0 -571
- schemathesis/experimental/__init__.py +0 -109
- schemathesis/extra/_aiohttp.py +0 -28
- schemathesis/extra/_flask.py +0 -13
- schemathesis/extra/_server.py +0 -18
- schemathesis/failures.py +0 -284
- schemathesis/fixups/__init__.py +0 -37
- schemathesis/fixups/fast_api.py +0 -41
- schemathesis/fixups/utf8_bom.py +0 -28
- schemathesis/generation/_methods.py +0 -44
- schemathesis/graphql.py +0 -3
- schemathesis/internal/__init__.py +0 -7
- schemathesis/internal/checks.py +0 -86
- schemathesis/internal/copy.py +0 -32
- schemathesis/internal/datetime.py +0 -5
- schemathesis/internal/deprecation.py +0 -37
- schemathesis/internal/diff.py +0 -15
- schemathesis/internal/extensions.py +0 -27
- schemathesis/internal/jsonschema.py +0 -36
- schemathesis/internal/output.py +0 -68
- schemathesis/internal/transformation.py +0 -26
- schemathesis/internal/validation.py +0 -34
- schemathesis/lazy.py +0 -474
- schemathesis/loaders.py +0 -122
- schemathesis/models.py +0 -1341
- schemathesis/parameters.py +0 -90
- schemathesis/runner/__init__.py +0 -605
- schemathesis/runner/events.py +0 -389
- schemathesis/runner/impl/__init__.py +0 -3
- schemathesis/runner/impl/context.py +0 -88
- schemathesis/runner/impl/core.py +0 -1280
- schemathesis/runner/impl/solo.py +0 -80
- schemathesis/runner/impl/threadpool.py +0 -391
- schemathesis/runner/serialization.py +0 -544
- schemathesis/sanitization.py +0 -252
- schemathesis/serializers.py +0 -328
- schemathesis/service/__init__.py +0 -18
- schemathesis/service/auth.py +0 -11
- schemathesis/service/ci.py +0 -202
- schemathesis/service/client.py +0 -133
- schemathesis/service/constants.py +0 -38
- schemathesis/service/events.py +0 -61
- schemathesis/service/extensions.py +0 -224
- schemathesis/service/hosts.py +0 -111
- schemathesis/service/metadata.py +0 -71
- schemathesis/service/models.py +0 -258
- schemathesis/service/report.py +0 -255
- schemathesis/service/serialization.py +0 -173
- schemathesis/service/usage.py +0 -66
- schemathesis/specs/graphql/loaders.py +0 -364
- schemathesis/specs/openapi/expressions/context.py +0 -16
- schemathesis/specs/openapi/links.py +0 -389
- schemathesis/specs/openapi/loaders.py +0 -707
- schemathesis/specs/openapi/stateful/statistic.py +0 -198
- schemathesis/specs/openapi/stateful/types.py +0 -14
- schemathesis/specs/openapi/validation.py +0 -26
- schemathesis/stateful/__init__.py +0 -147
- schemathesis/stateful/config.py +0 -97
- schemathesis/stateful/context.py +0 -135
- schemathesis/stateful/events.py +0 -274
- schemathesis/stateful/runner.py +0 -309
- schemathesis/stateful/sink.py +0 -68
- schemathesis/stateful/state_machine.py +0 -328
- schemathesis/stateful/statistic.py +0 -22
- schemathesis/stateful/validation.py +0 -100
- schemathesis/targets.py +0 -77
- schemathesis/transports/__init__.py +0 -369
- schemathesis/transports/asgi.py +0 -7
- schemathesis/transports/auth.py +0 -38
- schemathesis/transports/headers.py +0 -36
- schemathesis/transports/responses.py +0 -57
- schemathesis/types.py +0 -44
- schemathesis/utils.py +0 -164
- schemathesis-3.39.15.dist-info/METADATA +0 -293
- schemathesis-3.39.15.dist-info/RECORD +0 -160
- /schemathesis/{extra → cli/ext}/__init__.py +0 -0
- /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
- /schemathesis/{internal → core}/result.py +0 -0
- {schemathesis-3.39.15.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -3,29 +3,38 @@ from __future__ import annotations
|
|
3
3
|
import time
|
4
4
|
from contextlib import suppress
|
5
5
|
from dataclasses import dataclass
|
6
|
-
from typing import Any, Callable, Dict, Iterable, Optional
|
6
|
+
from typing import Any, Callable, Dict, Iterable, Optional, Union, cast
|
7
7
|
from urllib.parse import quote_plus
|
8
8
|
from weakref import WeakKeyDictionary
|
9
9
|
|
10
|
-
|
10
|
+
import jsonschema.protocols
|
11
|
+
from hypothesis import event, note, reject
|
11
12
|
from hypothesis import strategies as st
|
12
13
|
from hypothesis_jsonschema import from_schema
|
13
14
|
from requests.structures import CaseInsensitiveDict
|
14
|
-
from requests.utils import to_key_val_list
|
15
15
|
|
16
|
-
from
|
17
|
-
from
|
18
|
-
from
|
19
|
-
from
|
20
|
-
from
|
16
|
+
from schemathesis.config import GenerationConfig
|
17
|
+
from schemathesis.core import NOT_SET, NotSet, media_types
|
18
|
+
from schemathesis.core.control import SkipTest
|
19
|
+
from schemathesis.core.errors import SERIALIZERS_SUGGESTION_MESSAGE, SerializationNotPossible
|
20
|
+
from schemathesis.core.transforms import deepclone
|
21
|
+
from schemathesis.core.transport import prepare_urlencoded
|
22
|
+
from schemathesis.generation.meta import (
|
23
|
+
CaseMetadata,
|
24
|
+
ComponentInfo,
|
25
|
+
ComponentKind,
|
26
|
+
ExplicitPhaseData,
|
27
|
+
GeneratePhaseData,
|
28
|
+
GenerationInfo,
|
29
|
+
PhaseInfo,
|
30
|
+
TestPhase,
|
31
|
+
)
|
32
|
+
from schemathesis.openapi.generation.filters import is_valid_header, is_valid_path, is_valid_query, is_valid_urlencoded
|
33
|
+
from schemathesis.schemas import APIOperation
|
34
|
+
|
35
|
+
from ... import auths
|
36
|
+
from ...generation import GenerationMode
|
21
37
|
from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
|
22
|
-
from ...internal.copy import fast_deepcopy
|
23
|
-
from ...internal.validation import is_illegal_surrogate
|
24
|
-
from ...models import APIOperation, Case, GenerationMetadata, TestPhase, cant_serialize
|
25
|
-
from ...transports.content_types import parse_content_type
|
26
|
-
from ...transports.headers import has_invalid_characters, is_latin_1_encodable
|
27
|
-
from ...types import NotSet
|
28
|
-
from ...utils import skip
|
29
38
|
from .constants import LOCATION_TO_CONTAINER
|
30
39
|
from .formats import HEADER_FORMAT, STRING_FORMATS, get_default_format_strategies, header_values
|
31
40
|
from .media_types import MEDIA_TYPES
|
@@ -35,57 +44,27 @@ from .parameters import OpenAPIBody, OpenAPIParameter, parameters_to_json_schema
|
|
35
44
|
from .utils import is_header_location
|
36
45
|
|
37
46
|
SLASH = "/"
|
38
|
-
StrategyFactory = Callable[
|
39
|
-
|
40
|
-
|
41
|
-
def is_valid_header(headers: dict[str, Any]) -> bool:
|
42
|
-
"""Verify if the generated headers are valid."""
|
43
|
-
for name, value in headers.items():
|
44
|
-
if not is_latin_1_encodable(value):
|
45
|
-
return False
|
46
|
-
if has_invalid_characters(name, value):
|
47
|
-
return False
|
48
|
-
return True
|
49
|
-
|
50
|
-
|
51
|
-
def is_valid_query(query: dict[str, Any]) -> bool:
|
52
|
-
"""Surrogates are not allowed in a query string.
|
53
|
-
|
54
|
-
`requests` and `werkzeug` will fail to send it to the application.
|
55
|
-
"""
|
56
|
-
for name, value in query.items():
|
57
|
-
if is_illegal_surrogate(name) or is_illegal_surrogate(value):
|
58
|
-
return False
|
59
|
-
return True
|
60
|
-
|
61
|
-
|
62
|
-
def is_valid_urlencoded(data: Any) -> bool:
|
63
|
-
if data is NOT_SET:
|
64
|
-
return True
|
65
|
-
try:
|
66
|
-
for _, __ in to_key_val_list(data): # type: ignore[no-untyped-call]
|
67
|
-
pass
|
68
|
-
return True
|
69
|
-
except (TypeError, ValueError):
|
70
|
-
return False
|
47
|
+
StrategyFactory = Callable[
|
48
|
+
[Dict[str, Any], str, str, Optional[str], GenerationConfig, type[jsonschema.protocols.Validator]], st.SearchStrategy
|
49
|
+
]
|
71
50
|
|
72
51
|
|
73
52
|
@st.composite # type: ignore
|
74
|
-
def
|
53
|
+
def openapi_cases(
|
75
54
|
draw: Callable,
|
55
|
+
*,
|
76
56
|
operation: APIOperation,
|
77
57
|
hooks: HookDispatcher | None = None,
|
78
58
|
auth_storage: auths.AuthStorage | None = None,
|
79
|
-
|
80
|
-
generation_config: GenerationConfig | None = None,
|
59
|
+
generation_mode: GenerationMode = GenerationMode.POSITIVE,
|
81
60
|
path_parameters: NotSet | dict[str, Any] = NOT_SET,
|
82
61
|
headers: NotSet | dict[str, Any] = NOT_SET,
|
83
62
|
cookies: NotSet | dict[str, Any] = NOT_SET,
|
84
63
|
query: NotSet | dict[str, Any] = NOT_SET,
|
85
64
|
body: Any = NOT_SET,
|
86
65
|
media_type: str | None = None,
|
87
|
-
|
88
|
-
|
66
|
+
phase: TestPhase = TestPhase.FUZZING,
|
67
|
+
__is_stateful_phase: bool = False,
|
89
68
|
) -> Any:
|
90
69
|
"""A strategy that creates `Case` instances.
|
91
70
|
|
@@ -100,46 +79,58 @@ def get_case_strategy(
|
|
100
79
|
as it works with `body`.
|
101
80
|
"""
|
102
81
|
start = time.monotonic()
|
103
|
-
strategy_factory =
|
82
|
+
strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
|
104
83
|
|
105
|
-
|
84
|
+
phase_name = "stateful" if __is_stateful_phase else phase.value
|
85
|
+
generation_config = operation.schema.config.generation_for(operation=operation, phase=phase_name)
|
106
86
|
|
107
|
-
|
87
|
+
ctx = HookContext(operation=operation)
|
108
88
|
|
109
89
|
path_parameters_ = generate_parameter(
|
110
|
-
"path", path_parameters, operation, draw,
|
90
|
+
"path", path_parameters, operation, draw, ctx, hooks, generation_mode, generation_config
|
111
91
|
)
|
112
|
-
headers_ = generate_parameter("header", headers, operation, draw,
|
113
|
-
cookies_ = generate_parameter("cookie", cookies, operation, draw,
|
114
|
-
query_ = generate_parameter("query", query, operation, draw,
|
92
|
+
headers_ = generate_parameter("header", headers, operation, draw, ctx, hooks, generation_mode, generation_config)
|
93
|
+
cookies_ = generate_parameter("cookie", cookies, operation, draw, ctx, hooks, generation_mode, generation_config)
|
94
|
+
query_ = generate_parameter("query", query, operation, draw, ctx, hooks, generation_mode, generation_config)
|
115
95
|
|
116
96
|
if body is NOT_SET:
|
117
97
|
if operation.body:
|
118
|
-
body_generator =
|
119
|
-
if
|
98
|
+
body_generator = generation_mode
|
99
|
+
if generation_mode.is_negative:
|
120
100
|
# Consider only schemas that are possible to negate
|
121
101
|
candidates = [item for item in operation.body.items if can_negate(item.as_json_schema(operation))]
|
122
102
|
# Not possible to negate body, fallback to positive data generation
|
123
103
|
if not candidates:
|
124
104
|
candidates = operation.body.items
|
125
105
|
strategy_factory = make_positive_strategy
|
126
|
-
body_generator =
|
106
|
+
body_generator = GenerationMode.POSITIVE
|
127
107
|
else:
|
128
108
|
candidates = operation.body.items
|
129
109
|
parameter = draw(st.sampled_from(candidates))
|
130
110
|
strategy = _get_body_strategy(parameter, strategy_factory, operation, generation_config)
|
131
|
-
strategy = apply_hooks(operation,
|
111
|
+
strategy = apply_hooks(operation, ctx, hooks, strategy, "body")
|
132
112
|
# Parameter may have a wildcard media type. In this case, choose any supported one
|
133
|
-
possible_media_types = sorted(
|
113
|
+
possible_media_types = sorted(
|
114
|
+
operation.schema.transport.get_matching_media_types(parameter.media_type), key=lambda x: x[0]
|
115
|
+
)
|
134
116
|
if not possible_media_types:
|
135
117
|
all_media_types = operation.get_request_payload_content_types()
|
136
|
-
if all(
|
118
|
+
if all(
|
119
|
+
operation.schema.transport.get_first_matching_media_type(media_type) is None
|
120
|
+
for media_type in all_media_types
|
121
|
+
):
|
137
122
|
# None of media types defined for this operation are not supported
|
138
123
|
raise SerializationNotPossible.from_media_types(*all_media_types)
|
139
124
|
# Other media types are possible - avoid choosing this media type in the future
|
140
|
-
|
141
|
-
|
142
|
-
|
125
|
+
event_text = f"Can't serialize data to `{parameter.media_type}`."
|
126
|
+
note(f"{event_text} {SERIALIZERS_SUGGESTION_MESSAGE}")
|
127
|
+
event(event_text)
|
128
|
+
reject() # type: ignore
|
129
|
+
media_type, _ = draw(st.sampled_from(possible_media_types))
|
130
|
+
if media_type is not None and media_types.parse(media_type) == (
|
131
|
+
"application",
|
132
|
+
"x-www-form-urlencoded",
|
133
|
+
):
|
143
134
|
strategy = strategy.map(prepare_urlencoded).filter(is_valid_urlencoded)
|
144
135
|
body_ = ValueContainer(value=draw(strategy), location="body", generator=body_generator)
|
145
136
|
else:
|
@@ -152,35 +143,43 @@ def get_case_strategy(
|
|
152
143
|
raise SerializationNotPossible.from_media_types(*all_media_types)
|
153
144
|
body_ = ValueContainer(value=body, location="body", generator=None)
|
154
145
|
|
155
|
-
if operation.schema.validate_schema and operation.method.upper() == "GET" and operation.body:
|
156
|
-
raise BodyInGetRequestError("GET requests should not contain body parameters.")
|
157
146
|
# If we need to generate negative cases but no generated values were negated, then skip the whole test
|
158
|
-
if
|
159
|
-
if
|
160
|
-
|
147
|
+
if generation_mode.is_negative and not any_negated_values([query_, cookies_, headers_, path_parameters_, body_]):
|
148
|
+
if generation_config.modes == [GenerationMode.NEGATIVE]:
|
149
|
+
raise SkipTest("Impossible to generate negative test cases")
|
161
150
|
else:
|
162
151
|
reject()
|
163
|
-
|
164
|
-
|
165
|
-
|
152
|
+
|
153
|
+
_phase_data = {
|
154
|
+
TestPhase.EXAMPLES: ExplicitPhaseData(),
|
155
|
+
TestPhase.FUZZING: GeneratePhaseData(),
|
156
|
+
}[phase]
|
157
|
+
phase_data = cast(Union[ExplicitPhaseData, GeneratePhaseData], _phase_data)
|
158
|
+
|
159
|
+
instance = operation.Case(
|
166
160
|
media_type=media_type,
|
167
|
-
path_parameters=path_parameters_.value,
|
168
|
-
headers=
|
169
|
-
cookies=cookies_.value,
|
170
|
-
query=query_.value,
|
161
|
+
path_parameters=path_parameters_.value or {},
|
162
|
+
headers=headers_.value or CaseInsensitiveDict(),
|
163
|
+
cookies=cookies_.value or {},
|
164
|
+
query=query_.value or {},
|
171
165
|
body=body_.value,
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
166
|
+
_meta=CaseMetadata(
|
167
|
+
generation=GenerationInfo(
|
168
|
+
time=time.monotonic() - start,
|
169
|
+
mode=generation_mode,
|
170
|
+
),
|
171
|
+
phase=PhaseInfo(name=phase, data=phase_data),
|
172
|
+
components={
|
173
|
+
kind: ComponentInfo(mode=value.generator)
|
174
|
+
for kind, value in [
|
175
|
+
(ComponentKind.QUERY, query_),
|
176
|
+
(ComponentKind.PATH_PARAMETERS, path_parameters_),
|
177
|
+
(ComponentKind.HEADERS, headers_),
|
178
|
+
(ComponentKind.COOKIES, cookies_),
|
179
|
+
(ComponentKind.BODY, body_),
|
180
|
+
]
|
181
|
+
if value.generator is not None
|
182
|
+
},
|
184
183
|
),
|
185
184
|
)
|
186
185
|
auth_context = auths.AuthContext(
|
@@ -200,6 +199,8 @@ def _get_body_strategy(
|
|
200
199
|
operation: APIOperation,
|
201
200
|
generation_config: GenerationConfig,
|
202
201
|
) -> st.SearchStrategy:
|
202
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
203
|
+
|
203
204
|
if parameter.media_type in MEDIA_TYPES:
|
204
205
|
return MEDIA_TYPES[parameter.media_type]
|
205
206
|
# The cache key relies on object ids, which means that the parameter should not be mutated
|
@@ -208,7 +209,10 @@ def _get_body_strategy(
|
|
208
209
|
return _BODY_STRATEGIES_CACHE[parameter][strategy_factory]
|
209
210
|
schema = parameter.as_json_schema(operation)
|
210
211
|
schema = operation.schema.prepare_schema(schema)
|
211
|
-
|
212
|
+
assert isinstance(operation.schema, BaseOpenAPISchema)
|
213
|
+
strategy = strategy_factory(
|
214
|
+
schema, operation.label, "body", parameter.media_type, generation_config, operation.schema.validator_cls
|
215
|
+
)
|
212
216
|
if not parameter.is_required:
|
213
217
|
strategy |= st.just(NOT_SET)
|
214
218
|
_BODY_STRATEGIES_CACHE.setdefault(parameter, {})[strategy_factory] = strategy
|
@@ -220,7 +224,7 @@ def get_parameters_value(
|
|
220
224
|
location: str,
|
221
225
|
draw: Callable,
|
222
226
|
operation: APIOperation,
|
223
|
-
|
227
|
+
ctx: HookContext,
|
224
228
|
hooks: HookDispatcher | None,
|
225
229
|
strategy_factory: StrategyFactory,
|
226
230
|
generation_config: GenerationConfig,
|
@@ -232,13 +236,13 @@ def get_parameters_value(
|
|
232
236
|
"""
|
233
237
|
if isinstance(value, NotSet) or not value:
|
234
238
|
strategy = get_parameters_strategy(operation, strategy_factory, location, generation_config)
|
235
|
-
strategy = apply_hooks(operation,
|
239
|
+
strategy = apply_hooks(operation, ctx, hooks, strategy, location)
|
236
240
|
return draw(strategy)
|
237
241
|
strategy = get_parameters_strategy(operation, strategy_factory, location, generation_config, exclude=value.keys())
|
238
|
-
strategy = apply_hooks(operation,
|
242
|
+
strategy = apply_hooks(operation, ctx, hooks, strategy, location)
|
239
243
|
new = draw(strategy)
|
240
244
|
if new is not None:
|
241
|
-
copied =
|
245
|
+
copied = deepclone(value)
|
242
246
|
copied.update(new)
|
243
247
|
return copied
|
244
248
|
return value
|
@@ -253,7 +257,7 @@ class ValueContainer:
|
|
253
257
|
|
254
258
|
value: Any
|
255
259
|
location: str
|
256
|
-
generator:
|
260
|
+
generator: GenerationMode | None
|
257
261
|
|
258
262
|
__slots__ = ("value", "location", "generator")
|
259
263
|
|
@@ -265,7 +269,7 @@ class ValueContainer:
|
|
265
269
|
|
266
270
|
def any_negated_values(values: list[ValueContainer]) -> bool:
|
267
271
|
"""Check if any generated values are negated."""
|
268
|
-
return any(value.generator ==
|
272
|
+
return any(value.generator == GenerationMode.NEGATIVE for value in values if value.is_generated)
|
269
273
|
|
270
274
|
|
271
275
|
def generate_parameter(
|
@@ -273,9 +277,9 @@ def generate_parameter(
|
|
273
277
|
explicit: NotSet | dict[str, Any],
|
274
278
|
operation: APIOperation,
|
275
279
|
draw: Callable,
|
276
|
-
|
280
|
+
ctx: HookContext,
|
277
281
|
hooks: HookDispatcher | None,
|
278
|
-
generator:
|
282
|
+
generator: GenerationMode,
|
279
283
|
generation_config: GenerationConfig,
|
280
284
|
) -> ValueContainer:
|
281
285
|
"""Generate a value for a parameter.
|
@@ -289,13 +293,11 @@ def generate_parameter(
|
|
289
293
|
# If we can't negate any parameter, generate positive ones
|
290
294
|
# If nothing else will be negated, then skip the test completely
|
291
295
|
strategy_factory = make_positive_strategy
|
292
|
-
generator =
|
296
|
+
generator = GenerationMode.POSITIVE
|
293
297
|
else:
|
294
|
-
strategy_factory =
|
295
|
-
value = get_parameters_value(
|
296
|
-
|
297
|
-
)
|
298
|
-
used_generator: DataGenerationMethod | None = generator
|
298
|
+
strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generator]
|
299
|
+
value = get_parameters_value(explicit, location, draw, operation, ctx, hooks, strategy_factory, generation_config)
|
300
|
+
used_generator: GenerationMode | None = generator
|
299
301
|
if value == explicit:
|
300
302
|
# When we pass `explicit`, then its parts are excluded from generation of the final value
|
301
303
|
# If the final value is the same, then other parameters were generated at all
|
@@ -329,11 +331,7 @@ def get_schema_for_location(
|
|
329
331
|
) -> dict[str, Any]:
|
330
332
|
schema = parameters_to_json_schema(operation, parameters)
|
331
333
|
if location == "path":
|
332
|
-
|
333
|
-
# If schema validation is disabled, we try to generate data even if the parameter definition
|
334
|
-
# contains errors.
|
335
|
-
# In this case, we know that the `required` keyword should always be `True`.
|
336
|
-
schema["required"] = list(schema["properties"])
|
334
|
+
schema["required"] = list(schema["properties"])
|
337
335
|
for prop in schema.get("properties", {}).values():
|
338
336
|
if prop.get("type") == "string":
|
339
337
|
prop.setdefault("minLength", 1)
|
@@ -348,6 +346,8 @@ def get_parameters_strategy(
|
|
348
346
|
exclude: Iterable[str] = (),
|
349
347
|
) -> st.SearchStrategy:
|
350
348
|
"""Create a new strategy for the case's component from the API operation parameters."""
|
349
|
+
from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
|
350
|
+
|
351
351
|
parameters = getattr(operation, LOCATION_TO_CONTAINER[location])
|
352
352
|
if parameters:
|
353
353
|
# The cache key relies on object ids, which means that the parameter should not be mutated
|
@@ -355,17 +355,28 @@ def get_parameters_strategy(
|
|
355
355
|
if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
|
356
356
|
return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
|
357
357
|
schema = get_schema_for_location(operation, location, parameters)
|
358
|
-
|
359
|
-
#
|
360
|
-
|
361
|
-
schema["properties"]
|
362
|
-
|
363
|
-
|
358
|
+
if location == "header" and exclude:
|
359
|
+
# Remove excluded headers case-insensitively
|
360
|
+
exclude_lower = {name.lower() for name in exclude}
|
361
|
+
schema["properties"] = {
|
362
|
+
key: value for key, value in schema["properties"].items() if key.lower() not in exclude_lower
|
363
|
+
}
|
364
|
+
if "required" in schema:
|
365
|
+
schema["required"] = [key for key in schema["required"] if key.lower() not in exclude_lower]
|
366
|
+
elif exclude:
|
367
|
+
# Non-header locations: remove by exact name
|
368
|
+
for name in exclude:
|
369
|
+
schema["properties"].pop(name, None)
|
370
|
+
with suppress(ValueError):
|
371
|
+
schema["required"].remove(name)
|
364
372
|
if not schema["properties"] and strategy_factory is make_negative_strategy:
|
365
373
|
# Nothing to negate - all properties were excluded
|
366
374
|
strategy = st.none()
|
367
375
|
else:
|
368
|
-
|
376
|
+
assert isinstance(operation.schema, BaseOpenAPISchema)
|
377
|
+
strategy = strategy_factory(
|
378
|
+
schema, operation.label, location, None, generation_config, operation.schema.validator_cls
|
379
|
+
)
|
369
380
|
serialize = operation.get_parameter_serializer(location)
|
370
381
|
if serialize is not None:
|
371
382
|
strategy = strategy.map(serialize)
|
@@ -416,10 +427,10 @@ def _build_custom_formats(
|
|
416
427
|
custom_formats: dict[str, st.SearchStrategy] | None, generation_config: GenerationConfig
|
417
428
|
) -> dict[str, st.SearchStrategy]:
|
418
429
|
custom_formats = {**get_default_format_strategies(), **STRING_FORMATS, **(custom_formats or {})}
|
419
|
-
if generation_config.
|
420
|
-
custom_formats[HEADER_FORMAT] = generation_config.
|
430
|
+
if generation_config.exclude_header_characters is not None:
|
431
|
+
custom_formats[HEADER_FORMAT] = header_values(exclude_characters=generation_config.exclude_header_characters)
|
421
432
|
elif not generation_config.allow_x00:
|
422
|
-
custom_formats[HEADER_FORMAT] = header_values(
|
433
|
+
custom_formats[HEADER_FORMAT] = header_values(exclude_characters="\n\r\x00")
|
423
434
|
return custom_formats
|
424
435
|
|
425
436
|
|
@@ -429,6 +440,7 @@ def make_positive_strategy(
|
|
429
440
|
location: str,
|
430
441
|
media_type: str | None,
|
431
442
|
generation_config: GenerationConfig,
|
443
|
+
validator_cls: type[jsonschema.protocols.Validator],
|
432
444
|
custom_formats: dict[str, st.SearchStrategy] | None = None,
|
433
445
|
) -> st.SearchStrategy:
|
434
446
|
"""Strategy for generating values that fit the schema."""
|
@@ -459,6 +471,7 @@ def make_negative_strategy(
|
|
459
471
|
location: str,
|
460
472
|
media_type: str | None,
|
461
473
|
generation_config: GenerationConfig,
|
474
|
+
validator_cls: type[jsonschema.protocols.Validator],
|
462
475
|
custom_formats: dict[str, st.SearchStrategy] | None = None,
|
463
476
|
) -> st.SearchStrategy:
|
464
477
|
custom_formats = _build_custom_formats(custom_formats, generation_config)
|
@@ -469,38 +482,16 @@ def make_negative_strategy(
|
|
469
482
|
media_type=media_type,
|
470
483
|
custom_formats=custom_formats,
|
471
484
|
generation_config=generation_config,
|
485
|
+
validator_cls=validator_cls,
|
472
486
|
)
|
473
487
|
|
474
488
|
|
475
|
-
|
476
|
-
|
477
|
-
|
489
|
+
GENERATOR_MODE_TO_STRATEGY_FACTORY = {
|
490
|
+
GenerationMode.POSITIVE: make_positive_strategy,
|
491
|
+
GenerationMode.NEGATIVE: make_negative_strategy,
|
478
492
|
}
|
479
493
|
|
480
494
|
|
481
|
-
def is_valid_path(parameters: dict[str, Any]) -> bool:
|
482
|
-
"""Empty strings ("") are excluded from path by urllib3.
|
483
|
-
|
484
|
-
A path containing to "/" or "%2F" will lead to ambiguous path resolution in
|
485
|
-
many frameworks and libraries, such behaviour have been observed in both
|
486
|
-
WSGI and ASGI applications.
|
487
|
-
|
488
|
-
In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
|
489
|
-
Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
|
490
|
-
"""
|
491
|
-
disallowed_values = (SLASH, "")
|
492
|
-
|
493
|
-
return not any(
|
494
|
-
(
|
495
|
-
value in disallowed_values
|
496
|
-
or is_illegal_surrogate(value)
|
497
|
-
or isinstance(value, str)
|
498
|
-
and (SLASH in value or "}" in value or "{" in value)
|
499
|
-
)
|
500
|
-
for value in parameters.values()
|
501
|
-
)
|
502
|
-
|
503
|
-
|
504
495
|
def quote_all(parameters: dict[str, Any]) -> dict[str, Any]:
|
505
496
|
"""Apply URL quotation for all values in a dictionary."""
|
506
497
|
# Even though, "." is an unreserved character, it has a special meaning in "." and ".." strings.
|
@@ -522,16 +513,11 @@ def quote_all(parameters: dict[str, Any]) -> dict[str, Any]:
|
|
522
513
|
|
523
514
|
def apply_hooks(
|
524
515
|
operation: APIOperation,
|
525
|
-
|
516
|
+
ctx: HookContext,
|
526
517
|
hooks: HookDispatcher | None,
|
527
518
|
strategy: st.SearchStrategy,
|
528
519
|
location: str,
|
529
520
|
) -> st.SearchStrategy:
|
530
521
|
"""Apply all hooks related to the given location."""
|
531
522
|
container = LOCATION_TO_CONTAINER[location]
|
532
|
-
return apply_to_all_dispatchers(operation,
|
533
|
-
|
534
|
-
|
535
|
-
def clear_cache() -> None:
|
536
|
-
_PARAMETER_STRATEGIES_CACHE.clear()
|
537
|
-
_BODY_STRATEGIES_CACHE.clear()
|
523
|
+
return apply_to_all_dispatchers(operation, ctx, hooks, strategy, container)
|