schemathesis 3.39.16__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.
Files changed (255) hide show
  1. schemathesis/__init__.py +41 -79
  2. schemathesis/auths.py +111 -122
  3. schemathesis/checks.py +169 -60
  4. schemathesis/cli/__init__.py +15 -2117
  5. schemathesis/cli/commands/__init__.py +85 -0
  6. schemathesis/cli/commands/data.py +10 -0
  7. schemathesis/cli/commands/run/__init__.py +590 -0
  8. schemathesis/cli/commands/run/context.py +204 -0
  9. schemathesis/cli/commands/run/events.py +60 -0
  10. schemathesis/cli/commands/run/executor.py +157 -0
  11. schemathesis/cli/commands/run/filters.py +53 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/commands/run/handlers/cassettes.py +474 -0
  15. schemathesis/cli/commands/run/handlers/junitxml.py +55 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1628 -0
  17. schemathesis/cli/commands/run/loaders.py +114 -0
  18. schemathesis/cli/commands/run/validation.py +246 -0
  19. schemathesis/cli/constants.py +5 -58
  20. schemathesis/cli/core.py +19 -0
  21. schemathesis/cli/ext/fs.py +16 -0
  22. schemathesis/cli/ext/groups.py +84 -0
  23. schemathesis/cli/{options.py → ext/options.py} +36 -34
  24. schemathesis/config/__init__.py +189 -0
  25. schemathesis/config/_auth.py +51 -0
  26. schemathesis/config/_checks.py +268 -0
  27. schemathesis/config/_diff_base.py +99 -0
  28. schemathesis/config/_env.py +21 -0
  29. schemathesis/config/_error.py +156 -0
  30. schemathesis/config/_generation.py +149 -0
  31. schemathesis/config/_health_check.py +24 -0
  32. schemathesis/config/_operations.py +327 -0
  33. schemathesis/config/_output.py +171 -0
  34. schemathesis/config/_parameters.py +19 -0
  35. schemathesis/config/_phases.py +187 -0
  36. schemathesis/config/_projects.py +527 -0
  37. schemathesis/config/_rate_limit.py +17 -0
  38. schemathesis/config/_report.py +120 -0
  39. schemathesis/config/_validator.py +9 -0
  40. schemathesis/config/_warnings.py +25 -0
  41. schemathesis/config/schema.json +885 -0
  42. schemathesis/core/__init__.py +67 -0
  43. schemathesis/core/compat.py +32 -0
  44. schemathesis/core/control.py +2 -0
  45. schemathesis/core/curl.py +58 -0
  46. schemathesis/core/deserialization.py +65 -0
  47. schemathesis/core/errors.py +459 -0
  48. schemathesis/core/failures.py +315 -0
  49. schemathesis/core/fs.py +19 -0
  50. schemathesis/core/hooks.py +20 -0
  51. schemathesis/core/loaders.py +104 -0
  52. schemathesis/core/marks.py +66 -0
  53. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  54. schemathesis/core/output/__init__.py +46 -0
  55. schemathesis/core/output/sanitization.py +54 -0
  56. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  57. schemathesis/core/registries.py +31 -0
  58. schemathesis/core/transforms.py +113 -0
  59. schemathesis/core/transport.py +223 -0
  60. schemathesis/core/validation.py +54 -0
  61. schemathesis/core/version.py +7 -0
  62. schemathesis/engine/__init__.py +28 -0
  63. schemathesis/engine/context.py +118 -0
  64. schemathesis/engine/control.py +36 -0
  65. schemathesis/engine/core.py +169 -0
  66. schemathesis/engine/errors.py +464 -0
  67. schemathesis/engine/events.py +258 -0
  68. schemathesis/engine/phases/__init__.py +88 -0
  69. schemathesis/{runner → engine/phases}/probes.py +52 -68
  70. schemathesis/engine/phases/stateful/__init__.py +68 -0
  71. schemathesis/engine/phases/stateful/_executor.py +356 -0
  72. schemathesis/engine/phases/stateful/context.py +85 -0
  73. schemathesis/engine/phases/unit/__init__.py +212 -0
  74. schemathesis/engine/phases/unit/_executor.py +416 -0
  75. schemathesis/engine/phases/unit/_pool.py +82 -0
  76. schemathesis/engine/recorder.py +247 -0
  77. schemathesis/errors.py +43 -0
  78. schemathesis/filters.py +17 -98
  79. schemathesis/generation/__init__.py +5 -33
  80. schemathesis/generation/case.py +317 -0
  81. schemathesis/generation/coverage.py +282 -175
  82. schemathesis/generation/hypothesis/__init__.py +36 -0
  83. schemathesis/generation/hypothesis/builder.py +800 -0
  84. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  85. schemathesis/generation/hypothesis/given.py +66 -0
  86. schemathesis/generation/hypothesis/reporting.py +14 -0
  87. schemathesis/generation/hypothesis/strategies.py +16 -0
  88. schemathesis/generation/meta.py +115 -0
  89. schemathesis/generation/metrics.py +93 -0
  90. schemathesis/generation/modes.py +20 -0
  91. schemathesis/generation/overrides.py +116 -0
  92. schemathesis/generation/stateful/__init__.py +37 -0
  93. schemathesis/generation/stateful/state_machine.py +278 -0
  94. schemathesis/graphql/__init__.py +15 -0
  95. schemathesis/graphql/checks.py +109 -0
  96. schemathesis/graphql/loaders.py +284 -0
  97. schemathesis/hooks.py +80 -101
  98. schemathesis/openapi/__init__.py +13 -0
  99. schemathesis/openapi/checks.py +455 -0
  100. schemathesis/openapi/generation/__init__.py +0 -0
  101. schemathesis/openapi/generation/filters.py +72 -0
  102. schemathesis/openapi/loaders.py +313 -0
  103. schemathesis/pytest/__init__.py +5 -0
  104. schemathesis/pytest/control_flow.py +7 -0
  105. schemathesis/pytest/lazy.py +281 -0
  106. schemathesis/pytest/loaders.py +36 -0
  107. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +128 -108
  108. schemathesis/python/__init__.py +0 -0
  109. schemathesis/python/asgi.py +12 -0
  110. schemathesis/python/wsgi.py +12 -0
  111. schemathesis/schemas.py +537 -273
  112. schemathesis/specs/graphql/__init__.py +0 -1
  113. schemathesis/specs/graphql/_cache.py +1 -2
  114. schemathesis/specs/graphql/scalars.py +42 -6
  115. schemathesis/specs/graphql/schemas.py +141 -137
  116. schemathesis/specs/graphql/validation.py +11 -17
  117. schemathesis/specs/openapi/__init__.py +6 -1
  118. schemathesis/specs/openapi/_cache.py +1 -2
  119. schemathesis/specs/openapi/_hypothesis.py +142 -156
  120. schemathesis/specs/openapi/checks.py +368 -257
  121. schemathesis/specs/openapi/converter.py +4 -4
  122. schemathesis/specs/openapi/definitions.py +1 -1
  123. schemathesis/specs/openapi/examples.py +23 -21
  124. schemathesis/specs/openapi/expressions/__init__.py +31 -19
  125. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  126. schemathesis/specs/openapi/expressions/lexer.py +1 -1
  127. schemathesis/specs/openapi/expressions/nodes.py +36 -41
  128. schemathesis/specs/openapi/expressions/parser.py +1 -1
  129. schemathesis/specs/openapi/formats.py +35 -7
  130. schemathesis/specs/openapi/media_types.py +53 -12
  131. schemathesis/specs/openapi/negative/__init__.py +7 -4
  132. schemathesis/specs/openapi/negative/mutations.py +6 -5
  133. schemathesis/specs/openapi/parameters.py +7 -10
  134. schemathesis/specs/openapi/patterns.py +94 -31
  135. schemathesis/specs/openapi/references.py +12 -53
  136. schemathesis/specs/openapi/schemas.py +233 -307
  137. schemathesis/specs/openapi/security.py +1 -1
  138. schemathesis/specs/openapi/serialization.py +12 -6
  139. schemathesis/specs/openapi/stateful/__init__.py +268 -133
  140. schemathesis/specs/openapi/stateful/control.py +87 -0
  141. schemathesis/specs/openapi/stateful/links.py +209 -0
  142. schemathesis/transport/__init__.py +142 -0
  143. schemathesis/transport/asgi.py +26 -0
  144. schemathesis/transport/prepare.py +124 -0
  145. schemathesis/transport/requests.py +244 -0
  146. schemathesis/{_xml.py → transport/serialization.py} +69 -11
  147. schemathesis/transport/wsgi.py +171 -0
  148. schemathesis-4.0.0.dist-info/METADATA +204 -0
  149. schemathesis-4.0.0.dist-info/RECORD +164 -0
  150. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/entry_points.txt +1 -1
  151. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/licenses/LICENSE +1 -1
  152. schemathesis/_compat.py +0 -74
  153. schemathesis/_dependency_versions.py +0 -19
  154. schemathesis/_hypothesis.py +0 -717
  155. schemathesis/_override.py +0 -50
  156. schemathesis/_patches.py +0 -21
  157. schemathesis/_rate_limiter.py +0 -7
  158. schemathesis/cli/callbacks.py +0 -466
  159. schemathesis/cli/cassettes.py +0 -561
  160. schemathesis/cli/context.py +0 -75
  161. schemathesis/cli/debug.py +0 -27
  162. schemathesis/cli/handlers.py +0 -19
  163. schemathesis/cli/junitxml.py +0 -124
  164. schemathesis/cli/output/__init__.py +0 -1
  165. schemathesis/cli/output/default.py +0 -920
  166. schemathesis/cli/output/short.py +0 -59
  167. schemathesis/cli/reporting.py +0 -79
  168. schemathesis/cli/sanitization.py +0 -26
  169. schemathesis/code_samples.py +0 -151
  170. schemathesis/constants.py +0 -54
  171. schemathesis/contrib/__init__.py +0 -11
  172. schemathesis/contrib/openapi/__init__.py +0 -11
  173. schemathesis/contrib/openapi/fill_missing_examples.py +0 -24
  174. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  175. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  176. schemathesis/contrib/unique_data.py +0 -41
  177. schemathesis/exceptions.py +0 -571
  178. schemathesis/experimental/__init__.py +0 -109
  179. schemathesis/extra/_aiohttp.py +0 -28
  180. schemathesis/extra/_flask.py +0 -13
  181. schemathesis/extra/_server.py +0 -18
  182. schemathesis/failures.py +0 -284
  183. schemathesis/fixups/__init__.py +0 -37
  184. schemathesis/fixups/fast_api.py +0 -41
  185. schemathesis/fixups/utf8_bom.py +0 -28
  186. schemathesis/generation/_methods.py +0 -44
  187. schemathesis/graphql.py +0 -3
  188. schemathesis/internal/__init__.py +0 -7
  189. schemathesis/internal/checks.py +0 -86
  190. schemathesis/internal/copy.py +0 -32
  191. schemathesis/internal/datetime.py +0 -5
  192. schemathesis/internal/deprecation.py +0 -37
  193. schemathesis/internal/diff.py +0 -15
  194. schemathesis/internal/extensions.py +0 -27
  195. schemathesis/internal/jsonschema.py +0 -36
  196. schemathesis/internal/output.py +0 -68
  197. schemathesis/internal/transformation.py +0 -26
  198. schemathesis/internal/validation.py +0 -34
  199. schemathesis/lazy.py +0 -474
  200. schemathesis/loaders.py +0 -122
  201. schemathesis/models.py +0 -1341
  202. schemathesis/parameters.py +0 -90
  203. schemathesis/runner/__init__.py +0 -605
  204. schemathesis/runner/events.py +0 -389
  205. schemathesis/runner/impl/__init__.py +0 -3
  206. schemathesis/runner/impl/context.py +0 -88
  207. schemathesis/runner/impl/core.py +0 -1280
  208. schemathesis/runner/impl/solo.py +0 -80
  209. schemathesis/runner/impl/threadpool.py +0 -391
  210. schemathesis/runner/serialization.py +0 -544
  211. schemathesis/sanitization.py +0 -252
  212. schemathesis/serializers.py +0 -328
  213. schemathesis/service/__init__.py +0 -18
  214. schemathesis/service/auth.py +0 -11
  215. schemathesis/service/ci.py +0 -202
  216. schemathesis/service/client.py +0 -133
  217. schemathesis/service/constants.py +0 -38
  218. schemathesis/service/events.py +0 -61
  219. schemathesis/service/extensions.py +0 -224
  220. schemathesis/service/hosts.py +0 -111
  221. schemathesis/service/metadata.py +0 -71
  222. schemathesis/service/models.py +0 -258
  223. schemathesis/service/report.py +0 -255
  224. schemathesis/service/serialization.py +0 -173
  225. schemathesis/service/usage.py +0 -66
  226. schemathesis/specs/graphql/loaders.py +0 -364
  227. schemathesis/specs/openapi/expressions/context.py +0 -16
  228. schemathesis/specs/openapi/links.py +0 -389
  229. schemathesis/specs/openapi/loaders.py +0 -707
  230. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  231. schemathesis/specs/openapi/stateful/types.py +0 -14
  232. schemathesis/specs/openapi/validation.py +0 -26
  233. schemathesis/stateful/__init__.py +0 -147
  234. schemathesis/stateful/config.py +0 -97
  235. schemathesis/stateful/context.py +0 -135
  236. schemathesis/stateful/events.py +0 -274
  237. schemathesis/stateful/runner.py +0 -309
  238. schemathesis/stateful/sink.py +0 -68
  239. schemathesis/stateful/state_machine.py +0 -328
  240. schemathesis/stateful/statistic.py +0 -22
  241. schemathesis/stateful/validation.py +0 -100
  242. schemathesis/targets.py +0 -77
  243. schemathesis/transports/__init__.py +0 -369
  244. schemathesis/transports/asgi.py +0 -7
  245. schemathesis/transports/auth.py +0 -38
  246. schemathesis/transports/headers.py +0 -36
  247. schemathesis/transports/responses.py +0 -57
  248. schemathesis/types.py +0 -44
  249. schemathesis/utils.py +0 -164
  250. schemathesis-3.39.16.dist-info/METADATA +0 -293
  251. schemathesis-3.39.16.dist-info/RECORD +0 -160
  252. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  253. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  254. /schemathesis/{internal → core}/result.py +0 -0
  255. {schemathesis-3.39.16.dist-info → schemathesis-4.0.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,455 @@
1
+ from __future__ import annotations
2
+
3
+ import textwrap
4
+ from typing import TYPE_CHECKING, Any
5
+
6
+ from schemathesis.config import OutputConfig
7
+ from schemathesis.core.failures import Failure, Severity
8
+ from schemathesis.core.output import truncate_json
9
+
10
+ if TYPE_CHECKING:
11
+ from jsonschema import ValidationError
12
+
13
+
14
+ class UndefinedStatusCode(Failure):
15
+ """Response has a status code that is not defined in the schema."""
16
+
17
+ __slots__ = (
18
+ "operation",
19
+ "status_code",
20
+ "defined_status_codes",
21
+ "allowed_status_codes",
22
+ "message",
23
+ "title",
24
+ "case_id",
25
+ "severity",
26
+ )
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ operation: str,
32
+ status_code: int,
33
+ defined_status_codes: list[str],
34
+ allowed_status_codes: list[int],
35
+ message: str,
36
+ title: str = "Undocumented HTTP status code",
37
+ case_id: str | None = None,
38
+ ) -> None:
39
+ self.operation = operation
40
+ self.status_code = status_code
41
+ self.defined_status_codes = defined_status_codes
42
+ self.allowed_status_codes = allowed_status_codes
43
+ self.message = message
44
+ self.title = title
45
+ self.case_id = case_id
46
+ self.severity = Severity.MEDIUM
47
+
48
+ @property
49
+ def _unique_key(self) -> str:
50
+ return str(self.status_code)
51
+
52
+
53
+ class MissingHeaders(Failure):
54
+ """Some required headers are missing."""
55
+
56
+ __slots__ = ("operation", "missing_headers", "message", "title", "case_id", "severity")
57
+
58
+ def __init__(
59
+ self,
60
+ *,
61
+ operation: str,
62
+ missing_headers: list[str],
63
+ message: str,
64
+ title: str = "Missing required headers",
65
+ case_id: str | None = None,
66
+ ) -> None:
67
+ self.operation = operation
68
+ self.missing_headers = missing_headers
69
+ self.message = message
70
+ self.title = title
71
+ self.case_id = case_id
72
+ self.severity = Severity.MEDIUM
73
+
74
+
75
+ class JsonSchemaError(Failure):
76
+ """Additional information about JSON Schema validation errors."""
77
+
78
+ __slots__ = (
79
+ "operation",
80
+ "validation_message",
81
+ "schema_path",
82
+ "schema",
83
+ "instance_path",
84
+ "instance",
85
+ "message",
86
+ "title",
87
+ "case_id",
88
+ "severity",
89
+ )
90
+
91
+ def __init__(
92
+ self,
93
+ *,
94
+ operation: str,
95
+ validation_message: str,
96
+ schema_path: list[str | int],
97
+ schema: dict[str, Any] | bool,
98
+ instance_path: list[str | int],
99
+ instance: None | bool | float | str | list | dict[str, Any],
100
+ message: str,
101
+ title: str = "Response violates schema",
102
+ case_id: str | None = None,
103
+ ) -> None:
104
+ self.operation = operation
105
+ self.validation_message = validation_message
106
+ self.schema_path = schema_path
107
+ self.schema = schema
108
+ self.instance_path = instance_path
109
+ self.instance = instance
110
+ self.message = message
111
+ self.title = title
112
+ self.case_id = case_id
113
+ self.severity = Severity.HIGH
114
+
115
+ @property
116
+ def _unique_key(self) -> str:
117
+ return "/".join(map(str, self.schema_path))
118
+
119
+ @classmethod
120
+ def from_exception(
121
+ cls,
122
+ *,
123
+ title: str = "Response violates schema",
124
+ operation: str,
125
+ exc: ValidationError,
126
+ config: OutputConfig | None = None,
127
+ ) -> JsonSchemaError:
128
+ schema = textwrap.indent(
129
+ truncate_json(exc.schema, config=config or OutputConfig(), max_lines=20), prefix=" "
130
+ )
131
+ value = textwrap.indent(
132
+ truncate_json(exc.instance, config=config or OutputConfig(), max_lines=20), prefix=" "
133
+ )
134
+ schema_path = list(exc.absolute_schema_path)
135
+ if len(schema_path) > 1:
136
+ # Exclude the last segment, which is already in the schema
137
+ schema_title = "Schema at "
138
+ for segment in schema_path[:-1]:
139
+ schema_title += f"/{segment}"
140
+ else:
141
+ schema_title = "Schema"
142
+ message = f"{exc.message}\n\n{schema_title}:\n\n{schema}\n\nValue:\n\n{value}"
143
+ return cls(
144
+ operation=operation,
145
+ title=title,
146
+ message=message,
147
+ validation_message=exc.message,
148
+ schema_path=schema_path,
149
+ schema=exc.schema,
150
+ instance_path=list(exc.absolute_path),
151
+ instance=exc.instance,
152
+ )
153
+
154
+
155
+ class MissingContentType(Failure):
156
+ """Content type header is missing."""
157
+
158
+ __slots__ = ("operation", "media_types", "message", "title", "case_id", "severity")
159
+
160
+ def __init__(
161
+ self,
162
+ *,
163
+ operation: str,
164
+ media_types: list[str],
165
+ message: str,
166
+ title: str = "Missing Content-Type header",
167
+ case_id: str | None = None,
168
+ ) -> None:
169
+ self.operation = operation
170
+ self.media_types = media_types
171
+ self.message = message
172
+ self.title = title
173
+ self.case_id = case_id
174
+ self.severity = Severity.MEDIUM
175
+
176
+ @property
177
+ def _unique_key(self) -> str:
178
+ return ""
179
+
180
+
181
+ class MalformedMediaType(Failure):
182
+ """Media type name is malformed."""
183
+
184
+ __slots__ = ("operation", "actual", "defined", "message", "title", "case_id", "severity")
185
+
186
+ def __init__(
187
+ self,
188
+ *,
189
+ operation: str,
190
+ actual: str,
191
+ defined: str,
192
+ message: str,
193
+ title: str = "Malformed media type",
194
+ case_id: str | None = None,
195
+ ) -> None:
196
+ self.operation = operation
197
+ self.actual = actual
198
+ self.defined = defined
199
+ self.message = message
200
+ self.title = title
201
+ self.case_id = case_id
202
+ self.severity = Severity.MEDIUM
203
+
204
+
205
+ class UndefinedContentType(Failure):
206
+ """Response has Content-Type that is not documented in the schema."""
207
+
208
+ __slots__ = (
209
+ "operation",
210
+ "content_type",
211
+ "defined_content_types",
212
+ "message",
213
+ "title",
214
+ "case_id",
215
+ "severity",
216
+ )
217
+
218
+ def __init__(
219
+ self,
220
+ *,
221
+ operation: str,
222
+ content_type: str,
223
+ defined_content_types: list[str],
224
+ message: str,
225
+ title: str = "Undocumented Content-Type",
226
+ case_id: str | None = None,
227
+ ) -> None:
228
+ self.operation = operation
229
+ self.content_type = content_type
230
+ self.defined_content_types = defined_content_types
231
+ self.message = message
232
+ self.title = title
233
+ self.case_id = case_id
234
+ self.severity = Severity.MEDIUM
235
+
236
+ @property
237
+ def _unique_key(self) -> str:
238
+ return self.content_type
239
+
240
+
241
+ class UseAfterFree(Failure):
242
+ """Resource was used after a successful DELETE operation on it."""
243
+
244
+ __slots__ = ("operation", "message", "free", "usage", "title", "case_id", "severity")
245
+
246
+ def __init__(
247
+ self,
248
+ *,
249
+ operation: str,
250
+ message: str,
251
+ free: str,
252
+ usage: str,
253
+ title: str = "Use after free",
254
+ case_id: str | None = None,
255
+ ) -> None:
256
+ self.operation = operation
257
+ self.message = message
258
+ self.free = free
259
+ self.usage = usage
260
+ self.title = title
261
+ self.case_id = case_id
262
+ self.severity = Severity.CRITICAL
263
+
264
+ @property
265
+ def _unique_key(self) -> str:
266
+ return ""
267
+
268
+
269
+ class EnsureResourceAvailability(Failure):
270
+ """Resource is not available immediately after creation."""
271
+
272
+ __slots__ = ("operation", "message", "created_with", "not_available_with", "title", "case_id", "severity")
273
+
274
+ def __init__(
275
+ self,
276
+ *,
277
+ operation: str,
278
+ message: str,
279
+ created_with: str,
280
+ not_available_with: str,
281
+ title: str = "Resource is not available after creation",
282
+ case_id: str | None = None,
283
+ ) -> None:
284
+ self.operation = operation
285
+ self.message = message
286
+ self.created_with = created_with
287
+ self.not_available_with = not_available_with
288
+ self.title = title
289
+ self.case_id = case_id
290
+ self.severity = Severity.MEDIUM
291
+
292
+ @property
293
+ def _unique_key(self) -> str:
294
+ return ""
295
+
296
+
297
+ class IgnoredAuth(Failure):
298
+ """The API operation does not check the specified authentication."""
299
+
300
+ __slots__ = ("operation", "message", "title", "case_id", "severity")
301
+
302
+ def __init__(
303
+ self,
304
+ *,
305
+ operation: str,
306
+ message: str,
307
+ title: str = "API accepts requests without authentication",
308
+ case_id: str | None = None,
309
+ ) -> None:
310
+ self.operation = operation
311
+ self.message = message
312
+ self.title = title
313
+ self.case_id = case_id
314
+ self.severity = Severity.CRITICAL
315
+
316
+ @property
317
+ def _unique_key(self) -> str:
318
+ return ""
319
+
320
+
321
+ class AcceptedNegativeData(Failure):
322
+ """Response with negative data was accepted."""
323
+
324
+ __slots__ = ("operation", "message", "status_code", "expected_statuses", "title", "case_id", "severity")
325
+
326
+ def __init__(
327
+ self,
328
+ *,
329
+ operation: str,
330
+ message: str,
331
+ status_code: int,
332
+ expected_statuses: list[str],
333
+ title: str = "Accepted negative data",
334
+ case_id: str | None = None,
335
+ ) -> None:
336
+ self.operation = operation
337
+ self.message = message
338
+ self.status_code = status_code
339
+ self.expected_statuses = expected_statuses
340
+ self.title = title
341
+ self.case_id = case_id
342
+ self.severity = Severity.MEDIUM
343
+
344
+ @property
345
+ def _unique_key(self) -> str:
346
+ return str(self.status_code)
347
+
348
+
349
+ class RejectedPositiveData(Failure):
350
+ """Response with positive data was rejected."""
351
+
352
+ __slots__ = ("operation", "message", "status_code", "allowed_statuses", "title", "case_id", "severity")
353
+
354
+ def __init__(
355
+ self,
356
+ *,
357
+ operation: str,
358
+ message: str,
359
+ status_code: int,
360
+ allowed_statuses: list[str],
361
+ title: str = "Rejected positive data",
362
+ case_id: str | None = None,
363
+ ) -> None:
364
+ self.operation = operation
365
+ self.message = message
366
+ self.status_code = status_code
367
+ self.allowed_statuses = allowed_statuses
368
+ self.title = title
369
+ self.case_id = case_id
370
+ self.severity = Severity.MEDIUM
371
+
372
+ @property
373
+ def _unique_key(self) -> str:
374
+ return str(self.status_code)
375
+
376
+
377
+ class MissingHeaderNotRejected(Failure):
378
+ """API did not reject request without required header."""
379
+
380
+ __slots__ = (
381
+ "operation",
382
+ "header_name",
383
+ "status_code",
384
+ "expected_statuses",
385
+ "message",
386
+ "title",
387
+ "case_id",
388
+ "severity",
389
+ )
390
+
391
+ def __init__(
392
+ self,
393
+ *,
394
+ operation: str,
395
+ header_name: str,
396
+ status_code: int,
397
+ expected_statuses: list[int],
398
+ message: str,
399
+ title: str = "Missing header not rejected",
400
+ case_id: str | None = None,
401
+ ) -> None:
402
+ self.operation = operation
403
+ self.header_name = header_name
404
+ self.status_code = status_code
405
+ self.expected_statuses = expected_statuses
406
+ self.message = message
407
+ self.title = title
408
+ self.case_id = case_id
409
+ self.severity = Severity.MEDIUM
410
+
411
+ @property
412
+ def _unique_key(self) -> str:
413
+ return self.header_name
414
+
415
+
416
+ class UnsupportedMethodResponse(Failure):
417
+ """API response for unsupported HTTP method is incorrect."""
418
+
419
+ __slots__ = (
420
+ "operation",
421
+ "method",
422
+ "status_code",
423
+ "allow_header_present",
424
+ "failure_reason",
425
+ "message",
426
+ "title",
427
+ "case_id",
428
+ "severity",
429
+ )
430
+
431
+ def __init__(
432
+ self,
433
+ *,
434
+ operation: str,
435
+ method: str,
436
+ status_code: int,
437
+ allow_header_present: bool | None = None,
438
+ failure_reason: str, # "wrong_status" or "missing_allow_header"
439
+ message: str,
440
+ title: str = "Unsupported method incorrect response",
441
+ case_id: str | None = None,
442
+ ) -> None:
443
+ self.operation = operation
444
+ self.method = method
445
+ self.status_code = status_code
446
+ self.allow_header_present = allow_header_present
447
+ self.failure_reason = failure_reason
448
+ self.message = message
449
+ self.title = title
450
+ self.case_id = case_id
451
+ self.severity = Severity.MEDIUM
452
+
453
+ @property
454
+ def _unique_key(self) -> str:
455
+ return self.failure_reason
File without changes
@@ -0,0 +1,72 @@
1
+ from collections.abc import Mapping
2
+ from typing import Any
3
+
4
+ from schemathesis.core import NOT_SET
5
+ from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
6
+
7
+ __all__ = [
8
+ "is_valid_path",
9
+ "is_valid_header",
10
+ "is_valid_urlencoded",
11
+ "is_valid_query",
12
+ ]
13
+
14
+
15
+ def is_valid_path(parameters: dict[str, object]) -> bool:
16
+ """Empty strings ("") are excluded from path by urllib3.
17
+
18
+ A path containing to "/" or "%2F" will lead to ambiguous path resolution in
19
+ many frameworks and libraries, such behaviour have been observed in both
20
+ WSGI and ASGI applications.
21
+
22
+ In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
23
+ Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
24
+ """
25
+ return not any(is_invalid_path_parameter(value) for value in parameters.values())
26
+
27
+
28
+ def is_invalid_path_parameter(value: Any) -> bool:
29
+ return (
30
+ value in ("/", "")
31
+ or contains_unicode_surrogate_pair(value)
32
+ or (
33
+ isinstance(value, str)
34
+ and (
35
+ ("/" in value or "}" in value or "{" in value)
36
+ # Avoid situations when the path parameter contains only NULL bytes
37
+ # Many webservers remove such bytes and as the result, the test can target a different API operation
38
+ or (len(value) == value.count("\x00"))
39
+ )
40
+ )
41
+ )
42
+
43
+
44
+ def is_valid_header(headers: dict[str, object]) -> bool:
45
+ for name, value in headers.items():
46
+ if not is_latin_1_encodable(value):
47
+ return False
48
+ if has_invalid_characters(name, value):
49
+ return False
50
+ return True
51
+
52
+
53
+ def is_valid_query(query: dict[str, object]) -> bool:
54
+ for name, value in query.items():
55
+ if contains_unicode_surrogate_pair(name) or contains_unicode_surrogate_pair(value):
56
+ return False
57
+ return True
58
+
59
+
60
+ def is_valid_urlencoded(data: object) -> bool:
61
+ # TODO: write a test that will check if `requests` can send it
62
+ if data is NOT_SET or isinstance(data, Mapping):
63
+ return True
64
+
65
+ if hasattr(data, "__iter__"):
66
+ try:
67
+ for _, _ in data:
68
+ pass
69
+ return True
70
+ except (TypeError, ValueError):
71
+ return False
72
+ return False