schemathesis 4.3.16__py3-none-any.whl → 4.3.17__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.
Potentially problematic release.
This version of schemathesis might be problematic. Click here for more details.
- schemathesis/auths.py +24 -3
- schemathesis/cli/commands/run/handlers/output.py +5 -2
- schemathesis/core/errors.py +21 -0
- schemathesis/engine/errors.py +12 -0
- schemathesis/engine/phases/unit/__init__.py +2 -2
- schemathesis/engine/phases/unit/_executor.py +4 -0
- schemathesis/generation/coverage.py +143 -50
- schemathesis/generation/hypothesis/builder.py +28 -7
- schemathesis/generation/meta.py +77 -2
- schemathesis/pytest/lazy.py +58 -12
- schemathesis/pytest/plugin.py +2 -2
- schemathesis/specs/openapi/_hypothesis.py +18 -98
- schemathesis/specs/openapi/adapter/parameters.py +181 -11
- schemathesis/specs/openapi/checks.py +5 -7
- schemathesis/specs/openapi/schemas.py +5 -4
- schemathesis/transport/prepare.py +4 -3
- {schemathesis-4.3.16.dist-info → schemathesis-4.3.17.dist-info}/METADATA +6 -5
- {schemathesis-4.3.16.dist-info → schemathesis-4.3.17.dist-info}/RECORD +21 -21
- {schemathesis-4.3.16.dist-info → schemathesis-4.3.17.dist-info}/WHEEL +0 -0
- {schemathesis-4.3.16.dist-info → schemathesis-4.3.17.dist-info}/entry_points.txt +0 -0
- {schemathesis-4.3.16.dist-info → schemathesis-4.3.17.dist-info}/licenses/LICENSE +0 -0
schemathesis/auths.py
CHANGED
|
@@ -17,7 +17,7 @@ from typing import (
|
|
|
17
17
|
runtime_checkable,
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
from schemathesis.core.errors import IncorrectUsage
|
|
20
|
+
from schemathesis.core.errors import AuthenticationError, IncorrectUsage
|
|
21
21
|
from schemathesis.core.marks import Mark
|
|
22
22
|
from schemathesis.filters import FilterSet, FilterValue, MatcherFunc, attach_filter_chain
|
|
23
23
|
from schemathesis.generation.case import Case
|
|
@@ -128,6 +128,7 @@ class CachingAuthProvider(Generic[Auth]):
|
|
|
128
128
|
|
|
129
129
|
def get(self, case: Case, context: AuthContext) -> Auth | None:
|
|
130
130
|
"""Get cached auth value."""
|
|
131
|
+
__tracebackhide__ = True
|
|
131
132
|
cache_entry = self._get_cache_entry(case, context)
|
|
132
133
|
if cache_entry is None or self.timer() >= cache_entry.expires:
|
|
133
134
|
with self._refresh_lock:
|
|
@@ -136,7 +137,11 @@ class CachingAuthProvider(Generic[Auth]):
|
|
|
136
137
|
# Another thread updated the cache
|
|
137
138
|
return cache_entry.data
|
|
138
139
|
# We know that optional auth is possible only inside a higher-level wrapper
|
|
139
|
-
|
|
140
|
+
try:
|
|
141
|
+
data: Auth = self.provider.get(case, context) # type: ignore[assignment]
|
|
142
|
+
except Exception as exc:
|
|
143
|
+
provider_name = self.provider.__class__.__name__
|
|
144
|
+
raise AuthenticationError(provider_name, "get", str(exc)) from exc
|
|
140
145
|
self._set_cache_entry(data, case, context)
|
|
141
146
|
return data
|
|
142
147
|
return cache_entry.data
|
|
@@ -270,11 +275,25 @@ class SelectiveAuthProvider(Generic[Auth]):
|
|
|
270
275
|
filter_set: FilterSet
|
|
271
276
|
|
|
272
277
|
def get(self, case: Case, context: AuthContext) -> Auth | None:
|
|
278
|
+
__tracebackhide__ = True
|
|
273
279
|
if self.filter_set.match(context):
|
|
274
|
-
|
|
280
|
+
try:
|
|
281
|
+
return self.provider.get(case, context)
|
|
282
|
+
except AuthenticationError:
|
|
283
|
+
# Already wrapped, re-raise as-is
|
|
284
|
+
raise
|
|
285
|
+
except Exception as exc:
|
|
286
|
+
# Need to unwrap to get the actual provider class name
|
|
287
|
+
provider = self.provider
|
|
288
|
+
# Unwrap caching providers
|
|
289
|
+
while isinstance(provider, (CachingAuthProvider, KeyedCachingAuthProvider)):
|
|
290
|
+
provider = provider.provider
|
|
291
|
+
provider_name = provider.__class__.__name__
|
|
292
|
+
raise AuthenticationError(provider_name, "get", str(exc)) from exc
|
|
275
293
|
return None
|
|
276
294
|
|
|
277
295
|
def set(self, case: Case, data: Auth, context: AuthContext) -> None:
|
|
296
|
+
__tracebackhide__ = True
|
|
278
297
|
self.provider.set(case, data, context)
|
|
279
298
|
|
|
280
299
|
|
|
@@ -418,6 +437,7 @@ class AuthStorage(Generic[Auth]):
|
|
|
418
437
|
|
|
419
438
|
def set(self, case: Case, context: AuthContext) -> None:
|
|
420
439
|
"""Set authentication data on a generated test case."""
|
|
440
|
+
__tracebackhide__ = True
|
|
421
441
|
if not self.is_defined:
|
|
422
442
|
raise IncorrectUsage("No auth provider is defined.")
|
|
423
443
|
for provider in self.providers:
|
|
@@ -433,6 +453,7 @@ def set_on_case(case: Case, context: AuthContext, auth_storage: AuthStorage | No
|
|
|
433
453
|
|
|
434
454
|
If there is no auth defined, then this function is no-op.
|
|
435
455
|
"""
|
|
456
|
+
__tracebackhide__ = True
|
|
436
457
|
if auth_storage is not None:
|
|
437
458
|
auth_storage.set(case, context)
|
|
438
459
|
elif case.operation.schema.auth.is_defined:
|
|
@@ -20,13 +20,14 @@ from schemathesis.config import ProjectConfig, ReportFormat, SchemathesisWarning
|
|
|
20
20
|
from schemathesis.core.errors import LoaderError, LoaderErrorKind, format_exception, split_traceback
|
|
21
21
|
from schemathesis.core.failures import MessageBlock, Severity, format_failures
|
|
22
22
|
from schemathesis.core.output import prepare_response_payload
|
|
23
|
+
from schemathesis.core.parameters import ParameterLocation
|
|
23
24
|
from schemathesis.core.result import Ok
|
|
24
25
|
from schemathesis.core.version import SCHEMATHESIS_VERSION
|
|
25
26
|
from schemathesis.engine import Status, events
|
|
26
27
|
from schemathesis.engine.phases import PhaseName, PhaseSkipReason
|
|
27
28
|
from schemathesis.engine.phases.probes import ProbeOutcome
|
|
28
29
|
from schemathesis.engine.recorder import Interaction, ScenarioRecorder
|
|
29
|
-
from schemathesis.generation.meta import CoveragePhaseData
|
|
30
|
+
from schemathesis.generation.meta import CoveragePhaseData, CoverageScenario
|
|
30
31
|
from schemathesis.generation.modes import GenerationMode
|
|
31
32
|
from schemathesis.schemas import ApiStatistic
|
|
32
33
|
|
|
@@ -1079,7 +1080,9 @@ class OutputHandler(EventHandler):
|
|
|
1079
1080
|
return bool(
|
|
1080
1081
|
case.meta
|
|
1081
1082
|
and isinstance(case.meta.phase.data, CoveragePhaseData)
|
|
1082
|
-
and case.meta.phase.data.
|
|
1083
|
+
and case.meta.phase.data.scenario == CoverageScenario.MISSING_PARAMETER
|
|
1084
|
+
and case.meta.phase.data.parameter == "Authorization"
|
|
1085
|
+
and case.meta.phase.data.parameter_location == ParameterLocation.HEADER
|
|
1083
1086
|
)
|
|
1084
1087
|
|
|
1085
1088
|
if SchemathesisWarning.MISSING_AUTH in warnings:
|
schemathesis/core/errors.py
CHANGED
|
@@ -325,6 +325,27 @@ class IncorrectUsage(SchemathesisError):
|
|
|
325
325
|
"""Indicates incorrect usage of Schemathesis' public API."""
|
|
326
326
|
|
|
327
327
|
|
|
328
|
+
class AuthenticationError(SchemathesisError):
|
|
329
|
+
"""Error during authentication provider execution.
|
|
330
|
+
|
|
331
|
+
This error wraps exceptions that occur when obtaining or setting
|
|
332
|
+
authentication data via custom auth providers.
|
|
333
|
+
"""
|
|
334
|
+
|
|
335
|
+
def __init__(self, provider_name: str, method: str, message: str) -> None:
|
|
336
|
+
self.provider_name = provider_name
|
|
337
|
+
self.method = method
|
|
338
|
+
self.message = message
|
|
339
|
+
super().__init__(
|
|
340
|
+
f"Error in '{provider_name}.{method}()': {message}\n\n"
|
|
341
|
+
f"Common causes:\n"
|
|
342
|
+
f" - Auth endpoint returned an error response\n"
|
|
343
|
+
f" - Response format doesn't match expectations (text vs JSON)\n"
|
|
344
|
+
f" - Network or connection issues\n"
|
|
345
|
+
f" - Logic error in the authentication provider implementation"
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
|
|
328
349
|
class NoLinksFound(IncorrectUsage):
|
|
329
350
|
"""Raised when no valid links are available for stateful testing."""
|
|
330
351
|
|
schemathesis/engine/errors.py
CHANGED
|
@@ -16,6 +16,7 @@ from hypothesis import HealthCheck
|
|
|
16
16
|
|
|
17
17
|
from schemathesis import errors
|
|
18
18
|
from schemathesis.core.errors import (
|
|
19
|
+
AuthenticationError,
|
|
19
20
|
InfiniteRecursiveReference,
|
|
20
21
|
InvalidTransition,
|
|
21
22
|
SerializationNotPossible,
|
|
@@ -105,6 +106,7 @@ class EngineErrorInfo:
|
|
|
105
106
|
RuntimeErrorKind.HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR: "Unknown GraphQL Scalar",
|
|
106
107
|
RuntimeErrorKind.SERIALIZATION_UNBOUNDED_PREFIX: "XML serialization error",
|
|
107
108
|
RuntimeErrorKind.SERIALIZATION_NOT_POSSIBLE: "Serialization not possible",
|
|
109
|
+
RuntimeErrorKind.AUTHENTICATION_ERROR: "Authentication Error",
|
|
108
110
|
}.get(self._kind, "Runtime Error")
|
|
109
111
|
|
|
110
112
|
@property
|
|
@@ -165,6 +167,9 @@ class EngineErrorInfo:
|
|
|
165
167
|
|
|
166
168
|
@cached_property
|
|
167
169
|
def traceback(self) -> str:
|
|
170
|
+
# For AuthenticationError, show only the original exception's traceback
|
|
171
|
+
if isinstance(self._error, AuthenticationError) and self._error.__cause__ is not None:
|
|
172
|
+
return format_exception(self._error.__cause__, with_traceback=True)
|
|
168
173
|
return format_exception(self._error, with_traceback=True)
|
|
169
174
|
|
|
170
175
|
def format(self, *, bold: Callable[[str], str] = str, indent: str = " ") -> str:
|
|
@@ -254,6 +259,9 @@ class RuntimeErrorKind(str, enum.Enum):
|
|
|
254
259
|
CONNECTION_OTHER = "connection_other"
|
|
255
260
|
NETWORK_OTHER = "network_other"
|
|
256
261
|
|
|
262
|
+
# Authentication issues
|
|
263
|
+
AUTHENTICATION_ERROR = "authentication_error"
|
|
264
|
+
|
|
257
265
|
# Hypothesis issues
|
|
258
266
|
HYPOTHESIS_UNSATISFIABLE = "hypothesis_unsatisfiable"
|
|
259
267
|
HYPOTHESIS_UNSUPPORTED_GRAPHQL_SCALAR = "hypothesis_unsupported_graphql_scalar"
|
|
@@ -281,6 +289,10 @@ def _classify(*, error: Exception) -> RuntimeErrorKind:
|
|
|
281
289
|
import requests
|
|
282
290
|
from hypothesis import HealthCheck
|
|
283
291
|
|
|
292
|
+
# Authentication errors
|
|
293
|
+
if isinstance(error, AuthenticationError):
|
|
294
|
+
return RuntimeErrorKind.AUTHENTICATION_ERROR
|
|
295
|
+
|
|
284
296
|
# Network-related errors
|
|
285
297
|
if isinstance(error, requests.RequestException):
|
|
286
298
|
if isinstance(error, requests.exceptions.SSLError):
|
|
@@ -11,7 +11,7 @@ import warnings
|
|
|
11
11
|
from queue import Queue
|
|
12
12
|
from typing import TYPE_CHECKING, Any
|
|
13
13
|
|
|
14
|
-
from schemathesis.core.errors import InvalidSchema
|
|
14
|
+
from schemathesis.core.errors import AuthenticationError, InvalidSchema
|
|
15
15
|
from schemathesis.core.result import Ok
|
|
16
16
|
from schemathesis.engine import Status, events
|
|
17
17
|
from schemathesis.engine.phases import PhaseName, PhaseSkipReason
|
|
@@ -182,7 +182,7 @@ def worker_task(
|
|
|
182
182
|
as_strategy_kwargs=as_strategy_kwargs,
|
|
183
183
|
),
|
|
184
184
|
)
|
|
185
|
-
except (InvalidSchema, InvalidArgument) as exc:
|
|
185
|
+
except (InvalidSchema, InvalidArgument, AuthenticationError) as exc:
|
|
186
186
|
on_error(exc, method=operation.method, path=operation.path)
|
|
187
187
|
continue
|
|
188
188
|
|
|
@@ -19,6 +19,7 @@ from schemathesis.core.compat import BaseExceptionGroup
|
|
|
19
19
|
from schemathesis.core.control import SkipTest
|
|
20
20
|
from schemathesis.core.errors import (
|
|
21
21
|
SERIALIZERS_SUGGESTION_MESSAGE,
|
|
22
|
+
AuthenticationError,
|
|
22
23
|
InternalError,
|
|
23
24
|
InvalidHeadersExample,
|
|
24
25
|
InvalidRegexPattern,
|
|
@@ -173,6 +174,9 @@ def run_test(
|
|
|
173
174
|
# We need more clear error message here
|
|
174
175
|
status = Status.ERROR
|
|
175
176
|
yield non_fatal_error(build_unsatisfiable_error(operation, with_tip=False))
|
|
177
|
+
except AuthenticationError as exc:
|
|
178
|
+
status = Status.ERROR
|
|
179
|
+
yield non_fatal_error(exc)
|
|
176
180
|
except KeyboardInterrupt:
|
|
177
181
|
yield scenario_finished(Status.INTERRUPTED)
|
|
178
182
|
yield events.Interrupted(phase=phase)
|