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.
Files changed (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -1,119 +1,74 @@
1
- import re
2
- import string
3
- from base64 import b64encode
4
- from contextlib import suppress
5
- from copy import deepcopy
6
- from typing import Any, Callable, Dict, Iterable, NoReturn, Optional, Tuple, Union
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from dataclasses import dataclass
5
+ from typing import Any, Callable, Iterable, Optional, Union, cast
7
6
  from urllib.parse import quote_plus
8
- from weakref import WeakKeyDictionary
9
7
 
8
+ import jsonschema.protocols
9
+ from hypothesis import event, note, reject
10
10
  from hypothesis import strategies as st
11
11
  from hypothesis_jsonschema import from_schema
12
- from requests.auth import _basic_auth_str
13
12
  from requests.structures import CaseInsensitiveDict
14
13
 
15
- from ... import auth, serializers, utils
16
- from ...constants import DataGenerationMethod
17
- from ...exceptions import InvalidSchema, SerializationNotPossible, SkipTest
18
- from ...hooks import GLOBAL_HOOK_DISPATCHER, HookContext, HookDispatcher
19
- from ...models import APIOperation, Case, cant_serialize
20
- from ...types import NotSet
21
- from ...utils import NOT_SET, compose
22
- from .constants import LOCATION_TO_CONTAINER
23
- from .negative import negative_schema
14
+ from schemathesis.config import GenerationConfig
15
+ from schemathesis.core import NOT_SET, media_types
16
+ from schemathesis.core.control import SkipTest
17
+ from schemathesis.core.errors import SERIALIZERS_SUGGESTION_MESSAGE, MalformedMediaType, SerializationNotPossible
18
+ from schemathesis.core.jsonschema.types import JsonSchema
19
+ from schemathesis.core.parameters import ParameterLocation
20
+ from schemathesis.core.transport import prepare_urlencoded
21
+ from schemathesis.generation.meta import (
22
+ CaseMetadata,
23
+ ComponentInfo,
24
+ ExamplesPhaseData,
25
+ FuzzingPhaseData,
26
+ GenerationInfo,
27
+ PhaseInfo,
28
+ StatefulPhaseData,
29
+ TestPhase,
30
+ )
31
+ from schemathesis.openapi.generation.filters import is_valid_urlencoded
32
+ from schemathesis.schemas import APIOperation
33
+ from schemathesis.specs.openapi.adapter.parameters import FORM_MEDIA_TYPES, OpenApiBody, OpenApiParameterSet
34
+ from schemathesis.specs.openapi.negative.mutations import MutationMetadata
35
+
36
+ from ... import auths
37
+ from ...generation import GenerationMode
38
+ from ...hooks import HookContext, HookDispatcher, apply_to_all_dispatchers
39
+ from .formats import (
40
+ DEFAULT_HEADER_EXCLUDE_CHARACTERS,
41
+ HEADER_FORMAT,
42
+ STRING_FORMATS,
43
+ get_default_format_strategies,
44
+ header_values,
45
+ )
46
+ from .media_types import MEDIA_TYPES
47
+ from .negative import GeneratedValue, negative_schema
24
48
  from .negative.utils import can_negate
25
- from .parameters import OpenAPIBody, parameters_to_json_schema
26
- from .utils import is_header_location
27
49
 
28
- HEADER_FORMAT = "_header_value"
29
- PARAMETERS = frozenset(("path_parameters", "headers", "cookies", "query", "body"))
30
50
  SLASH = "/"
31
- STRING_FORMATS: Dict[str, st.SearchStrategy] = {}
32
- StrategyFactory = Callable[[Dict[str, Any], str, str, Optional[str]], st.SearchStrategy]
33
-
34
-
35
- def register_string_format(name: str, strategy: st.SearchStrategy) -> None:
36
- """Register a new strategy for generating data for specific string "format".
37
-
38
- :param str name: Format name. It should correspond the one used in the API schema as the "format" keyword value.
39
- :param strategy: Hypothesis strategy you'd like to use to generate values for this format.
40
- """
41
- if not isinstance(name, str):
42
- raise TypeError(f"name must be of type {str}, not {type(name)}")
43
- if not isinstance(strategy, st.SearchStrategy):
44
- raise TypeError(f"strategy must be of type {st.SearchStrategy}, not {type(strategy)}")
45
-
46
- STRING_FORMATS[name] = strategy
47
-
48
-
49
- def init_default_strategies() -> None:
50
- """Register all default "format" strategies."""
51
- register_string_format("binary", st.binary())
52
- register_string_format("byte", st.binary().map(lambda x: b64encode(x).decode()))
53
-
54
- def make_basic_auth_str(item: Tuple[str, str]) -> str:
55
- return _basic_auth_str(*item)
56
-
57
- latin1_text = st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255))
58
-
59
- # RFC 7230, Section 3.2.6
60
- register_string_format(
61
- "_header_name",
62
- st.text(min_size=1, alphabet=st.sampled_from("!#$%&'*+-.^_`|~" + string.digits + string.ascii_letters)),
63
- )
64
- # Define valid characters here to avoid filtering them out in `is_valid_header` later
65
- header_value = st.text(alphabet=st.characters(min_codepoint=0, max_codepoint=255, blacklist_characters="\n\r"))
66
- # Header values with leading non-visible chars can't be sent with `requests`
67
- register_string_format(HEADER_FORMAT, header_value.map(str.lstrip))
68
- register_string_format("_basic_auth", st.tuples(latin1_text, latin1_text).map(make_basic_auth_str)) # type: ignore
69
- register_string_format(
70
- "_bearer_auth",
71
- header_value.map("Bearer {}".format),
72
- )
73
-
74
-
75
- def is_valid_header(headers: Dict[str, Any]) -> bool:
76
- """Verify if the generated headers are valid."""
77
- for name, value in headers.items():
78
- if not utils.is_latin_1_encodable(value):
79
- return False
80
- if utils.has_invalid_characters(name, value):
81
- return False
82
- return True
83
-
51
+ StrategyFactory = Callable[
52
+ [JsonSchema, str, ParameterLocation, Optional[str], GenerationConfig, type[jsonschema.protocols.Validator]],
53
+ st.SearchStrategy,
54
+ ]
84
55
 
