schemathesis 3.25.5__py3-none-any.whl → 4.0.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +102 -82
  3. schemathesis/checks.py +126 -46
  4. schemathesis/cli/__init__.py +11 -1766
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +37 -0
  7. schemathesis/cli/commands/run/__init__.py +662 -0
  8. schemathesis/cli/commands/run/checks.py +80 -0
  9. schemathesis/cli/commands/run/context.py +117 -0
  10. schemathesis/cli/commands/run/events.py +35 -0
  11. schemathesis/cli/commands/run/executor.py +138 -0
  12. schemathesis/cli/commands/run/filters.py +194 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +18 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +494 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  17. schemathesis/cli/commands/run/handlers/output.py +746 -0
  18. schemathesis/cli/commands/run/hypothesis.py +105 -0
  19. schemathesis/cli/commands/run/loaders.py +129 -0
  20. schemathesis/cli/{callbacks.py → commands/run/validation.py} +103 -174
  21. schemathesis/cli/constants.py +5 -52
  22. schemathesis/cli/core.py +17 -0
  23. schemathesis/cli/ext/fs.py +14 -0
  24. schemathesis/cli/ext/groups.py +55 -0
  25. schemathesis/cli/{options.py → ext/options.py} +39 -10
  26. schemathesis/cli/hooks.py +36 -0
  27. schemathesis/contrib/__init__.py +1 -3
  28. schemathesis/contrib/openapi/__init__.py +1 -3
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -5
  30. schemathesis/core/__init__.py +58 -0
  31. schemathesis/core/compat.py +25 -0
  32. schemathesis/core/control.py +2 -0
  33. schemathesis/core/curl.py +58 -0
  34. schemathesis/core/deserialization.py +65 -0
  35. schemathesis/core/errors.py +370 -0
  36. schemathesis/core/failures.py +285 -0
  37. schemathesis/core/fs.py +19 -0
  38. schemathesis/{_lazy_import.py → core/lazy_import.py} +1 -0
  39. schemathesis/core/loaders.py +104 -0
  40. schemathesis/core/marks.py +66 -0
  41. schemathesis/{transports/content_types.py → core/media_types.py} +17 -13
  42. schemathesis/core/output/__init__.py +69 -0
  43. schemathesis/core/output/sanitization.py +197 -0
  44. schemathesis/core/rate_limit.py +60 -0
  45. schemathesis/core/registries.py +31 -0
  46. schemathesis/{internal → core}/result.py +1 -1
  47. schemathesis/core/transforms.py +113 -0
  48. schemathesis/core/transport.py +108 -0
  49. schemathesis/core/validation.py +38 -0
  50. schemathesis/core/version.py +7 -0
  51. schemathesis/engine/__init__.py +30 -0
  52. schemathesis/engine/config.py +59 -0
  53. schemathesis/engine/context.py +119 -0
  54. schemathesis/engine/control.py +36 -0
  55. schemathesis/engine/core.py +157 -0
  56. schemathesis/engine/errors.py +394 -0
  57. schemathesis/engine/events.py +337 -0
  58. schemathesis/engine/phases/__init__.py +66 -0
  59. schemathesis/{cli → engine/phases}/probes.py +63 -70
  60. schemathesis/engine/phases/stateful/__init__.py +65 -0
  61. schemathesis/engine/phases/stateful/_executor.py +326 -0
  62. schemathesis/engine/phases/stateful/context.py +85 -0
  63. schemathesis/engine/phases/unit/__init__.py +174 -0
  64. schemathesis/engine/phases/unit/_executor.py +321 -0
  65. schemathesis/engine/phases/unit/_pool.py +74 -0
  66. schemathesis/engine/recorder.py +241 -0
  67. schemathesis/errors.py +31 -0
  68. schemathesis/experimental/__init__.py +18 -14
  69. schemathesis/filters.py +103 -14
  70. schemathesis/generation/__init__.py +21 -37
  71. schemathesis/generation/case.py +190 -0
  72. schemathesis/generation/coverage.py +931 -0
  73. schemathesis/generation/hypothesis/__init__.py +30 -0
  74. schemathesis/generation/hypothesis/builder.py +585 -0
  75. schemathesis/generation/hypothesis/examples.py +50 -0
  76. schemathesis/generation/hypothesis/given.py +66 -0
  77. schemathesis/generation/hypothesis/reporting.py +14 -0
  78. schemathesis/generation/hypothesis/strategies.py +16 -0
  79. schemathesis/generation/meta.py +115 -0
  80. schemathesis/generation/modes.py +28 -0
  81. schemathesis/generation/overrides.py +96 -0
  82. schemathesis/generation/stateful/__init__.py +20 -0
  83. schemathesis/{stateful → generation/stateful}/state_machine.py +68 -81
  84. schemathesis/generation/targets.py +69 -0
  85. schemathesis/graphql/__init__.py +15 -0
  86. schemathesis/graphql/checks.py +115 -0
  87. schemathesis/graphql/loaders.py +131 -0
  88. schemathesis/hooks.py +99 -67
  89. schemathesis/openapi/__init__.py +13 -0
  90. schemathesis/openapi/checks.py +412 -0
  91. schemathesis/openapi/generation/__init__.py +0 -0
  92. schemathesis/openapi/generation/filters.py +63 -0
  93. schemathesis/openapi/loaders.py +178 -0
  94. schemathesis/pytest/__init__.py +5 -0
  95. schemathesis/pytest/control_flow.py +7 -0
  96. schemathesis/pytest/lazy.py +273 -0
  97. schemathesis/pytest/loaders.py +12 -0
  98. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +106 -127
  99. schemathesis/python/__init__.py +0 -0
  100. schemathesis/python/asgi.py +12 -0
  101. schemathesis/python/wsgi.py +12 -0
  102. schemathesis/schemas.py +537 -261
  103. schemathesis/specs/graphql/__init__.py +0 -1
  104. schemathesis/specs/graphql/_cache.py +25 -0
  105. schemathesis/specs/graphql/nodes.py +1 -0
  106. schemathesis/specs/graphql/scalars.py +7 -5
  107. schemathesis/specs/graphql/schemas.py +215 -187
  108. schemathesis/specs/graphql/validation.py +11 -18
  109. schemathesis/specs/openapi/__init__.py +7 -1
  110. schemathesis/specs/openapi/_cache.py +122 -0
  111. schemathesis/specs/openapi/_hypothesis.py +146 -165
  112. schemathesis/specs/openapi/checks.py +565 -67
  113. schemathesis/specs/openapi/converter.py +33 -6
  114. schemathesis/specs/openapi/definitions.py +11 -18
  115. schemathesis/specs/openapi/examples.py +153 -39
  116. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  117. schemathesis/specs/openapi/expressions/context.py +4 -6
  118. schemathesis/specs/openapi/expressions/extractors.py +23 -0
  119. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  120. schemathesis/specs/openapi/expressions/nodes.py +38 -14
  121. schemathesis/specs/openapi/expressions/parser.py +26 -5
  122. schemathesis/specs/openapi/formats.py +45 -0
  123. schemathesis/specs/openapi/links.py +65 -165
  124. schemathesis/specs/openapi/media_types.py +32 -0
  125. schemathesis/specs/openapi/negative/__init__.py +7 -3
  126. schemathesis/specs/openapi/negative/mutations.py +24 -8
  127. schemathesis/specs/openapi/parameters.py +46 -30
  128. schemathesis/specs/openapi/patterns.py +137 -0
  129. schemathesis/specs/openapi/references.py +47 -57
  130. schemathesis/specs/openapi/schemas.py +483 -367
  131. schemathesis/specs/openapi/security.py +25 -7
  132. schemathesis/specs/openapi/serialization.py +11 -6
  133. schemathesis/specs/openapi/stateful/__init__.py +185 -73
  134. schemathesis/specs/openapi/utils.py +6 -1
  135. schemathesis/transport/__init__.py +104 -0
  136. schemathesis/transport/asgi.py +26 -0
  137. schemathesis/transport/prepare.py +99 -0
  138. schemathesis/transport/requests.py +221 -0
  139. schemathesis/{_xml.py → transport/serialization.py} +143 -28
  140. schemathesis/transport/wsgi.py +165 -0
  141. schemathesis-4.0.0a1.dist-info/METADATA +297 -0
  142. schemathesis-4.0.0a1.dist-info/RECORD +152 -0
  143. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/WHEEL +1 -1
  144. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/entry_points.txt +1 -1
  145. schemathesis/_compat.py +0 -74
  146. schemathesis/_dependency_versions.py +0 -17
  147. schemathesis/_hypothesis.py +0 -246
  148. schemathesis/_override.py +0 -49
  149. schemathesis/cli/cassettes.py +0 -375
  150. schemathesis/cli/context.py +0 -55
  151. schemathesis/cli/debug.py +0 -26
  152. schemathesis/cli/handlers.py +0 -16
  153. schemathesis/cli/junitxml.py +0 -43
  154. schemathesis/cli/output/__init__.py +0 -1
  155. schemathesis/cli/output/default.py +0 -765
  156. schemathesis/cli/output/short.py +0 -40
  157. schemathesis/cli/sanitization.py +0 -20
  158. schemathesis/code_samples.py +0 -149
  159. schemathesis/constants.py +0 -55
  160. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  161. schemathesis/contrib/openapi/formats/uuid.py +0 -15
  162. schemathesis/contrib/unique_data.py +0 -41
  163. schemathesis/exceptions.py +0 -560
  164. schemathesis/extra/_aiohttp.py +0 -27
  165. schemathesis/extra/_flask.py +0 -10
  166. schemathesis/extra/_server.py +0 -17
  167. schemathesis/failures.py +0 -209
  168. schemathesis/fixups/__init__.py +0 -36
  169. schemathesis/fixups/fast_api.py +0 -41
  170. schemathesis/fixups/utf8_bom.py +0 -29
  171. schemathesis/graphql.py +0 -4
  172. schemathesis/internal/__init__.py +0 -7
  173. schemathesis/internal/copy.py +0 -13
  174. schemathesis/internal/datetime.py +0 -5
  175. schemathesis/internal/deprecation.py +0 -34
  176. schemathesis/internal/jsonschema.py +0 -35
  177. schemathesis/internal/transformation.py +0 -15
  178. schemathesis/internal/validation.py +0 -34
  179. schemathesis/lazy.py +0 -361
  180. schemathesis/loaders.py +0 -120
  181. schemathesis/models.py +0 -1231
  182. schemathesis/parameters.py +0 -86
  183. schemathesis/runner/__init__.py +0 -555
  184. schemathesis/runner/events.py +0 -309
  185. schemathesis/runner/impl/__init__.py +0 -3
  186. schemathesis/runner/impl/core.py +0 -986
  187. schemathesis/runner/impl/solo.py +0 -90
  188. schemathesis/runner/impl/threadpool.py +0 -400
  189. schemathesis/runner/serialization.py +0 -411
  190. schemathesis/sanitization.py +0 -248
  191. schemathesis/serializers.py +0 -315
  192. schemathesis/service/__init__.py +0 -18
  193. schemathesis/service/auth.py +0 -11
  194. schemathesis/service/ci.py +0 -201
  195. schemathesis/service/client.py +0 -100
  196. schemathesis/service/constants.py +0 -38
  197. schemathesis/service/events.py +0 -57
  198. schemathesis/service/hosts.py +0 -107
  199. schemathesis/service/metadata.py +0 -46
  200. schemathesis/service/models.py +0 -49
  201. schemathesis/service/report.py +0 -255
  202. schemathesis/service/serialization.py +0 -184
  203. schemathesis/service/usage.py +0 -65
  204. schemathesis/specs/graphql/loaders.py +0 -344
  205. schemathesis/specs/openapi/filters.py +0 -49
  206. schemathesis/specs/openapi/loaders.py +0 -667
  207. schemathesis/specs/openapi/stateful/links.py +0 -92
  208. schemathesis/specs/openapi/validation.py +0 -25
  209. schemathesis/stateful/__init__.py +0 -133
  210. schemathesis/targets.py +0 -45
  211. schemathesis/throttling.py +0 -41
  212. schemathesis/transports/__init__.py +0 -5
  213. schemathesis/transports/auth.py +0 -15
  214. schemathesis/transports/headers.py +0 -35
  215. schemathesis/transports/responses.py +0 -52
  216. schemathesis/types.py +0 -35
  217. schemathesis/utils.py +0 -169
  218. schemathesis-3.25.5.dist-info/METADATA +0 -356
  219. schemathesis-3.25.5.dist-info/RECORD +0 -134
  220. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  221. {schemathesis-3.25.5.dist-info → schemathesis-4.0.0a1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,412 @@
1
+ from __future__ import annotations
2
+
3
+ import textwrap
4
+ from dataclasses import dataclass, field
5
+ from typing import TYPE_CHECKING, Any
6
+
7
+ from schemathesis.core.failures import Failure, Severity
8
+ from schemathesis.core.output import OutputConfig, truncate_json
9
+
10
+ if TYPE_CHECKING:
11
+ from jsonschema import ValidationError
12
+
13
+
14
+ @dataclass
15
+ class NegativeDataRejectionConfig:
16
+ # 5xx will pass through
17
+ allowed_statuses: list[str] = field(default_factory=lambda: ["400", "401", "403", "404", "422", "428", "5xx"])
18
+
19
+
20
+ @dataclass
21
+ class PositiveDataAcceptanceConfig:
22
+ allowed_statuses: list[str] = field(default_factory=lambda: ["2xx", "401", "403", "404"])
23
+
24
+
25
+ @dataclass
26
+ class MissingRequiredHeaderConfig:
27
+ allowed_statuses: list[str] = field(default_factory=lambda: ["406"])
28
+
29
+
30
+ class UndefinedStatusCode(Failure):
31
+ """Response has a status code that is not defined in the schema."""
32
+
33
+ __slots__ = (
34
+ "operation",
35
+ "status_code",
36
+ "defined_status_codes",
37
+ "allowed_status_codes",
38
+ "message",
39
+ "title",
40
+ "code",
41
+ "case_id",
42
+ "severity",
43
+ )
44
+
45
+ def __init__(
46
+ self,
47
+ *,
48
+ operation: str,
49
+ status_code: int,
50
+ defined_status_codes: list[str],
51
+ allowed_status_codes: list[int],
52
+ message: str,
53
+ title: str = "Undocumented HTTP status code",
54
+ code: str = "undefined_status_code",
55
+ case_id: str | None = None,
56
+ ) -> None:
57
+ self.operation = operation
58
+ self.status_code = status_code
59
+ self.defined_status_codes = defined_status_codes
60
+ self.allowed_status_codes = allowed_status_codes
61
+ self.message = message
62
+ self.title = title
63
+ self.code = code
64
+ self.case_id = case_id
65
+ self.severity = Severity.MEDIUM
66
+
67
+ @property
68
+ def _unique_key(self) -> str:
69
+ return str(self.status_code)
70
+
71
+
72
+ class MissingHeaders(Failure):
73
+ """Some required headers are missing."""
74
+
75
+ __slots__ = ("operation", "missing_headers", "message", "title", "code", "case_id", "severity")
76
+
77
+ def __init__(
78
+ self,
79
+ *,
80
+ operation: str,
81
+ missing_headers: list[str],
82
+ message: str,
83
+ title: str = "Missing required headers",
84
+ code: str = "missing_headers",
85
+ case_id: str | None = None,
86
+ ) -> None:
87
+ self.operation = operation
88
+ self.missing_headers = missing_headers
89
+ self.message = message
90
+ self.title = title
91
+ self.code = code
92
+ self.case_id = case_id
93
+ self.severity = Severity.MEDIUM
94
+
95
+
96
+ class JsonSchemaError(Failure):
97
+ """Additional information about JSON Schema validation errors."""
98
+
99
+ __slots__ = (
100
+ "operation",
101
+ "validation_message",
102
+ "schema_path",
103
+ "schema",
104
+ "instance_path",
105
+ "instance",
106
+ "message",
107
+ "title",
108
+ "code",
109
+ "case_id",
110
+ "severity",
111
+ )
112
+
113
+ def __init__(
114
+ self,
115
+ *,
116
+ operation: str,
117
+ validation_message: str,
118
+ schema_path: list[str | int],
119
+ schema: dict[str, Any] | bool,
120
+ instance_path: list[str | int],
121
+ instance: None | bool | float | str | list | dict[str, Any],
122
+ message: str,
123
+ title: str = "Response violates schema",
124
+ code: str = "json_schema",
125
+ case_id: str | None = None,
126
+ ) -> None:
127
+ self.operation = operation
128
+ self.validation_message = validation_message
129
+ self.schema_path = schema_path
130
+ self.schema = schema
131
+ self.instance_path = instance_path
132
+ self.instance = instance
133
+ self.message = message
134
+ self.title = title
135
+ self.code = code
136
+ self.case_id = case_id
137
+ self.severity = Severity.HIGH
138
+
139
+ @property
140
+ def _unique_key(self) -> str:
141
+ return "/".join(map(str, self.schema_path))
142
+
143
+ @classmethod
144
+ def from_exception(
145
+ cls,
146
+ *,
147
+ title: str = "Response violates schema",
148
+ operation: str,
149
+ exc: ValidationError,
150
+ output_config: OutputConfig | None = None,
151
+ ) -> JsonSchemaError:
152
+ output_config = OutputConfig.from_parent(output_config, max_lines=20)
153
+ schema = textwrap.indent(truncate_json(exc.schema, config=output_config), prefix=" ")
154
+ value = textwrap.indent(truncate_json(exc.instance, config=output_config), prefix=" ")
155
+ schema_path = list(exc.absolute_schema_path)
156
+ if len(schema_path) > 1:
157
+ # Exclude the last segment, which is already in the schema
158
+ schema_title = "Schema at "
159
+ for segment in schema_path[:-1]:
160
+ schema_title += f"/{segment}"
161
+ else:
162
+ schema_title = "Schema"
163
+ message = f"{exc.message}\n\n{schema_title}:\n\n{schema}\n\nValue:\n\n{value}"
164
+ return cls(
165
+ operation=operation,
166
+ title=title,
167
+ message=message,
168
+ validation_message=exc.message,
169
+ schema_path=schema_path,
170
+ schema=exc.schema,
171
+ instance_path=list(exc.absolute_path),
172
+ instance=exc.instance,
173
+ )
174
+
175
+
176
+ class MissingContentType(Failure):
177
+ """Content type header is missing."""
178
+
179
+ __slots__ = ("operation", "media_types", "message", "title", "code", "case_id", "severity")
180
+
181
+ def __init__(
182
+ self,
183
+ *,
184
+ operation: str,
185
+ media_types: list[str],
186
+ message: str,
187
+ title: str = "Missing Content-Type header",
188
+ code: str = "missing_content_type",
189
+ case_id: str | None = None,
190
+ ) -> None:
191
+ self.operation = operation
192
+ self.media_types = media_types
193
+ self.message = message
194
+ self.title = title
195
+ self.code = code
196
+ self.case_id = case_id
197
+ self.severity = Severity.MEDIUM
198
+
199
+ @property
200
+ def _unique_key(self) -> str:
201
+ return ""
202
+
203
+
204
+ class MalformedMediaType(Failure):
205
+ """Media type name is malformed."""
206
+
207
+ __slots__ = ("operation", "actual", "defined", "message", "title", "code", "case_id", "severity")
208
+
209
+ def __init__(
210
+ self,
211
+ *,
212
+ operation: str,
213
+ actual: str,
214
+ defined: str,
215
+ message: str,
216
+ title: str = "Malformed media type",
217
+ code: str = "malformed_media_type",
218
+ case_id: str | None = None,
219
+ ) -> None:
220
+ self.operation = operation
221
+ self.actual = actual
222
+ self.defined = defined
223
+ self.message = message
224
+ self.title = title
225
+ self.code = code
226
+ self.case_id = case_id
227
+ self.severity = Severity.MEDIUM
228
+
229
+
230
+ class UndefinedContentType(Failure):
231
+ """Response has Content-Type that is not documented in the schema."""
232
+
233
+ __slots__ = (
234
+ "operation",
235
+ "content_type",
236
+ "defined_content_types",
237
+ "message",
238
+ "title",
239
+ "code",
240
+ "case_id",
241
+ "severity",
242
+ )
243
+
244
+ def __init__(
245
+ self,
246
+ *,
247
+ operation: str,
248
+ content_type: str,
249
+ defined_content_types: list[str],
250
+ message: str,
251
+ title: str = "Undocumented Content-Type",
252
+ code: str = "undefined_content_type",
253
+ case_id: str | None = None,
254
+ ) -> None:
255
+ self.operation = operation
256
+ self.content_type = content_type
257
+ self.defined_content_types = defined_content_types
258
+ self.message = message
259
+ self.title = title
260
+ self.code = code
261
+ self.case_id = case_id
262
+ self.severity = Severity.MEDIUM
263
+
264
+ @property
265
+ def _unique_key(self) -> str:
266
+ return self.content_type
267
+
268
+
269
+ class UseAfterFree(Failure):
270
+ """Resource was used after a successful DELETE operation on it."""
271
+
272
+ __slots__ = ("operation", "message", "free", "usage", "title", "code", "case_id", "severity")
273
+
274
+ def __init__(
275
+ self,
276
+ *,
277
+ operation: str,
278
+ message: str,
279
+ free: str,
280
+ usage: str,
281
+ title: str = "Use after free",
282
+ code: str = "use_after_free",
283
+ case_id: str | None = None,
284
+ ) -> None:
285
+ self.operation = operation
286
+ self.message = message
287
+ self.free = free
288
+ self.usage = usage
289
+ self.title = title
290
+ self.code = code
291
+ self.case_id = case_id
292
+ self.severity = Severity.CRITICAL
293
+
294
+ @property
295
+ def _unique_key(self) -> str:
296
+ return ""
297
+
298
+
299
+ class EnsureResourceAvailability(Failure):
300
+ """Resource is not available immediately after creation."""
301
+
302
+ __slots__ = ("operation", "message", "created_with", "not_available_with", "title", "code", "case_id", "severity")
303
+
304
+ def __init__(
305
+ self,
306
+ *,
307
+ operation: str,
308
+ message: str,
309
+ created_with: str,
310
+ not_available_with: str,
311
+ title: str = "Resource is not available after creation",
312
+ code: str = "ensure_resource_availability",
313
+ case_id: str | None = None,
314
+ ) -> None:
315
+ self.operation = operation
316
+ self.message = message
317
+ self.created_with = created_with
318
+ self.not_available_with = not_available_with
319
+ self.title = title
320
+ self.code = code
321
+ self.case_id = case_id
322
+ self.severity = Severity.MEDIUM
323
+
324
+ @property
325
+ def _unique_key(self) -> str:
326
+ return ""
327
+
328
+
329
+ class IgnoredAuth(Failure):
330
+ """The API operation does not check the specified authentication."""
331
+
332
+ __slots__ = ("operation", "message", "title", "code", "case_id", "severity")
333
+
334
+ def __init__(
335
+ self,
336
+ *,
337
+ operation: str,
338
+ message: str,
339
+ title: str = "Authentication declared but not enforced",
340
+ code: str = "ignored_auth",
341
+ case_id: str | None = None,
342
+ ) -> None:
343
+ self.operation = operation
344
+ self.message = message
345
+ self.title = title
346
+ self.code = code
347
+ self.case_id = case_id
348
+ self.severity = Severity.CRITICAL
349
+
350
+ @property
351
+ def _unique_key(self) -> str:
352
+ return ""
353
+
354
+
355
+ class AcceptedNegativeData(Failure):
356
+ """Response with negative data was accepted."""
357
+
358
+ __slots__ = ("operation", "message", "status_code", "allowed_statuses", "title", "code", "case_id", "severity")
359
+
360
+ def __init__(
361
+ self,
362
+ *,
363
+ operation: str,
364
+ message: str,
365
+ status_code: int,
366
+ allowed_statuses: list[str],
367
+ title: str = "Accepted negative data",
368
+ code: str = "accepted_negative_data",
369
+ case_id: str | None = None,
370
+ ) -> None:
371
+ self.operation = operation
372
+ self.message = message
373
+ self.status_code = status_code
374
+ self.allowed_statuses = allowed_statuses
375
+ self.title = title
376
+ self.code = code
377
+ self.case_id = case_id
378
+ self.severity = Severity.MEDIUM
379
+
380
+ @property
381
+ def _unique_key(self) -> str:
382
+ return str(self.status_code)
383
+
384
+
385
+ class RejectedPositiveData(Failure):
386
+ """Response with positive data was rejected."""
387
+
388
+ __slots__ = ("operation", "message", "status_code", "allowed_statuses", "title", "code", "case_id", "severity")
389
+
390
+ def __init__(
391
+ self,
392
+ *,
393
+ operation: str,
394
+ message: str,
395
+ status_code: int,
396
+ allowed_statuses: list[str],
397
+ title: str = "Rejected positive data",
398
+ code: str = "rejected_positive_data",
399
+ case_id: str | None = None,
400
+ ) -> None:
401
+ self.operation = operation
402
+ self.message = message
403
+ self.status_code = status_code
404
+ self.allowed_statuses = allowed_statuses
405
+ self.title = title
406
+ self.code = code
407
+ self.case_id = case_id
408
+ self.severity = Severity.MEDIUM
409
+
410
+ @property
411
+ def _unique_key(self) -> str:
412
+ return str(self.status_code)
File without changes
@@ -0,0 +1,63 @@
1
+ from collections.abc import Mapping
2
+
3
+ from schemathesis.core import NOT_SET
4
+ from schemathesis.core.validation import contains_unicode_surrogate_pair, has_invalid_characters, is_latin_1_encodable
5
+
6
+ __all__ = [
7
+ "is_valid_path",
8
+ "is_valid_header",
9
+ "is_valid_urlencoded",
10
+ "is_valid_query",
11
+ ]
12
+
13
+
14
+ def is_valid_path(parameters: dict[str, object]) -> bool:
15
+ """Empty strings ("") are excluded from path by urllib3.
16
+
17
+ A path containing to "/" or "%2F" will lead to ambiguous path resolution in
18
+ many frameworks and libraries, such behaviour have been observed in both
19
+ WSGI and ASGI applications.
20
+
21
+ In this case one variable in the path template will be empty, which will lead to 404 in most of the cases.
22
+ Because of it this case doesn't bring much value and might lead to false positives results of Schemathesis runs.
23
+ """
24
+ return not any(
25
+ (
26
+ value in ("/", "")
27
+ or contains_unicode_surrogate_pair(value)
28
+ or isinstance(value, str)
29
+ and ("/" in value or "}" in value or "{" in value)
30
+ )
31
+ for value in parameters.values()
32
+ )
33
+
34
+
35
+ def is_valid_header(headers: dict[str, object]) -> bool:
36
+ for name, value in headers.items():
37
+ if not is_latin_1_encodable(value):
38
+ return False
39
+ if has_invalid_characters(name, value):
40
+ return False
41
+ return True
42
+
43
+
44
+ def is_valid_query(query: dict[str, object]) -> bool:
45
+ for name, value in query.items():
46
+ if contains_unicode_surrogate_pair(name) or contains_unicode_surrogate_pair(value):
47
+ return False
48
+ return True
49
+
50
+
51
+ def is_valid_urlencoded(data: object) -> bool:
52
+ # TODO: write a test that will check if `requests` can send it
53
+ if data is NOT_SET or isinstance(data, Mapping):
54
+ return True
55
+
56
+ if hasattr(data, "__iter__"):
57
+ try:
58
+ for _, _ in data:
59
+ pass
60
+ return True
61
+ except (TypeError, ValueError):
62
+ return False
63
+ return False
@@ -0,0 +1,178 @@
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ import json
5
+ import re
6
+ from os import PathLike
7
+ from pathlib import Path
8
+ from typing import IO, TYPE_CHECKING, Any, Mapping
9
+
10
+ from schemathesis.core import media_types
11
+ from schemathesis.core.deserialization import deserialize_yaml
12
+ from schemathesis.core.errors import LoaderError, LoaderErrorKind
13
+ from schemathesis.core.loaders import load_from_url, prepare_request_kwargs, raise_for_status, require_relative_url
14
+ from schemathesis.hooks import HookContext, dispatch
15
+ from schemathesis.python import asgi, wsgi
16
+
17
+ if TYPE_CHECKING:
18
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
19
+
20
+
21
+ def from_asgi(path: str, app: Any, **kwargs: Any) -> BaseOpenAPISchema:
22
+ require_relative_url(path)
23
+ client = asgi.get_client(app)
24
+ response = load_from_url(client.get, url=path, **kwargs)
25
+ content_type = detect_content_type(headers=response.headers, path=path)
26
+ schema = load_content(response.text, content_type)
27
+ return from_dict(schema=schema).configure(app=app, location=path)
28
+
29
+
30
+ def from_wsgi(path: str, app: Any, **kwargs: Any) -> BaseOpenAPISchema:
31
+ require_relative_url(path)
32
+ prepare_request_kwargs(kwargs)
33
+ client = wsgi.get_client(app)
34
+ response = client.get(path=path, **kwargs)
35
+ raise_for_status(response)
36
+ content_type = detect_content_type(headers=response.headers, path=path)
37
+ schema = load_content(response.text, content_type)
38
+ return from_dict(schema=schema).configure(app=app, location=path)
39
+
40
+
41
+ def from_url(url: str, *, wait_for_schema: float | None = None, **kwargs: Any) -> BaseOpenAPISchema:
42
+ """Load from URL."""
43
+ import requests
44
+
45
+ response = load_from_url(requests.get, url=url, wait_for_schema=wait_for_schema, **kwargs)
46
+ content_type = detect_content_type(headers=response.headers, path=url)
47
+ schema = load_content(response.text, content_type)
48
+ return from_dict(schema=schema).configure(location=url)
49
+
50
+
51
+ def from_path(path: PathLike | str, *, encoding: str = "utf-8") -> BaseOpenAPISchema:
52
+ """Load from a filesystem path."""
53
+ with open(path, encoding=encoding) as file:
54
+ content_type = detect_content_type(headers=None, path=str(path))
55
+ schema = load_content(file.read(), content_type)
56
+ return from_dict(schema=schema).configure(location=Path(path).absolute().as_uri())
57
+
58
+
59
+ def from_file(file: IO[str] | str) -> BaseOpenAPISchema:
60
+ """Load from file-like object or string."""
61
+ if isinstance(file, str):
62
+ data = file
63
+ else:
64
+ data = file.read()
65
+ try:
66
+ schema = json.loads(data)
67
+ except json.JSONDecodeError:
68
+ schema = _load_yaml(data)
69
+ return from_dict(schema)
70
+
71
+
72
+ def from_dict(schema: dict[str, Any]) -> BaseOpenAPISchema:
73
+ """Base loader that others build upon."""
74
+ from schemathesis.specs.openapi.schemas import OpenApi30, SwaggerV20
75
+
76
+ if not isinstance(schema, dict):
77
+ raise LoaderError(LoaderErrorKind.OPEN_API_INVALID_SCHEMA, SCHEMA_INVALID_ERROR)
78
+ hook_context = HookContext()
79
+ dispatch("before_load_schema", hook_context, schema)
80
+
81
+ if "swagger" in schema:
82
+ instance = SwaggerV20(schema)
83
+ elif "openapi" in schema:
84
+ version = schema["openapi"]
85
+ if not OPENAPI_VERSION_RE.match(version):
86
+ raise LoaderError(
87
+ LoaderErrorKind.OPEN_API_UNSUPPORTED_VERSION,
88
+ f"The provided schema uses Open API {version}, which is currently not supported.",
89
+ )
90
+ instance = OpenApi30(schema)
91
+ else:
92
+ raise LoaderError(
93
+ LoaderErrorKind.OPEN_API_UNSPECIFIED_VERSION,
94
+ "Unable to determine the Open API version as it's not specified in the document.",
95
+ )
96
+ dispatch("after_load_schema", hook_context, instance)
97
+ return instance
98
+
99
+
100
+ class ContentType(enum.Enum):
101
+ """Known content types for schema files."""
102
+
103
+ JSON = enum.auto()
104
+ YAML = enum.auto()
105
+ UNKNOWN = enum.auto()
106
+
107
+
108
+ def detect_content_type(*, headers: Mapping[str, str] | None = None, path: str | None = None) -> ContentType:
109
+ """Detect content type from various sources."""
110
+ if headers is not None and (content_type := _detect_from_headers(headers)) != ContentType.UNKNOWN:
111
+ return content_type
112
+ if path is not None and (content_type := _detect_from_path(path)) != ContentType.UNKNOWN:
113
+ return content_type
114
+ return ContentType.UNKNOWN
115
+
116
+
117
+ def _detect_from_headers(headers: Mapping[str, str]) -> ContentType:
118
+ """Detect content type from HTTP headers."""
119
+ content_type = headers.get("Content-Type", "").lower()
120
+ try:
121
+ if content_type and media_types.is_json(content_type):
122
+ return ContentType.JSON
123
+ if content_type and media_types.is_yaml(content_type):
124
+ return ContentType.YAML
125
+ except ValueError:
126
+ pass
127
+ return ContentType.UNKNOWN
128
+
129
+
130
+ def _detect_from_path(path: str) -> ContentType:
131
+ """Detect content type from file path."""
132
+ suffix = Path(path).suffix.lower()
133
+ if suffix == ".json":
134
+ return ContentType.JSON
135
+ if suffix in (".yaml", ".yml"):
136
+ return ContentType.YAML
137
+ return ContentType.UNKNOWN
138
+
139
+
140
+ def load_content(content: str, content_type: ContentType) -> dict[str, Any]:
141
+ """Load content using appropriate parser."""
142
+ if content_type == ContentType.JSON:
143
+ return _load_json(content)
144
+ if content_type == ContentType.YAML:
145
+ return _load_yaml(content)
146
+ # If type is unknown, try JSON first, then YAML
147
+ try:
148
+ return _load_json(content)
149
+ except json.JSONDecodeError:
150
+ return _load_yaml(content)
151
+
152
+
153
+ def _load_json(content: str) -> dict[str, Any]:
154
+ try:
155
+ return json.loads(content)
156
+ except json.JSONDecodeError as exc:
157
+ raise LoaderError(
158
+ LoaderErrorKind.SYNTAX_ERROR,
159
+ SCHEMA_SYNTAX_ERROR,
160
+ extras=[entry for entry in str(exc).splitlines() if entry],
161
+ ) from exc
162
+
163
+
164
+ def _load_yaml(content: str) -> dict[str, Any]:
165
+ import yaml
166
+
167
+ try:
168
+ return deserialize_yaml(content)
169
+ except yaml.YAMLError as exc:
170
+ kind = LoaderErrorKind.SYNTAX_ERROR
171
+ message = SCHEMA_SYNTAX_ERROR
172
+ extras = [entry for entry in str(exc).splitlines() if entry]
173
+ raise LoaderError(kind, message, extras=extras) from exc
174
+
175
+
176
+ SCHEMA_INVALID_ERROR = "The provided API schema does not appear to be a valid OpenAPI schema"
177
+ SCHEMA_SYNTAX_ERROR = "API schema does not appear syntactically valid"
178
+ OPENAPI_VERSION_RE = re.compile(r"^3\.[01]\.[0-9](-.+)?$")
@@ -0,0 +1,5 @@
1
+ from schemathesis.pytest.loaders import from_fixture
2
+
3
+ __all__ = [
4
+ "from_fixture",
5
+ ]
@@ -0,0 +1,7 @@
1
+ from typing import NoReturn
2
+
3
+ import pytest
4
+
5
+
6
+ def fail_on_no_matches(node_id: str) -> NoReturn: # type: ignore
7
+ pytest.fail(f"Test function {node_id} does not match any API operations and therefore has no effect")