schemathesis 3.25.5__py3-none-any.whl → 3.39.7__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 (146) hide show
  1. schemathesis/__init__.py +6 -6
  2. schemathesis/_compat.py +2 -2
  3. schemathesis/_dependency_versions.py +4 -2
  4. schemathesis/_hypothesis.py +369 -56
  5. schemathesis/_lazy_import.py +1 -0
  6. schemathesis/_override.py +5 -4
  7. schemathesis/_patches.py +21 -0
  8. schemathesis/_rate_limiter.py +7 -0
  9. schemathesis/_xml.py +75 -22
  10. schemathesis/auths.py +78 -16
  11. schemathesis/checks.py +21 -9
  12. schemathesis/cli/__init__.py +793 -448
  13. schemathesis/cli/__main__.py +4 -0
  14. schemathesis/cli/callbacks.py +58 -13
  15. schemathesis/cli/cassettes.py +233 -47
  16. schemathesis/cli/constants.py +8 -2
  17. schemathesis/cli/context.py +24 -4
  18. schemathesis/cli/debug.py +2 -1
  19. schemathesis/cli/handlers.py +4 -1
  20. schemathesis/cli/junitxml.py +103 -22
  21. schemathesis/cli/options.py +15 -4
  22. schemathesis/cli/output/default.py +286 -115
  23. schemathesis/cli/output/short.py +25 -6
  24. schemathesis/cli/reporting.py +79 -0
  25. schemathesis/cli/sanitization.py +6 -0
  26. schemathesis/code_samples.py +5 -3
  27. schemathesis/constants.py +1 -0
  28. schemathesis/contrib/openapi/__init__.py +1 -1
  29. schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
  30. schemathesis/contrib/openapi/formats/uuid.py +2 -1
  31. schemathesis/contrib/unique_data.py +3 -3
  32. schemathesis/exceptions.py +76 -65
  33. schemathesis/experimental/__init__.py +35 -0
  34. schemathesis/extra/_aiohttp.py +1 -0
  35. schemathesis/extra/_flask.py +4 -1
  36. schemathesis/extra/_server.py +1 -0
  37. schemathesis/extra/pytest_plugin.py +17 -25
  38. schemathesis/failures.py +77 -9
  39. schemathesis/filters.py +185 -8
  40. schemathesis/fixups/__init__.py +1 -0
  41. schemathesis/fixups/fast_api.py +2 -2
  42. schemathesis/fixups/utf8_bom.py +1 -2
  43. schemathesis/generation/__init__.py +20 -36
  44. schemathesis/generation/_hypothesis.py +59 -0
  45. schemathesis/generation/_methods.py +44 -0
  46. schemathesis/generation/coverage.py +931 -0
  47. schemathesis/graphql.py +0 -1
  48. schemathesis/hooks.py +89 -12
  49. schemathesis/internal/checks.py +84 -0
  50. schemathesis/internal/copy.py +22 -3
  51. schemathesis/internal/deprecation.py +6 -2
  52. schemathesis/internal/diff.py +15 -0
  53. schemathesis/internal/extensions.py +27 -0
  54. schemathesis/internal/jsonschema.py +2 -1
  55. schemathesis/internal/output.py +68 -0
  56. schemathesis/internal/result.py +1 -1
  57. schemathesis/internal/transformation.py +11 -0
  58. schemathesis/lazy.py +138 -25
  59. schemathesis/loaders.py +7 -5
  60. schemathesis/models.py +323 -213
  61. schemathesis/parameters.py +4 -0
  62. schemathesis/runner/__init__.py +72 -22
  63. schemathesis/runner/events.py +86 -6
  64. schemathesis/runner/impl/context.py +104 -0
  65. schemathesis/runner/impl/core.py +447 -187
  66. schemathesis/runner/impl/solo.py +19 -29
  67. schemathesis/runner/impl/threadpool.py +70 -79
  68. schemathesis/{cli → runner}/probes.py +37 -25
  69. schemathesis/runner/serialization.py +150 -17
  70. schemathesis/sanitization.py +5 -1
  71. schemathesis/schemas.py +170 -102
  72. schemathesis/serializers.py +17 -4
  73. schemathesis/service/ci.py +1 -0
  74. schemathesis/service/client.py +39 -6
  75. schemathesis/service/events.py +5 -1
  76. schemathesis/service/extensions.py +224 -0
  77. schemathesis/service/hosts.py +6 -2
  78. schemathesis/service/metadata.py +25 -0
  79. schemathesis/service/models.py +211 -2
  80. schemathesis/service/report.py +6 -6
  81. schemathesis/service/serialization.py +60 -71
  82. schemathesis/service/usage.py +1 -0
  83. schemathesis/specs/graphql/_cache.py +26 -0
  84. schemathesis/specs/graphql/loaders.py +25 -5
  85. schemathesis/specs/graphql/nodes.py +1 -0
  86. schemathesis/specs/graphql/scalars.py +2 -2
  87. schemathesis/specs/graphql/schemas.py +130 -100
  88. schemathesis/specs/graphql/validation.py +1 -2
  89. schemathesis/specs/openapi/__init__.py +1 -0
  90. schemathesis/specs/openapi/_cache.py +123 -0
  91. schemathesis/specs/openapi/_hypothesis.py +79 -61
  92. schemathesis/specs/openapi/checks.py +504 -25
  93. schemathesis/specs/openapi/converter.py +31 -4
  94. schemathesis/specs/openapi/definitions.py +10 -17
  95. schemathesis/specs/openapi/examples.py +143 -31
  96. schemathesis/specs/openapi/expressions/__init__.py +37 -2
  97. schemathesis/specs/openapi/expressions/context.py +1 -1
  98. schemathesis/specs/openapi/expressions/extractors.py +26 -0
  99. schemathesis/specs/openapi/expressions/lexer.py +20 -18
  100. schemathesis/specs/openapi/expressions/nodes.py +29 -6
  101. schemathesis/specs/openapi/expressions/parser.py +26 -5
  102. schemathesis/specs/openapi/formats.py +44 -0
  103. schemathesis/specs/openapi/links.py +125 -42
  104. schemathesis/specs/openapi/loaders.py +77 -36
  105. schemathesis/specs/openapi/media_types.py +34 -0
  106. schemathesis/specs/openapi/negative/__init__.py +6 -3
  107. schemathesis/specs/openapi/negative/mutations.py +21 -6
  108. schemathesis/specs/openapi/parameters.py +39 -25
  109. schemathesis/specs/openapi/patterns.py +137 -0
  110. schemathesis/specs/openapi/references.py +37 -7
  111. schemathesis/specs/openapi/schemas.py +368 -242
  112. schemathesis/specs/openapi/security.py +25 -7
  113. schemathesis/specs/openapi/serialization.py +1 -0
  114. schemathesis/specs/openapi/stateful/__init__.py +198 -70
  115. schemathesis/specs/openapi/stateful/statistic.py +198 -0
  116. schemathesis/specs/openapi/stateful/types.py +14 -0
  117. schemathesis/specs/openapi/utils.py +6 -1
  118. schemathesis/specs/openapi/validation.py +1 -0
  119. schemathesis/stateful/__init__.py +35 -21
  120. schemathesis/stateful/config.py +97 -0
  121. schemathesis/stateful/context.py +135 -0
  122. schemathesis/stateful/events.py +274 -0
  123. schemathesis/stateful/runner.py +309 -0
  124. schemathesis/stateful/sink.py +68 -0
  125. schemathesis/stateful/state_machine.py +67 -38
  126. schemathesis/stateful/statistic.py +22 -0
  127. schemathesis/stateful/validation.py +100 -0
  128. schemathesis/targets.py +33 -1
  129. schemathesis/throttling.py +25 -5
  130. schemathesis/transports/__init__.py +354 -0
  131. schemathesis/transports/asgi.py +7 -0
  132. schemathesis/transports/auth.py +25 -2
  133. schemathesis/transports/content_types.py +3 -1
  134. schemathesis/transports/headers.py +2 -1
  135. schemathesis/transports/responses.py +9 -4
  136. schemathesis/types.py +9 -0
  137. schemathesis/utils.py +11 -16
  138. schemathesis-3.39.7.dist-info/METADATA +293 -0
  139. schemathesis-3.39.7.dist-info/RECORD +160 -0
  140. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
  141. schemathesis/specs/openapi/filters.py +0 -49
  142. schemathesis/specs/openapi/stateful/links.py +0 -92
  143. schemathesis-3.25.5.dist-info/METADATA +0 -356
  144. schemathesis-3.25.5.dist-info/RECORD +0 -134
  145. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
  146. {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/licenses/LICENSE +0 -0
schemathesis/_xml.py CHANGED
@@ -1,8 +1,11 @@
1
1
  """XML serialization."""
2
+
2
3
  from __future__ import annotations
4
+
5
+ import re
3
6
  from io import StringIO
4
7
  from typing import Any, Dict, List, Union
5
- from xml.etree import ElementTree
8
+ from unicodedata import normalize
6
9
 
7
10
  from .exceptions import UnboundPrefixError
8
11
  from .internal.copy import fast_deepcopy
@@ -30,24 +33,9 @@ def _to_xml(value: Any, raw_schema: dict[str, Any] | None, resolved_schema: dict
30
33
  namespace_stack: list[str] = []
31
34
  _write_xml(buffer, value, tag, resolved_schema, namespace_stack)
32
35
  data = buffer.getvalue()
33
- if not is_valid_xml(data):
34
- from hypothesis import reject
35
-
36
- reject()
37
36
  return {"data": data.encode("utf8")}
38
37
 
39
38
 
40
- _from_string = ElementTree.fromstring
41
-
42
-
43
- def is_valid_xml(data: str) -> bool:
44
- try:
45
- _from_string(f"<root xmlns:smp='{NAMESPACE_URL}'>{data}</root>")
46
- return True
47
- except ElementTree.ParseError:
48
- return False
49
-
50
-
51
39
  def _get_xml_tag(raw_schema: dict[str, Any] | None, resolved_schema: dict[str, Any] | None) -> str:
52
40
  # On the top level we need to detect the proper XML tag, in other cases it is known from object properties
53
41
  if (resolved_schema or {}).get("xml", {}).get("name"):
@@ -96,12 +84,15 @@ def _write_object(
96
84
  ) -> None:
97
85
  options = (schema or {}).get("xml", {})
98
86
  push_namespace_if_any(stack, options)
87
+ tag = _sanitize_xml_name(tag)
99
88
  if "prefix" in options:
100
89
  tag = f"{options['prefix']}:{tag}"
101
90
  buffer.write(f"<{tag}")
102
91
  if "namespace" in options:
103
92
  _write_namespace(buffer, options)
104
- attributes = []
93
+
94
+ attribute_namespaces = {}
95
+ attributes = {}
105
96
  children_buffer = StringIO()
106
97
  properties = (schema or {}).get("properties", {})
107
98
  for child_name, value in obj.items():
@@ -109,18 +100,35 @@ def _write_object(
109
100
  child_options = property_schema.get("xml", {})
110
101
  push_namespace_if_any(stack, child_options)
111
102
  child_tag = child_options.get("name", child_name)
103
+
104
+ if child_options.get("attribute", False):
105
+ if child_options.get("prefix") and child_options.get("namespace"):
106
+ _validate_prefix(child_options, stack)
107
+ prefix = child_options["prefix"]
108
+ attr_name = f"{prefix}:{_sanitize_xml_name(child_tag)}"
109
+ # Store namespace declaration
110
+ attribute_namespaces[prefix] = child_options["namespace"]
111
+ else:
112
+ attr_name = _sanitize_xml_name(child_tag)
113
+
114
+ if attr_name not in attributes: # Only keep first occurrence
115
+ attributes[attr_name] = f'{attr_name}="{_escape_xml(value)}"'
116
+ continue
117
+
118
+ child_tag = _sanitize_xml_name(child_tag)
112
119
  if child_options.get("prefix"):
113
120
  _validate_prefix(child_options, stack)
114
121
  prefix = child_options["prefix"]
115
122
  child_tag = f"{prefix}:{child_tag}"
116
- if child_options.get("attribute", False):
117
- attributes.append(f'{child_tag}="{value}"')
118
- continue
119
123
  _write_xml(children_buffer, value, child_tag, property_schema, stack)
120
124
  pop_namespace_if_any(stack, child_options)
121
125
 
126
+ # Write namespace declarations for attributes
127
+ for prefix, namespace in attribute_namespaces.items():
128
+ buffer.write(f' xmlns:{prefix}="{namespace}"')
129
+
122
130
  if attributes:
123
- buffer.write(f" {' '.join(attributes)}")
131
+ buffer.write(f" {' '.join(attributes.values())}")
124
132
  buffer.write(">")
125
133
  buffer.write(children_buffer.getvalue())
126
134
  buffer.write(f"</{tag}>")
@@ -167,7 +175,7 @@ def _write_primitive(
167
175
  buffer.write(f"<{tag}")
168
176
  if "namespace" in xml_options:
169
177
  _write_namespace(buffer, xml_options)
170
- buffer.write(f">{obj}</{tag}>")
178
+ buffer.write(f">{_escape_xml(obj)}</{tag}>")
171
179
 
172
180
 
173
181
  def _write_namespace(buffer: StringIO, options: dict[str, Any]) -> None:
@@ -180,3 +188,48 @@ def _write_namespace(buffer: StringIO, options: dict[str, Any]) -> None:
180
188
  def _get_tag_name_from_reference(reference: str) -> str:
181
189
  """Extract object name from a reference."""
182
190
  return reference.rsplit("/", maxsplit=1)[1]
191
+
192
+
193
+ def _escape_xml(value: JSON) -> str:
194
+ """Escape special characters in XML content."""
195
+ if isinstance(value, (int, float, bool)):
196
+ return str(value)
197
+ if value is None:
198
+ return ""
199
+
200
+ # Filter out invalid XML characters
201
+ cleaned = "".join(
202
+ char
203
+ for char in str(value)
204
+ if (
205
+ char in "\t\n\r"
206
+ or 0x20 <= ord(char) <= 0xD7FF
207
+ or 0xE000 <= ord(char) <= 0xFFFD
208
+ or 0x10000 <= ord(char) <= 0x10FFFF
209
+ )
210
+ )
211
+
212
+ replacements = {
213
+ "&": "&amp;",
214
+ "<": "&lt;",
215
+ ">": "&gt;",
216
+ '"': "&quot;",
217
+ "'": "&apos;",
218
+ }
219
+ return "".join(replacements.get(c, c) for c in cleaned)
220
+
221
+
222
+ def _sanitize_xml_name(name: str) -> str:
223
+ """Sanitize a string to be a valid XML element name."""
224
+ if not name:
225
+ return "element"
226
+
227
+ name = normalize("NFKC", str(name))
228
+
229
+ name = name.replace(":", "_")
230
+ sanitized = re.sub(r"[^a-zA-Z0-9_\-.]", "_", name)
231
+
232
+ if not sanitized[0].isalpha() and sanitized[0] != "_":
233
+ sanitized = "x_" + sanitized
234
+
235
+ return sanitized
schemathesis/auths.py CHANGED
@@ -1,5 +1,7 @@
1
1
  """Support for custom API authentication mechanisms."""
2
+
2
3
  from __future__ import annotations
4
+
3
5
  import inspect
4
6
  import threading
5
7
  import time
@@ -10,20 +12,22 @@ from typing import (
10
12
  Any,
11
13
  Callable,
12
14
  Generic,
15
+ Protocol,
13
16
  TypeVar,
17
+ Union,
14
18
  overload,
15
19
  runtime_checkable,
16
- Protocol,
17
20
  )
18
21
 
19
22
  from .exceptions import UsageError
20
23
  from .filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
21
- from .types import GenericTest
22
24
 
23
25
  if TYPE_CHECKING:
24
- from .models import APIOperation, Case
25
26
  import requests.auth
26
27
 
28
+ from .models import APIOperation, Case
29
+ from .types import GenericTest
30
+
27
31
  DEFAULT_REFRESH_INTERVAL = 300
28
32
  AUTH_STORAGE_ATTRIBUTE_NAME = "_schemathesis_auth"
29
33
  Auth = TypeVar("Auth")
@@ -41,6 +45,9 @@ class AuthContext:
41
45
  app: Any | None
42
46
 
43
47
 
48
+ CacheKeyFunction = Callable[["Case", "AuthContext"], Union[str, int]]
49
+
50
+
44
51
  @runtime_checkable
45
52
  class AuthProvider(Generic[Auth], Protocol):
46
53
  """Get authentication data for an API and set it on the generated test cases."""
@@ -96,16 +103,24 @@ class CachingAuthProvider(Generic[Auth]):
96
103
 
97
104
  def get(self, case: Case, context: AuthContext) -> Auth | None:
98
105
  """Get cached auth value."""
99
- if self.cache_entry is None or self.timer() >= self.cache_entry.expires:
106
+ cache_entry = self._get_cache_entry(case, context)
107
+ if cache_entry is None or self.timer() >= cache_entry.expires:
100
108
  with self._refresh_lock:
101
- if not (self.cache_entry is None or self.timer() >= self.cache_entry.expires):
109
+ cache_entry = self._get_cache_entry(case, context)
110
+ if not (cache_entry is None or self.timer() >= cache_entry.expires):
102
111
  # Another thread updated the cache
103
- return self.cache_entry.data
112
+ return cache_entry.data
104
113
  # We know that optional auth is possible only inside a higher-level wrapper
105
114
  data: Auth = _provider_get(self.provider, case, context) # type: ignore[assignment]
106
- self.cache_entry = CacheEntry(data=data, expires=self.timer() + self.refresh_interval)
115
+ self._set_cache_entry(data, case, context)
107
116
  return data
108
- return self.cache_entry.data
117
+ return cache_entry.data
118
+
119
+ def _get_cache_entry(self, case: Case, context: AuthContext) -> CacheEntry[Auth] | None:
120
+ return self.cache_entry
121
+
122
+ def _set_cache_entry(self, data: Auth, case: Case, context: AuthContext) -> None:
123
+ self.cache_entry = CacheEntry(data=data, expires=self.timer() + self.refresh_interval)
109
124
 
110
125
  def set(self, case: Case, data: Auth, context: AuthContext) -> None:
111
126
  """Set auth data on the `Case` instance.
@@ -115,6 +130,25 @@ class CachingAuthProvider(Generic[Auth]):
115
130
  self.provider.set(case, data, context)
116
131
 
117
132
 
133
+ def _noop_key_function(case: Case, context: AuthContext) -> str:
134
+ # Never used
135
+ raise NotImplementedError
136
+
137
+
138
+ @dataclass
139
+ class KeyedCachingAuthProvider(CachingAuthProvider[Auth]):
140
+ cache_by_key: CacheKeyFunction = _noop_key_function
141
+ cache_entries: dict[str | int, CacheEntry[Auth] | None] = field(default_factory=dict)
142
+
143
+ def _get_cache_entry(self, case: Case, context: AuthContext) -> CacheEntry[Auth] | None:
144
+ key = self.cache_by_key(case, context)
145
+ return self.cache_entries.get(key)
146
+
147
+ def _set_cache_entry(self, data: Auth, case: Case, context: AuthContext) -> None:
148
+ key = self.cache_by_key(case, context)
149
+ self.cache_entries[key] = CacheEntry(data=data, expires=self.timer() + self.refresh_interval)
150
+
151
+
118
152
  class FilterableRegisterAuth(Protocol):
119
153
  """Protocol that adds filters to the return value of `register`."""
120
154
 
@@ -243,6 +277,7 @@ class AuthStorage(Generic[Auth]):
243
277
  self,
244
278
  *,
245
279
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
280
+ cache_by_key: CacheKeyFunction | None = None,
246
281
  ) -> FilterableRegisterAuth:
247
282
  pass
248
283
 
@@ -252,6 +287,7 @@ class AuthStorage(Generic[Auth]):
252
287
  provider_class: type[AuthProvider],
253
288
  *,
254
289
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
290
+ cache_by_key: CacheKeyFunction | None = None,
255
291
  ) -> FilterableApplyAuth:
256
292
  pass
257
293
 
@@ -260,10 +296,11 @@ class AuthStorage(Generic[Auth]):
260
296
  provider_class: type[AuthProvider] | None = None,
261
297
  *,
262
298
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
299
+ cache_by_key: CacheKeyFunction | None = None,
263
300
  ) -> FilterableRegisterAuth | FilterableApplyAuth:
264
301
  if provider_class is not None:
265
- return self.apply(provider_class, refresh_interval=refresh_interval)
266
- return self.register(refresh_interval=refresh_interval)
302
+ return self.apply(provider_class, refresh_interval=refresh_interval, cache_by_key=cache_by_key)
303
+ return self.register(refresh_interval=refresh_interval, cache_by_key=cache_by_key)
267
304
 
268
305
  def set_from_requests(self, auth: requests.auth.AuthBase) -> FilterableRequestsAuth:
269
306
  """Use `requests` auth instance as an auth provider."""
@@ -283,6 +320,7 @@ class AuthStorage(Generic[Auth]):
283
320
  *,
284
321
  provider_class: type[AuthProvider],
285
322
  refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
323
+ cache_by_key: CacheKeyFunction | None = None,
286
324
  filter_set: FilterSet,
287
325
  ) -> None:
288
326
  if not issubclass(provider_class, AuthProvider):
@@ -292,16 +330,27 @@ class AuthStorage(Generic[Auth]):
292
330
  )
293
331
  provider: AuthProvider
294
332
  # Apply caching if desired
333
+ instance = provider_class()
295
334
  if refresh_interval is not None:
296
- provider = CachingAuthProvider(provider_class(), refresh_interval=refresh_interval)
335
+ if cache_by_key is None:
336
+ provider = CachingAuthProvider(instance, refresh_interval=refresh_interval)
337
+ else:
338
+ provider = KeyedCachingAuthProvider(
339
+ instance, refresh_interval=refresh_interval, cache_by_key=cache_by_key
340
+ )
297
341
  else:
298
- provider = provider_class()
342
+ provider = instance
299
343
  # Store filters if any
300
344
  if not filter_set.is_empty():
301
345
  provider = SelectiveAuthProvider(provider, filter_set)
302
346
  self.providers.append(provider)
303
347
 
304
- def register(self, *, refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL) -> FilterableRegisterAuth:
348
+ def register(
349
+ self,
350
+ *,
351
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
352
+ cache_by_key: CacheKeyFunction | None = None,
353
+ ) -> FilterableRegisterAuth:
305
354
  """Register a new auth provider.
306
355
 
307
356
  .. code-block:: python
@@ -323,7 +372,12 @@ class AuthStorage(Generic[Auth]):
323
372
  filter_set = FilterSet()
324
373
 
325
374
  def wrapper(provider_class: type[AuthProvider]) -> type[AuthProvider]:
326
- self._set_provider(provider_class=provider_class, refresh_interval=refresh_interval, filter_set=filter_set)
375
+ self._set_provider(
376
+ provider_class=provider_class,
377
+ refresh_interval=refresh_interval,
378
+ filter_set=filter_set,
379
+ cache_by_key=cache_by_key,
380
+ )
327
381
  return provider_class
328
382
 
329
383
  attach_filter_chain(wrapper, "apply_to", filter_set.include)
@@ -339,7 +393,11 @@ class AuthStorage(Generic[Auth]):
339
393
  self.providers = []
340
394
 
341
395
  def apply(
342
- self, provider_class: type[AuthProvider], *, refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL
396
+ self,
397
+ provider_class: type[AuthProvider],
398
+ *,
399
+ refresh_interval: int | None = DEFAULT_REFRESH_INTERVAL,
400
+ cache_by_key: CacheKeyFunction | None = None,
343
401
  ) -> FilterableApplyAuth:
344
402
  """Register auth provider only on one test function.
345
403
 
@@ -363,7 +421,10 @@ class AuthStorage(Generic[Auth]):
363
421
  def wrapper(test: GenericTest) -> GenericTest:
364
422
  auth_storage = self.add_auth_storage(test)
365
423
  auth_storage._set_provider(
366
- provider_class=provider_class, refresh_interval=refresh_interval, filter_set=filter_set
424
+ provider_class=provider_class,
425
+ refresh_interval=refresh_interval,
426
+ filter_set=filter_set,
427
+ cache_by_key=cache_by_key,
367
428
  )
368
429
  return test
369
430
 
@@ -389,6 +450,7 @@ class AuthStorage(Generic[Auth]):
389
450
  data: Auth | None = _provider_get(provider, case, context)
390
451
  if data is not None:
391
452
  provider.set(case, data, context)
453
+ case._has_explicit_auth = True
392
454
  break
393
455
 
394
456
 
schemathesis/checks.py CHANGED
@@ -1,22 +1,26 @@
1
1
  from __future__ import annotations
2
- from typing import TYPE_CHECKING
2
+
3
3
  import json
4
+ from typing import TYPE_CHECKING
4
5
 
5
6
  from . import failures
6
- from .exceptions import get_server_error, get_response_parsing_error
7
+ from .exceptions import get_response_parsing_error, get_server_error
7
8
  from .specs.openapi.checks import (
8
9
  content_type_conformance,
10
+ ignored_auth,
11
+ negative_data_rejection,
9
12
  response_headers_conformance,
10
13
  response_schema_conformance,
11
14
  status_code_conformance,
12
15
  )
13
16
 
14
17
  if TYPE_CHECKING:
18
+ from .internal.checks import CheckContext, CheckFunction
19
+ from .models import Case
15
20
  from .transports.responses import GenericResponse
16
- from .models import Case, CheckFunction
17
21
 
18
22
 
19
- def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
23
+ def not_a_server_error(ctx: CheckContext, response: GenericResponse, case: Case) -> bool | None:
20
24
  """A check to verify that the response is not a server-side error."""
21
25
  from .specs.graphql.schemas import GraphQLCase
22
26
  from .specs.graphql.validation import validate_graphql_response
@@ -24,25 +28,31 @@ def not_a_server_error(response: GenericResponse, case: Case) -> bool | None:
24
28
 
25
29
  status_code = response.status_code
26
30
  if status_code >= 500:
27
- exc_class = get_server_error(status_code)
31
+ exc_class = get_server_error(case.operation.verbose_name, status_code)
28
32
  raise exc_class(failures.ServerError.title, context=failures.ServerError(status_code=status_code))
29
33
  if isinstance(case, GraphQLCase):
30
34
  try:
31
35
  data = get_json(response)
32
36
  validate_graphql_response(data)
33
37
  except json.JSONDecodeError as exc:
34
- exc_class = get_response_parsing_error(exc)
38
+ exc_class = get_response_parsing_error(case.operation.verbose_name, exc)
35
39
  context = failures.JSONDecodeErrorContext.from_exception(exc)
36
40
  raise exc_class(context.title, context=context) from exc
37
41
  return None
38
42
 
39
43
 
44
+ def _make_max_response_time_failure_message(elapsed_time: float, max_response_time: int) -> str:
45
+ return f"Actual: {elapsed_time:.2f}ms\nLimit: {max_response_time}.00ms"
46
+
47
+
40
48
  DEFAULT_CHECKS: tuple[CheckFunction, ...] = (not_a_server_error,)
41
49
  OPTIONAL_CHECKS = (
42
50
  status_code_conformance,
43
51
  content_type_conformance,
44
52
  response_headers_conformance,
45
53
  response_schema_conformance,
54
+ negative_data_rejection,
55
+ ignored_auth,
46
56
  )
47
57
  ALL_CHECKS: tuple[CheckFunction, ...] = DEFAULT_CHECKS + OPTIONAL_CHECKS
48
58
 
@@ -55,14 +65,16 @@ def register(check: CheckFunction) -> CheckFunction:
55
65
  .. code-block:: python
56
66
 
57
67
  @schemathesis.check
58
- def new_check(response, case):
68
+ def new_check(ctx, response, case):
59
69
  # some awesome assertions!
60
70
  ...
61
71
  """
62
72
  from . import cli
73
+ from .internal.checks import wrap_check
63
74
 
75
+ _check = wrap_check(check)
64
76
  global ALL_CHECKS
65
77
 
66
- ALL_CHECKS += (check,)
67
- cli.CHECKS_TYPE.choices += (check.__name__,) # type: ignore
78
+ ALL_CHECKS += (_check,)
79
+ cli.CHECKS_TYPE.choices += (_check.__name__,) # type: ignore
68
80
  return check