schemathesis 3.39.7__py3-none-any.whl → 4.0.0a2__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 (229) hide show
  1. schemathesis/__init__.py +27 -65
  2. schemathesis/auths.py +26 -68
  3. schemathesis/checks.py +130 -60
  4. schemathesis/cli/__init__.py +5 -2105
  5. schemathesis/cli/commands/__init__.py +37 -0
  6. schemathesis/cli/commands/run/__init__.py +662 -0
  7. schemathesis/cli/commands/run/checks.py +80 -0
  8. schemathesis/cli/commands/run/context.py +117 -0
  9. schemathesis/cli/commands/run/events.py +30 -0
  10. schemathesis/cli/commands/run/executor.py +141 -0
  11. schemathesis/cli/commands/run/filters.py +202 -0
  12. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  13. schemathesis/cli/commands/run/handlers/base.py +18 -0
  14. schemathesis/cli/{cassettes.py → commands/run/handlers/cassettes.py} +178 -247
  15. schemathesis/cli/commands/run/handlers/junitxml.py +54 -0
  16. schemathesis/cli/commands/run/handlers/output.py +1368 -0
  17. schemathesis/cli/commands/run/hypothesis.py +105 -0
  18. schemathesis/cli/commands/run/loaders.py +129 -0
  19. schemathesis/cli/{callbacks.py → commands/run/validation.py} +59 -175
  20. schemathesis/cli/constants.py +5 -58
  21. schemathesis/cli/core.py +17 -0
  22. schemathesis/cli/ext/fs.py +14 -0
  23. schemathesis/cli/ext/groups.py +55 -0
  24. schemathesis/cli/{options.py → ext/options.py} +37 -16
  25. schemathesis/cli/hooks.py +36 -0
  26. schemathesis/contrib/__init__.py +1 -3
  27. schemathesis/contrib/openapi/__init__.py +1 -3
  28. schemathesis/contrib/openapi/fill_missing_examples.py +3 -7
  29. schemathesis/core/__init__.py +58 -0
  30. schemathesis/core/compat.py +25 -0
  31. schemathesis/core/control.py +2 -0
  32. schemathesis/core/curl.py +58 -0
  33. schemathesis/core/deserialization.py +65 -0
  34. schemathesis/core/errors.py +370 -0
  35. schemathesis/core/failures.py +315 -0
  36. schemathesis/core/fs.py +19 -0
  37. schemathesis/core/loaders.py +104 -0
  38. schemathesis/core/marks.py +66 -0
  39. schemathesis/{transports/content_types.py → core/media_types.py} +14 -12
  40. schemathesis/{internal/output.py → core/output/__init__.py} +1 -0
  41. schemathesis/core/output/sanitization.py +197 -0
  42. schemathesis/{throttling.py → core/rate_limit.py} +16 -17
  43. schemathesis/core/registries.py +31 -0
  44. schemathesis/core/transforms.py +113 -0
  45. schemathesis/core/transport.py +108 -0
  46. schemathesis/core/validation.py +38 -0
  47. schemathesis/core/version.py +7 -0
  48. schemathesis/engine/__init__.py +30 -0
  49. schemathesis/engine/config.py +59 -0
  50. schemathesis/engine/context.py +119 -0
  51. schemathesis/engine/control.py +36 -0
  52. schemathesis/engine/core.py +157 -0
  53. schemathesis/engine/errors.py +394 -0
  54. schemathesis/engine/events.py +243 -0
  55. schemathesis/engine/phases/__init__.py +66 -0
  56. schemathesis/{runner → engine/phases}/probes.py +49 -68
  57. schemathesis/engine/phases/stateful/__init__.py +66 -0
  58. schemathesis/engine/phases/stateful/_executor.py +301 -0
  59. schemathesis/engine/phases/stateful/context.py +85 -0
  60. schemathesis/engine/phases/unit/__init__.py +175 -0
  61. schemathesis/engine/phases/unit/_executor.py +322 -0
  62. schemathesis/engine/phases/unit/_pool.py +74 -0
  63. schemathesis/engine/recorder.py +246 -0
  64. schemathesis/errors.py +31 -0
  65. schemathesis/experimental/__init__.py +9 -40
  66. schemathesis/filters.py +7 -95
  67. schemathesis/generation/__init__.py +3 -3
  68. schemathesis/generation/case.py +190 -0
  69. schemathesis/generation/coverage.py +22 -22
  70. schemathesis/{_patches.py → generation/hypothesis/__init__.py} +15 -6
  71. schemathesis/generation/hypothesis/builder.py +585 -0
  72. schemathesis/generation/{_hypothesis.py → hypothesis/examples.py} +2 -11
  73. schemathesis/generation/hypothesis/given.py +66 -0
  74. schemathesis/generation/hypothesis/reporting.py +14 -0
  75. schemathesis/generation/hypothesis/strategies.py +16 -0
  76. schemathesis/generation/meta.py +115 -0
  77. schemathesis/generation/modes.py +28 -0
  78. schemathesis/generation/overrides.py +96 -0
  79. schemathesis/generation/stateful/__init__.py +20 -0
  80. schemathesis/{stateful → generation/stateful}/state_machine.py +84 -109
  81. schemathesis/generation/targets.py +69 -0
  82. schemathesis/graphql/__init__.py +15 -0
  83. schemathesis/graphql/checks.py +109 -0
  84. schemathesis/graphql/loaders.py +131 -0
  85. schemathesis/hooks.py +17 -62
  86. schemathesis/openapi/__init__.py +13 -0
  87. schemathesis/openapi/checks.py +387 -0
  88. schemathesis/openapi/generation/__init__.py +0 -0
  89. schemathesis/openapi/generation/filters.py +63 -0
  90. schemathesis/openapi/loaders.py +178 -0
  91. schemathesis/pytest/__init__.py +5 -0
  92. schemathesis/pytest/control_flow.py +7 -0
  93. schemathesis/pytest/lazy.py +273 -0
  94. schemathesis/pytest/loaders.py +12 -0
  95. schemathesis/{extra/pytest_plugin.py → pytest/plugin.py} +94 -107
  96. schemathesis/python/__init__.py +0 -0
  97. schemathesis/python/asgi.py +12 -0
  98. schemathesis/python/wsgi.py +12 -0
  99. schemathesis/schemas.py +456 -228
  100. schemathesis/specs/graphql/__init__.py +0 -1
  101. schemathesis/specs/graphql/_cache.py +1 -2
  102. schemathesis/specs/graphql/scalars.py +5 -3
  103. schemathesis/specs/graphql/schemas.py +122 -123
  104. schemathesis/specs/graphql/validation.py +11 -17
  105. schemathesis/specs/openapi/__init__.py +6 -1
  106. schemathesis/specs/openapi/_cache.py +1 -2
  107. schemathesis/specs/openapi/_hypothesis.py +97 -134
  108. schemathesis/specs/openapi/checks.py +238 -219
  109. schemathesis/specs/openapi/converter.py +4 -4
  110. schemathesis/specs/openapi/definitions.py +1 -1
  111. schemathesis/specs/openapi/examples.py +22 -20
  112. schemathesis/specs/openapi/expressions/__init__.py +11 -15
  113. schemathesis/specs/openapi/expressions/extractors.py +1 -4
  114. schemathesis/specs/openapi/expressions/nodes.py +33 -32
  115. schemathesis/specs/openapi/formats.py +3 -2
  116. schemathesis/specs/openapi/links.py +123 -299
  117. schemathesis/specs/openapi/media_types.py +10 -12
  118. schemathesis/specs/openapi/negative/__init__.py +2 -1
  119. schemathesis/specs/openapi/negative/mutations.py +3 -2
  120. schemathesis/specs/openapi/parameters.py +8 -6
  121. schemathesis/specs/openapi/patterns.py +1 -1
  122. schemathesis/specs/openapi/references.py +11 -51
  123. schemathesis/specs/openapi/schemas.py +177 -191
  124. schemathesis/specs/openapi/security.py +1 -1
  125. schemathesis/specs/openapi/serialization.py +10 -6
  126. schemathesis/specs/openapi/stateful/__init__.py +97 -91
  127. schemathesis/transport/__init__.py +104 -0
  128. schemathesis/transport/asgi.py +26 -0
  129. schemathesis/transport/prepare.py +99 -0
  130. schemathesis/transport/requests.py +221 -0
  131. schemathesis/{_xml.py → transport/serialization.py} +69 -7
  132. schemathesis/transport/wsgi.py +165 -0
  133. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/METADATA +18 -14
  134. schemathesis-4.0.0a2.dist-info/RECORD +151 -0
  135. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/entry_points.txt +1 -1
  136. schemathesis/_compat.py +0 -74
  137. schemathesis/_dependency_versions.py +0 -19
  138. schemathesis/_hypothesis.py +0 -559
  139. schemathesis/_override.py +0 -50
  140. schemathesis/_rate_limiter.py +0 -7
  141. schemathesis/cli/context.py +0 -75
  142. schemathesis/cli/debug.py +0 -27
  143. schemathesis/cli/handlers.py +0 -19
  144. schemathesis/cli/junitxml.py +0 -124
  145. schemathesis/cli/output/__init__.py +0 -1
  146. schemathesis/cli/output/default.py +0 -936
  147. schemathesis/cli/output/short.py +0 -59
  148. schemathesis/cli/reporting.py +0 -79
  149. schemathesis/cli/sanitization.py +0 -26
  150. schemathesis/code_samples.py +0 -151
  151. schemathesis/constants.py +0 -56
  152. schemathesis/contrib/openapi/formats/__init__.py +0 -9
  153. schemathesis/contrib/openapi/formats/uuid.py +0 -16
  154. schemathesis/contrib/unique_data.py +0 -41
  155. schemathesis/exceptions.py +0 -571
  156. schemathesis/extra/_aiohttp.py +0 -28
  157. schemathesis/extra/_flask.py +0 -13
  158. schemathesis/extra/_server.py +0 -18
  159. schemathesis/failures.py +0 -277
  160. schemathesis/fixups/__init__.py +0 -37
  161. schemathesis/fixups/fast_api.py +0 -41
  162. schemathesis/fixups/utf8_bom.py +0 -28
  163. schemathesis/generation/_methods.py +0 -44
  164. schemathesis/graphql.py +0 -3
  165. schemathesis/internal/__init__.py +0 -7
  166. schemathesis/internal/checks.py +0 -84
  167. schemathesis/internal/copy.py +0 -32
  168. schemathesis/internal/datetime.py +0 -5
  169. schemathesis/internal/deprecation.py +0 -38
  170. schemathesis/internal/diff.py +0 -15
  171. schemathesis/internal/extensions.py +0 -27
  172. schemathesis/internal/jsonschema.py +0 -36
  173. schemathesis/internal/transformation.py +0 -26
  174. schemathesis/internal/validation.py +0 -34
  175. schemathesis/lazy.py +0 -474
  176. schemathesis/loaders.py +0 -122
  177. schemathesis/models.py +0 -1341
  178. schemathesis/parameters.py +0 -90
  179. schemathesis/runner/__init__.py +0 -605
  180. schemathesis/runner/events.py +0 -389
  181. schemathesis/runner/impl/__init__.py +0 -3
  182. schemathesis/runner/impl/context.py +0 -104
  183. schemathesis/runner/impl/core.py +0 -1246
  184. schemathesis/runner/impl/solo.py +0 -80
  185. schemathesis/runner/impl/threadpool.py +0 -391
  186. schemathesis/runner/serialization.py +0 -544
  187. schemathesis/sanitization.py +0 -252
  188. schemathesis/serializers.py +0 -328
  189. schemathesis/service/__init__.py +0 -18
  190. schemathesis/service/auth.py +0 -11
  191. schemathesis/service/ci.py +0 -202
  192. schemathesis/service/client.py +0 -133
  193. schemathesis/service/constants.py +0 -38
  194. schemathesis/service/events.py +0 -61
  195. schemathesis/service/extensions.py +0 -224
  196. schemathesis/service/hosts.py +0 -111
  197. schemathesis/service/metadata.py +0 -71
  198. schemathesis/service/models.py +0 -258
  199. schemathesis/service/report.py +0 -255
  200. schemathesis/service/serialization.py +0 -173
  201. schemathesis/service/usage.py +0 -66
  202. schemathesis/specs/graphql/loaders.py +0 -364
  203. schemathesis/specs/openapi/expressions/context.py +0 -16
  204. schemathesis/specs/openapi/loaders.py +0 -708
  205. schemathesis/specs/openapi/stateful/statistic.py +0 -198
  206. schemathesis/specs/openapi/stateful/types.py +0 -14
  207. schemathesis/specs/openapi/validation.py +0 -26
  208. schemathesis/stateful/__init__.py +0 -147
  209. schemathesis/stateful/config.py +0 -97
  210. schemathesis/stateful/context.py +0 -135
  211. schemathesis/stateful/events.py +0 -274
  212. schemathesis/stateful/runner.py +0 -309
  213. schemathesis/stateful/sink.py +0 -68
  214. schemathesis/stateful/statistic.py +0 -22
  215. schemathesis/stateful/validation.py +0 -100
  216. schemathesis/targets.py +0 -77
  217. schemathesis/transports/__init__.py +0 -359
  218. schemathesis/transports/asgi.py +0 -7
  219. schemathesis/transports/auth.py +0 -38
  220. schemathesis/transports/headers.py +0 -36
  221. schemathesis/transports/responses.py +0 -57
  222. schemathesis/types.py +0 -44
  223. schemathesis/utils.py +0 -164
  224. schemathesis-3.39.7.dist-info/RECORD +0 -160
  225. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
  226. /schemathesis/{_lazy_import.py → core/lazy_import.py} +0 -0
  227. /schemathesis/{internal → core}/result.py +0 -0
  228. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/WHEEL +0 -0
  229. {schemathesis-3.39.7.dist-info → schemathesis-4.0.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -1,38 +1,48 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import enum
4
+ import http.client
4
5
  from dataclasses import dataclass
5
6
  from http.cookies import SimpleCookie
6
7
  from typing import TYPE_CHECKING, Any, Dict, Generator, NoReturn, cast
7
8
  from urllib.parse import parse_qs, urlparse
8
9
 
9
- from ... import failures
10
- from ...exceptions import (
11
- get_ensure_resource_availability_error,
12
- get_headers_error,
13
- get_ignored_auth_error,
14
- get_malformed_media_type_error,
15
- get_missing_content_type_error,
16
- get_negative_rejection_error,
17
- get_positive_acceptance_error,
18
- get_response_type_error,
19
- get_schema_validation_error,
20
- get_status_code_error,
21
- get_use_after_free_error,
10
+ import schemathesis
11
+ from schemathesis.checks import CheckContext
12
+ from schemathesis.core import media_types, string_to_boolean
13
+ from schemathesis.core.failures import Failure
14
+ from schemathesis.core.transport import Response
15
+ from schemathesis.generation.case import Case
16
+ from schemathesis.generation.meta import ComponentKind, CoveragePhaseData
17
+ from schemathesis.openapi.checks import (
18
+ AcceptedNegativeData,
19
+ EnsureResourceAvailability,
20
+ IgnoredAuth,
21
+ JsonSchemaError,
22
+ MalformedMediaType,
23
+ MissingContentType,
24
+ MissingHeaders,
25
+ MissingRequiredHeaderConfig,
26
+ NegativeDataRejectionConfig,
27
+ PositiveDataAcceptanceConfig,
28
+ RejectedPositiveData,
29
+ UndefinedContentType,
30
+ UndefinedStatusCode,
31
+ UseAfterFree,
22
32
  )
23
- from ...internal.transformation import convert_boolean_string
24
- from ...transports.content_types import parse_content_type
33
+ from schemathesis.specs.openapi.constants import LOCATION_TO_CONTAINER
34
+ from schemathesis.transport.prepare import prepare_path
35
+
25
36
  from .utils import expand_status_code, expand_status_codes
26
37
 
27
38
  if TYPE_CHECKING:
28
39
  from requests import PreparedRequest
29
40
 
30
- from ...internal.checks import CheckContext
31
- from ...models import APIOperation, Case
32
- from ...transports.responses import GenericResponse
41
+ from ...schemas import APIOperation
33
42
 
34
43
 
35
- def status_code_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
44
+ @schemathesis.check
45
+ def status_code_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
36
46
  from .schemas import BaseOpenAPISchema
37
47
 
38
48
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -45,15 +55,12 @@ def status_code_conformance(ctx: CheckContext, response: GenericResponse, case:
45
55
  if response.status_code not in allowed_status_codes:
46
56
  defined_status_codes = list(map(str, responses))
47
57
  responses_list = ", ".join(defined_status_codes)
48
- exc_class = get_status_code_error(case.operation.verbose_name, response.status_code)
49
- raise exc_class(
50
- failures.UndefinedStatusCode.title,
51
- context=failures.UndefinedStatusCode(
52
- message=f"Received: {response.status_code}\nDocumented: {responses_list}",
53
- status_code=response.status_code,
54
- defined_status_codes=defined_status_codes,
55
- allowed_status_codes=allowed_status_codes,
56
- ),
58
+ raise UndefinedStatusCode(
59
+ operation=case.operation.label,
60
+ status_code=response.status_code,
61
+ defined_status_codes=defined_status_codes,
62
+ allowed_status_codes=allowed_status_codes,
63
+ message=f"Received: {response.status_code}\nDocumented: {responses_list}",
57
64
  )
58
65
  return None # explicitly return None for mypy
59
66
 
@@ -63,7 +70,8 @@ def _expand_responses(responses: dict[str | int, Any]) -> Generator[int, None, N
63
70
  yield from expand_status_code(code)
64
71
 
65
72
 
66
- def content_type_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
73
+ @schemathesis.check
74
+ def content_type_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
67
75
  from .schemas import BaseOpenAPISchema
68
76
 
69
77
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -71,25 +79,24 @@ def content_type_conformance(ctx: CheckContext, response: GenericResponse, case:
71
79
  documented_content_types = case.operation.schema.get_content_types(case.operation, response)
72
80
  if not documented_content_types:
73
81
  return None
74
- content_type = response.headers.get("Content-Type")
75
- if not content_type:
76
- formatted_content_types = [f"\n- `{content_type}`" for content_type in documented_content_types]
77
- raise get_missing_content_type_error(case.operation.verbose_name)(
78
- failures.MissingContentType.title,
79
- context=failures.MissingContentType(
80
- message=f"The following media types are documented in the schema:{''.join(formatted_content_types)}",
81
- media_types=documented_content_types,
82
- ),
82
+ content_types = response.headers.get("content-type")
83
+ if not content_types:
84
+ all_media_types = [f"\n- `{content_type}`" for content_type in documented_content_types]
85
+ raise MissingContentType(
86
+ operation=case.operation.label,
87
+ message=f"The following media types are documented in the schema:{''.join(all_media_types)}",
88
+ media_types=documented_content_types,
83
89
  )
90
+ content_type = content_types[0]
84
91
  for option in documented_content_types:
85
92
  try:
86
- expected_main, expected_sub = parse_content_type(option)
87
- except ValueError as exc:
88
- _reraise_malformed_media_type(case, exc, "Schema", option, option)
93
+ expected_main, expected_sub = media_types.parse(option)
94
+ except ValueError:
95
+ _reraise_malformed_media_type(case, "Schema", option, option)
89
96
  try:
90
- received_main, received_sub = parse_content_type(content_type)
91
- except ValueError as exc:
92
- _reraise_malformed_media_type(case, exc, "Response", content_type, option)
97
+ received_main, received_sub = media_types.parse(content_type)
98
+ except ValueError:
99
+ _reraise_malformed_media_type(case, "Response", content_type, option)
93
100
  if (
94
101
  (expected_main == "*" and expected_sub == "*")
95
102
  or (expected_main == received_main and expected_sub == "*")
@@ -97,28 +104,25 @@ def content_type_conformance(ctx: CheckContext, response: GenericResponse, case:
97
104
  or (expected_main == received_main and expected_sub == received_sub)
98
105
  ):
99
106
  return None
100
- exc_class = get_response_type_error(
101
- case.operation.verbose_name, f"{expected_main}_{expected_sub}", f"{received_main}_{received_sub}"
102
- )
103
- raise exc_class(
104
- failures.UndefinedContentType.title,
105
- context=failures.UndefinedContentType(
106
- message=f"Received: {content_type}\nDocumented: {', '.join(documented_content_types)}",
107
- content_type=content_type,
108
- defined_content_types=documented_content_types,
109
- ),
107
+ raise UndefinedContentType(
108
+ operation=case.operation.label,
109
+ message=f"Received: {content_type}\nDocumented: {', '.join(documented_content_types)}",
110
+ content_type=content_type,
111
+ defined_content_types=documented_content_types,
110
112
  )
111
113
 
112
114
 
113
- def _reraise_malformed_media_type(case: Case, exc: ValueError, location: str, actual: str, defined: str) -> NoReturn:
114
- message = f"Media type for {location} is incorrect\n\nReceived: {actual}\nDocumented: {defined}"
115
- raise get_malformed_media_type_error(case.operation.verbose_name, message)(
116
- failures.MalformedMediaType.title,
117
- context=failures.MalformedMediaType(message=message, actual=actual, defined=defined),
118
- ) from exc
115
+ def _reraise_malformed_media_type(case: Case, location: str, actual: str, defined: str) -> NoReturn:
116
+ raise MalformedMediaType(
117
+ operation=case.operation.label,
118
+ message=f"Media type for {location} is incorrect\n\nReceived: {actual}\nDocumented: {defined}",
119
+ actual=actual,
120
+ defined=defined,
121
+ )
119
122
 
120
123
 
121
- def response_headers_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
124
+ @schemathesis.check
125
+ def response_headers_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
122
126
  import jsonschema
123
127
 
124
128
  from .parameters import OpenAPI20Parameter, OpenAPI30Parameter
@@ -136,23 +140,17 @@ def response_headers_conformance(ctx: CheckContext, response: GenericResponse, c
136
140
  missing_headers = [
137
141
  header
138
142
  for header, definition in defined_headers.items()
139
- if header not in response.headers and definition.get(case.operation.schema.header_required_field, False)
143
+ if header.lower() not in response.headers and definition.get(case.operation.schema.header_required_field, False)
140
144
  ]
141
- errors = []
145
+ errors: list[Failure] = []
142
146
  if missing_headers:
143
147
  formatted_headers = [f"\n- `{header}`" for header in missing_headers]
144
148
  message = f"The following required headers are missing from the response:{''.join(formatted_headers)}"
145
- exc_class = get_headers_error(case.operation.verbose_name, message)
146
- try:
147
- raise exc_class(
148
- failures.MissingHeaders.title,
149
- context=failures.MissingHeaders(message=message, missing_headers=missing_headers),
150
- )
151
- except Exception as exc:
152
- errors.append(exc)
149
+ errors.append(MissingHeaders(operation=case.operation.label, message=message, missing_headers=missing_headers))
153
150
  for name, definition in defined_headers.items():
154
- value = response.headers.get(name)
155
- if value is not None:
151
+ values = response.headers.get(name.lower())
152
+ if values is not None:
153
+ value = values[0]
156
154
  with case.operation.schema._validating_response(scopes) as resolver:
157
155
  if "$ref" in definition:
158
156
  _, definition = resolver.resolve(definition["$ref"])
@@ -173,14 +171,14 @@ def response_headers_conformance(ctx: CheckContext, response: GenericResponse, c
173
171
  format_checker=jsonschema.Draft202012Validator.FORMAT_CHECKER,
174
172
  )
175
173
  except jsonschema.ValidationError as exc:
176
- exc_class = get_schema_validation_error(case.operation.verbose_name, exc)
177
- error_ctx = failures.ValidationErrorContext.from_exception(
178
- exc, output_config=case.operation.schema.output_config
174
+ errors.append(
175
+ JsonSchemaError.from_exception(
176
+ title="Response header does not conform to the schema",
177
+ operation=case.operation.label,
178
+ exc=exc,
179
+ output_config=case.operation.schema.output_config,
180
+ )
179
181
  )
180
- try:
181
- raise exc_class("Response header does not conform to the schema", context=error_ctx) from exc
182
- except Exception as exc:
183
- errors.append(exc)
184
182
  return _maybe_raise_one_or_more(errors) # type: ignore[func-returns-value]
185
183
 
186
184
 
@@ -202,11 +200,12 @@ def _coerce_header_value(value: str, schema: dict[str, Any]) -> str | int | floa
202
200
  if schema_type == "null" and value.lower() == "null":
203
201
  return None
204
202
  if schema_type == "boolean":
205
- return convert_boolean_string(value)
203
+ return string_to_boolean(value)
206
204
  return value
207
205
 
208
206
 
209
- def response_schema_conformance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
207
+ @schemathesis.check
208
+ def response_schema_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
210
209
  from .schemas import BaseOpenAPISchema
211
210
 
212
211
  if not isinstance(case.operation.schema, BaseOpenAPISchema):
@@ -214,88 +213,85 @@ def response_schema_conformance(ctx: CheckContext, response: GenericResponse, ca
214
213
  return case.operation.validate_response(response)
215
214
 
216
215
 
217
- def negative_data_rejection(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
216
+ @schemathesis.check
217
+ def negative_data_rejection(ctx: CheckContext, response: Response, case: Case) -> bool | None:
218
218
  from .schemas import BaseOpenAPISchema
219
219
 
220
- if not isinstance(case.operation.schema, BaseOpenAPISchema):
220
+ if not isinstance(case.operation.schema, BaseOpenAPISchema) or case.meta is None:
221
221
  return True
222
222
 
223
- config = ctx.config.negative_data_rejection
223
+ config = ctx.config.get(negative_data_rejection, NegativeDataRejectionConfig())
224
224
  allowed_statuses = expand_status_codes(config.allowed_statuses or [])
225
225
 
226
226
  if (
227
- case.data_generation_method
228
- and case.data_generation_method.is_negative
227
+ case.meta.generation.mode.is_negative
229
228
  and response.status_code not in allowed_statuses
230
229
  and not has_only_additional_properties_in_non_body_parameters(case)
231
230
  ):
232
- message = f"Allowed statuses: {', '.join(config.allowed_statuses)}"
233
- exc_class = get_negative_rejection_error(case.operation.verbose_name, response.status_code)
234
- raise exc_class(
235
- failures.AcceptedNegativeData.title,
236
- context=failures.AcceptedNegativeData(
237
- message=message,
238
- status_code=response.status_code,
239
- allowed_statuses=config.allowed_statuses,
240
- ),
231
+ raise AcceptedNegativeData(
232
+ operation=case.operation.label,
233
+ message=f"Allowed statuses: {', '.join(config.allowed_statuses)}",
234
+ status_code=response.status_code,
235
+ allowed_statuses=config.allowed_statuses,
241
236
  )
242
237
  return None
243
238
 
244
239
 
245
- def positive_data_acceptance(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
240
+ @schemathesis.check
241
+ def positive_data_acceptance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
246
242
  from .schemas import BaseOpenAPISchema
247
243
 
248
- if not isinstance(case.operation.schema, BaseOpenAPISchema):
244
+ if not isinstance(case.operation.schema, BaseOpenAPISchema) or case.meta is None:
249
245
  return True
250
246
 
251
- config = ctx.config.positive_data_acceptance
247
+ config = ctx.config.get(positive_data_acceptance, PositiveDataAcceptanceConfig())
252
248
  allowed_statuses = expand_status_codes(config.allowed_statuses or [])
253
249
 
254
- if (
255
- case.data_generation_method
256
- and case.data_generation_method.is_positive
257
- and response.status_code not in allowed_statuses
258
- ):
259
- message = f"Allowed statuses: {', '.join(config.allowed_statuses)}"
260
- exc_class = get_positive_acceptance_error(case.operation.verbose_name, response.status_code)
261
- raise exc_class(
262
- failures.RejectedPositiveData.title,
263
- context=failures.RejectedPositiveData(
264
- message=message,
265
- status_code=response.status_code,
266
- allowed_statuses=config.allowed_statuses,
267
- ),
250
+ if case.meta.generation.mode.is_positive and response.status_code not in allowed_statuses:
251
+ raise RejectedPositiveData(
252
+ operation=case.operation.label,
253
+ message=f"Allowed statuses: {', '.join(config.allowed_statuses)}",
254
+ status_code=response.status_code,
255
+ allowed_statuses=config.allowed_statuses,
268
256
  )
269
257
  return None
270
258
 
271
259
 
272
- def missing_required_header(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
260
+ def missing_required_header(ctx: CheckContext, response: Response, case: Case) -> bool | None:
261
+ # NOTE: This check is intentionally not registered with `@schemathesis.check` because it is experimental
262
+ meta = case.meta
263
+ if meta is None or not isinstance(meta.phase.data, CoveragePhaseData):
264
+ return None
265
+ data = meta.phase.data
273
266
  if (
274
- case.meta
275
- and case.meta.parameter_location == "header"
276
- and case.meta.parameter
277
- and case.meta.description
278
- and case.meta.description.startswith("Missing ")
267
+ data.parameter
268
+ and data.parameter_location == "header"
269
+ and data.description
270
+ and data.description.startswith("Missing ")
279
271
  ):
280
- if case.meta.parameter.lower() == "authorization":
272
+ if data.parameter.lower() == "authorization":
281
273
  allowed_statuses = {401}
282
274
  else:
283
- config = ctx.config.missing_required_header
275
+ config = ctx.config.get(missing_required_header, MissingRequiredHeaderConfig())
284
276
  allowed_statuses = expand_status_codes(config.allowed_statuses or [])
285
277
  if response.status_code not in allowed_statuses:
286
- allowed = f"Allowed statuses: {', '.join(map(str,allowed_statuses))}"
278
+ allowed = f"Allowed statuses: {', '.join(map(str, allowed_statuses))}"
287
279
  raise AssertionError(f"Unexpected response status for a missing header: {response.status_code}\n{allowed}")
288
280
  return None
289
281
 
290
282
 
291
- def unsupported_method(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
292
- if case.meta and case.meta.description and case.meta.description.startswith("Unspecified HTTP method:"):
283
+ def unsupported_method(ctx: CheckContext, response: Response, case: Case) -> bool | None:
284
+ meta = case.meta
285
+ if meta is None or not isinstance(meta.phase.data, CoveragePhaseData):
286
+ return None
287
+ data = meta.phase.data
288
+ if data.description and data.description.startswith("Unspecified HTTP method:"):
293
289
  if response.status_code != 405:
294
290
  raise AssertionError(
295
291
  f"Unexpected response status for unspecified HTTP method: {response.status_code}\nExpected: 405"
296
292
  )
297
293
 
298
- allow_header = response.headers.get("Allow")
294
+ allow_header = response.headers.get("allow")
299
295
  if not allow_header:
300
296
  raise AssertionError("Missing 'Allow' header in 405 Method Not Allowed response")
301
297
  return None
@@ -311,14 +307,17 @@ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
311
307
  if meta is None:
312
308
  # Ignore manually created cases
313
309
  return False
314
- if (meta.body and meta.body.is_negative) or (meta.path_parameters and meta.path_parameters.is_negative):
310
+ if (ComponentKind.BODY in meta.components and meta.components[ComponentKind.BODY].mode.is_negative) or (
311
+ ComponentKind.PATH_PARAMETERS in meta.components
312
+ and meta.components[ComponentKind.PATH_PARAMETERS].mode.is_negative
313
+ ):
315
314
  # Body or path negations always imply other negations
316
315
  return False
317
316
  validator_cls = case.operation.schema.validator_cls # type: ignore[attr-defined]
318
- for container in ("query", "headers", "cookies"):
319
- meta_for_location = getattr(meta, container)
320
- value = getattr(case, container)
321
- if value is not None and meta_for_location is not None and meta_for_location.is_negative:
317
+ for container in (ComponentKind.QUERY, ComponentKind.HEADERS, ComponentKind.COOKIES):
318
+ meta_for_location = meta.components.get(container)
319
+ value = getattr(case, container.value)
320
+ if value is not None and meta_for_location is not None and meta_for_location.mode.is_negative:
322
321
  parameters = getattr(case.operation, container)
323
322
  value_without_additional_properties = {k: v for k, v in value.items() if k in parameters}
324
323
  schema = get_schema_for_location(case.operation, container, parameters)
@@ -329,80 +328,94 @@ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
329
328
  return True
330
329
 
331
330
 
332
- def use_after_free(ctx: CheckContext, response: GenericResponse, original: Case) -> bool | None:
333
- from ...transports.responses import get_reason
331
+ @schemathesis.check
332
+ def use_after_free(ctx: CheckContext, response: Response, case: Case) -> bool | None:
334
333
  from .schemas import BaseOpenAPISchema
335
334
 
336
- if not isinstance(original.operation.schema, BaseOpenAPISchema):
335
+ if not isinstance(case.operation.schema, BaseOpenAPISchema):
337
336
  return True
338
- if response.status_code == 404 or not original.source or response.status_code >= 500:
337
+ if response.status_code == 404 or response.status_code >= 500:
339
338
  return None
340
- response = original.source.response
341
- case = original.source.case
342
- while True:
343
- # Find the most recent successful DELETE call that corresponds to the current operation
344
- if case.operation.method.lower() == "delete" and 200 <= response.status_code < 300:
339
+
340
+ for related_case in ctx.find_related(case_id=case.id):
341
+ parent = ctx.find_parent(case_id=related_case.id)
342
+ if not parent:
343
+ continue
344
+
345
+ parent_response = ctx.find_response(case_id=parent.id)
346
+
347
+ if (
348
+ related_case.operation.method.lower() == "delete"
349
+ and parent_response is not None
350
+ and 200 <= parent_response.status_code < 300
351
+ ):
345
352
  if _is_prefix_operation(
353
+ ResourcePath(related_case.path, related_case.path_parameters or {}),
346
354
  ResourcePath(case.path, case.path_parameters or {}),
347
- ResourcePath(original.path, original.path_parameters or {}),
348
355
  ):
349
- free = f"{case.operation.method.upper()} {case.formatted_path}"
350
- usage = f"{original.operation.method} {original.formatted_path}"
351
- exc_class = get_use_after_free_error(case.operation.verbose_name)
352
- reason = get_reason(response.status_code)
353
- message = (
354
- "The API did not return a `HTTP 404 Not Found` response "
355
- f"(got `HTTP {response.status_code} {reason}`) for a resource that was previously deleted.\n\nThe resource was deleted with `{free}`"
356
- )
357
- raise exc_class(
358
- failures.UseAfterFree.title,
359
- context=failures.UseAfterFree(
360
- message=message,
361
- free=free,
362
- usage=usage,
356
+ free = f"{related_case.operation.method.upper()} {prepare_path(related_case.path, related_case.path_parameters)}"
357
+ usage = f"{case.operation.method.upper()} {prepare_path(case.path, case.path_parameters)}"
358
+ reason = http.client.responses.get(response.status_code, "Unknown")
359
+ raise UseAfterFree(
360
+ operation=related_case.operation.label,
361
+ message=(
362
+ "The API did not return a `HTTP 404 Not Found` response "
363
+ f"(got `HTTP {response.status_code} {reason}`) for a resource that was previously deleted.\n\nThe resource was deleted with `{free}`"
363
364
  ),
365
+ free=free,
366
+ usage=usage,
364
367
  )
365
- if case.source is None:
366
- break
367
- response = case.source.response
368
- case = case.source.case
368
+
369
369
  return None
370
370
 
371
371
 
372
- def ensure_resource_availability(ctx: CheckContext, response: GenericResponse, original: Case) -> bool | None:
373
- from ...transports.responses import get_reason
372
+ @schemathesis.check
373
+ def ensure_resource_availability(ctx: CheckContext, response: Response, case: Case) -> bool | None:
374
374
  from .schemas import BaseOpenAPISchema
375
375
 
376
- if not isinstance(original.operation.schema, BaseOpenAPISchema):
376
+ if not isinstance(case.operation.schema, BaseOpenAPISchema):
377
377
  return True
378
+
379
+ parent = ctx.find_parent(case_id=case.id)
380
+ if parent is None:
381
+ return None
382
+ parent_response = ctx.find_response(case_id=parent.id)
383
+ if parent_response is None:
384
+ return None
385
+
386
+ overrides = case._override
387
+ overrides_all_parameters = True
388
+ for parameter in case.operation.iter_parameters():
389
+ container = LOCATION_TO_CONTAINER[parameter.location]
390
+ if parameter.name not in getattr(overrides, container, {}):
391
+ overrides_all_parameters = False
392
+ break
393
+
378
394
  if (
379
395
  # Response indicates a client error, even though all available parameters were taken from links
380
396
  # and comes from a POST request. This case likely means that the POST request actually did not
381
397
  # save the resource and it is not available for subsequent operations
382
398
  400 <= response.status_code < 500
383
- and original.source
384
- and original.source.case.operation.method.upper() == "POST"
385
- and 200 <= original.source.response.status_code < 400
386
- and original.source.overrides_all_parameters
399
+ and parent.operation.method.upper() == "POST"
400
+ and 200 <= parent_response.status_code < 400
401
+ and overrides_all_parameters
387
402
  and _is_prefix_operation(
388
- ResourcePath(original.source.case.path, original.source.case.path_parameters or {}),
389
- ResourcePath(original.path, original.path_parameters or {}),
403
+ ResourcePath(parent.path, parent.path_parameters or {}),
404
+ ResourcePath(case.path, case.path_parameters or {}),
390
405
  )
391
406
  ):
392
- created_with = original.source.case.operation.verbose_name
393
- not_available_with = original.operation.verbose_name
394
- exc_class = get_ensure_resource_availability_error(created_with)
395
- reason = get_reason(response.status_code)
396
- message = (
397
- f"The API returned `{response.status_code} {reason}` for a resource that was just created.\n\n"
398
- f"Created with : `{created_with}`\n"
399
- f"Not available with: `{not_available_with}`"
400
- )
401
- raise exc_class(
402
- failures.EnsureResourceAvailability.title,
403
- context=failures.EnsureResourceAvailability(
404
- message=message, created_with=created_with, not_available_with=not_available_with
407
+ created_with = parent.operation.label
408
+ not_available_with = case.operation.label
409
+ reason = http.client.responses.get(response.status_code, "Unknown")
410
+ raise EnsureResourceAvailability(
411
+ operation=created_with,
412
+ message=(
413
+ f"The API returned `{response.status_code} {reason}` for a resource that was just created.\n\n"
414
+ f"Created with : `{created_with}`\n"
415
+ f"Not available with: `{not_available_with}`"
405
416
  ),
417
+ created_with=created_with,
418
+ not_available_with=not_available_with,
406
419
  )
407
420
  return None
408
421
 
@@ -412,7 +425,8 @@ class AuthKind(enum.Enum):
412
425
  GENERATED = "generated"
413
426
 
414
427
 
415
- def ignored_auth(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
428
+ @schemathesis.check
429
+ def ignored_auth(ctx: CheckContext, response: Response, case: Case) -> bool | None:
416
430
  """Check if an operation declares authentication as a requirement but does not actually enforce it."""
417
431
  from .schemas import BaseOpenAPISchema
418
432
 
@@ -425,7 +439,7 @@ def ignored_auth(ctx: CheckContext, response: GenericResponse, case: Case) -> bo
425
439
  if auth == AuthKind.EXPLICIT:
426
440
  # Auth is explicitly set, it is expected to be valid
427
441
  # Check if invalid auth will give an error
428
- _remove_auth_from_case(case, security_parameters)
442
+ no_auth_case = remove_auth(case, security_parameters)
429
443
  kwargs = ctx.transport_kwargs or {}
430
444
  kwargs.copy()
431
445
  if "headers" in kwargs:
@@ -433,46 +447,36 @@ def ignored_auth(ctx: CheckContext, response: GenericResponse, case: Case) -> bo
433
447
  _remove_auth_from_explicit_headers(headers, security_parameters)
434
448
  kwargs["headers"] = headers
435
449
  kwargs.pop("session", None)
436
- new_response = case.operation.schema.transport.send(case, **kwargs)
437
- if new_response.status_code != 401:
438
- _update_response(response, new_response)
439
- _raise_no_auth_error(new_response, case.operation.verbose_name, "that requires authentication")
450
+ ctx.record_case(parent_id=case.id, case=no_auth_case)
451
+ no_auth_response = case.operation.schema.transport.send(no_auth_case, **kwargs)
452
+ ctx.record_response(case_id=no_auth_case.id, response=no_auth_response)
453
+ if no_auth_response.status_code != 401:
454
+ _raise_no_auth_error(no_auth_response, no_auth_case, "that requires authentication")
440
455
  # Try to set invalid auth and check if it succeeds
441
456
  for parameter in security_parameters:
442
- _set_auth_for_case(case, parameter)
443
- new_response = case.operation.schema.transport.send(case, **kwargs)
444
- if new_response.status_code != 401:
445
- _update_response(response, new_response)
446
- _raise_no_auth_error(new_response, case.operation.verbose_name, "with any auth")
447
- _remove_auth_from_case(case, security_parameters)
457
+ invalid_auth_case = remove_auth(case, security_parameters)
458
+ _set_auth_for_case(invalid_auth_case, parameter)
459
+ ctx.record_case(parent_id=case.id, case=invalid_auth_case)
460
+ invalid_auth_response = case.operation.schema.transport.send(invalid_auth_case, **kwargs)
461
+ ctx.record_response(case_id=invalid_auth_case.id, response=invalid_auth_response)
462
+ if invalid_auth_response.status_code != 401:
463
+ _raise_no_auth_error(invalid_auth_response, invalid_auth_case, "with any auth")
448
464
  elif auth == AuthKind.GENERATED:
449
465
  # If this auth is generated which means it is likely invalid, then
450
466
  # this request should have been an error
451
- _raise_no_auth_error(response, case.operation.verbose_name, "with invalid auth")
467
+ _raise_no_auth_error(response, case, "with invalid auth")
452
468
  else:
453
469
  # Successful response when there is no auth
454
- _raise_no_auth_error(response, case.operation.verbose_name, "that requires authentication")
470
+ _raise_no_auth_error(response, case, "that requires authentication")
455
471
  return None
456
472
 
457
473
 
458
- def _update_response(old: GenericResponse, new: GenericResponse) -> None:
459
- # Mutate the response object in place on the best effort basis
460
- if hasattr(old, "__attrs__"):
461
- for attribute in new.__attrs__:
462
- setattr(old, attribute, getattr(new, attribute))
463
- else:
464
- old.__dict__.update(new.__dict__)
465
-
466
-
467
- def _raise_no_auth_error(response: GenericResponse, operation: str, suffix: str) -> NoReturn:
468
- from ...transports.responses import get_reason
469
-
470
- exc_class = get_ignored_auth_error(operation)
471
- reason = get_reason(response.status_code)
472
- message = f"The API returned `{response.status_code} {reason}` for `{operation}` {suffix}."
473
- raise exc_class(
474
- failures.IgnoredAuth.title,
475
- context=failures.IgnoredAuth(message=message),
474
+ def _raise_no_auth_error(response: Response, case: Case, suffix: str) -> NoReturn:
475
+ reason = http.client.responses.get(response.status_code, "Unknown")
476
+ raise IgnoredAuth(
477
+ operation=case.operation.label,
478
+ message=f"The API returned `{response.status_code} {reason}` for `{case.operation.label}` {suffix}.",
479
+ case_id=case.id,
476
480
  )
477
481
 
478
482
 
@@ -540,19 +544,34 @@ def _contains_auth(
540
544
  return None
541
545
 
542
546
 
543
- def _remove_auth_from_case(case: Case, security_parameters: list[SecurityParameter]) -> None:
547
+ def remove_auth(case: Case, security_parameters: list[SecurityParameter]) -> Case:
544
548
  """Remove security parameters from a generated case.
545
549
 
546
550
  It mutates `case` in place.
547
551
  """
552
+ headers = case.headers.copy() if case.headers else None
553
+ query = case.query.copy() if case.query else None
554
+ cookies = case.cookies.copy() if case.cookies else None
548
555
  for parameter in security_parameters:
549
556
  name = parameter["name"]
550
- if parameter["in"] == "header" and case.headers:
551
- case.headers.pop(name, None)
552
- if parameter["in"] == "query" and case.query:
553
- case.query.pop(name, None)
554
- if parameter["in"] == "cookie" and case.cookies:
555
- case.cookies.pop(name, None)
557
+ if parameter["in"] == "header" and headers:
558
+ headers.pop(name, None)
559
+ if parameter["in"] == "query" and query:
560
+ query.pop(name, None)
561
+ if parameter["in"] == "cookie" and cookies:
562
+ cookies.pop(name, None)
563
+ return Case(
564
+ operation=case.operation,
565
+ method=case.method,
566
+ path=case.path,
567
+ path_parameters=case.path_parameters.copy() if case.path_parameters else None,
568
+ headers=headers,
569
+ cookies=cookies,
570
+ query=query,
571
+ body=case.body.copy() if isinstance(case.body, (list, dict)) else case.body,
572
+ media_type=case.media_type,
573
+ meta=case.meta,
574
+ )
556
575
 
557
576
 
558
577
  def _remove_auth_from_explicit_headers(headers: dict, security_parameters: list[SecurityParameter]) -> None: