schemathesis 3.15.4__py3-none-any.whl → 4.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (251) hide show
  1. schemathesis/__init__.py +53 -25
  2. schemathesis/auths.py +507 -0
  3. schemathesis/checks.py +190 -25
  4. schemathesis/cli/__init__.py +27 -1219
  5. schemathesis/cli/__main__.py +4 -0
  6. schemathesis/cli/commands/__init__.py +133 -0
  7. schemathesis/cli/commands/data.py +10 -0
  8. schemathesis/cli/commands/run/__init__.py +602 -0
  9. schemathesis/cli/commands/run/context.py +228 -0
  10. schemathesis/cli/commands/run/events.py +60 -0
  11. schemathesis/cli/commands/run/executor.py +157 -0
  12. schemathesis/cli/commands/run/filters.py +53 -0
  13. schemathesis/cli/commands/run/handlers/__init__.py +46 -0
  14. schemathesis/cli/commands/run/handlers/base.py +45 -0
  15. schemathesis/cli/commands/run/handlers/cassettes.py +464 -0
  16. schemathesis/cli/commands/run/handlers/junitxml.py +60 -0
  17. schemathesis/cli/commands/run/handlers/output.py +1750 -0
  18. schemathesis/cli/commands/run/loaders.py +118 -0
  19. schemathesis/cli/commands/run/validation.py +256 -0
  20. schemathesis/cli/constants.py +5 -0
  21. schemathesis/cli/core.py +19 -0
  22. schemathesis/cli/ext/fs.py +16 -0
  23. schemathesis/cli/ext/groups.py +203 -0
  24. schemathesis/cli/ext/options.py +81 -0
  25. schemathesis/config/__init__.py +202 -0
  26. schemathesis/config/_auth.py +51 -0
  27. schemathesis/config/_checks.py +268 -0
  28. schemathesis/config/_diff_base.py +101 -0
  29. schemathesis/config/_env.py +21 -0
  30. schemathesis/config/_error.py +163 -0
  31. schemathesis/config/_generation.py +157 -0
  32. schemathesis/config/_health_check.py +24 -0
  33. schemathesis/config/_operations.py +335 -0
  34. schemathesis/config/_output.py +171 -0
  35. schemathesis/config/_parameters.py +19 -0
  36. schemathesis/config/_phases.py +253 -0
  37. schemathesis/config/_projects.py +543 -0
  38. schemathesis/config/_rate_limit.py +17 -0
  39. schemathesis/config/_report.py +120 -0
  40. schemathesis/config/_validator.py +9 -0
  41. schemathesis/config/_warnings.py +89 -0
  42. schemathesis/config/schema.json +975 -0
  43. schemathesis/core/__init__.py +72 -0
  44. schemathesis/core/adapter.py +34 -0
  45. schemathesis/core/compat.py +32 -0
  46. schemathesis/core/control.py +2 -0
  47. schemathesis/core/curl.py +100 -0
  48. schemathesis/core/deserialization.py +210 -0
  49. schemathesis/core/errors.py +588 -0
  50. schemathesis/core/failures.py +316 -0
  51. schemathesis/core/fs.py +19 -0
  52. schemathesis/core/hooks.py +20 -0
  53. schemathesis/core/jsonschema/__init__.py +13 -0
  54. schemathesis/core/jsonschema/bundler.py +183 -0
  55. schemathesis/core/jsonschema/keywords.py +40 -0
  56. schemathesis/core/jsonschema/references.py +222 -0
  57. schemathesis/core/jsonschema/types.py +41 -0
  58. schemathesis/core/lazy_import.py +15 -0
  59. schemathesis/core/loaders.py +107 -0
  60. schemathesis/core/marks.py +66 -0
  61. schemathesis/core/media_types.py +79 -0
  62. schemathesis/core/output/__init__.py +46 -0
  63. schemathesis/core/output/sanitization.py +54 -0
  64. schemathesis/core/parameters.py +45 -0
  65. schemathesis/core/rate_limit.py +60 -0
  66. schemathesis/core/registries.py +34 -0
  67. schemathesis/core/result.py +27 -0
  68. schemathesis/core/schema_analysis.py +17 -0
  69. schemathesis/core/shell.py +203 -0
  70. schemathesis/core/transforms.py +144 -0
  71. schemathesis/core/transport.py +223 -0
  72. schemathesis/core/validation.py +73 -0
  73. schemathesis/core/version.py +7 -0
  74. schemathesis/engine/__init__.py +28 -0
  75. schemathesis/engine/context.py +152 -0
  76. schemathesis/engine/control.py +44 -0
  77. schemathesis/engine/core.py +201 -0
  78. schemathesis/engine/errors.py +446 -0
  79. schemathesis/engine/events.py +284 -0
  80. schemathesis/engine/observations.py +42 -0
  81. schemathesis/engine/phases/__init__.py +108 -0
  82. schemathesis/engine/phases/analysis.py +28 -0
  83. schemathesis/engine/phases/probes.py +172 -0
  84. schemathesis/engine/phases/stateful/__init__.py +68 -0
  85. schemathesis/engine/phases/stateful/_executor.py +364 -0
  86. schemathesis/engine/phases/stateful/context.py +85 -0
  87. schemathesis/engine/phases/unit/__init__.py +220 -0
  88. schemathesis/engine/phases/unit/_executor.py +459 -0
  89. schemathesis/engine/phases/unit/_pool.py +82 -0
  90. schemathesis/engine/recorder.py +254 -0
  91. schemathesis/errors.py +47 -0
  92. schemathesis/filters.py +395 -0
  93. schemathesis/generation/__init__.py +25 -0
  94. schemathesis/generation/case.py +478 -0
  95. schemathesis/generation/coverage.py +1528 -0
  96. schemathesis/generation/hypothesis/__init__.py +121 -0
  97. schemathesis/generation/hypothesis/builder.py +992 -0
  98. schemathesis/generation/hypothesis/examples.py +56 -0
  99. schemathesis/generation/hypothesis/given.py +66 -0
  100. schemathesis/generation/hypothesis/reporting.py +285 -0
  101. schemathesis/generation/meta.py +227 -0
  102. schemathesis/generation/metrics.py +93 -0
  103. schemathesis/generation/modes.py +20 -0
  104. schemathesis/generation/overrides.py +127 -0
  105. schemathesis/generation/stateful/__init__.py +37 -0
  106. schemathesis/generation/stateful/state_machine.py +294 -0
  107. schemathesis/graphql/__init__.py +15 -0
  108. schemathesis/graphql/checks.py +109 -0
  109. schemathesis/graphql/loaders.py +285 -0
  110. schemathesis/hooks.py +270 -91
  111. schemathesis/openapi/__init__.py +13 -0
  112. schemathesis/openapi/checks.py +467 -0
  113. schemathesis/openapi/generation/__init__.py +0 -0
  114. schemathesis/openapi/generation/filters.py +72 -0
  115. schemathesis/openapi/loaders.py +315 -0
  116. schemathesis/pytest/__init__.py +5 -0
  117. schemathesis/pytest/control_flow.py +7 -0
  118. schemathesis/pytest/lazy.py +341 -0
  119. schemathesis/pytest/loaders.py +36 -0
  120. schemathesis/pytest/plugin.py +357 -0
  121. schemathesis/python/__init__.py +0 -0
  122. schemathesis/python/asgi.py +12 -0
  123. schemathesis/python/wsgi.py +12 -0
  124. schemathesis/schemas.py +682 -257
  125. schemathesis/specs/graphql/__init__.py +0 -1
  126. schemathesis/specs/graphql/nodes.py +26 -2
  127. schemathesis/specs/graphql/scalars.py +77 -12
  128. schemathesis/specs/graphql/schemas.py +367 -148
  129. schemathesis/specs/graphql/validation.py +33 -0
  130. schemathesis/specs/openapi/__init__.py +9 -1
  131. schemathesis/specs/openapi/_hypothesis.py +555 -318
  132. schemathesis/specs/openapi/adapter/__init__.py +10 -0
  133. schemathesis/specs/openapi/adapter/parameters.py +729 -0
  134. schemathesis/specs/openapi/adapter/protocol.py +59 -0
  135. schemathesis/specs/openapi/adapter/references.py +19 -0
  136. schemathesis/specs/openapi/adapter/responses.py +368 -0
  137. schemathesis/specs/openapi/adapter/security.py +144 -0
  138. schemathesis/specs/openapi/adapter/v2.py +30 -0
  139. schemathesis/specs/openapi/adapter/v3_0.py +30 -0
  140. schemathesis/specs/openapi/adapter/v3_1.py +30 -0
  141. schemathesis/specs/openapi/analysis.py +96 -0
  142. schemathesis/specs/openapi/checks.py +748 -82
  143. schemathesis/specs/openapi/converter.py +176 -37
  144. schemathesis/specs/openapi/definitions.py +599 -4
  145. schemathesis/specs/openapi/examples.py +581 -165
  146. schemathesis/specs/openapi/expressions/__init__.py +52 -5
  147. schemathesis/specs/openapi/expressions/extractors.py +25 -0
  148. schemathesis/specs/openapi/expressions/lexer.py +34 -31
  149. schemathesis/specs/openapi/expressions/nodes.py +97 -46
  150. schemathesis/specs/openapi/expressions/parser.py +35 -13
  151. schemathesis/specs/openapi/formats.py +122 -0
  152. schemathesis/specs/openapi/media_types.py +75 -0
  153. schemathesis/specs/openapi/negative/__init__.py +93 -73
  154. schemathesis/specs/openapi/negative/mutations.py +294 -103
  155. schemathesis/specs/openapi/negative/utils.py +0 -9
  156. schemathesis/specs/openapi/patterns.py +458 -0
  157. schemathesis/specs/openapi/references.py +60 -81
  158. schemathesis/specs/openapi/schemas.py +647 -666
  159. schemathesis/specs/openapi/serialization.py +53 -30
  160. schemathesis/specs/openapi/stateful/__init__.py +403 -68
  161. schemathesis/specs/openapi/stateful/control.py +87 -0
  162. schemathesis/specs/openapi/stateful/dependencies/__init__.py +232 -0
  163. schemathesis/specs/openapi/stateful/dependencies/inputs.py +428 -0
  164. schemathesis/specs/openapi/stateful/dependencies/models.py +341 -0
  165. schemathesis/specs/openapi/stateful/dependencies/naming.py +491 -0
  166. schemathesis/specs/openapi/stateful/dependencies/outputs.py +34 -0
  167. schemathesis/specs/openapi/stateful/dependencies/resources.py +339 -0
  168. schemathesis/specs/openapi/stateful/dependencies/schemas.py +447 -0
  169. schemathesis/specs/openapi/stateful/inference.py +254 -0
  170. schemathesis/specs/openapi/stateful/links.py +219 -78
  171. schemathesis/specs/openapi/types/__init__.py +3 -0
  172. schemathesis/specs/openapi/types/common.py +23 -0
  173. schemathesis/specs/openapi/types/v2.py +129 -0
  174. schemathesis/specs/openapi/types/v3.py +134 -0
  175. schemathesis/specs/openapi/utils.py +7 -6
  176. schemathesis/specs/openapi/warnings.py +75 -0
  177. schemathesis/transport/__init__.py +224 -0
  178. schemathesis/transport/asgi.py +26 -0
  179. schemathesis/transport/prepare.py +126 -0
  180. schemathesis/transport/requests.py +278 -0
  181. schemathesis/transport/serialization.py +329 -0
  182. schemathesis/transport/wsgi.py +175 -0
  183. schemathesis-4.4.2.dist-info/METADATA +213 -0
  184. schemathesis-4.4.2.dist-info/RECORD +192 -0
  185. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info}/WHEEL +1 -1
  186. schemathesis-4.4.2.dist-info/entry_points.txt +6 -0
  187. {schemathesis-3.15.4.dist-info → schemathesis-4.4.2.dist-info/licenses}/LICENSE +1 -1
  188. schemathesis/_compat.py +0 -57
  189. schemathesis/_hypothesis.py +0 -123
  190. schemathesis/auth.py +0 -214
  191. schemathesis/cli/callbacks.py +0 -240
  192. schemathesis/cli/cassettes.py +0 -351
  193. schemathesis/cli/context.py +0 -38
  194. schemathesis/cli/debug.py +0 -21
  195. schemathesis/cli/handlers.py +0 -11
  196. schemathesis/cli/junitxml.py +0 -41
  197. schemathesis/cli/options.py +0 -70
  198. schemathesis/cli/output/__init__.py +0 -1
  199. schemathesis/cli/output/default.py +0 -521
  200. schemathesis/cli/output/short.py +0 -40
  201. schemathesis/constants.py +0 -88
  202. schemathesis/exceptions.py +0 -257
  203. schemathesis/extra/_aiohttp.py +0 -27
  204. schemathesis/extra/_flask.py +0 -10
  205. schemathesis/extra/_server.py +0 -16
  206. schemathesis/extra/pytest_plugin.py +0 -251
  207. schemathesis/failures.py +0 -145
  208. schemathesis/fixups/__init__.py +0 -29
  209. schemathesis/fixups/fast_api.py +0 -30
  210. schemathesis/graphql.py +0 -5
  211. schemathesis/internal.py +0 -6
  212. schemathesis/lazy.py +0 -301
  213. schemathesis/models.py +0 -1113
  214. schemathesis/parameters.py +0 -91
  215. schemathesis/runner/__init__.py +0 -470
  216. schemathesis/runner/events.py +0 -242
  217. schemathesis/runner/impl/__init__.py +0 -3
  218. schemathesis/runner/impl/core.py +0 -791
  219. schemathesis/runner/impl/solo.py +0 -85
  220. schemathesis/runner/impl/threadpool.py +0 -367
  221. schemathesis/runner/serialization.py +0 -206
  222. schemathesis/serializers.py +0 -253
  223. schemathesis/service/__init__.py +0 -18
  224. schemathesis/service/auth.py +0 -10
  225. schemathesis/service/client.py +0 -62
  226. schemathesis/service/constants.py +0 -25
  227. schemathesis/service/events.py +0 -39
  228. schemathesis/service/handler.py +0 -46
  229. schemathesis/service/hosts.py +0 -74
  230. schemathesis/service/metadata.py +0 -42
  231. schemathesis/service/models.py +0 -21
  232. schemathesis/service/serialization.py +0 -184
  233. schemathesis/service/worker.py +0 -39
  234. schemathesis/specs/graphql/loaders.py +0 -215
  235. schemathesis/specs/openapi/constants.py +0 -7
  236. schemathesis/specs/openapi/expressions/context.py +0 -12
  237. schemathesis/specs/openapi/expressions/pointers.py +0 -29
  238. schemathesis/specs/openapi/filters.py +0 -44
  239. schemathesis/specs/openapi/links.py +0 -303
  240. schemathesis/specs/openapi/loaders.py +0 -453
  241. schemathesis/specs/openapi/parameters.py +0 -430
  242. schemathesis/specs/openapi/security.py +0 -129
  243. schemathesis/specs/openapi/validation.py +0 -24
  244. schemathesis/stateful.py +0 -358
  245. schemathesis/targets.py +0 -32
  246. schemathesis/types.py +0 -38
  247. schemathesis/utils.py +0 -475
  248. schemathesis-3.15.4.dist-info/METADATA +0 -202
  249. schemathesis-3.15.4.dist-info/RECORD +0 -99
  250. schemathesis-3.15.4.dist-info/entry_points.txt +0 -7
  251. /schemathesis/{extra → cli/ext}/__init__.py +0 -0