85
- def is_illegal_surrogate(item: Any) -> bool:
86
- def check(value: Any) -> bool:
87
- return isinstance(value, str) and bool(re.search(r"[\ud800-\udfff]", value))
88
56
 
89
- if isinstance(item, list):
90
- return any(check(item_) for item_ in item)
91
- return check(item)
92
-
93
-
94
- def is_valid_query(query: Dict[str, Any]) -> bool:
95
- """Surrogates are not allowed in a query string.
96
-
97
- `requests` and `werkzeug` will fail to send it to the application.
98
- """
99
- for name, value in query.items():
100
- if is_illegal_surrogate(name) or is_illegal_surrogate(value):
101
- return False
102
- return True
103
-
104
-
105
- @st.composite # type: ignore
106
- def get_case_strategy( # pylint: disable=too-many-locals
107
- draw: Callable,
57
+ @st.composite # type: ignore[misc]
58
+ def openapi_cases(
59
+ draw: st.DrawFn,
60
+ *,
108
61
  operation: APIOperation,
109
- hooks: Optional[HookDispatcher] = None,
110
- auth_storage: Optional[auth.AuthStorage] = None,
111
- data_generation_method: DataGenerationMethod = DataGenerationMethod.default(),
112
- path_parameters: Union[NotSet, Dict[str, Any]] = NOT_SET,
113
- headers: Union[NotSet, Dict[str, Any]] = NOT_SET,
114
- cookies: Union[NotSet, Dict[str, Any]] = NOT_SET,
115
- query: Union[NotSet, Dict[str, Any]] = NOT_SET,
62
+ hooks: HookDispatcher | None = None,
63
+ auth_storage: auths.AuthStorage | None = None,
64
+ generation_mode: GenerationMode = GenerationMode.POSITIVE,
65
+ path_parameters: dict[str, Any] | None = None,
66
+ headers: dict[str, Any] | None = None,
67
+ cookies: dict[str, Any] | None = None,
68
+ query: dict[str, Any] | None = None,
116
69
  body: Any = NOT_SET,
70
+ media_type: str | None = None,
71
+ phase: TestPhase = TestPhase.FUZZING,
117
72
  ) -> Any:
118
73
  """A strategy that creates `Case` instances.
119
74
 
@@ -127,272 +82,573 @@ def get_case_strategy( # pylint: disable=too-many-locals
127
82
  The primary purpose of this behavior is to prevent sending incomplete explicit examples by generating missing parts
128
83
  as it works with `body`.
129
84
  """
130
- to_strategy = DATA_GENERATION_METHOD_TO_STRATEGY_FACTORY[data_generation_method]
85
+ start = time.monotonic()
131
86
 
132
- hook_context = HookContext(operation)
87
+ generation_config = operation.schema.config.generation_for(operation=operation, phase=phase.value)
133
88
 
134
- path_parameters_value = get_parameters_value(
135
- path_parameters, "path", draw, operation, hook_context, hooks, to_strategy
136
- )
137
- headers_value = get_parameters_value(headers, "header", draw, operation, hook_context, hooks, to_strategy)
138
- cookies_value = get_parameters_value(cookies, "cookie", draw, operation, hook_context, hooks, to_strategy)
139
- query_value = get_parameters_value(query, "query", draw, operation, hook_context, hooks, to_strategy)
89
+ ctx = HookContext(operation=operation)
140
90
 
141
- has_generated_parameters = any(
142
- component is not None for component in (query_value, cookies_value, headers_value, path_parameters_value)
91
+ path_parameters_ = generate_parameter(
92
+ ParameterLocation.PATH, path_parameters, operation, draw, ctx, hooks, generation_mode, generation_config
93
+ )
94
+ headers_ = generate_parameter(
95
+ ParameterLocation.HEADER, headers, operation, draw, ctx, hooks, generation_mode, generation_config
96
+ )
97
+ cookies_ = generate_parameter(
98
+ ParameterLocation.COOKIE, cookies, operation, draw, ctx, hooks, generation_mode, generation_config
99
+ )
100
+ query_ = generate_parameter(
101
+ ParameterLocation.QUERY, query, operation, draw, ctx, hooks, generation_mode, generation_config
143
102
  )
144
103
 
145
- media_type = None
146
104
  if body is NOT_SET:
147
105
  if operation.body:
148
- if data_generation_method.is_negative:
106
+ body_generator = generation_mode
107
+ if generation_mode.is_negative:
149
108
  # Consider only schemas that are possible to negate
150
- candidates = [item for item in operation.body.items if can_negate(item.as_json_schema(operation))]
151
- # Not possible to negate body
109
+ candidates = [item for item in operation.body.items if can_negate(item.optimized_schema)]
110
+ # Not possible to negate body, fallback to positive data generation
152
111
  if not candidates:
153
- # If other components are negated, then generate body that matches the schema
154
- # Other components were negated, therefore the whole test case will be negative
155
- if has_generated_parameters:
156
- candidates = operation.body.items
157
- to_strategy = make_positive_strategy
158
- else:
159
- skip(operation.verbose_name)
112
+ candidates = operation.body.items
113
+ body_generator = GenerationMode.POSITIVE
160
114
  else:
161
115
  candidates = operation.body.items
162
116
  parameter = draw(st.sampled_from(candidates))
163
- strategy = _get_body_strategy(parameter, to_strategy, operation)
164
- strategy = apply_hooks(operation, hook_context, hooks, strategy, "body")
117
+ strategy = _get_body_strategy(parameter, operation, generation_config, draw, body_generator)
118
+ strategy = apply_hooks(operation, ctx, hooks, strategy, ParameterLocation.BODY)
165
119
  # Parameter may have a wildcard media type. In this case, choose any supported one
166
- possible_media_types = sorted(serializers.get_matching_media_types(parameter.media_type))
120
+ possible_media_types = sorted(
121
+ operation.schema.transport.get_matching_media_types(parameter.media_type), key=lambda x: x[0]
122
+ )
167
123
  if not possible_media_types:
168
124
  all_media_types = operation.get_request_payload_content_types()
169
- if all(serializers.get_first_matching_media_type(media_type) is None for media_type in all_media_types):
125
+ if all(
126
+ operation.schema.transport.get_first_matching_media_type(media_type) is None
127
+ for media_type in all_media_types
128
+ ):
170
129
  # None of media types defined for this operation are not supported
171
- raise SerializationNotPossible.from_media_types(*all_media_types)
130
+ raise SerializationNotPossible.from_media_types(*all_media_types) from None
172
131
  # Other media types are possible - avoid choosing this media type in the future
173
- cant_serialize(parameter.media_type)
174
- media_type = draw(st.sampled_from(possible_media_types))
175
- body = draw(strategy)
132
+ event_text = f"Can't serialize data to `{parameter.media_type}`."
133
+ note(f"{event_text} {SERIALIZERS_SUGGESTION_MESSAGE}")
134
+ event(event_text)
135
+ reject()
136
+ media_type, _ = draw(st.sampled_from(possible_media_types))
137
+ if media_type is not None and media_types.parse(media_type) == (
138
+ "application",
139
+ "x-www-form-urlencoded",
140
+ ):
141
+ if body_generator.is_negative:
142
+ # For negative strategies, unwrap GeneratedValue, apply transformation, then rewrap
143
+ strategy = strategy.map(
144
+ lambda x: GeneratedValue(prepare_urlencoded(x.value), x.meta)
145
+ if isinstance(x, GeneratedValue)
146
+ else prepare_urlencoded(x)
147
+ ).filter(lambda x: is_valid_urlencoded(x.value if isinstance(x, GeneratedValue) else x))
148
+ else:
149
+ strategy = strategy.map(prepare_urlencoded).filter(is_valid_urlencoded)
150
+ body_result = draw(strategy)
151
+ body_metadata = None
152
+ # Negative strategy returns GeneratedValue, positive returns just value
153
+ if isinstance(body_result, GeneratedValue):
154
+ body_metadata = body_result.meta
155
+ body_result = body_result.value
156
+ body_ = ValueContainer(value=body_result, location="body", generator=body_generator, meta=body_metadata)
157
+ else:
158
+ body_ = ValueContainer(value=body, location="body", generator=None, meta=None)
176
159
  else:
177
- media_types = operation.get_request_payload_content_types() or ["application/json"]
178
- # Take the first available media type.
179
- # POSSIBLE IMPROVEMENT:
180
- # - Test examples for each available media type on Open API 2.0;
181
- # - On Open API 3.0, media types are explicit, and each example has it.
182
- # We can pass `OpenAPIBody.media_type` here from the examples handling code.
183
- media_type = media_types[0]
184
-
185
- if operation.schema.validate_schema and operation.method.upper() == "GET" and operation.body:
186
- raise InvalidSchema("Body parameters are defined for GET request.")
187
- if data_generation_method.is_negative and isinstance(body, NotSet) and not has_generated_parameters:
188
- skip(operation.verbose_name)
189
- instance = Case(
190
- operation=operation,
160
+ # This explicit body payload comes for a media type that has a custom strategy registered
161
+ # Such strategies only support binary payloads, otherwise they can't be serialized
162
+ if not isinstance(body, bytes) and media_type and _find_media_type_strategy(media_type) is not None:
163
+ all_media_types = operation.get_request_payload_content_types()
164
+ raise SerializationNotPossible.from_media_types(*all_media_types)
165
+ body_ = ValueContainer(value=body, location="body", generator=None, meta=None)
166
+
167
+ # If we need to generate negative cases but no generated values were negated, then skip the whole test
168
+ if generation_mode.is_negative and not any_negated_values([query_, cookies_, headers_, path_parameters_, body_]):
169
+ if generation_config.modes == [GenerationMode.NEGATIVE]:
170
+ raise SkipTest("Impossible to generate negative test cases")
171
+ else:
172
+ reject()
173
+
174
+ # Extract mutation metadata from negated values and create phase-appropriate data
175
+ if generation_mode.is_negative:
176
+ negated_container = None
177
+ for container in [query_, cookies_, headers_, path_parameters_, body_]:
178
+ if container.generator == GenerationMode.NEGATIVE and container.meta is not None:
179
+ negated_container = container
180
+ break
181
+
182
+ if negated_container and negated_container.meta:
183
+ metadata = negated_container.meta
184
+ location_map = {
185
+ "query": ParameterLocation.QUERY,
186
+ "path_parameters": ParameterLocation.PATH,
187
+ "headers": ParameterLocation.HEADER,
188
+ "cookies": ParameterLocation.COOKIE,
189
+ "body": ParameterLocation.BODY,
190
+ }
191
+ parameter_location = location_map.get(negated_container.location)
192
+ _phase_data = {
193
+ TestPhase.EXAMPLES: ExamplesPhaseData(
194
+ description=metadata.description,
195
+ parameter=metadata.parameter,
196
+ parameter_location=parameter_location,
197
+ location=metadata.location,
198
+ ),
199
+ TestPhase.FUZZING: FuzzingPhaseData(
200
+ description=metadata.description,
201
+ parameter=metadata.parameter,
202
+ parameter_location=parameter_location,
203
+ location=metadata.location,
204
+ ),
205
+ TestPhase.STATEFUL: StatefulPhaseData(
206
+ description=metadata.description,
207
+ parameter=metadata.parameter,
208
+ parameter_location=parameter_location,
209
+ location=metadata.location,
210
+ ),
211
+ }[phase]
212
+ phase_data = cast(Union[ExamplesPhaseData, FuzzingPhaseData, StatefulPhaseData], _phase_data)
213
+ else:
214
+ _phase_data = {
215
+ TestPhase.EXAMPLES: ExamplesPhaseData(
216
+ description="Schema mutated",
217
+ parameter=None,
218
+ parameter_location=None,
219
+ location=None,
220
+ ),
221
+ TestPhase.FUZZING: FuzzingPhaseData(
222
+ description="Schema mutated",
223
+ parameter=None,
224
+ parameter_location=None,
225
+ location=None,
226
+ ),
227
+ TestPhase.STATEFUL: StatefulPhaseData(
228
+ description="Schema mutated",
229
+ parameter=None,
230
+ parameter_location=None,
231
+ location=None,
232
+ ),
233
+ }[phase]
234
+ phase_data = cast(Union[ExamplesPhaseData, FuzzingPhaseData, StatefulPhaseData], _phase_data)
235
+ else:
236
+ _phase_data = {
237
+ TestPhase.EXAMPLES: ExamplesPhaseData(
238
+ description="Positive test case",
239
+ parameter=None,
240
+ parameter_location=None,
241
+ location=None,
242
+ ),
243
+ TestPhase.FUZZING: FuzzingPhaseData(
244
+ description="Positive test case",
245
+ parameter=None,
246
+ parameter_location=None,
247
+ location=None,
248
+ ),
249
+ TestPhase.STATEFUL: StatefulPhaseData(
250
+ description="Positive test case",
251
+ parameter=None,
252
+ parameter_location=None,
253
+ location=None,
254
+ ),
255
+ }[phase]
256
+ phase_data = cast(Union[ExamplesPhaseData, FuzzingPhaseData, StatefulPhaseData], _phase_data)
257
+
258
+ instance = operation.Case(
191
259
  media_type=media_type,
192
- path_parameters=path_parameters_value,
193
- headers=CaseInsensitiveDict(headers_value) if headers_value is not None else headers_value,
194
- cookies=cookies_value,
195
- query=query_value,
196
- body=body,
197
- data_generation_method=data_generation_method,
260
+ path_parameters=path_parameters_.value or {},
261
+ headers=headers_.value or CaseInsensitiveDict(),
262
+ cookies=cookies_.value or {},
263
+ query=query_.value or {},
264
+ body=body_.value,
265
+ _meta=CaseMetadata(
266
+ generation=GenerationInfo(
267
+ time=time.monotonic() - start,
268
+ mode=generation_mode,
269
+ ),
270
+ phase=PhaseInfo(name=phase, data=phase_data),
271
+ components={
272
+ kind: ComponentInfo(mode=value.generator)
273
+ for kind, value in [
274
+ (ParameterLocation.QUERY, query_),
275
+ (ParameterLocation.PATH, path_parameters_),
276
+ (ParameterLocation.HEADER, headers_),
277
+ (ParameterLocation.COOKIE, cookies_),
278
+ (ParameterLocation.BODY, body_),
279
+ ]
280
+ if value.generator is not None
281
+ },
282
+ ),
198
283
  )
199
- auth_context = auth.AuthContext(
284
+ auth_context = auths.AuthContext(
200
285
  operation=operation,
201
286
  app=operation.app,
202
287
  )
203
- auth.set_on_case(instance, auth_context, auth_storage)
288
+ auths.set_on_case(instance, auth_context, auth_storage)
204
289
  return instance
205
290
 
206
291
 
207
- def skip(operation_name: str) -> NoReturn:
208
- raise SkipTest(f"It is not possible to generate negative test cases for `{operation_name}`")
292
+ OPTIONAL_BODY_RATE = 0.05
293
+
294
+
295
+ def _find_media_type_strategy(content_type: str) -> st.SearchStrategy[bytes] | None:
296
+ """Find a registered strategy for a content type, supporting wildcard patterns."""
297
+ # Try exact match first
298
+ if content_type in MEDIA_TYPES:
299
+ return MEDIA_TYPES[content_type]
300
+
301
+ try:
302
+ main, sub = media_types.parse(content_type)
303
+ except MalformedMediaType:
304
+ return None
305
+
306
+ # Check registered media types for wildcard matches
307
+ for registered_type, strategy in MEDIA_TYPES.items():
308
+ try:
309
+ target_main, target_sub = media_types.parse(registered_type)
310
+ except MalformedMediaType:
311
+ continue
312
+ # Match if both main and sub types are compatible
313
+ # "*" in either the requested or registered type acts as a wildcard
314
+ main_match = main == "*" or target_main == "*" or main == target_main
315
+ sub_match = sub == "*" or target_sub == "*" or sub == target_sub
316
+ if main_match and sub_match:
317
+ return strategy
209
318
 
319
+ return None
210
320
 
211
- _BODY_STRATEGIES_CACHE: WeakKeyDictionary = WeakKeyDictionary()
321
+
322
+ def _build_form_strategy_with_encoding(
323
+ parameter: OpenApiBody,
324
+ operation: APIOperation,
325
+ generation_config: GenerationConfig,
326
+ generation_mode: GenerationMode,
327
+ ) -> st.SearchStrategy | None:
328
+ """Build a strategy for form bodies that have custom encoding contentType.
329
+
330
+ Supports wildcard media type matching (e.g., "image/*" matches "image/png").
331
+
332
+ Returns `None` if no custom encoding with registered strategies is found.
333
+ """
334
+ schema = parameter.optimized_schema
335
+ if not isinstance(schema, dict) or schema.get("type") != "object":
336
+ return None
337
+
338
+ properties = schema.get("properties", {})
339
+ if not properties:
340
+ return None
341
+
342
+ # Check which properties have custom content types with registered strategies
343
+ custom_property_strategies = {}
344
+ for property_name in properties:
345
+ content_type = parameter.get_property_content_type(property_name)
346
+
347
+ if content_type is not None and not isinstance(content_type, str):
348
+ # Happens in broken schemas
349
+ continue # type: ignore[unreachable]
350
+
351
+ if content_type:
352
+ # Handle multiple content types (e.g., "image/png, image/jpeg")
353
+ content_types = [ct.strip() for ct in content_type.split(",")]
354
+ strategies_for_types = []
355
+ for ct in content_types:
356
+ strategy = _find_media_type_strategy(ct)
357
+ if strategy is not None:
358
+ strategies_for_types.append(strategy)
359
+
360
+ if strategies_for_types:
361
+ custom_property_strategies[property_name] = st.one_of(*strategies_for_types)
362
+
363
+ if not custom_property_strategies:
364
+ return None
365
+
366
+ # Build strategies for properties
367
+ property_strategies = {}
368
+ for property_name, subschema in properties.items():
369
+ if property_name in custom_property_strategies:
370
+ property_strategies[property_name] = custom_property_strategies[property_name]
371
+ else:
372
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
373
+
374
+ assert isinstance(operation.schema, BaseOpenAPISchema)
375
+ strategy_factory = GENERATOR_MODE_TO_STRATEGY_FACTORY[generation_mode]
376
+ property_strategies[property_name] = strategy_factory(
377
+ subschema,
378
+ operation.label,
379
+ ParameterLocation.BODY,
380
+ parameter.media_type,
381
+ generation_config,
382
+ operation.schema.adapter.jsonschema_validator_cls,
383
+ )
384
+
385
+ # Build fixed dictionary strategy with optional properties
386
+ required = set(schema.get("required", []))
387
+ required_strategies = {k: v for k, v in property_strategies.items() if k in required}
388
+ optional_strategies = {k: st.just(NOT_SET) | v for k, v in property_strategies.items() if k not in required}
389
+
390
+ @st.composite # type: ignore[misc]
391
+ def build_body(draw: st.DrawFn) -> dict[str, Any]:
392
+ body: dict[str, Any] = {}
393
+ # Generate required properties
394
+ for key, strategy in required_strategies.items():
395
+ body[key] = draw(strategy)
396
+ # Generate optional properties, filtering out NOT_SET
397
+ for key, strategy in optional_strategies.items():
398
+ value = draw(strategy)
399
+ if value is not NOT_SET:
400
+ body[key] = value
401
+ return body
402
+
403
+ return build_body()
212
404
 
213
405
 
214
406
  def _get_body_strategy(
215
- parameter: OpenAPIBody,
216
- to_strategy: StrategyFactory,
407
+ parameter: OpenApiBody,
217
408
  operation: APIOperation,
409
+ generation_config: GenerationConfig,
410
+ draw: st.DrawFn,
411
+ generation_mode: GenerationMode,
218
412
  ) -> st.SearchStrategy:
219
- # The cache key relies on object ids, which means that the parameter should not be mutated
220
- # Note, the parent schema is not included as each parameter belong only to one schema
221
- if parameter in _BODY_STRATEGIES_CACHE and to_strategy in _BODY_STRATEGIES_CACHE[parameter]:
222
- return _BODY_STRATEGIES_CACHE[parameter][to_strategy]
223
- schema = parameter.as_json_schema(operation)
224
- schema = operation.schema.prepare_schema(schema)
225
- strategy = to_strategy(schema, operation.verbose_name, "body", parameter.media_type)
226
- if not parameter.is_required:
413
+ # Check for custom encoding in form bodies (multipart/form-data or application/x-www-form-urlencoded)
414
+ if parameter.media_type in FORM_MEDIA_TYPES:
415
+ custom_strategy = _build_form_strategy_with_encoding(parameter, operation, generation_config, generation_mode)
416
+ if custom_strategy is not None:
417
+ return custom_strategy
418
+
419
+ # Check for custom media type strategy
420
+ custom_strategy = _find_media_type_strategy(parameter.media_type)
421
+ if custom_strategy is not None:
422
+ return custom_strategy
423
+
424
+ # Use the cached strategy from the parameter
425
+ strategy = parameter.get_strategy(operation, generation_config, generation_mode)
426
+
427
+ # It is likely will be rejected, hence choose it rarely
428
+ if (
429
+ not parameter.is_required
430
+ and draw(st.floats(min_value=0.0, max_value=1.0, allow_infinity=False, allow_nan=False, allow_subnormal=False))
431
+ < OPTIONAL_BODY_RATE
432
+ ):
227
433
  strategy |= st.just(NOT_SET)
228
- _BODY_STRATEGIES_CACHE.setdefault(parameter, {})[to_strategy] = strategy
229
434
  return strategy
230
435
 
231
436
 
232
437
  def get_parameters_value(
233
- value: Union[NotSet, Dict[str, Any]],
234
- location: str,
235
- draw: Callable,
438
+ value: dict[str, Any] | None,
439
+ location: ParameterLocation,
440
+ draw: st.DrawFn,
236
441
  operation: APIOperation,
237
- context: HookContext,
238
- hooks: Optional[HookDispatcher],
239
- to_strategy: StrategyFactory,
240
- ) -> Optional[Dict[str, Any]]:
442
+ ctx: HookContext,
443
+ hooks: HookDispatcher | None,
444
+ generation_mode: GenerationMode,
445
+ generation_config: GenerationConfig,
446
+ ) -> tuple[dict[str, Any] | None, Any]:
241
447
  """Get the final value for the specified location.
242
448
 
243
449
  If the value is not set, then generate it from the relevant strategy. Otherwise, check what is missing in it and
244
450
  generate those parts.
245
451
  """
246
- if isinstance(value, NotSet):
247
- strategy = get_parameters_strategy(operation, to_strategy, location)
248
- strategy = apply_hooks(operation, context, hooks, strategy, location)
249
- return draw(strategy)
250
- strategy = get_parameters_strategy(operation, to_strategy, location, exclude=value.keys())
251
- strategy = apply_hooks(operation, context, hooks, strategy, location)
252
- value = deepcopy(value)
253
- value.update(draw(strategy))
254
- return value
255
-
256
-
257
- _PARAMETER_STRATEGIES_CACHE: WeakKeyDictionary = WeakKeyDictionary()
452
+ if value is None:
453
+ strategy = get_parameters_strategy(operation, generation_mode, location, generation_config)
454
+ strategy = apply_hooks(operation, ctx, hooks, strategy, location)
455
+ result = draw(strategy)
456
+ # Negative strategy returns GeneratedValue, positive returns just value
457
+ if isinstance(result, GeneratedValue):
458
+ return result.value, result.meta
459
+ return result, None
460
+ strategy = get_parameters_strategy(operation, generation_mode, location, generation_config, exclude=value.keys())
461
+ strategy = apply_hooks(operation, ctx, hooks, strategy, location)
462
+ new = draw(strategy)
463
+ metadata = None
464
+ # Negative strategy returns GeneratedValue, positive returns just value
465
+ if isinstance(new, GeneratedValue):
466
+ new, metadata = new.value, new.meta
467
+ if new is not None:
468
+ copied = dict(value)
469
+ copied.update(new)
470
+ return copied, metadata
471
+ return value, metadata
472
+
473
+
474
+ @dataclass
475
+ class ValueContainer:
476
+ """Container for a value generated by a data generator or explicitly provided."""
477
+
478
+ value: Any
479
+ location: str
480
+ generator: GenerationMode | None
481
+ meta: MutationMetadata | None
482
+
483
+ __slots__ = ("value", "location", "generator", "meta")
484
+
485
+ @property
486
+ def is_generated(self) -> bool:
487
+ """If value was generated."""
488
+ return self.generator is not None and (self.location == "body" or self.value is not None)
489
+
490
+
491
+ def any_negated_values(values: list[ValueContainer]) -> bool:
492
+ """Check if any generated values are negated."""
493
+ return any(value.generator == GenerationMode.NEGATIVE for value in values if value.is_generated)
494
+
495
+
496
+ def generate_parameter(
497
+ location: ParameterLocation,
498
+ explicit: dict[str, Any] | None,
499
+ operation: APIOperation,
500
+ draw: st.DrawFn,
501
+ ctx: HookContext,
502
+ hooks: HookDispatcher | None,
503
+ generator: GenerationMode,
504
+ generation_config: GenerationConfig,
505
+ ) -> ValueContainer:
506
+ """Generate a value for a parameter.
507
+
508
+ Fallback to positive data generator if parameter can not be negated.
509
+ """
510
+ if generator.is_negative and (
511
+ (location == ParameterLocation.PATH and not can_negate_path_parameters(operation))
512
+ or (location.is_in_header and not can_negate_headers(operation, location))
513
+ ):
514
+ # If we can't negate any parameter, generate positive ones
515
+ # If nothing else will be negated, then skip the test completely
516
+ generator = GenerationMode.POSITIVE
517
+ value, metadata = get_parameters_value(
518
+ explicit, location, draw, operation, ctx, hooks, generator, generation_config
519
+ )
520
+ used_generator: GenerationMode | None = generator
521
+ if value == explicit:
522
+ # When we pass `explicit`, then its parts are excluded from generation of the final value
523
+ # If the final value is the same, then other parameters were generated at all
524
+ if value is not None and location == ParameterLocation.PATH:
525
+ value = quote_all(value)
526
+ used_generator = None
527
+ return ValueContainer(value=value, location=location, generator=used_generator, meta=metadata)
528
+
529
+
530
+ def can_negate_path_parameters(operation: APIOperation) -> bool:
531
+ """Check if any path parameter can be negated."""
532
+ # No path parameters to negate
533
+ parameters = cast(OpenApiParameterSet, operation.path_parameters).schema["properties"]
534
+ if not parameters:
535
+ return True
536
+ return any(can_negate(parameter) for parameter in parameters.values())
537
+
538
+
539
+ def can_negate_headers(operation: APIOperation, location: ParameterLocation) -> bool:
540
+ """Check if any header can be negated."""
541
+ container = getattr(operation, location.container_name)
542
+ # No headers to negate
543
+ headers = container.schema["properties"]
544
+ if not headers:
545
+ return True
546
+ return any(
547
+ header not in ({"type": "string"}, {"type": "string", "format": HEADER_FORMAT}) for header in headers.values()
548
+ )
258
549
 
259
550
 
260
551
  def get_parameters_strategy(
261
552
  operation: APIOperation,
262
- to_strategy: StrategyFactory,
263
- location: str,
553
+ generation_mode: GenerationMode,
554
+ location: ParameterLocation,
555
+ generation_config: GenerationConfig,
264
556
  exclude: Iterable[str] = (),
265
557
  ) -> st.SearchStrategy:
266
558
  """Create a new strategy for the case's component from the API operation parameters."""
267
- parameters = getattr(operation, LOCATION_TO_CONTAINER[location])
268
- if parameters:
269
- # The cache key relies on object ids, which means that the parameter should not be mutated
270
- nested_cache_key = (to_strategy, location, tuple(sorted(exclude)))
271
- if operation in _PARAMETER_STRATEGIES_CACHE and nested_cache_key in _PARAMETER_STRATEGIES_CACHE[operation]:
272
- return _PARAMETER_STRATEGIES_CACHE[operation][nested_cache_key]
273
- schema = parameters_to_json_schema(operation, parameters)
274
- if not operation.schema.validate_schema and location == "path":
275
- # If schema validation is disabled, we try to generate data even if the parameter definition
276
- # contains errors.
277
- # In this case, we know that the `required` keyword should always be `True`.
278
- schema["required"] = list(schema["properties"])
279
- schema = operation.schema.prepare_schema(schema)
280
- for name in exclude:
281
- # Values from `exclude` are not necessarily valid for the schema - they come from user-defined examples
282
- # that may be invalid
283
- schema["properties"].pop(name, None)
284
- with suppress(ValueError):
285
- schema["required"].remove(name)
286
- strategy = to_strategy(schema, operation.verbose_name, location, None)
287
- serialize = operation.get_parameter_serializer(location)
288
- if serialize is not None:
289
- strategy = strategy.map(serialize)
290
- filter_func = {
291
- "path": is_valid_path,
292
- "header": is_valid_header,
293
- "cookie": is_valid_header,
294
- "query": is_valid_query,
295
- }[location]
296
- # Headers with special format do not need filtration
297
- if not (is_header_location(location) and _can_skip_header_filter(schema)):
298
- strategy = strategy.filter(filter_func)
299
- # Path & query parameters will be cast to string anyway, but having their JSON equivalents for
300
- # `True` / `False` / `None` improves chances of them passing validation in apps that expect boolean / null types
301
- # and not aware of Python-specific representation of those types
302
- map_func = {
303
- "path": compose(quote_all, jsonify_python_specific_types),
304
- "query": jsonify_python_specific_types,
305
- }.get(location)
306
- if map_func:
307
- strategy = strategy.map(map_func) # type: ignore
308
- _PARAMETER_STRATEGIES_CACHE.setdefault(operation, {})[nested_cache_key] = strategy
309
- return strategy
559
+ container = getattr(operation, location.container_name)
560
+ if container:
561
+ return container.get_strategy(operation, generation_config, generation_mode, exclude)
310
562
  # No parameters defined for this location
311
563
  return st.none()
312
564
 
313
565
 
314
- def _jsonify_leaves(value: Any) -> Any:
315
- if isinstance(value, dict):
316
- for key, sub_item in value.items():
317
- value[key] = _jsonify_leaves(sub_item)
318
- elif isinstance(value, list):
319
- value = [_jsonify_leaves(sub_item) for sub_item in value]
320
- elif isinstance(value, bool):
321
- return "true" if value else "false"
322
- elif value is None:
323
- return "null"
566
+ def jsonify_python_specific_types(value: dict[str, Any]) -> dict[str, Any]:
567
+ """Convert Python-specific values to their JSON equivalents."""
568
+ stack: list = [value]
569
+ while stack:
570
+ item = stack.pop()
571
+ if isinstance(item, dict):
572
+ for key, sub_item in item.items():
573
+ if isinstance(sub_item, bool):
574
+ item[key] = "true" if sub_item else "false"
575
+ elif sub_item is None:
576
+ item[key] = "null"
577
+ elif isinstance(sub_item, dict):
578
+ stack.append(sub_item)
579
+ elif isinstance(sub_item, list):
580
+ stack.extend(item)
581
+ elif isinstance(item, list):
582
+ stack.extend(item)
324
583
  return value
325
584
 
326
585
 
327
- def jsonify_python_specific_types(value: Dict[str, Any]) -> Dict[str, Any]:
328
- """Convert Python-specific values to their JSON equivalents."""
329
- return _jsonify_leaves(value)
586
+ def _build_custom_formats(generation_config: GenerationConfig) -> dict[str, st.SearchStrategy]:
587
+ custom_formats = {**get_default_format_strategies(), **STRING_FORMATS}
588
+ header_values_kwargs = {}
589
+ if generation_config.exclude_header_characters is not None:
590
+ header_values_kwargs["exclude_characters"] = generation_config.exclude_header_characters
591
+ if not generation_config.allow_x00:
592
+ header_values_kwargs["exclude_characters"] += "\x00"
593
+ elif not generation_config.allow_x00:
594
+ header_values_kwargs["exclude_characters"] = DEFAULT_HEADER_EXCLUDE_CHARACTERS + "\x00"
595
+ if generation_config.codec is not None:
596
+ header_values_kwargs["codec"] = generation_config.codec
597
+ if header_values_kwargs:
598
+ custom_formats[HEADER_FORMAT] = header_values(**header_values_kwargs)
599
+ return custom_formats
330
600
 
331
601
 
332
602
  def make_positive_strategy(
333
- schema: Dict[str, Any],
603
+ schema: JsonSchema,
334
604
  operation_name: str,
335
- location: str,
336
- media_type: Optional[str],
337
- custom_formats: Optional[Dict[str, st.SearchStrategy]] = None,
605
+ location: ParameterLocation,
606
+ media_type: str | None,
607
+ generation_config: GenerationConfig,
608
+ validator_cls: type[jsonschema.protocols.Validator],
338
609
  ) -> st.SearchStrategy:
339
610
  """Strategy for generating values that fit the schema."""
340
- if is_header_location(location):
341
- # We try to enforce the right header values via "format"
342
- # This way, only allowed values will be used during data generation, which reduces the amount of filtering later
343
- # If a property schema contains `pattern` it leads to heavy filtering and worse performance - therefore, skip it
344
- for sub_schema in schema.get("properties", {}).values():
345
- if list(sub_schema) == ["type"]:
346
- sub_schema.setdefault("format", HEADER_FORMAT)
347
- return from_schema(schema, custom_formats={**STRING_FORMATS, **(custom_formats or {})})
611
+ custom_formats = _build_custom_formats(generation_config)
612
+ return from_schema(
613
+ schema,
614
+ custom_formats=custom_formats,
615
+ allow_x00=generation_config.allow_x00,
616
+ codec=generation_config.codec,
617
+ )
348
618
 
349
619
 
350
- def _can_skip_header_filter(schema: Dict[str, Any]) -> bool:
620
+ def _can_skip_header_filter(schema: dict[str, Any]) -> bool:
351
621
  # All headers should contain HEADER_FORMAT in order to avoid header filter
352
622
  return all(sub_schema.get("format") == HEADER_FORMAT for sub_schema in schema.get("properties", {}).values())
353
623
 
354
624
 
355
625
  def make_negative_strategy(
356
- schema: Dict[str, Any],
626
+ schema: JsonSchema,
357
627
  operation_name: str,
358
- location: str,
359
- media_type: Optional[str],
360
- custom_formats: Optional[Dict[str, st.SearchStrategy]] = None,
628
+ location: ParameterLocation,
629
+ media_type: str | None,
630
+ generation_config: GenerationConfig,
631
+ validator_cls: type[jsonschema.protocols.Validator],
361
632
  ) -> st.SearchStrategy:
633
+ custom_formats = _build_custom_formats(generation_config)
362
634
  return negative_schema(
363
635
  schema,
364
636
  operation_name=operation_name,
365
637
  location=location,
366
638
  media_type=media_type,
367
- custom_formats={**STRING_FORMATS, **(custom_formats or {})},
639
+ custom_formats=custom_formats,
640
+ generation_config=generation_config,
641
+ validator_cls=validator_cls,
368
642
  )
369
643
 
370
644
 
371
- DATA_GENERATION_METHOD_TO_STRATEGY_FACTORY = {
372
- DataGenerationMethod.positive: make_positive_strategy,
373
- DataGenerationMethod.negative: make_negative_strategy,
645
+ GENERATOR_MODE_TO_STRATEGY_FACTORY = {
646
+ GenerationMode.POSITIVE: make_positive_strategy,
647
+ GenerationMode.NEGATIVE: make_negative_strategy,
374
648
  }
375
649
 
376
650
 
377
- def is_valid_path(parameters: Dict[str, Any]) -> bool:
378
- """Empty strings ("") are excluded from path by urllib3.
379
-
380
- A path containing to "/" or "%2F" will lead to ambiguous path resolution in
381
- many frameworks and libraries, such behaviour have been observed in both
382
- WSGI and ASGI applications.
383
-
384
- In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
385
- Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
386
- """
387
- disallowed_values = (SLASH, "")
388
-
389
- return not any(
390
- (value in disallowed_values or is_illegal_surrogate(value) or isinstance(value, str) and SLASH in value)
391
- for value in parameters.values()
392
- )
393
-
394
-
395
- def quote_all(parameters: Dict[str, Any]) -> Dict[str, Any]:
651
+ def quote_all(parameters: dict[str, Any]) -> dict[str, Any]:
396
652
  """Apply URL quotation for all values in a dictionary."""
397
653
  # Even though, "." is an unreserved character, it has a special meaning in "." and ".." strings.
398
654
  # It will change the path:
@@ -400,42 +656,23 @@ def quote_all(parameters: Dict[str, Any]) -> Dict[str, Any]:
400
656
  # - http://localhost/foo/../ -> http://localhost/
401
657
  # Which is not desired as we need to test precisely the original path structure.
402
658
 
403
- def quote(value: str) -> str:
404
- quoted = quote_plus(value)
405
- if quoted == ".":
406
- return "%2E"
407
- if quoted == "..":
408
- return "%2E%2E"
409
- return quoted
410
-
411
- return {key: quote(value) if isinstance(value, str) else value for key, value in parameters.items()}
659
+ for key, value in parameters.items():
660
+ if isinstance(value, str):
661
+ if value == ".":
662
+ parameters[key] = "%2E"
663
+ elif value == "..":
664
+ parameters[key] = "%2E%2E"
665
+ else:
666
+ parameters[key] = quote_plus(value)
667
+ return parameters
412
668
 
413
669
 
414
670
  def apply_hooks(
415
671
  operation: APIOperation,
416
- context: HookContext,
417
- hooks: Optional[HookDispatcher],
672
+ ctx: HookContext,
673
+ hooks: HookDispatcher | None,
418
674
  strategy: st.SearchStrategy,
419
- location: str,
420
- ) -> st.SearchStrategy:
421
- """Apply all `before_generate_` hooks related to the given location."""
422
- strategy = _apply_hooks(context, GLOBAL_HOOK_DISPATCHER, strategy, location)
423
- strategy = _apply_hooks(context, operation.schema.hooks, strategy, location)
424
- if hooks is not None:
425
- strategy = _apply_hooks(context, hooks, strategy, location)
426
- return strategy
427
-
428
-
429
- def _apply_hooks(
430
- context: HookContext, hooks: HookDispatcher, strategy: st.SearchStrategy, location: str
675
+ location: ParameterLocation,
431
676
  ) -> st.SearchStrategy:
432
- """Apply all `before_generate_` hooks related to the given location & dispatcher."""
433
- container = LOCATION_TO_CONTAINER[location]
434
- for hook in hooks.get_all_by_name(f"before_generate_{container}"):
435
- strategy = hook(context, strategy)
436
- return strategy
437
-
438
-
439
- def clear_cache() -> None:
440
- _PARAMETER_STRATEGIES_CACHE.clear()
441
- _BODY_STRATEGIES_CACHE.clear()
677
+ """Apply all hooks related to the given location."""
678
+ return apply_to_all_dispatchers(operation, ctx, hooks, strategy, location.container_name)