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.
- schemathesis/__init__.py +6 -6
- schemathesis/_compat.py +2 -2
- schemathesis/_dependency_versions.py +4 -2
- schemathesis/_hypothesis.py +369 -56
- schemathesis/_lazy_import.py +1 -0
- schemathesis/_override.py +5 -4
- schemathesis/_patches.py +21 -0
- schemathesis/_rate_limiter.py +7 -0
- schemathesis/_xml.py +75 -22
- schemathesis/auths.py +78 -16
- schemathesis/checks.py +21 -9
- schemathesis/cli/__init__.py +793 -448
- schemathesis/cli/__main__.py +4 -0
- schemathesis/cli/callbacks.py +58 -13
- schemathesis/cli/cassettes.py +233 -47
- schemathesis/cli/constants.py +8 -2
- schemathesis/cli/context.py +24 -4
- schemathesis/cli/debug.py +2 -1
- schemathesis/cli/handlers.py +4 -1
- schemathesis/cli/junitxml.py +103 -22
- schemathesis/cli/options.py +15 -4
- schemathesis/cli/output/default.py +286 -115
- schemathesis/cli/output/short.py +25 -6
- schemathesis/cli/reporting.py +79 -0
- schemathesis/cli/sanitization.py +6 -0
- schemathesis/code_samples.py +5 -3
- schemathesis/constants.py +1 -0
- schemathesis/contrib/openapi/__init__.py +1 -1
- schemathesis/contrib/openapi/fill_missing_examples.py +3 -1
- schemathesis/contrib/openapi/formats/uuid.py +2 -1
- schemathesis/contrib/unique_data.py +3 -3
- schemathesis/exceptions.py +76 -65
- schemathesis/experimental/__init__.py +35 -0
- schemathesis/extra/_aiohttp.py +1 -0
- schemathesis/extra/_flask.py +4 -1
- schemathesis/extra/_server.py +1 -0
- schemathesis/extra/pytest_plugin.py +17 -25
- schemathesis/failures.py +77 -9
- schemathesis/filters.py +185 -8
- schemathesis/fixups/__init__.py +1 -0
- schemathesis/fixups/fast_api.py +2 -2
- schemathesis/fixups/utf8_bom.py +1 -2
- schemathesis/generation/__init__.py +20 -36
- schemathesis/generation/_hypothesis.py +59 -0
- schemathesis/generation/_methods.py +44 -0
- schemathesis/generation/coverage.py +931 -0
- schemathesis/graphql.py +0 -1
- schemathesis/hooks.py +89 -12
- schemathesis/internal/checks.py +84 -0
- schemathesis/internal/copy.py +22 -3
- schemathesis/internal/deprecation.py +6 -2
- schemathesis/internal/diff.py +15 -0
- schemathesis/internal/extensions.py +27 -0
- schemathesis/internal/jsonschema.py +2 -1
- schemathesis/internal/output.py +68 -0
- schemathesis/internal/result.py +1 -1
- schemathesis/internal/transformation.py +11 -0
- schemathesis/lazy.py +138 -25
- schemathesis/loaders.py +7 -5
- schemathesis/models.py +323 -213
- schemathesis/parameters.py +4 -0
- schemathesis/runner/__init__.py +72 -22
- schemathesis/runner/events.py +86 -6
- schemathesis/runner/impl/context.py +104 -0
- schemathesis/runner/impl/core.py +447 -187
- schemathesis/runner/impl/solo.py +19 -29
- schemathesis/runner/impl/threadpool.py +70 -79
- schemathesis/{cli → runner}/probes.py +37 -25
- schemathesis/runner/serialization.py +150 -17
- schemathesis/sanitization.py +5 -1
- schemathesis/schemas.py +170 -102
- schemathesis/serializers.py +17 -4
- schemathesis/service/ci.py +1 -0
- schemathesis/service/client.py +39 -6
- schemathesis/service/events.py +5 -1
- schemathesis/service/extensions.py +224 -0
- schemathesis/service/hosts.py +6 -2
- schemathesis/service/metadata.py +25 -0
- schemathesis/service/models.py +211 -2
- schemathesis/service/report.py +6 -6
- schemathesis/service/serialization.py +60 -71
- schemathesis/service/usage.py +1 -0
- schemathesis/specs/graphql/_cache.py +26 -0
- schemathesis/specs/graphql/loaders.py +25 -5
- schemathesis/specs/graphql/nodes.py +1 -0
- schemathesis/specs/graphql/scalars.py +2 -2
- schemathesis/specs/graphql/schemas.py +130 -100
- schemathesis/specs/graphql/validation.py +1 -2
- schemathesis/specs/openapi/__init__.py +1 -0
- schemathesis/specs/openapi/_cache.py +123 -0
- schemathesis/specs/openapi/_hypothesis.py +79 -61
- schemathesis/specs/openapi/checks.py +504 -25
- schemathesis/specs/openapi/converter.py +31 -4
- schemathesis/specs/openapi/definitions.py +10 -17
- schemathesis/specs/openapi/examples.py +143 -31
- schemathesis/specs/openapi/expressions/__init__.py +37 -2
- schemathesis/specs/openapi/expressions/context.py +1 -1
- schemathesis/specs/openapi/expressions/extractors.py +26 -0
- schemathesis/specs/openapi/expressions/lexer.py +20 -18
- schemathesis/specs/openapi/expressions/nodes.py +29 -6
- schemathesis/specs/openapi/expressions/parser.py +26 -5
- schemathesis/specs/openapi/formats.py +44 -0
- schemathesis/specs/openapi/links.py +125 -42
- schemathesis/specs/openapi/loaders.py +77 -36
- schemathesis/specs/openapi/media_types.py +34 -0
- schemathesis/specs/openapi/negative/__init__.py +6 -3
- schemathesis/specs/openapi/negative/mutations.py +21 -6
- schemathesis/specs/openapi/parameters.py +39 -25
- schemathesis/specs/openapi/patterns.py +137 -0
- schemathesis/specs/openapi/references.py +37 -7
- schemathesis/specs/openapi/schemas.py +368 -242
- schemathesis/specs/openapi/security.py +25 -7
- schemathesis/specs/openapi/serialization.py +1 -0
- schemathesis/specs/openapi/stateful/__init__.py +198 -70
- schemathesis/specs/openapi/stateful/statistic.py +198 -0
- schemathesis/specs/openapi/stateful/types.py +14 -0
- schemathesis/specs/openapi/utils.py +6 -1
- schemathesis/specs/openapi/validation.py +1 -0
- schemathesis/stateful/__init__.py +35 -21
- schemathesis/stateful/config.py +97 -0
- schemathesis/stateful/context.py +135 -0
- schemathesis/stateful/events.py +274 -0
- schemathesis/stateful/runner.py +309 -0
- schemathesis/stateful/sink.py +68 -0
- schemathesis/stateful/state_machine.py +67 -38
- schemathesis/stateful/statistic.py +22 -0
- schemathesis/stateful/validation.py +100 -0
- schemathesis/targets.py +33 -1
- schemathesis/throttling.py +25 -5
- schemathesis/transports/__init__.py +354 -0
- schemathesis/transports/asgi.py +7 -0
- schemathesis/transports/auth.py +25 -2
- schemathesis/transports/content_types.py +3 -1
- schemathesis/transports/headers.py +2 -1
- schemathesis/transports/responses.py +9 -4
- schemathesis/types.py +9 -0
- schemathesis/utils.py +11 -16
- schemathesis-3.39.7.dist-info/METADATA +293 -0
- schemathesis-3.39.7.dist-info/RECORD +160 -0
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/WHEEL +1 -1
- schemathesis/specs/openapi/filters.py +0 -49
- schemathesis/specs/openapi/stateful/links.py +0 -92
- schemathesis-3.25.5.dist-info/METADATA +0 -356
- schemathesis-3.25.5.dist-info/RECORD +0 -134
- {schemathesis-3.25.5.dist-info → schemathesis-3.39.7.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
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
|
+
"&": "&",
|
|
214
|
+
"<": "<",
|
|
215
|
+
">": ">",
|
|
216
|
+
'"': """,
|
|
217
|
+
"'": "'",
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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.
|
|
115
|
+
self._set_cache_entry(data, case, context)
|
|
107
116
|
return data
|
|
108
|
-
return
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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(
|
|
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,
|
|
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,
|
|
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
|
-
|
|
2
|
+
|
|
3
3
|
import json
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from . import failures
|
|
6
|
-
from .exceptions import
|
|
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 += (
|
|
67
|
-
cli.CHECKS_TYPE.choices += (
|
|
78
|
+
ALL_CHECKS += (_check,)
|
|
79
|
+
cli.CHECKS_TYPE.choices += (_check.__name__,) # type: ignore
|
|
68
80
|
return check
|