@@ -1,116 +1,782 @@
1
- from typing import TYPE_CHECKING, Any, Dict, Generator, NoReturn, Optional, Union
2
-
3
- from ... import failures
4
- from ...exceptions import (
5
- get_headers_error,
6
- get_malformed_media_type_error,
7
- get_missing_content_type_error,
8
- get_response_type_error,
9
- get_status_code_error,
1
+ from __future__ import annotations
2
+
3
+ import enum
4
+ import http.client
5
+ from dataclasses import dataclass
6
+ from http.cookies import SimpleCookie
7
+ from typing import TYPE_CHECKING, Any, Iterator, Mapping, NoReturn, cast
8
+ from urllib.parse import parse_qs, urlparse
9
+
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.parameters import ParameterLocation
15
+ from schemathesis.core.transport import Response
16
+ from schemathesis.generation.case import Case
17
+ from schemathesis.generation.meta import CoveragePhaseData, CoverageScenario
18
+ from schemathesis.openapi.checks import (
19
+ AcceptedNegativeData,
20
+ EnsureResourceAvailability,
21
+ IgnoredAuth,
22
+ JsonSchemaError,
23
+ MalformedMediaType,
24
+ MissingContentType,
25
+ MissingHeaderNotRejected,
26
+ MissingHeaders,
27
+ RejectedPositiveData,
28
+ UndefinedContentType,
29
+ UndefinedStatusCode,
30
+ UnsupportedMethodResponse,
31
+ UseAfterFree,
10
32
  )
11
- from ...utils import GenericResponse, parse_content_type
12
- from .schemas import BaseOpenAPISchema
13
- from .utils import expand_status_code
33
+ from schemathesis.transport.prepare import prepare_path
34
+
35
+ from .utils import expand_status_code, expand_status_codes
14
36
 
15
37
  if TYPE_CHECKING:
16
- from ...models import Case
38
+ from schemathesis.schemas import APIOperation
39
+ from schemathesis.specs.openapi.adapter.parameters import OpenApiParameterSet
40
+
41
+
42
+ def is_unexpected_http_status_case(case: Case) -> bool:
43
+ # Skip checks for requests using HTTP methods not defined in the API spec
44
+ return bool(
45
+ case.meta
46
+ and isinstance(case.meta.phase.data, CoveragePhaseData)
47
+ and case.meta.phase.data.scenario == CoverageScenario.UNSPECIFIED_HTTP_METHOD
48
+ )
17
49
 
18
50
 
19
- def status_code_conformance(response: GenericResponse, case: "Case") -> Optional[bool]:
20
- if not isinstance(case.operation.schema, BaseOpenAPISchema):
21
- raise TypeError("This check can be used only with Open API schemas")
22
- responses = case.operation.definition.raw.get("responses", {})
51
+ @schemathesis.check
52
+ def status_code_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
53
+ from .schemas import BaseOpenAPISchema
54
+
55
+ if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
56
+ return True
57
+ status_codes = case.operation.responses.status_codes
23
58
  # "default" can be used as the default response object for all HTTP codes that are not covered individually
24
- if "default" in responses:
59
+ if "default" in status_codes:
25
60
  return None
26
- allowed_status_codes = list(_expand_responses(responses))
61
+ allowed_status_codes = list(_expand_status_codes(status_codes))
27
62
  if response.status_code not in allowed_status_codes:
28
- defined_status_codes = list(map(str, responses))
63
+ defined_status_codes = list(map(str, status_codes))
29
64
  responses_list = ", ".join(defined_status_codes)
30
- message = (
31
- f"Received a response with a status code, which is not defined in the schema: "
32
- f"{response.status_code}\n\nDeclared status codes: {responses_list}"
33
- )
34
- exc_class = get_status_code_error(response.status_code)
35
- raise exc_class(
36
- message,
37
- context=failures.UndefinedStatusCode(
38
- status_code=response.status_code,
39
- defined_status_codes=defined_status_codes,
40
- allowed_status_codes=allowed_status_codes,
41
- ),
65
+ raise UndefinedStatusCode(
66
+ operation=case.operation.label,
67
+ status_code=response.status_code,
68
+ defined_status_codes=defined_status_codes,
69
+ allowed_status_codes=allowed_status_codes,
70
+ message=f"Received: {response.status_code}\nDocumented: {responses_list}",
42
71
  )
43
72
  return None # explicitly return None for mypy
44
73
 
45
74
 
46
- def _expand_responses(responses: Dict[Union[str, int], Any]) -> Generator[int, None, None]:
75
+ def _expand_status_codes(responses: tuple[str, ...]) -> Iterator[int]:
47
76
  for code in responses:
48
77
  yield from expand_status_code(code)
49
78
 
50
79
 
51
- def content_type_conformance(response: GenericResponse, case: "Case") -> Optional[bool]:
52
- if not isinstance(case.operation.schema, BaseOpenAPISchema):
53
- raise TypeError("This check can be used only with Open API schemas")
54
- defined_content_types = case.operation.schema.get_content_types(case.operation, response)
55
- if not defined_content_types:
80
+ @schemathesis.check
81
+ def content_type_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
82
+ from .schemas import BaseOpenAPISchema
83
+
84
+ if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
85
+ return True
86
+ documented_content_types = case.operation.schema.get_content_types(case.operation, response)
87
+ if not documented_content_types:
56
88
  return None
57
- content_type = response.headers.get("Content-Type")
58
- if not content_type:
59
- formatted_media_types = "\n ".join(defined_content_types)
60
- raise get_missing_content_type_error()(
61
- "The response is missing the `Content-Type` header. The schema defines the following media types:\n\n"
62
- f" {formatted_media_types}",
63
- context=failures.MissingContentType(defined_content_types),
89
+ content_types = response.headers.get("content-type")
90
+ if not content_types:
91
+ all_media_types = [f"\n- `{content_type}`" for content_type in documented_content_types]
92
+ raise MissingContentType(
93
+ operation=case.operation.label,
94
+ message=f"The following media types are documented in the schema:{''.join(all_media_types)}",
95
+ media_types=documented_content_types,
64
96
  )
65
- for option in defined_content_types:
97
+ content_type = content_types[0]
98
+ for option in documented_content_types:
66
99
  try:
67
- expected_main, expected_sub = parse_content_type(option)
68
- except ValueError as exc:
69
- _reraise_malformed_media_type(exc, "Schema", option, option)
100
+ expected_main, expected_sub = media_types.parse(option)
101
+ except ValueError:
102
+ _reraise_malformed_media_type(case, "Schema", option, option)
70
103
  try:
71
- received_main, received_sub = parse_content_type(content_type)
72
- except ValueError as exc:
73
- _reraise_malformed_media_type(exc, "Response", content_type, option)
74
- if (expected_main, expected_sub) == (received_main, received_sub):
104
+ received_main, received_sub = media_types.parse(content_type)
105
+ except ValueError:
106
+ _reraise_malformed_media_type(case, "Response", content_type, option)
107
+ if (
108
+ (expected_main == "*" and expected_sub == "*")
109
+ or (expected_main == received_main and expected_sub == "*")
110
+ or (expected_main == "*" and expected_sub == received_sub)
111
+ or (expected_main == received_main and expected_sub == received_sub)
112
+ ):
75
113
  return None
76
- exc_class = get_response_type_error(f"{expected_main}_{expected_sub}", f"{received_main}_{received_sub}")
77
- raise exc_class(
78
- f"Received a response with '{content_type}' Content-Type, "
79
- f"but it is not declared in the schema.\n\n"
80
- f"Defined content types: {', '.join(defined_content_types)}",
81
- context=failures.UndefinedContentType(content_type=content_type, defined_content_types=defined_content_types),
114
+ raise UndefinedContentType(
115
+ operation=case.operation.label,
116
+ message=f"Received: {content_type}\nDocumented: {', '.join(documented_content_types)}",
117
+ content_type=content_type,
118
+ defined_content_types=documented_content_types,
82
119
  )
83
120
 
84
121
 
85
- def _reraise_malformed_media_type(exc: ValueError, location: str, actual: str, defined: str) -> NoReturn:
86
- message = (
87
- f"{location} has a malformed media type: `{actual}`. Please, ensure that this media type conforms to "
88
- f"the `type-name/subtype-name` format defined by RFC 6838."
122
+ def _reraise_malformed_media_type(case: Case, location: str, actual: str, defined: str) -> NoReturn:
123
+ raise MalformedMediaType(
124
+ operation=case.operation.label,
125
+ message=f"Media type for {location} is incorrect\n\nReceived: {actual}\nDocumented: {defined}",
126
+ actual=actual,
127
+ defined=defined,
89
128
  )
90
- raise get_malformed_media_type_error(message)(
91
- message, context=failures.MalformedMediaType(actual=actual, defined=defined)
92
- ) from exc
93
129
 
94
130
 
95
- def response_headers_conformance(response: GenericResponse, case: "Case") -> Optional[bool]:
96
- if not isinstance(case.operation.schema, BaseOpenAPISchema):
97
- raise TypeError("This check can be used only with Open API schemas")
98
- defined_headers = case.operation.schema.get_headers(case.operation, response)
99
- if not defined_headers:
131
+ @schemathesis.check
132
+ def response_headers_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
133
+ import jsonschema
134
+
135
+ from .schemas import BaseOpenAPISchema, _maybe_raise_one_or_more
136
+
137
+ if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
138
+ return True
139
+
140
+ # Find the matching response definition
141
+ response_definition = case.operation.responses.find_by_status_code(response.status_code)
142
+ if response_definition is None:
143
+ return None
144
+ # Check whether the matching response definition has headers defined
145
+ headers = response_definition.headers
146
+ if not headers:
147
+ return None
148
+
149
+ errors: list[Failure] = []
150
+
151
+ missing_headers = []
152
+
153
+ for name, header in headers.items():
154
+ values = response.headers.get(name.lower())
155
+ if values is not None:
156
+ value = values[0]
157
+ coerced = _coerce_header_value(value, header.schema)
158
+ try:
159
+ header.validator.validate(coerced)
160
+ except jsonschema.ValidationError as exc:
161
+ errors.append(
162
+ JsonSchemaError.from_exception(
163
+ title="Response header does not conform to the schema",
164
+ operation=case.operation.label,
165
+ exc=exc,
166
+ config=case.operation.schema.config.output,
167
+ )
168
+ )
169
+ elif header.is_required:
170
+ missing_headers.append(name)
171
+
172
+ if missing_headers:
173
+ formatted_headers = [f"\n- `{header}`" for header in missing_headers]
174
+ message = f"The following required headers are missing from the response:{''.join(formatted_headers)}"
175
+ errors.append(MissingHeaders(operation=case.operation.label, message=message, missing_headers=missing_headers))
176
+
177
+ return _maybe_raise_one_or_more(errors) # type: ignore[func-returns-value]
178
+
179
+
180
+ def _coerce_header_value(value: str, schema: dict[str, Any]) -> str | int | float | None | bool:
181
+ schema_type = schema.get("type")
182
+
183
+ if schema_type == "string":
184
+ return value
185
+ if schema_type == "integer":
186
+ try:
187
+ return int(value)
188
+ except ValueError:
189
+ return value
190
+ if schema_type == "number":
191
+ try:
192
+ return float(value)
193
+ except ValueError:
194
+ return value
195
+ if schema_type == "null" and value.lower() == "null":
196
+ return None
197
+ if schema_type == "boolean":
198
+ return string_to_boolean(value)
199
+ return value
200
+
201
+
202
+ @schemathesis.check
203
+ def response_schema_conformance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
204
+ from .schemas import BaseOpenAPISchema
205
+
206
+ if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
207
+ return True
208
+ return case.operation.validate_response(response, case=case)
209
+
210
+
211
+ @schemathesis.check
212
+ def negative_data_rejection(ctx: CheckContext, response: Response, case: Case) -> bool | None:
213
+ from .schemas import BaseOpenAPISchema
214
+
215
+ if (
216
+ not isinstance(case.operation.schema, BaseOpenAPISchema)
217
+ or case.meta is None
218
+ or is_unexpected_http_status_case(case)
219
+ ):
220
+ return True
221
+
222
+ config = ctx.config.negative_data_rejection
223
+ allowed_statuses = expand_status_codes(config.expected_statuses or [])
224
+
225
+ if (
226
+ case.meta.generation.mode.is_negative
227
+ and response.status_code not in allowed_statuses
228
+ and not has_only_additional_properties_in_non_body_parameters(case)
229
+ ):
230
+ extra_info = ""
231
+ phase = case.meta.phase
232
+ if phase.data.description:
233
+ parts: list[str] = []
234
+ # Special case: CoveragePhaseData descriptions for "Missing" scenarios are already complete
235
+ if isinstance(phase.data, CoveragePhaseData) and phase.data.scenario in (
236
+ CoverageScenario.MISSING_PARAMETER,
237
+ CoverageScenario.OBJECT_MISSING_REQUIRED_PROPERTY,
238
+ ):
239
+ extra_info = f"\nInvalid component: {phase.data.description}"
240
+ else:
241
+ # Build structured message: parameter `name` in location - description
242
+ # For body, don't show parameter name (it's the media type, not useful)
243
+ location = phase.data.parameter_location
244
+ if phase.data.parameter and location != ParameterLocation.BODY:
245
+ parts.append(f"parameter `{phase.data.parameter}`")
246
+ if location:
247
+ parts.append(f"in {location.name.lower()}")
248
+ # Lowercase first letter of description for consistency
249
+ description = phase.data.description
250
+ if description:
251
+ description = description[0].lower() + description[1:] if len(description) > 0 else description
252
+ if parts:
253
+ parts.append(f"- {description}")
254
+ else:
255
+ parts.append(description)
256
+ extra_info = "\nInvalid component: " + " ".join(parts)
257
+ raise AcceptedNegativeData(
258
+ operation=case.operation.label,
259
+ message=f"Invalid data should have been rejected\nExpected: {', '.join(config.expected_statuses)}{extra_info}",
260
+ status_code=response.status_code,
261
+ expected_statuses=config.expected_statuses,
262
+ )
263
+ return None
264
+
265
+
266
+ @schemathesis.check
267
+ def positive_data_acceptance(ctx: CheckContext, response: Response, case: Case) -> bool | None:
268
+ from .schemas import BaseOpenAPISchema
269
+
270
+ if (
271
+ not isinstance(case.operation.schema, BaseOpenAPISchema)
272
+ or case.meta is None
273
+ or is_unexpected_http_status_case(case)
274
+ ):
275
+ return True
276
+
277
+ config = ctx.config.positive_data_acceptance
278
+ allowed_statuses = expand_status_codes(config.expected_statuses or [])
279
+
280
+ if case.meta.generation.mode.is_positive and response.status_code not in allowed_statuses:
281
+ raise RejectedPositiveData(
282
+ operation=case.operation.label,
283
+ message=f"Valid data should have been accepted\nExpected: {', '.join(config.expected_statuses)}",
284
+ status_code=response.status_code,
285
+ allowed_statuses=config.expected_statuses,
286
+ )
287
+ return None
288
+
289
+
290
+ @schemathesis.check
291
+ def missing_required_header(ctx: CheckContext, response: Response, case: Case) -> bool | None:
292
+ meta = case.meta
293
+ if meta is None or not isinstance(meta.phase.data, CoveragePhaseData) or is_unexpected_http_status_case(case):
294
+ return None
295
+ data = meta.phase.data
296
+ if (
297
+ data.parameter
298
+ and data.parameter_location == ParameterLocation.HEADER
299
+ and data.scenario == CoverageScenario.MISSING_PARAMETER
300
+ ):
301
+ if data.parameter.lower() == "authorization":
302
+ expected_statuses = {401}
303
+ else:
304
+ config = ctx.config.missing_required_header
305
+ expected_statuses = expand_status_codes(config.expected_statuses or [])
306
+ if response.status_code not in expected_statuses:
307
+ allowed = ", ".join(map(str, expected_statuses))
308
+ raise MissingHeaderNotRejected(
309
+ operation=f"{case.method} {case.path}",
310
+ header_name=data.parameter,
311
+ status_code=response.status_code,
312
+ expected_statuses=list(expected_statuses),
313
+ message=f"Missing header not rejected (got {response.status_code}, expected {allowed})",
314
+ )
315
+ return None
316
+
317
+
318
+ @schemathesis.check
319
+ def unsupported_method(ctx: CheckContext, response: Response, case: Case) -> bool | None:
320
+ meta = case.meta
321
+ if meta is None or not isinstance(meta.phase.data, CoveragePhaseData) or response.request.method == "OPTIONS":
322
+ return None
323
+ data = meta.phase.data
324
+ if data.scenario == CoverageScenario.UNSPECIFIED_HTTP_METHOD:
325
+ if response.status_code != 405:
326
+ raise UnsupportedMethodResponse(
327
+ operation=case.operation.label,
328
+ method=cast(str, response.request.method),
329
+ status_code=response.status_code,
330
+ failure_reason="wrong_status",
331
+ message=f"Unsupported method {response.request.method} returned {response.status_code}, expected 405 Method Not Allowed\n\nReturn 405 for methods not listed in the OpenAPI spec",
332
+ )
333
+
334
+ allow_header = response.headers.get("allow")
335
+ if not allow_header:
336
+ raise UnsupportedMethodResponse(
337
+ operation=case.operation.label,
338
+ method=cast(str, response.request.method),
339
+ status_code=response.status_code,
340
+ allow_header_present=False,
341
+ failure_reason="missing_allow_header",
342
+ message=f"{response.request.method} returned 405 without required `Allow` header\n\nAdd `Allow` header listing supported methods (required by RFC 9110)",
343
+ )
344
+ return None
345
+
346
+
347
+ def has_only_additional_properties_in_non_body_parameters(case: Case) -> bool:
348
+ # Check if the case contains only additional properties in query, headers, or cookies.
349
+ # This function is used to determine if negation is solely in the form of extra properties,
350
+ # which are often ignored for backward-compatibility by the tested apps
351
+ from .schemas import BaseOpenAPISchema
352
+
353
+ meta = case.meta
354
+ if meta is None or not isinstance(case.operation.schema, BaseOpenAPISchema):
355
+ # Ignore manually created cases
356
+ return False
357
+ if (ParameterLocation.BODY in meta.components and meta.components[ParameterLocation.BODY].mode.is_negative) or (
358
+ ParameterLocation.PATH in meta.components and meta.components[ParameterLocation.PATH].mode.is_negative
359
+ ):
360
+ # Body or path negations always imply other negations
361
+ return False
362
+ validator_cls = case.operation.schema.adapter.jsonschema_validator_cls
363
+ for location in (ParameterLocation.QUERY, ParameterLocation.HEADER, ParameterLocation.COOKIE):
364
+ meta_for_location = meta.components.get(location)
365
+ value = getattr(case, location.container_name)
366
+ if value is not None and meta_for_location is not None and meta_for_location.mode.is_negative:
367
+ container = getattr(case.operation, location.container_name)
368
+ schema = container.schema
369
+
370
+ if _has_serialization_sensitive_types(schema, container):
371
+ # Can't reliably determine if only additional properties were added
372
+ continue
373
+
374
+ value_without_additional_properties = {k: v for k, v in value.items() if k in container}
375
+ if not validator_cls(schema).is_valid(value_without_additional_properties):
376
+ # Other types of negation found
377
+ return False
378
+ # Only additional properties are added
379
+ return True
380
+
381
+
382
+ def _has_serialization_sensitive_types(schema: dict, container: OpenApiParameterSet) -> bool:
383
+ """Check if schema contains array or object types in defined parameters.
384
+
385
+ In query/header/cookie parameters, arrays and objects are serialized to strings.
386
+ This makes post-serialization validation against the original schema unreliable:
387
+
388
+ - Generated: ["foo", "bar"] (array)
389
+ - Serialized: "foo,bar" (string)
390
+
391
+ Validation of string against array schema fails incorrectly.
392
+ A better approach would be to apply serialization later on in the process.
393
+
394
+ """
395
+ from schemathesis.core.jsonschema import get_type
396
+
397
+ properties = schema.get("properties", {})
398
+ for prop_name, prop_schema in properties.items():
399
+ if prop_name in container:
400
+ types = get_type(prop_schema)
401
+ if "array" in types or "object" in types:
402
+ return True
403
+ return False
404
+
405
+
406
+ @schemathesis.check
407
+ def use_after_free(ctx: CheckContext, response: Response, case: Case) -> bool | None:
408
+ from .schemas import BaseOpenAPISchema
409
+
410
+ if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
411
+ return True
412
+
413
+ # Only check for use-after-free on successful responses (2xx) or redirects (3xx)
414
+ # Other status codes indicate request-level issues / server errors, not successful resource access
415
+ if not (200 <= response.status_code < 400):
416
+ return None
417
+
418
+ for related_case in ctx._find_related(case_id=case.id):
419
+ parent = ctx._find_parent(case_id=related_case.id)
420
+ if not parent:
421
+ continue
422
+
423
+ parent_response = ctx._find_response(case_id=parent.id)
424
+
425
+ if (
426
+ related_case.operation.method.lower() == "delete"
427
+ and parent_response is not None
428
+ and 200 <= parent_response.status_code < 300
429
+ ):
430
+ if _is_prefix_operation(
431
+ ResourcePath(related_case.path, related_case.path_parameters or {}),
432
+ ResourcePath(case.path, case.path_parameters or {}),
433
+ ):
434
+ free = f"{related_case.operation.method.upper()} {prepare_path(related_case.path, related_case.path_parameters)}"
435
+ usage = f"{case.operation.method.upper()} {prepare_path(case.path, case.path_parameters)}"
436
+ reason = http.client.responses.get(response.status_code, "Unknown")
437
+ raise UseAfterFree(
438
+ operation=related_case.operation.label,
439
+ message=(
440
+ "The API did not return a `HTTP 404 Not Found` response "
441
+ f"(got `HTTP {response.status_code} {reason}`) for a resource that was previously deleted.\n\nThe resource was deleted with `{free}`"
442
+ ),
443
+ free=free,
444
+ usage=usage,
445
+ )
446
+
447
+ return None
448
+
449
+
450
+ @schemathesis.check
451
+ def ensure_resource_availability(ctx: CheckContext, response: Response, case: Case) -> bool | None:
452
+ from .schemas import BaseOpenAPISchema
453
+
454
+ if not isinstance(case.operation.schema, BaseOpenAPISchema) or is_unexpected_http_status_case(case):
455
+ return True
456
+
457
+ # Only check for 404 (Not Found) responses - other 4XX are not resource availability issues
458
+ # 422 / 400: Validation errors (bad request data)
459
+ # 401 / 403: Auth issues (expired tokens, permissions)
460
+ # 409: Conflict errors
461
+ if response.status_code != 404:
462
+ return None
463
+
464
+ parent = ctx._find_parent(case_id=case.id)
465
+ if parent is None:
466
+ return None
467
+ parent_response = ctx._find_response(case_id=parent.id)
468
+ if parent_response is None:
100
469
  return None
101
470
 
102
- missing_headers = [header for header in defined_headers if header not in response.headers]
103
- if not missing_headers:
471
+ if not (
472
+ parent.operation.method.upper() == "POST"
473
+ and 200 <= parent_response.status_code < 400
474
+ and _is_prefix_operation(
475
+ ResourcePath(parent.path, parent.path_parameters or {}),
476
+ ResourcePath(case.path, case.path_parameters or {}),
477
+ )
478
+ ):
479
+ return None
480
+
481
+ # Check if all parameters come from links
482
+ overrides = case._override
483
+ overrides_all_parameters = True
484
+ for parameter in case.operation.iter_parameters():
485
+ container = parameter.location.container_name
486
+ if parameter.name not in getattr(overrides, container, {}):
487
+ overrides_all_parameters = False
488
+ break
489
+ if not overrides_all_parameters:
104
490
  return None
105
- message = ",".join(missing_headers)
106
- exc_class = get_headers_error(message)
107
- raise exc_class(
108
- f"Received a response with missing headers: {message}",
109
- context=failures.MissingHeaders(missing_headers=missing_headers),
491
+
492
+ # Look for any successful DELETE operations on this resource
493
+ for related_case in ctx._find_related(case_id=case.id):
494
+ related_response = ctx._find_response(case_id=related_case.id)
495
+ if (
496
+ related_case.operation.method.upper() == "DELETE"
497
+ and related_response is not None
498
+ and 200 <= related_response.status_code < 300
499
+ and _is_prefix_operation(
500
+ ResourcePath(related_case.path, related_case.path_parameters or {}),
501
+ ResourcePath(case.path, case.path_parameters or {}),
502
+ )
503
+ ):
504
+ # Resource was properly deleted, 404 is expected
505
+ return None
506
+
507
+ # If we got here:
508
+ # 1. Resource was created successfully
509
+ # 2. Current operation returned 4XX
510
+ # 3. All parameters come from links
511
+ # 4. No successful DELETE operations found
512
+ created_with = parent.operation.label
513
+ not_available_with = case.operation.label
514
+ reason = http.client.responses.get(response.status_code, "Unknown")
515
+ raise EnsureResourceAvailability(
516
+ operation=created_with,
517
+ message=(
518
+ f"The API returned `{response.status_code} {reason}` for a resource that was just created.\n\n"
519
+ f"Created with : `{created_with}`\n"
520
+ f"Not available with: `{not_available_with}`"
521
+ ),
522
+ created_with=created_with,
523
+ not_available_with=not_available_with,
110
524
  )
111
525
 
112
526
 
113
- def response_schema_conformance(response: GenericResponse, case: "Case") -> None:
114
- if not isinstance(case.operation.schema, BaseOpenAPISchema):
115
- raise TypeError("This check can be used only with Open API schemas")
116
- return case.operation.validate_response(response)
527
+ class AuthScenario(str, enum.Enum):
528
+ NO_AUTH = "no_auth"
529
+ INVALID_AUTH = "invalid_auth"
530
+ GENERATED_AUTH = "generated_auth"
531
+
532
+
533
+ class AuthKind(str, enum.Enum):
534
+ EXPLICIT = "explicit"
535
+ GENERATED = "generated"
536
+
537
+
538
+ @schemathesis.check
539
+ def ignored_auth(ctx: CheckContext, response: Response, case: Case) -> bool | None:
540
+ """Check if an operation declares authentication as a requirement but does not actually enforce it."""
541
+ from schemathesis.specs.openapi.adapter.security import has_optional_auth
542
+ from schemathesis.specs.openapi.schemas import BaseOpenAPISchema
543
+
544
+ operation = case.operation
545
+ if (
546
+ not isinstance(operation.schema, BaseOpenAPISchema)
547
+ or is_unexpected_http_status_case(case)
548
+ or has_optional_auth(operation.schema.raw_schema, operation.definition.raw)
549
+ ):
550
+ return True
551
+ security_parameters = _get_security_parameters(case.operation)
552
+ # Authentication is required for this API operation and response is successful
553
+ if security_parameters and 200 <= response.status_code < 300:
554
+ auth = _contains_auth(ctx, case, response, security_parameters)
555
+ if auth == AuthKind.EXPLICIT:
556
+ # Auth is explicitly set, it is expected to be valid
557
+ # Check if invalid auth will give an error
558
+ no_auth_case = remove_auth(case, security_parameters)
559
+ kwargs = ctx._transport_kwargs or {}
560
+ kwargs.copy()
561
+ for location, container_name in (
562
+ ("header", "headers"),
563
+ ("cookie", "cookies"),
564
+ ("query", "query"),
565
+ ):
566
+ if container_name in kwargs:
567
+ container = kwargs[container_name].copy()
568
+ _remove_auth_from_container(container, security_parameters, location=location)
569
+ kwargs[container_name] = container
570
+ kwargs.pop("session", None)
571
+ if case.operation.app is not None:
572
+ kwargs.setdefault("app", case.operation.app)
573
+ ctx._record_case(parent_id=case.id, case=no_auth_case)
574
+ no_auth_response = case.operation.schema.transport.send(no_auth_case, **kwargs)
575
+ ctx._record_response(case_id=no_auth_case.id, response=no_auth_response)
576
+ if no_auth_response.status_code != 401:
577
+ _raise_no_auth_error(no_auth_response, no_auth_case, AuthScenario.NO_AUTH)
578
+ # Try to set invalid auth and check if it succeeds
579
+ for parameter in security_parameters:
580
+ invalid_auth_case = remove_auth(case, security_parameters)
581
+ _set_auth_for_case(invalid_auth_case, parameter)
582
+ ctx._record_case(parent_id=case.id, case=invalid_auth_case)
583
+ invalid_auth_response = case.operation.schema.transport.send(invalid_auth_case, **kwargs)
584
+ ctx._record_response(case_id=invalid_auth_case.id, response=invalid_auth_response)
585
+ if invalid_auth_response.status_code != 401:
586
+ _raise_no_auth_error(invalid_auth_response, invalid_auth_case, AuthScenario.INVALID_AUTH)
587
+ elif auth == AuthKind.GENERATED:
588
+ # If this auth is generated which means it is likely invalid, then
589
+ # this request should have been an error
590
+ _raise_no_auth_error(response, case, AuthScenario.GENERATED_AUTH)
591
+ else:
592
+ # Successful response when there is no auth
593
+ _raise_no_auth_error(response, case, AuthScenario.NO_AUTH)
594
+ return None
595
+
596
+
597
+ def _raise_no_auth_error(response: Response, case: Case, auth: AuthScenario) -> NoReturn:
598
+ reason = http.client.responses.get(response.status_code, "Unknown")
599
+
600
+ if auth == AuthScenario.NO_AUTH:
601
+ title = "API accepts requests without authentication"
602
+ detail = None
603
+ elif auth == AuthScenario.INVALID_AUTH:
604
+ title = "API accepts invalid authentication"
605
+ detail = "invalid credentials provided"
606
+ else:
607
+ title = "API accepts invalid authentication"
608
+ detail = "generated auth likely invalid"
609
+
610
+ message = f"Expected 401, got `{response.status_code} {reason}` for `{case.operation.label}`"
611
+ if detail is not None:
612
+ message = f"{message} ({detail})"
613
+
614
+ raise IgnoredAuth(
615
+ operation=case.operation.label,
616
+ message=message,
617
+ title=title,
618
+ case_id=case.id,
619
+ )
620
+
621
+
622
+ def _get_security_parameters(operation: APIOperation) -> list[Mapping[str, Any]]:
623
+ """Extract security definitions that are active for the given operation and convert them into parameters."""
624
+ from schemathesis.specs.openapi.adapter.security import ORIGINAL_SECURITY_TYPE_KEY
625
+
626
+ return [
627
+ param
628
+ for param in operation.security.iter_parameters()
629
+ if param[ORIGINAL_SECURITY_TYPE_KEY] in ["apiKey", "basic", "http"]
630
+ ]
631
+
632
+
633
+ def _contains_auth(
634
+ ctx: CheckContext, case: Case, response: Response, security_parameters: list[Mapping[str, Any]]
635
+ ) -> AuthKind | None:
636
+ """Whether a request has authentication declared in the schema."""
637
+ from requests.cookies import RequestsCookieJar
638
+
639
+ # If auth comes from explicit `auth` option or a custom auth, it is always explicit
640
+ if ctx._auth is not None or case._has_explicit_auth:
641
+ return AuthKind.EXPLICIT
642
+ request = response.request
643
+ parsed = urlparse(request.url)
644
+ query = parse_qs(parsed.query) # type: ignore[type-var]
645
+ # Load the `Cookie` header separately, because it is possible that `request._cookies` and the header are out of sync
646
+ header_cookies: SimpleCookie = SimpleCookie()
647
+ raw_cookie = request.headers.get("Cookie")
648
+ if raw_cookie is not None:
649
+ header_cookies.load(raw_cookie)
650
+
651
+ def has_header(p: Mapping[str, Any]) -> bool:
652
+ return p["in"] == "header" and p["name"] in request.headers
653
+
654
+ def has_query(p: Mapping[str, Any]) -> bool:
655
+ return p["in"] == "query" and p["name"] in query
656
+
657
+ def has_cookie(p: Mapping[str, Any]) -> bool:
658
+ cookies = cast(RequestsCookieJar, request._cookies) # type: ignore[attr-defined]
659
+ return p["in"] == "cookie" and (p["name"] in cookies or p["name"] in header_cookies)
660
+
661
+ for parameter in security_parameters:
662
+ name = parameter["name"]
663
+ if has_header(parameter):
664
+ if (
665
+ # Explicit CLI headers
666
+ (ctx._headers is not None and name in ctx._headers)
667
+ # Other kinds of overrides
668
+ or (ctx._override and name in ctx._override.headers)
669
+ or (response._override and name in response._override.headers)
670
+ ):
671
+ return AuthKind.EXPLICIT
672
+ return AuthKind.GENERATED
673
+ if has_cookie(parameter):
674
+ for headers in [
675
+ ctx._headers,
676
+ (ctx._override.headers if ctx._override else None),
677
+ (response._override.headers if response._override else None),
678
+ ]:
679
+ if headers is not None and "Cookie" in headers:
680
+ jar = cast(RequestsCookieJar, headers["Cookie"])
681
+ if name in jar:
682
+ return AuthKind.EXPLICIT
683
+
684
+ if (ctx._override and name in ctx._override.cookies) or (
685
+ response._override and name in response._override.cookies
686
+ ):
687
+ return AuthKind.EXPLICIT
688
+ return AuthKind.GENERATED
689
+ if has_query(parameter):
690
+ if (ctx._override and name in ctx._override.query) or (
691
+ response._override and name in response._override.query
692
+ ):
693
+ return AuthKind.EXPLICIT
694
+ return AuthKind.GENERATED
695
+
696
+ return None
697
+
698
+
699
+ def remove_auth(case: Case, security_parameters: list[Mapping[str, Any]]) -> Case:
700
+ """Remove security parameters from a generated case.
701
+
702
+ It mutates `case` in place.
703
+ """
704
+ headers = case.headers.copy()
705
+ query = case.query.copy()
706
+ cookies = case.cookies.copy()
707
+ for parameter in security_parameters:
708
+ name = parameter["name"]
709
+ if parameter["in"] == "header" and headers:
710
+ headers.pop(name, None)
711
+ if parameter["in"] == "query" and query:
712
+ query.pop(name, None)
713
+ if parameter["in"] == "cookie" and cookies:
714
+ cookies.pop(name, None)
715
+ return Case(
716
+ operation=case.operation,
717
+ method=case.method,
718
+ path=case.path,
719
+ path_parameters=case.path_parameters.copy(),
720
+ headers=headers,
721
+ cookies=cookies,
722
+ query=query,
723
+ body=case.body.copy() if isinstance(case.body, (list, dict)) else case.body,
724
+ media_type=case.media_type,
725
+ meta=case.meta,
726
+ )
727
+
728
+
729
+ def _remove_auth_from_container(container: dict, security_parameters: list[Mapping[str, Any]], location: str) -> None:
730
+ for parameter in security_parameters:
731
+ name = parameter["name"]
732
+ if parameter["in"] == location:
733
+ container.pop(name, None)
734
+
735
+
736
+ def _set_auth_for_case(case: Case, parameter: Mapping[str, Any]) -> None:
737
+ name = parameter["name"]
738
+ for location, attr_name in (
739
+ ("header", "headers"),
740
+ ("query", "query"),
741
+ ("cookie", "cookies"),
742
+ ):
743
+ if parameter["in"] == location:
744
+ container = getattr(case, attr_name, {})
745
+ # Could happen in the negative testing mode
746
+ if not isinstance(container, dict):
747
+ container = {}
748
+ container[name] = "SCHEMATHESIS-INVALID-VALUE"
749
+ setattr(case, attr_name, container)
750
+
751
+
752
+ @dataclass
753
+ class ResourcePath:
754
+ """A path to a resource with variables."""
755
+
756
+ value: str
757
+ variables: dict[str, str]
758
+
759
+ __slots__ = ("value", "variables")
760
+
761
+ def get(self, key: str) -> str:
762
+ return self.variables[key.lstrip("{").rstrip("}")]
763
+
764
+
765
+ def _is_prefix_operation(lhs: ResourcePath, rhs: ResourcePath) -> bool:
766
+ lhs_parts = lhs.value.rstrip("/").split("/")
767
+ rhs_parts = rhs.value.rstrip("/").split("/")
768
+
769
+ # Left has more parts, can't be a prefix
770
+ if len(lhs_parts) > len(rhs_parts):
771
+ return False
772
+
773
+ for left, right in zip(lhs_parts, rhs_parts):
774
+ if left.startswith("{") and right.startswith("{"):
775
+ if str(lhs.get(left)) != str(rhs.get(right)):
776
+ return False
777
+ elif left != right and left.rstrip("s") != right.rstrip("s"):
778
+ # Parts don't match, not a prefix
779
+ return False
780
+
781
+ # If we've reached this point, the LHS path is a prefix of the RHS path
782
+ return True